之前总结了关于Websocket协议的握手连接方式等其他细节,现在对socket连接建立后的数据帧传输和关闭细节总结。
一、数据帧格式
数据传输使用的是一系列数据帧,出于安全考虑和避免网络截获,客户端发送的数据帧必须进行掩码处理后才能发送到服务器,不论是否是在TLS安全协议上都要进行掩码处理。服务器如果没有收到掩码处理的数据帧时应该关闭连接,发送一个1002的状态码。服务器不能将发送到客户端的数据进行掩码处理,如果客户端收到掩码处理的数据帧必须关闭连接。
基本的数据帧为一个opcode、一个payload长度和发送的应用数据,根据ABNF的定义,详细信息如下图
这里使用的是数据存储的位(bit),当进行加密的时候,最终要的一位就是最左边的第一个。
- FIN :1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧。
- RSV1,RSV2,RSV3:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。
- Opcode:4bit,解释Payload数据,规定有以下不同的状态,如果是未知的,接收方必须马上关闭连接。状态如下:0x0(附加数据帧) 0x1(文本数据帧) 0x2(二进制数据帧) 0x3-7(保留为之后非控制帧使用) 0xB-F(保留为后面的控制帧使用) 0x8(关闭连接帧) 0x9(ping) 0xA(pong)
- Mask:1bit,掩码,定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理。
Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
- Payload length:7位,7 + 16位,7+64位,payload数据的长度,如果是0-125,就是真实的payload长度,如果是126,那么接着后面的2个字节对应的16位无符号整数就是payload数据长度;如果是127,那么接着后面的8个字节对应的64位无符号整数就是payload数据的长度。
- Masking-key:0到4字节,如果MASK位设为1则有4个字节的掩码解密密钥,否则就没有。
- Payload data:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。
二、客户端到服务器端掩码处理
三、消息分片
分片目的是发送长度未知的消息。如果不分片发送,即一帧,就需要缓存整个消息,计算其长度,构建frame并发送;使用分片的话,可使用一个大小合适的buffer,用消息内容填充buffer,填满即发送出去。
分片规则:
1.一个未分片的消息只有一帧(FIN为1,opcode非0)
2.一个分片的消息由起始帧(FIN为0,opcode非0),若干(0个或多个)帧(FIN为0,opcode为0),结束帧(FIN为1,opcode为0)。
3.控制帧可以出现在分片消息中间,但控制帧本身不允许分片。
4.分片消息必须按次序逐帧发送。
5.如果未协商扩展的情况下,两个分片消息的帧之间不允许交错。
6.能够处理存在于分片消息帧之间的控制帧
7.发送端为非控制消息构建长度任意的分片
8.client和server兼容接收分片消息与非分片消息
9.控制帧不允许分片,中间媒介不允许改变分片结构(即为控制帧分片)
10.如果使用保留位,中间媒介不知道其值表示的含义,那么中间媒介不允许改变消息的分片结构
11.如果协商扩展,中间媒介不知道,那么中间媒介不允许改变消息的分片结构,同样地,如果中间媒介不了解一个连接的握手信息,也不允许改变该连接的消息的分片结构
12.由于上述规则,一个消息的所有分片是同一数据类型(由第一个分片的opcode定义)的数据。因为控制帧不允许分片,所以一个消息的所有分片的数据类型是文本、二进制、opcode保留类型中的一种。
需要注意的是,如果控制帧不允许夹杂在一个消息的分片之间,延迟会较大,比如说当前正在传输一个较大的消息,此时的ping必须等待消息传输完成,才能发送出去,会导致较大的延迟。为了避免类似问题,需要允许控制帧夹杂在消息分片之间。
数据帧示例:
未掩码处理的文本单数据帧: 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")
掩码处理的文本单数据帧: 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58
分片未掩码处理的文本消息: 0x01 0x03 0x48 0x65 0x6c (contains "Hel")
0x80 0x02 0x6c 0x6f (contains "lo")
未掩码处理的Ping请求和掩码处理的响应:
0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains a body of "Hello", but the contents of the body are arbitrary)
0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains a body of "Hello", matching the body of the ping)
64K的二进制数据:0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]
四、发送和接收数据
1、发送
- 端点必须确保WebSocket连接处于OPEN状态。如果在任何时刻WebSocket连接的状态改变了,端点必须终止以下步骤。
- 端点必须封装/data/到一个WebSocket帧。如果要发送的数据太大或如果在端点想要开始发生数据时数据作为一个整体不可用,端点可以交替地封装数据到一系列的帧中。
- 第一个包含数据的帧的操作码(帧-opcode)必须设置为适当的值用于接收者解释数据是文本还是二进制数据。
- 包含数据的最后帧的FIN位(帧-fin)必须设置位1。
- 如果数据正由客户端发送,帧被掩码。
- 如果任何扩展已经协商用于WebSocket连接,额外的考虑可以按照这些扩展定义来应用。
- 已成形的帧必须在底层网络连接之上传输。
2、接收
为了接收WebSocket数据,端点监听底层网络连接。传入数据必须解析为WebSocket帧。当接收到一个数据帧时,端点必须注意由操作码(帧-opcode)定义的数据的/type/。这个帧的“应用数据”被定义为消息的/data/。如果帧由一个未分片的消息组成,这是说已经接收到一个WebSocket消息,其类型为/type/且数据为/data/。如果帧是一个分片消息的一部分,随后数据帧的“应用数据”连接在一起形成/data/。当接收到由FIN位(帧-fin)指示的最后的片段时,这是说已经接收到一个WebSocket消息,其数据为/data/(由连续片段的“应用数据”组成)且类型为/type/(分配消息的第一个帧指出)。随后的数据帧必须被解释为属于一个新的WebSocket消息。
扩展可以改变数据如何读的语义,尤其包括什么组成一个消息的边界。扩展,除了在负载中的“应用数据”之前添加“扩展数据”外,也可以修改“应用数据”(例如压缩它)。服务器必须为从客户端接收到的数据帧移除掩码。
五、Websocket关闭
通信的两端中任意一端关闭都可以关闭socket连接,关闭时应该清楚所有的TCP连接资源和TLS回话的资源,同时要丢弃所有的可能接收的字节数据。首先关闭的一方一般都应该是服务器端,然后处于TIME_WAIT状态。
为了使用一个状态码关闭websocket,一端必须发送一个关闭的控制帧,当两端都发送了关闭数据帧时,双方都要关闭所有的连接资源。控制帧为一个“状态码”和一个“原因说明”,当关闭之后,双方处于CLOSED状态。