正常我们在日常开发中, 使用 nodejs 的 ws 包也好, golang 的 websocket 包也好, python 的 websockets 包也好, 在底层都已经对保持连接(ping-pong)进行了封装处理, 在业务层无需太过关心这些问题.
但是如果你使用的是 PHP(swoole), C/C++ 做长连接, 那么就会遇到需要自己维护连接保持(ping-pong)数据包.
在没有深入了解 websocket 协议之前, 你可能以为接收 ping 包就是简单的接收内容为 ping
的字符串数据. 但是你错了..
Websocket 数据包构成
websocket 协议中数据大致由以下几部分组成
- FIN 数据终止标记
- opcode 数据包类型
- MASK 数据是否编码
- Payload Length 表示实际数据的长度
- Masking-Key 编码用的 key 值
- Payload Data 真实数据
FIN 传输终止标记
表示该数据是否为最后一条数据.
1: 表示为最后的数据, 可以进行数据的进一步操作.
0: 表示应该继续监听后续数据, 拿到后续数据后进行拼接, 直到 FIN 为 1 为止.
opcode 数据包类型
数据包类型分为以下几种(以 opcode 数据头进行区分):
- 0x0 表示消息需要拼接
- 0x1 表示数据为 UTF8 文本
- 0x2 表示数据为 二进制
0x3
-0x7
和0xB
-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 | let DECODED = ''; |
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 标记.
参考文档: