Websocket数据帧的封装和传输其实和处理握手请求的流程差不太多,都需要通过bytebuffer写入Socket的输出流或者从输入流读取。
数据帧格式
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 ... |
+---------------------------------------------------------------+
具体每一bit的意思
FIN 1bit 表示信息的最后一帧
RSV 1-3 1bit each 以后备用的 默认都为 0
Opcode 4bit 帧类型,稍后细说
Mask 1bit 掩码,是否加密数据,默认必须置为1
Payload len 7bit 数据的长度
Masking-key 1 or 4 bit 掩码
Payload data (x + y) bytes 数据
Extension data x bytes 扩展数据
Application data y bytes 程序数据
这个看着比较蛋疼的一点是顶部用的十进制而不是八进制,仔细看一下每行有4个字节
Fin
为1表示是最后一个一个数据帧,有时候数据需要分成多个数据包来发送,这就需要用到分片,也就是使用多个数据帧来传输一个数据。
Opcode
表示帧的类型,例如这个传输的帧是文本类型还是二进制类型,二进制类型传输的数据可以是图片或者语音之类的。
OPCODE:4位
解释PayloadData,如果接收到未知的opcode,接收端必须关闭连接。
0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留
Mask
表示是否经过掩码处理
Payload len
占据七位用来描述消息长度,由于7位最多只能描述127所以这个值会代表三种情况,一种是消息内容少于126存储消息长度,如果消息长度少于UINT16的情况此值为126,当消息长度大于UINT16的情况下此值为127;这两种情况的消息长度存储到紧随后面的byte[],分别是UINT16(2位byte)和UINT64(4位byte)。
客户端到服务器端掩码处理
客户端发送到服务器端的数据必须进行掩码处理,掩码的密钥是一个32位的随机值。所有数据都需要与掩码做一次异或运算。
j = i mod 4(i 是传输数据中的十进制的索引下标)
转换后的数据 d = original ^ mask[j]
消息分片
分片的目的是允许发送未知长度的消息。如果消息不能被碎片化,那么一端就必须将消息整个地载入内存缓冲,计算长度,构建frame并发送。有了碎片化的机制,服务端或者中间件就可以选取适用的内存缓冲长度,然后当缓冲满了之后就发送一个消息碎片。
分片规则:
- 一个未分片的消息只有一帧(FIN为1,opcode非0)
- 一个分片的消息由起始帧(FIN为0,opcode非0),若干(0个或多个)帧(FIN为0,opcode为0),结束帧(FIN为1,opcode为0)。
- 控制帧可以出现在分片消息中间,但控制帧本身不允许分片。控制帧是通过它的 opcode 的最高有效位是 1 去确定的。当前已经定义了的控制帧包括 0x8 (close),0x9 (Ping),0xA (Pong)。
- 消息帧必须以其被发送时的顺序传递到接收端。
- 组成消息的所有帧都是相同的数据类型,在第一个帧中的 opcode 中指明。因为控制帧不能被碎片化,组成消息的碎片类型必须是文本、二进制、或者其他的保留类型。
未分片的消息
FIN=0,Opcode>0
分片的消息分片可以分为三个类型:
开始帧:FIN=0,Opcode>0;一个
传输帧:FIN=0,Opcode=0;零个或多个
终止帧:FIN=1,Opcode=0;一个
例子解析
比如我们接受到了一段消息的数据部分是810548656c6c6f
81 05: 10000001 00000101
48 65: 01001000 01100101
6c 6c: 01001100 01001100
6f : 01001111
0:1,这是最后一帧
1~3:全为0
4~7:0001,文本数据帧
8:0,PlayloadData未经过掩码
9~15:0000101 = 5 < 125,因此数据长度位5
剩下的:48 65 6c 6c 6f即为Hello
比如我们发送的消息123456789数据部分是818911eb9db220d9ae8624ddaa8a28
81 89 : 10000001 10001001
11 eb : 00010001 11101011
9d b2 : 10011101 10110010
20 d9 : 00100000 11011001
ae 86 : 10101110 10000110
24 dd : 00100100 11011101
aa 8a : 10101010 10001010
28 : 00101000
0:1,这是最后一帧
1~3:全为0
4~7:0001,文本数据帧
8:1,PlayloadData经过掩码,(所有的由客户端发往服务端的帧此数位都被设置成 1。)
9~15:0001001 = 9 < 125,因此数据长度位9
11 eb 9d b2:掩码
20 d9 ae 86 24 dd aa 8a 28:数据
下面演示消息123456789怎么通过掩码加密成20 d9 ae 86 24 dd aa 8a 28
1 2 3 发送的消息
31 32 33 消息对应的ASCII
00110001 00110010 00110011
11 eb 9d 掩码
00010001 11101011 10011101
交配
00110001 00110010 00110011
00010001 11101011 10011101
00100000 11011001 10101110
20 d9 ae
关闭帧
前面提到了关闭帧的操作码opcode是0x8
关闭帧也可以包含消息体,通过数据帧的“应用数据部分表示关闭原因”,消息体的前两个字节必须是无符号的整型数(采用网络字节序),以此整型数去表示状态码。在两个字节的无符号整型数之后,可以跟上以 UTF-8 编码的数据表示 /reason/,/reason/ 数据的具体解释方式此文档并没有定义。并且 /reason/ 的内容不一定是人类可读的数据,只要是有利于发起连接的脚本进行调试就可以。因为 /reason/ 并不一定就是人类可读的,所以客户端必须不将此内容展示给最终用户。
应用程序在发送了关闭帧之后就不可以再发送其他数据帧了。
如果接收到关闭帧的一端之前没有发送过关闭帧的话,那么它必须发送一个关闭帧作为响应。(当发送一个关闭帧作为响应的时候,发送端通常在作为响应的关闭帧中采用和其接收到的关闭帧相同的状态码)。并且响应必须尽快的发送。一端可以延迟关闭帧的发送,比如一个重要的消息已经发送了一半,那么可以在消息的剩余部分发送完之后再发送关闭帧。但是作为首先发送了关闭帧,并在等待另一端进行关闭响应的那一端来说,并不一定保证其会继续处理数据内容。
在发送和接收到了关闭帧之后,一端就可以认为 WebSocket 连接已经关闭,并且必须关闭底层相关的 TCP 连接。如果是服务端首先发送了关闭帧,那么在接收到客户端返回的关闭帧之后,服务端必须立即关闭底层相关的 TCP 连接;但是如果是客户端首先发送了关闭帧,并接收到了服务端返回的关闭帧之后,可以选择其认为合适的时间关闭连接,比如,在一段时间内没有接收到服务端的 TCP 关闭握手。
如果客户端和服务端同时发送了关闭消息,那么它们两端都将会接收到来自对方的关闭消息,那么它们就可以认为 WebSocket 连接已经关闭,并且关闭底层相关的 TCP 连接。
已经定义的状态码
1000 表明这是一个正常的关闭
1001 表明一端是即将关闭的,比如服务端将关闭或者浏览器跳转到了其他页面
1002 表明一端正在因为协议错误而关闭连接
1003 表明一端因为接收到了无法受理的数据而关闭连接(比如只能处理文本的一端接收到了一个二进制的消息)
1004 保留的。
1005 是一个保留值,并且必须不可以作为关闭帧的状态码
1006 是一个保留值,并且必须不可以作为关闭帧的状态码
1007 表明一端接收到的消息内容与之标记的类型不符而需要关闭连接(比如文本消息中出现了非 UTF-8 的内容)
1008 表明了一端接收到的消息内容违反了其接收消息的策略而需要关闭连接
1009 表明一端接收了非常大的数据而其无法处理时需要关闭连接
1010 表明了客户端希望服务端协商一个或多个扩展,但是服务端在返回的握手信息中包含协商信息
1011 表明了一端遇到了异常情况使得其无法完成请求而需要关闭连接
1015 是一个保留值,并且必须不可以作为关闭帧的状态码