TT Lab
开始
学习 学习路径 课程

实时通信 — WebSocket、gRPC 流式调用与 WebRTC

逐字节读懂 WebSocket

在 TT Lab 中继续学习

一句话总结

WebSocket 以 HTTP 开始,用 101 切换之后,通过只有客户端才做掩码的小帧来传送双向消息,并用关闭码留下断开的原因。

为什么需要它

HTTP 的形态是客户端提问、服务器回答,所以由服务器主动发起对话的功能,长期以来都靠权宜之计来填补。长轮询是扣住响应的方式,每条消息都要重新发送一个请求,头部比消息本身还大。RFC 6455 的 WebSocket 打开一个连接,让双方可以随时发送消息。并且它以 HTTP 请求开始,以便原样穿过现有的 HTTP 基础设施——80、443 端口、代理、Cookie。

虽然库会代劳这一切,但故障记录中往往只留下“因 1006 而断开”这样的一行。要读懂这一行,就得知道字节长什么样。

工作原理

打开握手(第 4 节)是一个普通的 HTTP/1.1 GET。客户端发送 Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Version: 13,以及把随机 16 字节用 base64 写成的 Sec-WebSocket-Key。服务器在该密钥字符串后面附上固定的 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,用 SHA-1 求哈希,把这 20 字节再做成 base64,放进 Sec-WebSocket-Accept,并以 101 Switching Protocols 作答。第 1.3 节的示例密钥 dGhlIHNhbXBsZSBub25jZQ== 会变成 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=。这个计算不是为了保守机密,而是为了确认对方确实是一个理解 WebSocket 的服务器。如果版本不匹配,服务器会连同 426 一起告知所支持的版本。

此后就是帧了(第 5.2 节)。

 0               1               2               3
|F|R|R|R| opcode|M| 길이(7)     | 확장 길이(0·2·8바이트) ...
|I|S|S|S|  (4)  |A|             |
|N|V|V|V|       |S|             | 마스크 열쇠(마스킹할 때 4바이트) | 본문 ...

opcode 为 0(延续)、1(文本)、2(二进制)、8(关闭)、9(ping)、10(pong)。长度不超过 125 时直接写在 7 位中,是 126 时写在后面 2 字节,是 127 时写在后面 8 字节,8 字节长度的最高位必须为 0。RSV 位只有在就扩展(例如压缩的 permessage-deflate)达成一致时才能置位,如果收到未经协商就置位的帧,必须让连接失败。

客户端发送的帧必须做掩码,服务器发送的帧则不做掩码(第 5.1 节)。掩码只是把正文的第 i 个字节与掩码密钥的第 i mod 4 个字节做 XOR,而且掩码密钥原样放在帧里一起发送。目的不是保密,而是防止第 10.3 节所说明的中间缓存投毒攻击。因为如果攻击者挑选的字节原样发到线路上,不认识 WebSocket 的透明代理就可能把它误认为 HTTP 请求和响应而放进缓存。服务器收到未做掩码的客户端帧时,必须关闭连接。

控制帧(关闭、ping、pong)的正文必须不超过 125 字节且不能分片,并且可以插入到被分片的数据消息的各个片段之间。pong 会原样返回所收到 ping 的正文。文本消息必须是 UTF-8。

关闭(第 7 节)由 2 字节的状态码和可选的 UTF-8 原因构成。一方发送 close 之后,另一方也要以 close 作答,随后由服务器先关闭 TCP。

码 含义
1000 正常终止
1001 离开(服务器关闭、页面跳转)
1002 协议错误
1006 没有 close 就断开——这是接收方起的名字,不能发送
1007 消息内容与格式不符(不是 UTF-8 的文本)
1008 违反策略
1009 消息过大
1011 服务器内部错误
4000–4999 留给应用程序使用的区间

1012(服务重启)和 1013(稍后重试)并不在 RFC 正文中,而是登记在 IANA 的 WebSocket 关闭码注册表中的值。

在现场相遇的样子

“只有在代理后面才连不上”,通常是因为代理没有转发 Upgrade 和 Connection 头,导致握手以 400 或 200 结束。在 nginx 中,必须通过配置显式地转发这两个头。浏览器的 WebSocket API 无法附加自定义请求头,所以认证要通过 Cookie、第一条消息,或 URL 中的一次性令牌来完成。本实验平台的 Web 终端通过 Cookie 确认身份,也是出于同样的原因。

下一项实验要做什么

用标准库依次构建:accept 密钥的计算、帧的编码与解析、握手响应、回显服务器,直到关闭码。评分器会通过原始套接字连接,以字节检查服务器帧的 MASK 位和关闭码,最后查看 websockets 库的客户端是否接受你的服务器。