3、WebSocket协议详解:WebSocket握手过程、数据帧格式、掩码机制、心跳维持

聊到交易所的实时行情推送,WebSocket 是绕不开的核心协议。说实话,我最早接触它是在做币安行情接入的时候——那时候直接用轮询,结果被限流封了IP,才老老实实研究WebSocket。今天咱们就把这个协议掰开揉碎,从握手到数据帧,再到心跳,一条龙讲清楚。

3.1 WebSocket 握手过程

WebSocket 的握手,说白了就是一次「升级版」的 HTTP 请求。客户端发一个 GET 请求,服务端回一个 101 状态码,连接就建立起来了。嗯,这里要注意:握手过程本身不传输业务数据,它只是确认双方都支持 WebSocket。

我习惯把握手拆成三步:

  1. 客户端发起握手:发送一个 HTTP Upgrade 请求,带上 Sec-WebSocket-Key(一个随机 base64 字符串)。
  2. 服务端计算响应:把 Sec-WebSocket-Key 加上固定 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11),做 SHA-1 哈希,再 base64 编码,得到 Sec-WebSocket-Accept。
  3. 服务端返回 101:客户端验证 Sec-WebSocket-Accept 正确,握手完成。

你想想看,为什么要有这个 Key 和 Accept 的校验?其实是为了防止缓存代理把旧的 HTTP 响应误发给 WebSocket 客户端。我在对接某所时,就遇到过代理服务器没正确处理 Upgrade 头,导致握手一直失败——后来加了 Connection: Upgrade 才解决。

核心要点:握手必须走 HTTP 80/443 端口,但握手成功后协议就从 HTTP 切换为 WebSocket。这个切换是单向的,服务端不能主动发起握手。

3.2 数据帧格式

握手完成后,数据就开始以「帧」为单位传输。WebSocket 的帧格式设计得很精巧,我当年第一次看 RFC 6455 时,觉得这结构简直是为低延迟量身定做的。

一个数据帧长这样:

 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)                  |
+---------------------------------------------------------------+

我来拆解几个关键字段:

  • FIN(1 bit):标记这是否是消息的最后一帧。为 1 表示结束,为 0 表示还有后续帧。
  • opcode(4 bits):帧类型。0x1 是文本帧,0x2 是二进制帧,0x8 是关闭帧,0x9 是 ping,0xA 是 pong。
  • MASK(1 bit):客户端发往服务端的数据必须为 1,服务端发往客户端必须为 0。这就是掩码机制。
  • Payload length(7/16/64 bits):数据长度。7 位能表示 0-125 字节;如果值是 126,则后面跟 16 位长度;如果是 127,则跟 64 位长度。

我的经验:解析帧时,Payload length 的变长设计是个坑。我曾经写解析器时忘了处理 126 和 127 的情况,结果遇到大消息直接崩了。建议写个状态机来逐字节解析,别偷懒用固定长度。

3.3 掩码机制

掩码机制是 WebSocket 里一个容易被忽略但很重要的设计。为什么客户端发往服务端的数据必须加掩码?说白了,是为了防止缓存污染攻击。

你想想看:如果攻击者能控制 WebSocket 客户端发送的数据,而中间有 HTTP 缓存代理,它可能把 WebSocket 数据误认为是 HTTP 响应,从而污染缓存。加上掩码后,数据在传输过程中是「混淆」的,缓存代理无法直接利用。

掩码的计算方式很简单:

// 客户端生成 4 字节随机掩码 key
// 每个字节与 payload 对应位置做 XOR
for i in range(len(payload)):
    payload[i] ^= masking_key[i % 4]

我在做量化交易系统时,曾经自己实现过 WebSocket 客户端。当时为了性能,直接用 C 扩展做掩码 XOR,比 Python 原生循环快了 10 倍。嗯,如果你用 Python 做高频行情解析,建议用 struct 模块或者 numpy 来批量处理掩码。

注意:服务端发往客户端的数据不能加掩码。如果服务端加了掩码,客户端收到后必须解掩码,但 RFC 规定服务端 MASK 必须为 0。我曾经见过某交易所的测试环境服务端误加了掩码,导致客户端解析乱码——排查了整整一天。

3.4 心跳维持

WebSocket 连接建立后,如果长时间没有数据交互,中间的网络设备(比如 NAT 网关、负载均衡器)可能会把连接断开。这就是为什么需要心跳。

WebSocket 原生支持两种心跳帧:

  • Ping 帧(opcode 0x9):由任一方主动发起,可以携带应用数据。
  • Pong 帧(opcode 0xA):收到 Ping 后必须回复 Pong,Pong 的数据必须与 Ping 相同。

我个人习惯的做法是:客户端每隔 30 秒发一个 Ping,服务端如果在 60 秒内没收到任何数据(包括 Ping),就主动断开连接。这个时间窗口可以根据网络环境调整——比如在海外节点,我会把间隔拉大到 45 秒,避免网络抖动导致误断。

避坑指南:我曾经在对接某所时,发现它的 WebSocket 服务端根本不响应 Ping 帧。后来查文档才知道,它要求客户端用「应用层心跳」——也就是定期发一个空的消息帧。所以,别盲目依赖协议层的心跳,先看交易所的 API 文档怎么说。

另外,心跳的定时器实现也有讲究。用 time.sleep 做定时任务?那在高并发场景下会阻塞事件循环。我建议用 asyncioloop.call_later 或者 asyncio.create_task 配合 asyncio.sleep,这样不会阻塞其他协程。

3.5 知识体系总览

下面这张图把 WebSocket 协议的核心逻辑串起来了。从握手开始,到数据帧解析,再到心跳维持,每一步都有对应的处理策略。

WebSocket 协议核心逻辑 1. HTTP 握手 Upgrade: websocket 2. 数据帧传输 FIN + opcode + payload 3. 心跳维持 Ping / Pong 握手细节 • 客户端发送 Sec-WebSocket-Key • 服务端计算 Sec-WebSocket-Accept • 返回 101 Switching Protocols • 连接建立成功 数据帧细节 • FIN: 消息结束标志 • opcode: 文本/二进制/Ping • MASK: 客户端必须掩码 • Payload length: 变长编码 心跳细节 • Ping 帧 (opcode 0x9) • Pong 帧 (opcode 0xA) • 间隔 30-60 秒 • 超时自动断开 协议解析引擎 = 握手校验 + 帧解析 + 掩码处理 + 心跳管理

这张图把 WebSocket 协议拆成了三个核心阶段。你从左边看到右边,就是一次完整的数据交互流程。我个人建议,在实现解析引擎时,把这三个阶段拆成独立的模块——握手模块、帧解析模块、心跳模块。这样后期维护和调试都方便很多。

好了,WebSocket 协议的核心内容就这些。下一节咱们会深入实战,用 Python 手写一个完整的 WebSocket 客户端,把今天讲的知识点全部落地。


专注资料整理