WebSocket 工作原理
握手过程
WebSocket 握手过程是一个将 HTTP 升级为 WebSocket 的过程
首先由客户端发送一个 GET 请求
1
2
3
4
5
6
7
8GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13Upgrade
: 规定必需字段,值必需为 websocket,如果不是,则握手会失败Connection
: 规定必需字段,值必需为 Upgrade,如果不是,则握手会失败Sec-WebSocket-Key
: 必需字段,随机字符串Sec-WebSocket-Protocol
: 可选字段,表示应用层的协议Sec-WebSocket-Version
: 必需字段,代表 WebSocket 协议的版本,值必需为13,否则握手失败
服务端会返回一个状态码为101的响应
1
2
3
4
5HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chatUpgrade
: 规定必需字段,值必需为 websocket,如果不是,则握手会失败Connection
: 规定必需字段,值必需为 Upgrade,如果不是,则握手会失败Sec-WebSocket-Accept
: 规定必需字段,该字段是通过固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
加上请求中的Sec-WebSocket-Key
值,计算 SHA1 哈希摘要的结果Sec-WebSocket-Protocol
: 对应请求中的Sec-WebSocket-Protocol
WebSocket 协议数据帧
数据帧的定义如下
1 | 0 1 2 3 |
以上数据,一行代表 32 bit(位),也就是 4 bytes,总体包含以上两部分,帧头部和数据内容。每个从 WebSocket 链接中接受到的数据帧,都要按照以上格式进行解析,这样才能知道该数据帧是用于控制的还是用于传送数据的。部分字段解释:
FIN
: 1 bit,当该比特位值为%x0
时,表示后面还有更多的数据帧,%x1
表示为最后一个数据帧RSV1
,RSV2
,RSV3
: 各占 1 bit,一般全为 0,当客户端和服务端协商采用 WebSocket 扩展时,三个标志位为非 0,且该值由扩展进行定义。如果没有采用 WebSocket 扩展,且为非零时,连接出错opcode
- 4 bit,用于表明数据帧的类型,一共可以表示 16 种帧类型%x0
- 表示这是一个分片的帧,属于前面帧的后续帧%x1
- 表示数据帧携带的数据类型是文本类型,且编码为 UTF-8%x2
- 表示携带的是二进制数据%x3-7
- 保留未使用%x8
- 表示该帧用于关闭 WebSocket 链接%x9
- 表示该帧代表了 Ping 操作%xA
- 表示该帧代表了 Pong 回应%xB-F
- 保留未使用
MASK
- 1 bit,%x0
表示数据帧没有经过掩码计算,%x1
表示数据帧经过了掩码计算,需要解码才能得到真正的数据,通常浏览器发送给服务器才会进行掩码计算Payload len
- 7 bit,表示数据帧携带的数据长度,7 bit 代表的最大值为 127,会存在三种情况%x0-7D
- 0-125,表示数据长度,数据总长度的就是 7 bit 表示的长度%x7E
- 126,则后续的 2 个字节(16 bit)表示一个16位的无符号数,这个数用来表示数据长度%x7F
- 127,则后续的 8 个字节(64 bit)表示一个64位的无符号数,这个数用来表示数据长度
Masking-key
- 32 bit,表示用于解码的 key,当只有 MASK 比特位的值为 %x1 时使用Payload Data
- 余下的比特位用于存储具体的数据
通过分析可知,WebSocket 的最大头部为 14 bytes,要实现 WebSocket,最主要的工作就是实现对数据帧的解析
WebSocket 协议中的一些算法
Sec-WebSocket-Accept
的计算
Sec-WebSocket-Accept
值的计算方法是通过 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
加上请求中的 Sec-WebSocket-Accept
的值,然后对结果求 SHA1 哈希摘要,最后再转为base64。代码如下
1 | var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") |
1 | KEY_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' |
掩码处理
如果浏览器发送的数据帧中 MASK
比特位的值为 %x1,那么就需要对数据进行掩码处理
掩码处理的流程为以下几个步骤
- 将数据和
Masking-key
处理按字节处理 - 取出数据中的第
i
个字节item1
, - 通过
j = i MOD 4
计算获得j
,从Masking-key
中取出第j
个字节的数据item2
- 将
item1
与item2
进行异或得到最后结果
1 | func maskBytes(key [4]byte, pos int, b[]byte) int { |
1 | def mask_bytes(key: str, pos: int, b: list) -> int: |
注意:
pos & 3
的结果和pos % 4
的结果是一样的,因为a % (2^n)
等价于a & (2^n - 1)
,但是&
比%
的效率要高- 此处返回的
pos
是为了下次继续使用,如果数据帧中的数据长度大于 125,那么就需要多次进行掩码处理
代码实现
参考 websocket