搞懂现代 Web 端即时通讯技术一文就够:WebSocket、socket.io、SSE
![搞懂现代Web端即时通讯技术一文就够:WebSocket、socket.io、SSE](https://static001.geekbang.org/infoq/bd/bd430ab054f7d4a0e59a4e748ebf8388.png)
本文引用自“ 豆米博客”的《JS 实时通信三把斧》系列文章,有优化和改动。
1、引言
有关 Web 端即时通讯技术的文章我已整理过很多篇,阅读过的读者可能都很熟悉,早期的 Web 端即时通讯方案,受限于 Web 客户端的技术限制,想实现真正的“即时”通信,难度相当大。
传统的 Web 端即时通讯技术从短轮询到长连询,再到Comet技术,在如此原始的 HTML 标准之下,为了实现所谓的“即时”通信,技术上可谓绞尽脑汁,极尽所能。
自从 HTML5 标准发布之后,WebSocket这类技术横空出世,实现 Web 端即时通讯技术的便利性大大提前,以往想都不敢想的真正全双工实时通信,如此早已成为可能。
本文将专门介绍 WebSocket、socket.io、SSE这几种现代的 Web 端即时通讯技术,从适用场景到技术原理,通俗又不失深度的文字,特别适合对 Web 端即时通讯技术有一定了解,且想深入学习 WebSocket 等现代 Web 端“实时”通信技术,却又不想花时间去深读枯燥的 IETF 技术手册的读者。
学习交流:
- 移动端 IM 开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源 IM 框架源码:https://github.com/JackJiang2011/MobileIMSDK
2、本文作者
“豆米”:现居杭州,热爱前端,热爱互联网,豆米是“洋芋(土豆-豆)”和“米喳(米)”的简称。
作者 Github:https://github.com/linxiaowu66/
3、知识预备
如果你对 Web 端即时通讯技术的前世今生不曾了解,建议先读以下文章:
如果你对本文将要介绍的技术已有了解,建议进行专项学习,以便深入掌握:
4、WebSocket
![](https://static001.geekbang.org/infoq/31/31eabb14e229302905e43ad52d1583a4.png)
在这里不打算详细介绍整个WebSocket协议的内容,根据我本人以前协议的学习思路,我挑重点使用问答方式来介绍该协议,这样读起来就不那么枯燥。
4.1 基本情况
协议运行在 OSI 的哪层?
应用层,WebSocket 协议是一个独立的基于 TCP 的协议。 它与 HTTP 唯一的关系是它的握手是由 HTTP 服务器解释为一个 Upgrade 请求。
协议运行的标准端口号是多少?
默认情况下,WebSocket 协议使用端口 80 用于常规的 WebSocket 连接、端口 443 用于 WebSocket 连接的在传输层安全(TLS)RFC2818之上的隧道化口。
4.2 协议是如何工作的?
协议的工作流程可以参考下图:
![](https://static001.geekbang.org/infoq/a1/a1a932d2f86ea6ff26e503e85e1457b7.png)
其中帧的一些重要字段需要解释一下:
1)Upgrade:`upgrade`是 HTTP1.1 中用于定义转换协议的`header`域。它表示,如果服务器支持的话,客户端希望使用现有的「网络层」已经建立好的这个「连接(此处是 TCP 连接)」,切换到另外一个「应用层」(此处是 WebSocket)协议;
2)Connection:`Upgrade`固定字段。Connection 还有其他字段,可以自己给自己科普一下;
3)Sec-WebSocket-Key:用来发送给服务器使用(服务器会使用此字段组装成另一个 key 值放在握手返回信息里发送客户端);
4)Sec-WebSocket-Protocol:标识了客户端支持的子协议的列表;
5)Sec-WebSocket-Version:标识了客户端支持的 WS 协议的版本列表,如果服务器不支持这个版本,必须回应自己支持的版本;
6)Origin:作安全使用,防止跨站攻击,浏览器一般会使用这个来标识原始域;
7)Sec-WebSocket-Accept:服务器响应,包含 Sec-WebSocket-Key 的签名值,证明它支持请求的协议版本。
关于 Sec-WebSocket-Key 和 Sec-WebSocket-Accept 的计算是这样的:
所有兼容 RFC 6455 的 WebSocket 服务器都使用相同的算法计算客户端挑战的答案:将 Sec-WebSocket-Key 的内容与标准定义的唯一 GUID 字符(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)串拼接起来,计算出 SHA1 散列值,结果是一个 base-64 编码的字符串,把这个字符串发给客户端即可。
用代码就是实现如下:
const key = crypto.createHash('sha1')
.update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')
.digest('base64')
至于为什么需要这么一个步骤,可以参考《理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性》一文。
引用如下:
Sec-WebSocket-Key/Sec-WebSocket-Accept 在主要作用在于提供基础的防护,减少恶意连接、意外连接。
作用大致归纳如下:
1)避免服务端收到非法的 websocket 连接(比如 http 客户端不小心请求连接 websocket 服务,此时服务端可以直接拒绝连接);
2)确保服务端理解 websocket 连接。因为 ws 握手阶段采用的是 http 协议,因此可能 ws 连接是被一个 http 服务器处理并返回的,此时客户端可以通过 Sec-WebSocket-Key 来确保服务端认识 ws 协议。(并非百分百保险,比如总是存在那么些无聊的 http 服务器,光处理 Sec-WebSocket-Key,但并没有实现 ws 协议。。。);
3)用浏览器里发起 ajax 请求,设置 header 时,Sec-WebSocket-Key 以及其他相关的 header 是被禁止的。这样可以避免客户端发送 ajax 请求时,意外请求协议升级(websocket upgrade);
4)可以防止反向代理(不理解 ws 协议)返回错误的数据。比如反向代理前后收到两次 ws 连接的升级请求,反向代理把第一次请求的返回给 cache 住,然后第二次请求到来时直接把 cache 住的请求给返回(无意义的返回);
5)Sec-WebSocket-Key 主要目的并不是确保数据的安全性,因为 Sec-WebSocket-Key、Sec-WebSocket-Accept 的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。
强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但连接是否安全、数据是否安全、客户端/服务端是否合法的 ws 客户端、ws 服务端,其实并没有实际性的保证。
4.3 协议传输的帧格式是什么?
帧格式定义的格式如下:
![](https://static001.geekbang.org/infoq/a0/a0289ffa0559840a485e90ba67041abf.jpeg?x-oss-process=image/resize,p_80/auto-orient,1)
各个字段的解释如下:
1)FIN: 1bit,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断;
2)RSV1,RSV2,RSV3: 分别都是 1 位,如果双方之间没有约定自定义协议,那么这几位的值都必须为 0,否则必须断掉 WebSocket 连接。在 ws 中就用到了 RSV1 来表示是否消息压缩了的;
3)opcode:4 bit,表示被传输帧的类型:
- %x0 表示连续消息片断;
- %x1 表示文本消息片断;
- %x2 表未二进制消息片断;
- %x3-7 为将来的非控制消息片断保留的操作码;
- %x8 表示连接关闭;
- %x9 表示心跳检查的 ping;
- %xA 表示心跳检查的 pong;
- %xB-F 为将来的控制消息片断的保留操作码。
4)Mask: 1 bit。定义传输的数据是否有加掩码,如果设置为 1,掩码键必须放在 masking-key 区域,客户端发送给服务端的所有消息,此位都是 1;
5)Payload length:传输数据的长度,以字节的形式表示:7 位、7+16 位、或者 7+64 位。如果这个值以字节表示是 0-125 这个范围,那这个值就表示传输数据的长度;如果这个值是 126,则随后的两个字节表示的是一个 16 进制无符号数,用来表示传输数据的长度;如果这个值是 127,则随后的是 8 个字节表示的一个 64 位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为 0,因而此时负载数据的长度就为应用数据的长度;
6)Masking-key:0 或 4 个字节,客户端发送给服务端的数据,都是通过内嵌的一个 32 位值作为掩码的;掩码键只有在掩码位设置为 1 的时候存在;
7)Extension data: x 位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为 0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内;
8)Application data: y 位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度;
9)Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和;
更多细节请参考RFC6455-数据帧,这里不作赘述。
针对上面的各个字段的介绍,有一个 Mask 的需要说一下。
掩码键(Masking-key)是由客户端挑选出来的 32 位的随机数。掩码操作不会影响数据载荷的长度。
掩码、反掩码操作都采用如下算法。
首先,假设:
1)original-octet-i:为原始数据的第 i 字节;
2)transformed-octet-i:为转换后的数据的第 i 字节;
3)j:为 i mod 4 的结果;
4)masking-key-octet-j:为 mask key 第 j 字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
即: j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
用代码实现:
const mask = (source, mask, output, offset, length) => {
for(vari = 0; i < length; i++) {
output[offset + i] = source[i ] ^ mask[i & 3];
}
};
解掩码是反过来的操作:
const unmask = (buffer, mask) => {
// Required until [url=https://github.com/nodejs/node/issues/9006]https://github.com/nodejs/node/issues/9006[/url] is resolved.
const length = buffer.length;
for(vari = 0; i < length; i++) {
buffer[i ] ^= mask[i & 3];
}
};
同样的为什么需要掩码操作,也可以参考之前的那篇文章:《理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性》,完整的我就不列举了。
需要注意的重点,我引用一下:
WebSocket 协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。
那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。
答案还是两个字: 安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。
5、socket.io
5.1 本节引言
![](https://static001.geekbang.org/infoq/fc/fc226a33f0c2c0b4238b93b641f97d20.png)
介绍完上一节 WebSocket 协议,我们把视线转移到现代 Web 端即时通讯技术的第二个利器:socket.io。
估计有读者就会问,WebSocket 和 socket.io 有啥区别啊?
在了解 socket.io 之前,我们先聊聊传统 Web 端即时通讯“长连接”技术的实现背景。
5.2 传统 Web 长连接的技术实现背景
在现实的 Web 端产品中,并不是所有的 Web 客户端都支持长连接的,或者换句话说,在 WebSocket 协议出来之前,是三种方式去实现 WebSocket 类似的功能的。
这三种方式是:
1)Flash:使用 Flash 是一种简单的方法。不过很明显的缺点就是 Flash 并不会安装在所有客户端上,比如 iPhone/iPad。
2)Long-Polling:也就是众所周之的“长轮询”,在过去,这是一种有效的技术,但并没有对消息发送进行优化。虽然我不会把 AJAX 长轮询当做一种 hack 技术,但它确实不是一个最优方法;
3)Comet:在过去,这被称为 Web 端的“服务器推”技术,相对于传统的 Web 应用, 开发 Comet 应用具有一定的挑战性,具体请见《Comet技术详解:基于HTTP长连接的Web端实时通信技术》。
那么如果单纯地使用 WebSocket 的话,那些不支持的客户端怎么办呢?难道直接放弃掉?
当然不是。Guillermo Rauch大神写了 socket.io 这个库,对 WebSocket 进行封装,从而让长连接满足所有的场景,不过当然得配合使用对应的客户端代码。
socket.io 将会使用特性检测的方式来决定以 websocket/ajax 长轮询/flash 等方式建立连接。
那么 socket.io 是如何做到这些的呢?
我们带着以下几个问题去学习:
1)socket.io 到底有什么新特性?
2)socket.io 是怎么实现特性检测的?
3)socket.io 有哪些坑呢?
4)socket.io 的实际应用是怎样的,需要注意些什么?
如果有童鞋对上述问题已经清楚,想必就没有往下读的必要了。
5.3 socket.io 的介绍
通过前面章节,读者们都知道了 WebSocket 的功能,那么 socket.io 相对于 WebSocket,在此基础上封装了一些什么新东西呢?
socket.io 其实是有一套封装了 websocket 的协议,叫做engine.io协议,在此协议上实现了一套底层双向通信的引擎Engine.io。
而socket.io则是建立在 engine.io 上的一个应用层框架而已。所以我们研究的重点便是 engine.io 协议。
在socket.io的 README 中提到了其实现的一些新特性(回答了问题一):
1)可靠性:连接依然可以建立即使应用环境存在: 代理或者负载均衡器 个人防火墙或者反病毒软件;
2)支持自动连接: 除非特别指定,否则一个断开的客户端会一直重连服务器直到服务器恢复可用状态;
3)断开连接检测:在 Engine.io 层实现了一个心跳机制,这样允许客户端和服务器知道什么时候其中的一方不能响应。该功能是通过设置在服务端和客户端的定时器实现的,在连接握手的时候,服务器会主动告知客户端心跳的间隔时间以及超时时间;
4)二进制的支持:任何序列化的数据结构都可以用来发送;
5)跨浏览器的支持:该库甚至支持到 IE8;
6)支持复用:为了在应用程序中将创建的关注点隔离开来,Socket.io 允许你创建多个 namespace,这些 namespace 拥有单独的通信通道,但将共享相同的底层连接;
7)支持 Room:在每一个 namespace 下,你可以定义任意数量的通道,我们称之为"房间",你可以加入或者离开房间,甚至广播消息到指定的房间。
注意:Socket.IO 不是 WebSocket 的实现,虽然 Socket.IO 确实在可能的情况下会去使用 WebSocket 作为一个 transport,但是它添加了很多元数据到每一个报文中:报文的类型以及 namespace 和 ack Id。这也是为什么标准 WebSocket 客户端不能够成功连接上 Socket.IO 服务器,同样一个 Socket.IO 客户端也连接不上标准 WebSocket 服务器的原因。
5.4 engine.io 协议介绍
完整的 engine.io 协议的握手过程如下图:
![](https://static001.geekbang.org/infoq/98/985f1cbc90527a6a007fcaf64db8ea2f.png)
当前 engine.io 协议的版本是 3,我们根据上图来大致介绍一下 engine.io 协议。
5.4.1)engine.io 协议请求字段:
我们看到的是请求的 url 和 WebSocket 不大一样,解释一下:
1)EIO=3: 表示的是使用的是 Engine.io 协议版本 3;
2)transport=polling/websocket: 表示使用的长连接方式是轮询还是 WebSocket;
3)t=xxxxx: 代码中使用 yeast 根据时间戳生成一个唯一的字符串;
4)sid=xxxx: 客户端和服务器建立连接之后获取到的 session id,客户端拿到之后必须在每次请求中追加这个字段。
除了上述的 3 个字段,协议还描述了下面几个字段:
1)j: 如果 transport 是 polling,但是要求有一个 JSONP 的响应,那么 j 就应该设置为 JSONP 响应的索引值;
2)b64: 如果客户端不支持 XHR,那么客户端应该设置 b64=1 传给服务器,告知服务器所有的二进制数据应该以 base64 编码后再发送。
另外 engine.io 默认的 path 是 /engine.io,socket.io 在初始化的时候设置为了 /socket.io,所以大家看到的 path 就都是 /socket.io 了:
function Server(srv, opts){
if(!(this instanceof Server)) return new Server(srv, opts);
if('object'== typeof srv && srv instanceof Object && !srv.listen) {
opts = srv;
srv = null;
}
opts = opts || {};
this.nsps = {};
this.parentNsps = new Map();
this.path(opts.path || '/socket.io');
5.4.2)数据包编码要求:
engine.io 协议的数据包编码有自己的一套格式,在协议介绍上engine.io-protocol,定义了两种编码类型: packet 和 payload。
一个编码过的 packet 是下面这种格式:
<packettype id>[<data>]
然后协议定义了下面几种 packet type(采用数字进行标识):
1)0(open): 当开始一个新的 transport 的时候,服务端会发送该类型的 packet;
2)1(close): 请求关闭这个 transport 但是不要自己关闭关闭连接;
3)2(ping): 由客户端发送的 ping 包,服务端必须回应一个包含相同数据的 pong 包;
4)3(pong): 响应 ping 包,服务端发送;
5)4(message): 实际消息,在客户端和服务端都可以监听 message 事件获取消息内容;
6)5(upgrade): 在 engine.io 切换 transport 之前,它会用来测试服务端和客户端是否在该 transport 上通信。如果测试成功,客户端会发送一个 upgrade 包去让服务器刷新它的缓存并切换到新的 transport;
7)6(noop): 主要用来强制一个轮询循环当收到一个 WebSocket 连接的时候。
那 payload 也有对应的格式要求:
1)如果当只有发送 string 并且不支持 XHR 的时候,其编码格式是::[:[...]];
2)当不支持 XHR2 并且发送二进制数据,但是使用 base64 编码字符串的时候,其编码格式是::b[...];
3)当支持 XHR2 的时候,所有的数据都被编码成二进制,格式是:<0 for string data, 1 for binary data>[...];
4)如果发送的内容混杂着 UTF-8 的字符和二进制数据,字符串的每个字符被写成一个字符编码,用 1 个字节表示。
注意:payload 的编码要求不适用于 WebSocket 的通信。
针对上面的编码要求,我们随便举个例子.
之前在第一条 polling 请求的时候,服务端编码发送了这个数据:
97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}2:40
根据上面的知识,我们知道第一次服务端会发送一个 open 的数据包。
所以组装出来的 packet 是:
0
然后服务端会告知客户端去尝试升级到 websocket,并且告知对应的 sid。
于是整合后便是:
0{"sid":"Peed250dk55pprwgAAAA","upgrades":"websocket","pingInterval":25000,"pingTimeout":60000}
接着根据 payload 的编码格式,因为是 string,且长度是 97 个字节。
所以是:
97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":"websocket","pingInterval":25000,"pingTimeout":60000}
接着第二部分数据是 message 包类型,并且数据是 0,所以是 40,长度为 2 字节,所以是 2:40,最后就拼成刚才大家看到的结果。
注意:
ping/pong 的间隔时间是服务端告知客户端的:"pingInterval":25000,"pingTimeout":60000,也就是说心跳时间默认是 25 秒,并且等待 pong 响应的时间默认是 60s。
5.5 升级协议的必备过程
协议定义了 transport 升级到 websocket 需要经历一个必须的过程。
如下图:
![](https://static001.geekbang.org/infoq/8d/8d26831cca4ee0452a41d3ca94a9d498.png)
WebSocket 的测试开始于发送 probe,如果服务器也响应 probe 的话,客户端就必须发送一个 upgrade 包。
为了确保不会丢包,只有在当前 transport 的所有 buffer 被刷新并且 transport 被认为 paused 的时候才可以发送 upgrade 包。服务端收到 upgrade 包的时候,服务端必须假设这是一个新的通道并发送所有已存的缓存到这个通道上
在 Chrome 上的效果如下:
![](https://static001.geekbang.org/infoq/f1/f1bb212735cd054f601601d6713fb395.png)
5.6 engine.io 的代码实现
熟悉了 engine.io 协议之后,我们看看代码是怎么实现主流程的。
客户端的 engine.io 的主要实现流程我们在上面文字介绍了。
结合代码 engine.io,画了这么一个客户端流程图:
![](https://static001.geekbang.org/infoq/b0/b0ca94529a95b3df8326934b99253d04.png)
服务端的代码和客户端非常相似,其实现流程图如下:
![](https://static001.geekbang.org/infoq/75/75b613d4c8ae53adbb443059c1d48606.png)
6、SSE
6.1 本节引言
![](https://static001.geekbang.org/infoq/a6/a695fc175d52943de23f3cfd2e0075e6.png)
本文前两节分析了 WebSocket 和 socket.io,现在我们来看看 SSE。
很多人也许好奇,有了 WebSocket 这种实时通信,为什么还需要 SSE 呢?
答案其实很简单:那就是 SSE 其实是单向通信,而 WebSocket 是双向通信。
比如:在股票行情、新闻推送的这种只需要服务器发送消息给客户端场景中,使用 SSE 可能更加合适。
另外:SSE 是使用 HTTP 传输的,这意味着我们不需要一个特殊的协议或者额外的实现就可以使用。而 WebSocket 要求全双工连接和一个新的 WebSocket 服务器去处理。加上 SSE 在设计的时候就有一些 WebSocket 没有的特性,比如自动重连接、event IDs、以及发送随机事件的能力,所以各有各的特长,我们需要根据实际应用场景,去选择不同的应用方案。
6.2 SSE 介绍
SSE 的简单模型是:一个客户端去从服务器端订阅一条“流”,之后服务端可以发送消息给客户端直到服务端或者客户端关闭该“流”,所以 SSE 全称叫“server-sent-event”。
相比以前的轮询,SSE 可以为 B2C 带来更高的效率。
有一张图片画出了二者的区别:
![](https://static001.geekbang.org/infoq/7e/7e03b3e669920c1df36c5a0e1f3306a0.png)
6.3 SSE 数据帧的格式
SSE 必须编码成 utf-8 的格式,消息的每个字段使用"\n"来做分割,并且需要下面 4 个规范定义好的字段。
这 4 个字段是:
1)Event: 事件类型;
2)Data: 发送的数据;
3)ID: 每一条事件流的 ID;
4)Retry: 告知浏览器在所有的连接丢失之后重新开启新的连接等待的时间,在自动重新连接的过程中,之前收到的最后一个事件流 ID 会被发送到服务端。
下图是通过 wireshark 抓包得到的数据包的原始格式:
![](https://static001.geekbang.org/infoq/ad/ad45d7277a801b1aff30af56dae9fdfc.png)
6.4 SSE 通信过程
SSE 的通信过程比较简单,底层的一些实现都被浏览器给封装好了,包括数据的处理。
大致流程如下:
![](https://static001.geekbang.org/infoq/e3/e34261b0a4754732bb7b1b3657a0beb0.png)
在浏览器中截图如下:
![](https://static001.geekbang.org/infoq/3f/3fbf8b5e3367ad624029842a7dfa9c22.png)
携带的数据是 JSON 格式的,浏览器都帮你整合成为一个 Object:
![](https://static001.geekbang.org/infoq/2d/2dccaf814130189bc74b10dc9dd63743.png)
在 wireshark 中,其通信流程如下。
发送请求:
![](https://static001.geekbang.org/infoq/a7/a703e6e683043cb9aa7578c3c9b1e365.png)
得到响应:
![](https://static001.geekbang.org/infoq/f8/f836ddc68d62746905f31c3c1dadd63f.png)
在开始推送信息流之前,服务器还会发送一个客户端会忽略掉的包,这个具体原因不清楚:
![](https://static001.geekbang.org/infoq/d9/d96651d8c9cf4a675d66868b08ebac71.png)
断开连接后的重传:
![](https://static001.geekbang.org/infoq/1b/1b4e0171bddb3d06f8ccff4cd8d2efcc.png)
6.5 SSE 的简单使用示例
浏览器端的使用:
const es = new EventSource('/sse')
服务端的使用:
const sseStream = new SseStream(req)
sseStream.pipe(res)
sseStream.write({
id: sendCount,
event: 'server-time',
retry: 20000, // 告诉客户端,如果断开连接后,20 秒后再重试连接
data: {ts: newDate().toTimeString(), count: sendCount++}
})
更多 API 使用和 demo 介绍分别参考:SSE API、demo代码。
6.6 兼容性及缺点
兼容性:
![](https://static001.geekbang.org/infoq/ee/ee397f78acb8da7b781c3a50f6da8269.png)
▲ 上图来自 https://caniuse.com/?search=Server-Sent-Events
缺点:
1)因为是服务器 -> 客户端的,所以它不能处理客户端请求流;
2)因为是明确指定用于传输 UTF-8 数据的,所以对于传输二进制流是低效率的,即使你转为 base64 的话,反而增加带宽的负载,得不偿失。
7、参考资料
[1] WebSocket API文档
[2] SSE API文档
[4] Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE
[5] SSE技术详解:一种全新的HTML5服务器推送事件技术
[6] Comet技术详解:基于HTTP长连接的Web端实时通信技术
[8] WebSocket详解(三):深入WebSocket通信协议细节
[9] WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)
[10] WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)
[11] 使用WebSocket和SSE技术实现Web端消息推送
[12] 详解Web端通信方式的演进:从Ajax、JSONP 到 SSE、Websocket
[13] MobileIMSDK-Web的网络层框架为何使用的是Socket.io而不是Netty?
[14] 理论联系实际:从零理解WebSocket的通信原理、协议格式、安全性
[16] WebSocket硬核入门:200行代码,教你徒手撸一个WebSocket服务器
[17] 网页端IM通信技术快速入门:短轮询、长轮询、SSE、WebSocket
本文已同步发布于“即时通讯技术圈”公众号。
评论