Websocket 保持长连接的 PingPong 包

正常我们在日常开发中, 使用 nodejs 的 ws 包也好, golang 的 websocket 包也好, python 的 websockets 包也好, 在底层都已经对保持连接(ping-pong)进行了封装处理, 在业务层无需太过关心这些问题.

但是如果你使用的是 PHP(swoole), C/C++ 做长连接, 那么就会遇到需要自己维护连接保持(ping-pong)数据包.

在没有深入了解 websocket 协议之前, 你可能以为接收 ping 包就是简单的接收内容为 ping 的字符串数据. 但是你错了..

Websocket 数据包构成

websocket 协议中数据大致由以下几部分组成

  1. FIN 数据终止标记
  2. opcode 数据包类型
  3. MASK 数据是否编码
  4. Payload Length 表示实际数据的长度
  5. Masking-Key 编码用的 key 值
  6. Payload Data 真实数据

FIN 传输终止标记

表示该数据是否为最后一条数据.

1: 表示为最后的数据, 可以进行数据的进一步操作.
0: 表示应该继续监听后续数据, 拿到后续数据后进行拼接, 直到 FIN 为 1 为止.

opcode 数据包类型

数据包类型分为以下几种(以 opcode 数据头进行区分):

  • 0x0 表示消息需要拼接
  • 0x1 表示数据为 UTF8 文本
  • 0x2 表示数据为 二进制
  • 0x3 - 0x70xB - 0xF 无意义
  • 0x9 表示 ping 包
  • 0xA 表示 pong 包

MASK 编码标记

MASK 位(MASK Bit)用于标记数据是否编码过(Encoded), 如果加密, 应该将 MASK 码(MASK Key)一同于数据发送.

从客户端到服务器的数据必须为编码过的, 即 MASK 位值为 1. 如果客户端发送给服务器的数据, 出现 MASK 位值为 0 的时候, 服务器应该主动与客户端断开连接.

从服务器到客户端的数据, 无需编码, 即 MASK 位值设置为 0, 同时不发送 MASK 码.

即使使用了 WSS 安全传输协议, 也应该对数据设置 MASK.

解析编码数据的过程

MASK 位设置为 1 的时候, Payload 数据后面的 4 个 8 进制数据(总共 32 位, 看成是长度为 4 的八进制数据数组), 即为 MASK 码. 在解析 Payload 和 Mask 码之后, 才是正式的数据. 需要使用 MASK 码对加密的数据进行解密, 首先将加密数据转换成八进制数据数组, 循环数组中的八进制数据, 每个数据对 MASK 码的第 i%4 进行异或(XOR)操作, 就得到了解密的数据.

1
2
3
4
let DECODED = '';
for (let i = 0; i < ENCODED.length; i++) {
DECODED += ENCODED[i] ^ MASK[i % 4];
}

FIN 与 opcode 联合使用

FIN = 0 且 opcode = 0x0 时, 进行数据拼接, 直到收到 FIN = 1 为止, 数据拼接完成.

Ping Pong 数据包

在握手之后客户端或服务端都可以选择向另一方发送 ping. 收到 ping 消息后,接受者必须尽快发回 pong 消息。您可以使用此方式来确保客户端仍处于连接状态.

ping 和 pong 只是一个常规的消息帧, 但它是一个 管理帧. ping 的 opcode 为 0x9, pong 的 opcode 为 0xA. 当你收到 ping 消息后, 需要回复一个 pong 消息, 带有完全相同的数据(发送 opcode 为 ping 和 pong 的同时, 允许同时发送一段 Payload 数据, 对于 ping 和 pong, 最大 Payload 数据长度为 125 字节). 可能在你没法 ping 的时候也能收到 pong 消息, 这种情况下直接忽略就可以了.

如果在你回 pong 消息之前你收到了不止一个 ping 消息, 那么你只用回一个 pong 消息即可.

现在你知道什么是 ping pong 数据包了吧, 不是简单的发送 ping pong 字符串而已, 最主要的是 opcode 标记.

参考文档:

Donate - Support to make this site better.
捐助 - 支持我让我做得更好.