目录
1.WebSocket协议的帧数据详解
1.1.帧结构
WebSocket客户端与服务器通信的最小单位是帧(frame),一条完整消息是由一个或多个帧组成的。数据交互时,发送方会将消息切割为多个帧发送给接收方,接收方接收到消息帧后会重新组装为完整的消息。
当WebSocket接收方接收到一个数据帧时会根据FIN(数据帧中的一个标识,用来判断当前帧是否当前消息的最后一帧)的值来判断是否已经接收到消息的最后一个数据帧。当接收到消息的最后一帧时,即可对消息进行处理。
WebSocket 帧由多个字段组成,各字段按顺序排列,这些字段共同构成了帧的头部和可选的负载数据。帧的基本结构如下:
字段 | 长度(字节) | 描述 |
---|---|---|
FIN | 1 位 | 表示该帧是否为消息的最后一帧。1 表示是最后一帧,0 表示还有后续帧。 |
RSV1 - RSV3 | 各 1 位 | 保留位,默认值为 0。如果要使用,需要在握手阶段进行协商。 |
Opcode | 4 位 | 定义帧的操作类型,如文本帧、二进制帧、关闭帧等。 |
Mask | 1 位 | 表示负载数据是否经过掩码处理。客户端发送的帧必须进行掩码处理。 |
Payload length | 7 位、7 + 16 位或 7 + 64 位 | 表示负载数据的长度。如果值在 0 - 125 之间,直接使用 7 位表示;如果是 126,则后续 2 个字节表示长度;如果是 127,则后续 8 个字节表示长度。 |
Masking - key | 0 或 4 字节 | 如果 Mask 位为 1,则存在 4 字节的掩码密钥,用于对负载数据进行掩码处理。 |
各字段详细解释
1. FIN 字段
- 作用:用来指示当前帧是否为一个消息的最后一帧。在发送大消息时,消息可能会被拆分成多个帧,
FIN
位可以帮助接收方判断消息是否完整。 - 取值:
1
:表示这是消息的最后一帧。0
:表示还有后续帧。
2. RSV1 - RSV3 字段
- 作用:这三个保留位主要是为未来扩展 WebSocket 协议预留的。目前默认值都为 0,如果要使用这些位,需要在握手阶段进行协商。
- 取值:通常为 0。
3. Opcode 字段
- 作用:定义了帧的操作类型,接收方根据
Opcode
的值来决定如何处理接收到的帧。 - 常见取值及含义:
0x0
:表示延续帧,用于继续之前未完成的消息。0x1
:表示文本帧,负载数据为 UTF - 8 编码的文本。0x2
:表示二进制帧,负载数据为二进制数据。0x3-0x7:保留
帧,留作未来非控制帧扩展使用0x8
:表示关闭帧,用于关闭 WebSocket 连接。0x9
:表示心跳检测的 ping 帧。0xA
:表示心跳检测的 pong 帧,是对 ping 帧的响应。0xB-0xF:保留
帧, 留作未来控制帧扩展使用
4. Mask 字段
- 作用:指示负载数据是否经过掩码处理。为了提高安全性,客户端发送的帧必须将负载数据进行掩码处理,服务器发送的帧不需要进行掩码处理。
- 取值:
1
:表示负载数据经过掩码处理。0
:表示负载数据未经过掩码处理。
5. Payload length 字段
- 作用:表示负载数据的长度。由于负载数据长度可能不同,该字段采用了可变长度的编码方式。
- 取值及编码方式:
- 如果值在 0 - 125 之间,直接使用 7 位表示负载数据的长度。
- 如果值为 126,则后续 2 个字节(16 位)表示负载数据的长度。
- 如果值为 127,则后续 8 个字节(64 位)表示负载数据的长度。
6. Masking - key 字段
- 作用:如果
Mask
位为 1,该字段存在,且长度为 4 字节。它是一个随机生成的掩码密钥,用于对负载数据进行掩码处理和解掩码处理。 - 掩码处理和解掩码处理:假设
P
是原始的负载数据,M
是掩码密钥,C
是经过掩码处理后的数据,则C[i] = P[i] ^ M[i % 4]
,接收方可以使用相同的掩码密钥和异或运算对C
进行解掩码得到P
。
7. Payload data 字段
- 作用:实际传输的数据,可以是文本、二进制数据等,具体取决于
Opcode
的值。
示例
假设客户端要发送一个简单的文本消息 "Hello",以下是一个简化的帧结构分析:
- FIN:由于这是消息的唯一一帧,
FIN
为 1。 - RSV1 - RSV3:默认值为 0。
- Opcode:消息是文本,
Opcode
为 0x1。 - Mask:客户端发送的帧需要掩码处理,
Mask
为 1。 - Payload length:消息长度为 5 字节,小于 126,直接用 7 位表示,值为 5。
- Masking - key:随机生成 4 字节的掩码密钥,例如
0x12345678
。 - Payload data:将 "Hello" 进行掩码处理后的数据。
1.2.生成数据帧
从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的。我们自己需要去生成数据帧,解析数据帧的时候我们需要分片。
消息分片:
有时候数据需要分成多个数据包发送,需要使用到分片,也就是说多个数据帧来传输一个数据。比如将大数据分成多个数据包传输,分片的目的是允许发送未知长度的消息。这样做的好处是:
- 大数据的传输可以分片传输,不用考虑到数据大小导致的长度标志位不够的情况
- 和http的chunk一样,可以边生成数据边传递消息,可以提高传输效率。
如果大数据不能被碎片化,那么发送端就必须将数据整个载入内存缓冲之中,然后进行计算长度等操作并发送。但是有了碎片化机制,服务器就可以选取适用的内存缓冲长度,然后当缓冲满了之后就发送一个消息碎片。
分片规则:
- 如果一个消息不分片的话,那么该消息只有一帧(FIN为1,opcode非0);
- 如果一个消息分片的话,它的构成是由
- 1个起始帧:FIN为0,opcode非0
- 然后若干帧:FIN为0,opcode为0
- 1个结束帧:FIN为1,opcode为0
注意:
- 当前已经定义的控制帧包括 0x8(close)、0x9(Ping)、0xA(Pong)。控制帧可以出现在分片消息中间,但是控制帧不允许分片,控制帧是通过它的opcode的最高有效位是1去确定的。
- 组成消息的所有帧都是相同的数据类型,在第一帧中的opcode中指明。组成消息的碎片类型必须是文本,二进制,或者其他的保留类型。
2.WebSocket协议控制帧结构详解
目前,控制帧的操作码定义了0x08(关闭帧)、0x09(Ping帧)、0x0A(Pong帧)。0x0B-0x0F是为那些将来可能定义而目前尚未定义的控制帧预留的。
控制帧用于WebSocket协议交换状态信息,控制帧可以插在消息片段之间。
注意:所有的控制帧的负载长度务必不大于125字节,并且禁止对控制帧进行分片处理。
2.1.关闭帧
关闭帧的操作码Opcode是0x08。
关闭帧可能包含数据部分(应用数据帧),该部分表明了关闭的原因,例如:端点关闭、端点接收帧过大或端点收到的帧不符合预期。
- 如果有数据部分,则数据的前两个字节必须是一个无符号整数(网络字节序),该无符号整数表示了一个状态码,具体定义哪些关闭码将在后面的文章中介绍。
- 在无符号整数后面,可能还有一个UTF-8编码的数据,表示关闭原因,关闭原因由开发者自行定义(可选),并无规范。关闭原因并不一定是对人可读的,但会对调试或传递相关信息起到一定的作用。由于数据不能保证人类可读,所以客户端一定不能将其显示给用户(会在关闭事件onclose中)
客户端发送给服务器的关闭帧必须掩码处理。应用程序在发送了一个关闭帧后,禁止再发送任何数据(此时处于CLOSING状态)。
如果端点(客户端或服务器)收到了一个关闭帧,并且之前没有发送过关闭帧,则端点必须发送一个关闭帧作为响应。(当端点发送一个关闭帧回应时,通常会显示它收到的状态码)。当端点可以发送关闭响应时应尽快发送关闭响应。一个端点可以延迟发送响应直到它的当前消息发送完毕(例如,已经发送了大多数的消息片段,则端点可能会在发送关闭响应帧前先将剩下的消息帧发送出去)。但不能保证对方在已经发送了关闭帧后还能够继续处理这些数据。
在双方都已发送并接收了关闭帧后,端点需要断掉WebSocket连接并且必须关闭底层的TCP连接。服务器必须立即切断底层TCP连接,客户端最好等待服务器断开连接,但也可以在发送并接收了关闭帧后任何时候断开连接,例如在一段时间内服务器仍没有断开TCP连接。
如果服务器和客户端同时发送了关闭帧,两端都会接收关闭帧,并且都需要断开TCP连接。
关闭帧示例
假设客户端要发送一个关闭帧,状态码为 1000(正常关闭),没有额外的关闭原因。其帧结构如下:
- FIN:1
- RSV1 - RSV3:0 0 0
- Opcode:0x8
- Mask:1(客户端发送)
- Payload length:2(状态码占 2 字节)
- Masking - key:随机生成的 4 字节密钥
- Payload data:状态码 1000(2 字节)
2.2.ping帧
Ping帧的操作码为0x09。
Ping帧可以包含应用数据。
一旦接到了一个Ping帧,端点必须返回一个Pong帧作为响应,除非它收到了一个关闭帧。它应在可以发送时尽快发送Pong帧响应。
端点可以在连接建立后一直到连接关闭前任何时候发送Ping帧。
ping 帧示例
客户端发送一个 ping 帧,包含自定义的心跳信息 "HEARTBEAT"。其帧结构如下:
- FIN:1
- RSV1 - RSV3:0 0 0
- Opcode:0x9
- Mask:1(客户端发送)
- Payload length:9("HEARTBEAT" 长度为 9)
- Masking - key:随机生成的 4 字节密钥
- Payload data:经过掩码处理的 "HEARTBEAT"
2.3.pong帧
Pong帧的操作码为0x0A。
Pong帧必须与Ping帧拥有相同的应用数据部分。
如果端点收到了多个Ping帧,但还没来的及全部回应,可以只回应最后一个Ping帧。
Pong帧可以在未收到Ping帧时就被发送,用作单向心跳包。
对未被请求的Pong帧(对方主动发送的Pong帧)进行回应是不需要的。
3.WebSocket心跳机制
心跳机制的核心是客户端和服务器之间定期发送和接收特定的心跳消息。一般而言,客户端和服务器中的一方会定期发送心跳消息(如 ping 帧),另一方收到后则立即回复相应的响应消息(如 pong 帧)。若在规定时间内未收到响应,就判定连接出现问题,进而进行重连或其他处理。
WebSocket 协议本身定义了 ping 帧和 pong 帧用于心跳检测。
- ping 帧:由客户端或服务器发送,用于主动检测对方的连接状态。
- pong 帧:是对 ping 帧的响应,当一方收到 ping 帧后,必须尽快发送一个 pong 帧,且 pong 帧的负载数据应与 ping 帧的负载数据相同。