(给ImportNew加星标,提高Java技能)
一. 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-stream 上面三行之中,第一行的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./ 为例。 打开这个网站,网页一渲染就会开启一个 wss 的握手请求。握手请求如下:
这里和普通的 HTTP 协议相比,不同的地方有以下几处: 请求的 URL 是 ws:// 或者 wss:// 开头的,而不是 HTTP:// 或者 HTTPS://。由于 websocket 可能会被用在浏览器以外的场景,所以这里就使用了自定义的 URI。类比 HTTP,ws协议:普通请求,占用与 HTTP 相同的 80 端口;wss协议:基于 SSL 的安全传输,占用与 TLS 相同的 443 端口。 Connection: Upgrade 这两处是普通的 HTTP 报文一般没有的,这里利用 Upgrade 进行了协议升级,指明升级到 websocket 协议。
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 注册中心,但并将不会被使用。 +--------+-----------------------------------------+----------+
Sec-WebSocket-Key 是由浏览器随机生成的,提供基本的防护,防止恶意或者无意的连接。 Sec-WebSocket-Extensions 是属于升级协商的部分,这里放在下一章节进行详细讲解。 接着来看看 Response:
在 Response 中,用 HTTP 101 响应码回应,确认升级到 WebSocket 协议。 同样也有两个 WebSocket 的 header: Sec-WebSocket-Accept: 375guuMrnCICpulKbj7+JGkOhok= Sec-WebSocket-Accept 是经过服务器确认后,并且加密之后的 Sec-WebSocket-Key。 Sec-WebSocket-Accept 的计算方法如下:
伪代码:
同样,Sec-WebSocket-Key/Sec-WebSocket-Accept 只是在握手的时候保证握手成功,但是对数据安全并不保证,用 wss:// 会稍微安全一点。 握手中的子协议 WebSocket 握手有可能会涉及到子协议的问题。 先来看看 WebSocket 的对象初始化函数: WebSocket WebSocket( 这里有一个 optional ,是一个可以协商协议的数组。
在创建 WebSocket 对象的时候,可以传递一个可选的子协议数组,告诉服务器,客户端可以理解哪些协议或者希望服务器接收哪些协议。服务器可以从数据里面选择几个支持的协议进行返回,如果一个都不支持,那么会直接导致握手失败。触发 onerror 回调,并断开连接。 这里的子协议可以是自定义的协议。 多版本的 websocket 握手 使用 WebSocket 版本通知能力( Sec-WebSocket-Version 头字段),客户端可以初始请求它选择的 WebSocket 协议的版本(这并不一定必须是客户端支持的最新的)。如果服务器支持请求的版本且握手消息是本来有效的,服务器将接受该版本。如果服务器不支持请求的版本,它必须以一个包含所有它将使用的版本的 Sec-WebSocket-Version 头字段(或多个 Sec-WebSocket-Version 头字段)来响应。此时,如果客户端支持一个通知的版本,它可以使用新的版本值重做 WebSocket 握手。 举个例子: GET /chat HTTP/1.1 服务器不支持 25 的版本,则会返回:
客户端支持 13 版本的,则需要重新握手: GET /chat HTTP/1.1 四. WebSocket 升级协商 在 WebSocket 握手阶段,会 5 个带 WebSocket 的 header。这 5 个 header 都是和升级协商相关的。
协商是在握手阶段,握手完成以后,HTTP 通信结束,接下来的全双工全部都交给 WebSocket 协议管理(TCP 通信)。 五. WebSocket 协议扩展 负责制定 WebSocket 规范的 HyBi Working Group 就进行了两项扩展 Sec-WebSocket-Extensions:
如果不进行多路复用扩展,每个 WebSocket 连接都只能独享专门的一个 TCP 连接,而且当遇到一个巨大的消息分成多个帧的时候,容易产生队首阻塞的情况。队首阻塞会导致延迟,所以分成多个帧的时候能尽量的小是关键。不过在进行了多路复用扩展以后,多个连接复用一个 TCP 连接,每个信道依旧会存在队首阻塞的问题。除了多路复用,还要进行多路并行发送消息。 如果通过 HTTP2 进行 WebSocket 传输,性能会更好一点,毕竟 HTTP2 原生就支持了流的多路复用。利用 HTTP2 的分帧机制进行 WebSocket 的分帧,多个 WebSocket 可以在同一个会话中传输。 六. WebSocket 数据帧 WebSocket 另一个高级组件是:二进制消息分帧机制。WebSocket 会把应用的消息分割成一个或多个帧,接收方接到到多个帧会进行组装,等到接收到完整消息之后再通知接收端。 WebSocket 数据帧结构 WebSocket 数据帧格式如下:
一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用 WebSocket 扩展,连接出错。
%x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片; %x1:表示这是一个文本帧(text frame); %x2:表示这是一个二进制帧(binary frame); %x3-7:保留的操作代码,用于后续定义的非控制帧; %x8:表示连接断开; %x9:表示这是一个心跳请求(ping); %xA:表示这是一个心跳响应(pong); %xB-F:保留的操作代码,用于后续定义的控制帧。
表示是否要对数据载荷进行掩码异或操作。1表示需要,0表示不需要。(只适用于客户端发给服务器的消息,客户端给服务器发送消息,这里一定为 1)
表示数据载荷的长度,这里有 3 种情况:
如果 Mask = 0,则没有 Masking-key,如果 Mask = 1,则 Masking-key 长度为 4 字节,32位。 掩码是由客户端随机选择的 32 位值。当准备一个掩码的帧时,客户端必须从允许的 32 位值集合中选择一个新的掩码键。掩码键需要是不可预测的;因此,掩码键必须来自一个强大的熵源, 且用于给定帧的掩码键必须不容易被服务器/代理预测用于后续帧的掩码键。掩码键的不可预测性对防止恶意应用的作者选择出现在报文上的字节是必要的。RFC 4086 [RFC4086]讨论了什么需要一个用于安全敏感应用的合适的熵源。 掩码不影响“负载数据”的长度。变换掩码数据到解掩码数据,或反之亦然,以下算法被应用。相同的算法应用,不管转化的方向,例如,相同的步骤即应用到掩码数据也应用到解掩码数据。
变换数据的八位位组 i ('transformed-octet-i')是原始数据的八位位组 i('original-octet-i')异或(XOR)i 取模 4 位置的掩码键的八位位组('masking-key-octet-j'): j = i MOD 4 算法简单描述:按位做循环异或运算,先对该位的索引取模来获得 Masking-key 中对应的值 x,然后对该位与 x 做异或,从而得到真实的 byte 数据。 注意:掩码的作用并不是为了防止数据泄密,而是为了防止客户端中运行的恶意脚本对不支持 WebSocket 的中间设备进行代理缓存投毒攻击(proxy cache poisoning attack)
攻击主要分2步, 第一步,先进行一次 WebSocket 连接。黑客通过代理服务器向自己的服务器进行 WebSocket 握手,由于 WebSocket 握手是 HTTP 消息,所以代理服务器把黑客自己服务器的 Response 转发回给黑客的时候,会认为本次 HTTP 请求结束。 第二步,在代理服务器上面制造“投毒”攻击。由于 WebSocket 握手成功,所以黑客可以向自己的服务器上发送数据了,发送一条精心设置过的 HTTP 格式的文本信息。这条数据的 host 需要伪造成普通用户即将要访问的服务器,请求的资源是普通用户即将要请求的资源。代理服务器会认为这是一条新的请求,于是向黑客自己的服务器请求,这时候也需要黑客自己服务器配合,收到这条“投毒”以后的消息以后,立即返回“毒药”,返回一些恶意的脚本资源等等。至此,“投毒”成功。 当用户通过代理服务器请求要请求的安全资源的时候,由于 host 和 url 之前已经被黑客利用 HTTP 格式的文本信息缓存进了代理服务器,“投毒”的资源也被缓存了,这个时候用户请求相同的 host 和 url 的资源的时候,代理缓存服务器发现已经缓存了,就立即会把“投毒”以后的恶意脚本或资源返回给用户。这时候用户就收到了攻击。 所以客户端发给服务器的时候这里必须要带上 Masking-key,它用于标记 Payload data (包含扩展数据和应用数据两种)。
载荷数据分为扩展数据和应用数据两种。
WebSocket 控制帧 控制帧由操作码确定,操作码最高位为 1。当前定义的用于控制帧的操作码包括 0x8 (Close)、0x9(Ping)、和0xA(Pong)。操作码 0xB-0xF 保留用于未来尚未定义的控制帧。 控制帧用于传达有关 WebSocket 的状态。控制帧可以插入到分帧消息的中间。 所有的控制帧必须有一个小于等于125字节的有效载荷长度,控制帧必须不能被分帧。
在 RFC6455 中给出了关闭时候建议的状态码,没有规范的定义,只是给了一个预定义的状态码。
WebSocket 分帧规则 分帧规则由 RFC6455 进行定义,应用对如何分帧是无感知的。分帧这一步由客户端和服务器完成。 分帧也可以更好的利用多路复用的协议扩展,多路复用需要可以分割消息为更小的分段来更好的共享输出通道。 RFC 6455 规定的分帧规则如下:
例子:对于一个作为三个片段发送的文本消息,第一个片段将有一个 0x1 操作码和一个 FIN 位清零,第二个片段将有一个 0x0 操作码和一个 FIN 位清零,且第三个片段将有 0x0 操作码和一个 FIN 位设置。(0x0 操作码在上面讲解过,表示一个延续帧。当 O操作码 为 0x0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片;)
注意:如果控制帧不能被插入,一个 ping 延迟,例如,如果跟着一个大消息将是非常长的。因此,要求在分片消息的中间处理控制帧。 实现注意:在没有任何扩展时,一个接收者不必按顺序缓冲整个帧来处理它。例如,如果使用了一个流式 API,一个帧的一部分能被交付到应用。但是,请注意这个假设可能不适用所有未来的 WebSocket 扩展。 WebSocket 分帧开销 由于一个分帧了的消息包含:开始于:单个帧,FIN 设为 0,opcode 非 0;后接 :0 个或多个帧,FIN 设为 0,opcode 设为 0;终结于:单个帧,FIN 设为 1,opcode 设为 0。一个分帧了消息在概念上等价于一个未分帧的大消息,它的有效载荷长度等于所有帧的有效载荷长度的累加;然而,有扩展时,这可能不成立,因为扩展定义了出现的 Extension data 的解释。例如,Extension data 可能只出现在第一帧,并用于后续的所有帧,或者 Extension data 出现于所有帧,且只应用于特定的那个帧。
那么通常来说,服务器分帧就 3 种,开始帧,中间帧,结束帧。开始帧和结束帧可以带数据也可以不带数据。分帧开销主要花费在新增加的帧头信息上。开销大小是 1 + 3 + 4 + 1 + 7 + 0 = 16 bit = 2 Byte。中间帧带数据的情况下,开销大小是 1 + 3 + 4 + 1 + 7 + 64 = 80 bit = 10 Byte (假设数据长度 127 bit,所以 playload len 要加上 64 bit) 服务器分帧开销范围就在 [2,10] 字节,客户端要比服务器多增加 Masking-key,这个占用 4 字节 (32 位),所以客户端分帧开销在 [6,14] 字节。 七. WebSocket API 及数据格式 1. WebSocket API WebSocket API 及其简洁,可以调用的函数只有下面这么几个:
除去新建 WebSocket 对象和 send() 方法以外,剩下的就是4个回调方法了。 上述的这些方法中,send() 方法需要额外注意一点的是,这个方法是异步的,并不是同步方法。意味着当我们把要发送的内容丢到这个函数中的时候,函数就异步返回了,此时不要误认为已经发送出去了。WebSocket 自身有一个排队的机制,数据会先丢到数据缓存区中,然后按照排队的顺序进行发送。 如果是一个巨大的文件排队中,后面又来了一些优先级比这个消息高的消息,比如系统出错,需要立即断开连接。由于排队排在大文件之后,必须等待大文件发送完毕才能发送这个优先级更高的消息。这就造成了队首阻塞的问题了,导致优先级更高的消息延迟。 WebSocket API 制定者考虑到了这个问题,于是给了我们另外 2 个为数不多的可以改变 WebSocket 对象行为的属性,一个是 bufferedAmount,另外一个是 binaryType。 if (ws.bufferedAmount == 0) 在上述这种情况下就可以使用 bufferedAmount 监听缓存区的数量,从而避免队首阻塞的问题,更进一步也可以和 Priority Queue 结合到一起,实现按照优先级高低来发送消息。 2. 数据格式 WebSocket 对传输的格式没有任何限制,可以是文本也可以是二进制,都可以。协议中通过 Opcode 类型字段来区分是 UTF-8 还是二进制。WebSocket API 可以接收 UTF-8 编码的 DOMString 对象,也可以接收 ArrayBuffer、 ArrayBufferView 或 Blob 等二进制数据。 浏览器对接收到的数据,如果不手动设置任何其他选项的话,默认处理是,文本是默认转成 DOMString 对象,二进制数据或者 Blob 对象会直接转给给应用,中间不做任何处理。
唯一能干涉的地方就是把接收到的二进制数据全部都强制转换成 arraybuffer 类型而不是 Blob 类型。至于为何要转换成 arraybuffer 类型, W3C 的候选人给出的建议如下:
简单的说:如果转换成了 Blob 对象,就代表了一个不可变的文件对象或者原始数据。如果不需要修改或者不需要切分它,保留成 Blob 对象是一个好的选择。如果要处理这段原始数据,放进内存里面处理明显会更加合适,那么就请转换成 arraybuffer 类型。 八. WebSocket 性能和使用场景 有一张来自 WebSocket.org 网站的测试,用 XHR 轮询和 WebSocket 进行对比: 上图中,我们先看蓝色的柱状图,是 Polling 轮询消耗的流量,这次测试,HTTP 请求和响应头信息开销总共包括 871 字节。当然每次测试不同的请求,头的开销不同。这次测试都以 871 字节的请求来测试。 Use case A: 1,000 clients polling every second: Network throughput is (871 x 1,000) = 871,000 bytes = 6,968,000 bits per second (6.6 Mbps) Use case B: 10,000 clients polling every second: Network throughput is (871 x 10,000) = 8,710,000 bytes = 69,680,000 bits per second (66 Mbps) Use case C: 100,000 clients polling every 1 second: Network throughput is (871 x 100,000) = 87,100,000 bytes = 696,800,000 bits per second (665 Mbps) 而 Websocket 的 Frame 是 just two bytes of overhead instead of 871,仅仅用 2 个字节就代替了轮询的 871 字节! Use case A: 1,000 clients receive 1 message per second: Network throughput is (2 x 1,000) = 2,000 bytes = 16,000 bits per second (0.015 Mbps) Use case B: 10,000 clients receive 1 message per second: Network throughput is (2 x 10,000) = 20,000 bytes = 160,000 bits per second (0.153 Mbps) Use case C: 100,000 clients receive 1 message per second: Network throughput is (2 x 100,000) = 200,000 bytes = 1,600,000 bits per second (1.526 Mbps) 相同的每秒客户端轮询的次数,当次数高达 10W/s 的高频率次数的时候,Polling 轮询需要消耗 665Mbps,而 Websocket 仅仅只花费了 1.526Mbps,将近 435 倍!! 从结果上看, WebSocket 确实比轮询效率和网速消耗都要好很多。 从使用场景来说,XHR、SSE、WebSocket 各有优缺点。 XHR 相对其他两种方式更加简单,依靠 HTTP 完善的基础设施,很容易实现。不过它不支持请求流,对相应流也不是完美支持(需要支持 Streams API 才能支持响应流)。传输数据格式方面,文本和二进制都支持,也支持压缩。HTTP 对它的报文负责分帧。 SSE 也同样不支持请求流,在进行一次握手以后,服务端就可以以事件源协议把数据作为响应流发给客户端。SSE 只支持文本数据,不能支持二进制。因为 SSE 不是为传输二进制而设计的,如果有必要,可以把二进制对象编码为 base64 形式,然后再使用 SSE 进行传输。SSE 也支持压缩,事件流负责对它进行分帧。 WebSocket 是目前唯一一个通过同一个 TCP 连接实现的全双工的协议,请求流和响应流都完美支持。支持文本和二进制数据,本身自带二进制分帧。在压缩方面差一些,因为有些不支持,例如 x-webkit-deflate-frame 扩展,在笔者上文中距离的那个 ws 请求中服务器就没有支持压缩。 如果所有的网络环境都可以支持 WebSocket 或者 SSE 当然是最好不过的了。但是这是不现实的,网络环境千变万化,有些网络可能就屏蔽了 WebSocket 通信,或者用户设备就不支持 WebSocket 协议,于是 XHR 也就有了用武之地。 如果客户端不需要给服务端发消息,只需要不断的实时更新,那么考虑用 SSE 也是不错的选择。不过 SSE 目前在 IE 和 Edge 上支持的较差。WebSocket 在这方面比 SSE 强。 所以应该根据不同场景选择不同的协议,各取所长。 reference: |
|