一. WebSocket 是什么? WebSocket 是一种网络通信协议。在 2009 年诞生,于 2011 年被 IETF 定为标准 RFC 6455 通信标准。并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。 WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工(full-duplex)通讯的协议。没有了 Request 和 Response 的概念,两者地位完全平等,连接一旦建立,就建立了真•持久性连接,双方可以随时向对方发送数据。 (HTML5 是 HTML 最新版本,包含一些新的标签和全新的 API。HTTP 是一种协议,目前最新版本是 HTTP/2 ,所以 WebSocket 和 HTTP 有一些交集,两者相异的地方还是很多。两者交集的地方在 HTTP 握手阶段,握手成功后,数据就直接从 TCP 通道传输。) 二. 为什么要发明 WebSocket ? 在没有 WebSocket 之前,Web 为了实现即时通信,有以下几种方案,最初的 polling ,到之后的 Long polling,最后的基于 streaming 方式,再到最后的 SSE,也是经历了几个不种的演进方式。 (1) 最开始的短轮询 Polling 阶段 这种方式下,是不适合获取实时信息的,客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 HTTP 的 Header,会很耗流量,也会消耗 CPU 的利用率。 这个阶段可以看到,一个 Request 对应一个 Response,一来一回一来一回。 在 Web 端,短轮询用 AJAX JSONP Polling 轮询实现。 由于 HTTP 无法无限时长的保持连接,所以不能在服务器和 Web 浏览器之间频繁的长时间进行数据推送,所以 Web 应用通过通过频繁的异步 JavaScript 和 XML (AJAX) 请求来实现轮循。
(2) 改进版的长轮询 Long polling 阶段(Comet Long polling) 长轮询是对轮询的改进版,客户端发送 HTTP 给服务器之后,有没有新消息,如果没有新消息,就一直等待。直到有消息或者超时了,才会返回给客户端。消息返回后,客户端再次建立连接,如此反复。这种做法在某种程度上减小了网络带宽和 CPU 利用率等问题。 这种方式也有一定的弊端,实时性不高。如果是高实时的系统,肯定不会采用这种办法。因为一个 GET 请求来回需要 2个 RTT,很可能在这段时间内,数据变化很大,客户端拿到的数据已经延后很多了。 另外,网络带宽低利用率的问题也没有从根源上解决。每个 Request 都会带相同的 Header。 对应的,Web 也有 AJAX 长轮询,也叫 XHR 长轮询。 客户端打开一个到服务器端的 AJAX 请求,然后等待响应,服务器端需要一些特定的功能来允许请求被挂起,只要一有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求。客户端在处理完服务器返回的信息后,再次发出请求,重新建立连接,如此循环。
(3) 基于流(Comet Streaming) 1. 基于 Iframe 及 htmlfile 的流(Iframe Streaming) iframe 流方式是在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创建一条长链接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 JavaScript),来实时更新页面。iframe 流方式的优点是浏览器兼容好。 使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox 下端的进度栏都会显示加载没有完成,而且 IE 上方的图标会不停的转动,表示加载正在进行。 Google 的天才们使用一个称为 “htmlfile” 的 ActiveX 解决了在 IE 中的加载显示问题,并将这种方法用到了 gmail+gtalk 产品中。Alex Russell 在 “What else is burried down in the depth's of Google's amazing JavaScript?”文章中介绍了这种方法。Zeitoun 网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe 和 htmlfile 的 JavaScript comet 对象,支持 IE、Mozilla Firefox 浏览器,可以作为参考。
2. AJAX multipart streaming(XHR Streaming) 实现思路:浏览器必须支持 multi-part 标志,客户端通过 AJAX 发出请求 Request,服务器保持住这个连接,然后可以通过 HTTP1.1 的 chunked encoding 机制(分块传输编码)不断 push 数据给客户端,直到 timeout 或者手动断开连接。
3. Flash Socket(Flash Streaming) 实现思路:在页面中内嵌入一个使用了 Socket 类的 Flash 程序,JavaScript 通过调用此 Flash 程序提供的 Socket 接口与服务器端的 Socket 接口进行通信,JavaScript 通过 Flash Socket 接收到服务器端传送的数据。
4. Server-Sent Events 服务器发送事件(SSE)也是 HTML5 公布的一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如自动重新连接、事件 ID 以及发送任意事件的能力。 SSE 就是利用服务器向客户端声明,接下来要发送的是流信息(streaming),会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,可以类比视频流。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。 SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。 服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,具有如下的 HTTP 头信息。 Content-Type: text/event-streamCache-Control: no-cache Connection: keep-alive 上面三行之中,第一行的Content-Type必须指定 MIME 类型为event-steam
以上是常见的四种基于流的做法,Iframe Streaming、XHR Streaming、Flash Streaming、Server-Sent Events。 从浏览器兼容难度看 —— 短轮询/AJAX > 长轮询/Comet > 长连接/SSE WebSocket 的到来 从上面这几种演进的方式来看,也是不断改进的过程。 短轮询效率低,非常浪费资源(网络带宽和计算资源)。有一定延迟、服务器压力较大,并且大部分是无效请求。 长轮询虽然省去了大量无效请求,减少了服务器压力和一定的网络带宽的占用,但是还是需要保持大量的连接。 最后到了基于流的方式,在服务器往客户端推送,这个方向的流实时性比较好。但是依旧是单向的,客户端请求服务器依然还需要一次 HTTP 请求。 那么人们就在考虑了,有没有这样一个完美的方案,即能双向通信,又可以节约请求的 header 网络开销,并且有更强的扩展性,最好还可以支持二进制帧,压缩等特性呢? 于是人们就发明了这样一个目前看似“完美”的解决方案 —— WebSocket。 在 HTML5 中公布了 WebSocket 标准以后,直接取代了 Comet 成为服务器推送的新方法。
一句话总结一下 WebSocket: WebSocket 是 HTML5 开始提供的一种独立在单个 TCP 连接上进行全双工通讯的有状态的协议(它不同于无状态的 HTTP),并且还能支持二进制帧、扩展协议、部分自定义的子协议、压缩等特性。 目前看来,WebSocket 是可以完美替代 AJAX 轮询和 Comet 。但是某些场景还是不能替代 SSE,WebSocket 和 SSE 各有所长! 三. WebSocket 握手 WebSocket 的 RFC6455 标准中制定了 2 个高级组件,一个是开放性 HTTP 握手用于协商连接参数,另一个是二进制消息分帧机制用于支持低开销的基于消息的文本和二进制数据传输。接下来就好好谈谈这两个高级组件,这一章节详细的谈谈握手的细节,下一个章节再谈谈二进制消息分帧机制。 首先,在 RFC6455 中写了这样一段话:
从这段话中我们可看出制定 WebSocket 协议的人的“野心”或者说对未来的规划有多远,WebSocket 制定之初就已经支持了可以在任意端口上进行握手,而不仅仅是要依靠 HTTP 握手。 不过目前用的对多的还是依靠 HTTP 进行握手。因为 HTTP 的基础设施已经相当完善了。 标准的握手流程 接下来看一个具体的 WebSocket 握手的例子。以笔者自己的网站 https://threes.halfrost.com/ 为例。 打开这个网站,网页一渲染就会开启一个 wss 的握手请求。握手请求如下: GET wss://threes.halfrost.com/sockjs/689/8x5nnke6/websocket HTTP/1.1 // 请求的方法必须是GET,HTTP版本必须至少是1.1 Host: threes.halfrost.com Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket // 请求升级到 WebSocket 协议 Origin: https://threes.halfrost.com Sec-WebSocket-Version: 13 // 客户端使用的 WebSocket 协议版本 User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Mobile Safari/537.36 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cookie: _ga=GA1.2.00000006.14111111496; _gid=GA1.2.23232376.14343448247; Hm_lvt_d60c126319=1524898423,1525574369,1526206975,1526784803; Hm_lpvt_d606319=1526784803; _gat_53806_2=1 Sec-WebSocket-Key: wZgx0uTOgNUsHGpdWc0T+w== // 自动生成的键,以验证服务器对协议的支持,其值必须是 nonce 组成的随机选择的 16 字节的被 base64 编码后的值 Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits // 可选的客户端支持的协议扩展列表,指示了客户端希望使用的协议级别的扩展 这里和普通的 HTTP 协议相比,不同的地方有以下几处: 请求的 URL 是 ws:// 或者 wss:// 开头的,而不是 HTTP:// 或者 HTTPS://。由于 websocket 可能会被用在浏览器以外的场景,所以这里就使用了自定义的 URI。类比 HTTP,ws协议:普通请求,占用与 HTTP 相同的 80 端口;wss协议:基于 SSL 的安全传输,占用与 TLS 相同的 443 端口。 Connection: Upgrade Upgrade: websocket 这两处是普通的 HTTP 报文一般没有的,这里利用 Upgrade 进行了协议升级,指明升级到 websocket 协议。 Sec-WebSocket-Version: 13 Sec-WebSocket-Key: wZgx0uTOgNUsHGpdWc0T+w== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Version 表示 WebSocket 的版本,最初 WebSocket 协议太多,不同厂商都有自己的协议版本,不过现在已经定下来了。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Version,里面包含服务端支持的版本号。 最新版本就是 13,当然有可能存在非常早期的版本 7 ,8(目前基本不会不存在 7,8 的版本了) 注意:尽管本文档的草案版本(09、10、11、和 12)发布了(它们多不是编辑上的修改和澄清而不是改变电报协议 [wire protocol]),值 9、10、11、和 12 不被用作有效的 Sec-WebSocket-Version。这些值被保留在 IANA 注册中心,但并将不会被使用。 +--------+-----------------------------------------+----------+ |Version | Reference | Status | | Number | | | +--------+-----------------------------------------+----------+ | 0 + draft-ietf-hybi-thewebsocketprotocol-00 | Interim | +--------+-----------------------------------------+----------+ | 1 + draft-ietf-hybi-thewebsocketprotocol-01 | Interim | +--------+-----------------------------------------+----------+ | 2 + draft-ietf-hybi-thewebsocketprotocol-02 | Interim | +--------+-----------------------------------------+----------+ | 3 + draft-ietf-hybi-thewebsocketprotocol-03 | Interim | +--------+-----------------------------------------+----------+ | 4 + draft-ietf-hybi-thewebsocketprotocol-04 | Interim | +--------+-----------------------------------------+----------+ | 5 + draft-ietf-hybi-thewebsocketprotocol-05 | Interim | +--------+-----------------------------------------+----------+ | 6 + draft-ietf-hybi-thewebsocketprotocol-06 | Interim | +--------+-----------------------------------------+----------+ | 7 + draft-ietf-hybi-thewebsocketprotocol-07 | Interim | +--------+-----------------------------------------+----------+ | 8 + draft-ietf-hybi-thewebsocketprotocol-08 | Interim | +--------+-----------------------------------------+----------+ | 9 + Reserved | | +--------+-----------------------------------------+----------+ | 10 + Reserved | | +--------+-----------------------------------------+----------+ | 11 + Reserved | | +--------+-----------------------------------------+----------+ | 12 + Reserved | | +--------+-----------------------------------------+----------+ | 13 + RFC 6455 | Standard | +--------+-----------------------------------------+----------+
Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。 Sec-WebSocket-Extensions 是属于升级协商的部分,这里放在下一章节进行详细讲解。 接着来看看 Response: HTTP/1.1 101 Switching Protocols// 101 HTTP 响应码确认升级到 WebSocket 协议 Server: nginx/1.12.1 Date: Sun, 20 May 2018 09:06:28 GMT Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok= // 签名的键值验证协议支持 Sec-WebSocket-Extensions: permessage-deflate // 服务器选择的WebSocket 扩展 在 Response 中,用 HTTP 101 响应码回应,确认升级到 WebSocket 协议。 同样也有两个 WebSocket 的 header: Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok=// 签名的键值验证协议支持 Sec-WebSocket-Extensions: permessage-deflate // 服务器选择的 WebSocket 扩展 Sec-WebSocket-Accept 是经过服务器确认后,并且加密之后的 Sec-WebSocket-Key。 Sec-WebSocket-Accept 的计算方法如下:
伪代码: > toBase64(sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )) 同样,Sec-WebSocket-Key/Sec-WebSocket-Accept 只是在握手的时候保证握手成功,但是对数据安全并不保证,用 wss:// 会稍微安全一点。 握手中的子协议 WebSocket 握手有可能会涉及到子协议的问题。 先来看看 WebSocket 的对象初始化函数: WebSocket WebSocket( in DOMString url, // 表示要连接的URL。这个URL应该为响应WebSocket的地址。 in optional DOMString protocols // 可以是一个单个的协议名字字符串或者包含多个协议名字字符串的数组。默认设为一个空字符串。 ); 这里有一个 optional ,是一个可以协商协议的数组。 var ws = new WebSocket('wss://example.com/socket', ['appProtocol', 'appProtocol-v2']); ws.onopen = function () { if (ws.protocol == 'appProtocol-v2') { ... } else { ... } } 在创建 WebSocket 对象的时候,可以传递一个可选的子协议数组,告诉服务器,客户端可以理解哪些协议或者希望服务器接收哪些协议。服务器可以从数据里面选择几个支持的协议进行返回,如果一个都不支持,那么会直接导致握手失败。触发 onerror 回调,并断开连接。 这里的子协议可以是自定义的协议。 多版本的 websocket 握手 使用 WebSocket 版本通知能力( Sec-WebSocket-Version 头字段),客户端可以初始请求它选择的 WebSocket 协议的版本(这并不一定必须是客户端支持的最新的)。如果服务器支持请求的版本且握手消息是本来有效的,服务器将接受该版本。如果服务器不支持请求的版本,它必须以一个包含所有它将使用的版本的 Sec-WebSocket-Version 头字段(或多个 Sec-WebSocket-Version 头字段)来响应。此时,如果客户端支持一个通知的版本,它可以使用新的版本值重做 WebSocket 握手。 举个例子: GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade ... Sec-WebSocket-Version: 25 服务器不支持 25 的版本,则会返回: HTTP/1.1 400 Bad Request ... Sec-WebSocket-Version: 13, 8, 7 客户端支持 13 版本的,则需要重新握手: GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade ... Sec-WebSocket-Version: 13 四. WebSocket 升级协商 在 WebSocket 握手阶段,会 5 个带 WebSocket 的 header。这 5 个 header 都是和升级协商相关的。
|
|免责声明|本站介绍|工控课堂
( 沪ICP备14007696号-3 )|网站地图
GMT+8, 2019-9-14 20:08 , Processed in 0.061772 second(s), 41 queries .
Powered by Discuz! X3.4
© 2001-2017 Comsenz Inc.