
Websocket:实时双向通信的演变与实践
知其然:主动推送与实时双向通信
WebSocket协议
- RFC文档:RFC6455
- 工作层次:应用层
- 定位:为服务器提供主动推送数据的方式并在TCP连接的基础上实现全双工的实时通信
关键特征
-
全双工通信:允许服务器和客户端同时发送和接收数据
-
持久连接:Websocket连接会在长时间内保持开放状态
-
可靠连接:基于TCP协议
-
消息格式:Websocket消息允许为文本或二进制格式
-
跨平台兼容性强:Webscoket通过HTTP进行协议升级建立连接,通过80/443进行通信,提供了为HTTP协议场景提供服务的强大兼容性
-
高效节能:建立Websocket仅需要1次请求,避免了HTTP频繁请求、频繁连接带来的资源消耗
应用场景
以实时聊天(IM)为例,当用户打开聊天窗口,客户端与服务器之间通过 Websocket 建立起连接。用户发送的消息能够迅速地被服务器接收并转发给其他相关客户端,同时,用户也能实时收到其他参与者发送的新消息,无需像传统网页应用那样频繁刷新页面来获取最新信息。(我想吐槽的是,在的实习经历中得到的教训:写一个IM最好不要从WebSocket开始搓,我会考虑单写一篇开发应用讲述我的经历)
此外,在我实习的团队认为,在大部分情况下使用Websocket来进行“通知”而不是“传输”。经过调研后,我并不完全赞同这一观点,Websocket的消息传递能力足够高效,特别是对于绝大部分IM和数据更新场景,用Websocket直接传递信息是更优解,也不会造成网络负担或系统设计的复杂化。
相比之下,前端同学提出的方案是我深表赞同的的,即在首次加载页面以及更多“初始化”的设计中使用HTTP请求进行数据获取,而在后续的所有通信中采用Websocket由后端向前端主动推送。
知其所以然:WebSocket的工作流程
-
启动连接:客户端向服务端发送一个HTTP请求,该请求头包含以下字段,表明通信需要升级到Websocket协议,我们关注这些信息:
GET ws://example.com/api/ws/demo/chat-demo-id HTTP/1.1 Host: example.com Connection: Upgrade Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X [OS Version]) AppleWebKit/[WebKit Version] (KHTML, like Gecko) Chrome/[Chrome Version] Safari/[Safari Version] Upgrade: websocket Origin: http://example.com Sec-WebSocket-Version: 13 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: [Cookie-related values] Sec-WebSocket-Key: Dktqa9fL1I5V+h1AetA2+Q== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Connection: Upgrade
:当前HTTP连接需要升级Upgrade: websocket
:客户端希望升级的协议是WebsocketSec - WebSocket - Version: 13
:客户端支持的协议版本是13Sec - WebSocket - Key: Dktqa9fL1I5V+h1AetA2+Q==
:随机生成的Base64编码密钥,用于在Websocket握手过程中进行安全验证,本文不展开安全验证相关过程Sec - WebSocket - Extensions: permessage - deflate; client_max_window_bits
:客户端期望使用permessage - deflate
扩展对Websocket消息进行压缩,并指定了参数client_max_window_bits
-
握手协议:服务端收到升级请求后,会检查自己是否兼容Websocket协议,随后返回状态码为
101 - swithching protocals
的响应如下HTTP/1.1 101 Switching Protocols Server: nginx/1.18.0 (Ubuntu) Date: Mon, 30 Dec 2024 06:16:41 GMT Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: SYRKbDm8G658Rr+K2PStQPEu+oU= Sec-WebSocket-Extensions: permessage-deflate
HTTP/1.1 101 Switching Protocols
:服务器理解并同意客户端升级请求Sec-WebSocket-Accept: SYRKbDm8G658Rr+K2PStQPEu+oU=
:对Sec - WebSocket - Key
的回应,服务器会将 “Sec-WebSocket-Key” 与一个固定的字符串拼接,然后进行 SHA-1 哈希计算,再把结果进行 Base64 编码,生成这个 “Sec-WebSocket-Accept” 的值Sec-WebSocket-Extensions: permessage-deflate
:服务器同意使用该扩展
-
数据传输:Websocket通信以帧为单位组织数据,区分为普通文本和二进制两种。其中
opcode
占4 bits,其含义为负载数据,这一标志会区分负载类型,完整的定义可以参考RFC文档:%x1
:文本帧(text frame)%x2
:二进制帧(binary frame)
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
-
关闭连接:Websocket可能在这些情况下关闭连接:通信结束、网络异常、主动退出等等。连接关闭会释放端口、连接内存等资源
知其曾已然:实时通信不止于此
最早设计HTTP协议时,我们考虑“请求 - 响应”的工作模式,即服务端请求,服务器响应后整个连接过程结束。但后来发现,在许多场景下,我们需要将服务端的数据更新主动推送到客户端,而HTTP的工作模式显然无法满足我们的需求,于是开发人员想出了一个办法——短轮询。
1. 短轮询
为了实现向客户端实时呈现数据更新,我们让客户端每间隔一段时间就向服务器发送请求,更新数据,当这个间隔足够短时,我们便呈现出了显示实时数据的效果。这个方案简洁有效,且无需在现有环境下做出任何改变。例如这样一个js客户端:
function poll() {
fetch('/api/data')
.then(response => response.json())
.then(data => {
console.log(data);
// 处理接收到的数据
})
.catch(error => console.error(error))
.finally(() => setTimeout(poll, 5000)); // 5 秒后再次轮询
}
poll();
而这也带来了一些问题:
- 数据并非是定时更新的,定时轮询可能导致数据高频率数据更新延迟,或在低频率的更新中产生大量不必要的请求导致网络资源的浪费
- 在很多场景下,我们需要获取的数据资源往往很少,而HTTP请求的头部与之相比就显得繁重不堪了
2. Comet
随着对实时性的要求提高,短轮询的低效率问题凸显,Comet 应运而生,旨在解决短轮询实时性差的问题。Comet有两种常见的实现形式,一是长轮询,二是流模式。
a. 长轮询
长轮询的原理是在客户端向服务器发起请求后,服务器会保持连接直到有数据要返回时再响应,或者超时后返回。相对来说,更适合请求频率较低,对实时性要求一般的场景。
优点:简单易实现,适合小规模的实时通信
缺点:每次请求都会建立和关闭连接,存在一定的延迟和资源消耗,尤其是在高并发场景下
b. 流模式
流模式(通常指服务器推送数据到客户端)不同于长轮询,服务器会在连接上持续不断地发送数据,直到客户端断开连接为止。
-
优点:更高效,客户端和服务器之间保持一个持久连接,适合高频实时数据传输
-
缺点:实现起来较复杂,尤其是要处理连接保持、错误恢复和并发
3. SSE
作为另一种实现服务器到客户端实时推送的技术,SSE(Server-Sent Events) 专注于服务器向客户端的单向推送,在某些场景下可以作为 Websocket 的补充。
- 优点:
- 实现简单,专门用于服务器向客户端的单向数据推送,适合一些仅需服务器推送的场景
- 基于 HTTP 协议,兼容性好,无需额外的协议升级,适用于大多数浏览器
- 缺点:
- 仅支持服务器向客户端的单向通信,无法满足需要双向通信的场景