本文旨在用最简短的语言描述 WebSocket 的核心内容,帮助初学者快速掌握 WebSocket 相关的知识点。
是什么
WebSocket 是一种基于 TCP 实现的双向通信协议。
协议的主要内容:
- 握手过程
- 传输格式
握手过程
WebSocket 的握手过程是基于 HTTP 实现的,下面只介绍必不可少的几个头部。
请求头:
GET /chat HTTP/1.1
必须使用 HTTP/1.1 或以上版本的 GET 请求Connection: Upgrade
告诉服务器,将请求升级Upgrade: websocket
告诉服务器,将请求升级为 WebSocket 协议Sec-WebSocket-Key
一个随机值,服务器使用指定的算法签名后返回来,用来验证服务器是否真的实现了 WebSocket 协议Sec-WebSocket-Version
WebSocket 的版本号,当前最新为 13,服务器需要判断是否支持
响应头:
HTTP/1.1 101 Switching Protocols
告诉客户端,切换协议Connection: Upgrade
告诉客户端,已将请求升级Upgrade: websocket
告诉客户端,已将请求升级为 WebSocket 协议Sec-WebSocket-Accept
对Sec-WebSocket-Key
的签名,签名算法如下所示
Sec-WebSocket-Accept
的签名算法:
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" // 这是一个固定的 magic string,不必纠结为什么
SecWebSocketAccept = base64(sha1(SecWebSocketKey + GUID))
一个典型的 WebSocket 握手请求如下所示:
GET /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: 13
一个典型的 WebSocket 握手响应如下所示:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
以上示例为成功的 WebSocket 握手过程,如果握手失败(例如,缺少必要的头部),那么应该返回失败响应,和普通的 HTTP 没有任何区别,此处不再细说。
以上示例为 ws://
类型的握手,如果是 wss://
类型,则基于 HTTPS 进行握手,其区别就是 HTTP 和 HTTPS 的区别,此处不再细说。
传输格式
数据帧
长话短说:客户端和服务器使用相同的格式进行数据传输,称其为数据帧;一条消息可以被分成多个数据帧传输,即分片;协议本身只支持文本类型和二进制类型,其他类型可以通过拓展协议实现。
一个数据帧格式如下所示:
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 ... |
+---------------------------------------------------------------+
数据帧格式解释:
FIN
占1位,1表示一条消息的最后一帧RSV1/2/3
,各占1位,用于拓展,如果没有拓展,必须是0opcode
,占4位,操作码,表示帧类型- %x0 表示一个延续帧
- %x1 文本类型
- %x2 二进制类型
- %x3-7 保留的非控制帧
- %x8 连接关闭
- %x9 ping 消息(心跳)
- %xA pong 消息(心跳)
- %xB-F 保留的控制帧
MASK
占1位,1表示使用掩码,客户端发给服务端必须使用掩码Payload len
和Extended payload length
占 7 或 7+16 或 7+64 位,表示数据部分的长度(单位:字节),因为数据有长有短,使用变长的 Payload len 可以节省传输的字节,具体计算方式下面会说Masking-key
占 32 位(如果MASK
为 1)或 0 位(如果MASK
为 0),掩码,用于数据部分的解析- 以上统称为帧的首部,剩下的都是帧的数据载荷部分,其长度由
Payload len
和Extended payload length
决定
数据载荷部分的长度计算方式:
- 把 Payload len 称为第1段,把 Extended payload length 称为第2段
- 先读第1段
- 如果第1段的值小于 126,则第1段的值就是数据部分的长度,无须读第2段
- 如果第1段的值等于 126,则第2段只读 16 位,并把第2段得到的值作为数据部分的长度
- 如果第1段的值等于 127,则第2段读 64 位,并把第2段得到的值作为数据部分的长度
- 无论是第1段还是第2段,都是按照无符号整数读的,也就是说,最大可表示的值为 2^64 - 1
数据载荷部分的解析:
- 如果 MASK 为 1,则 Masking-key 肯定有值,也意味着数据载荷必须使用掩码进行解码
- 解码算法就是使用掩码进行异或运算,由于异或运算的特性,实际上解码和编码的算法是完全一样的
- 掩码运算公式如下,其中:
encoded
表示已编码的数据的字节数组decoded
表示已解码的数据的字节数组MaskingKey
表示掩码的字节数组,占4个字节i
表示字节数组的下标
decoded[i] = encoded[i] XOR MaskingKey[i % 4]
分片的实现
有 2 个首部字段和分片有关:
- FIN 为 1 时表示一条消息的最后一帧,反之不是最后一帧
- opcode 为 0 时表示当前帧为延续帧
下面通过1个例子来看如何实现分片:
- FIN=1,opcode=1 当前帧是一个文本帧,且已经是最后一帧,说明当前帧包含了一条完整的消息
- FIN=0,opcode=1 当前帧是一个文本帧,但不是最后一帧,说明当前帧包含的内容是不完整的,不能解析出完整消息(帧内容要暂存起来)
- FIN=0,opcode=0 当前帧是一个延续帧,且不是最后一帧,说明当前帧应该拼接到上一帧后面,不能解析出完整消息(帧内容要暂存起来)
- FIN=1,opcode=0 当前帧是一个延续帧,且是最后一帧,说明当前帧应该拼接到上一帧后面,且当前消息所需要的所有帧都已经拿到,可以解析出完整消息
心跳机制
当 opcode 为 %x9 或 %xA 时,分别表示一个 ping 消息帧或 pong 消息帧。这两种类型是专用于实现 WebSocket 心跳的。
此处的心跳应该区分应用层的心跳。如果应用层要实现心跳机制,例如一个基于 WebSocket 的 IM 应用,可以在应用层使用 opcode 为 1 或 2 的帧定义应用层的心跳协议。
实现 WebSocket 通信的最小 Demo
源码已开源,仅供学习。
HTTP VS WebSocket
WebSocket 的握手过程需要依赖 HTTP 协议。握手请求/响应完全遵循 HTTP 协议的要求,跟普通的 HTTP 请求/响应没有任何区别,只不过需要一些特殊的头部进行标识。
一旦连接成功,就跟 HTTP 没有任何关系了。从网络协议栈的角度来看,我们并不能把 WebSocket 看作是 HTTP 协议之上的一种协议,因为 WebSocket 和 HTTP 同属于应用层协议。
WebSocket 是为了实现双向通信而发明的。在 WebSocket 之前,如果要实现双向通信,可以用 HTTP Long Pollinig 或 HTTP Streaming 等基于 HTTP 协议实现的技术。
Q&A
为什么客户端给服务端发数据必须加掩码?
为了防止缓存代理服务器污染攻击。详见RFC6455 10.3小节
长话短说,看图:
我们假设客户端不使用掩码,且服务器不检查掩码
虽然被攻击的是缓存代理服务器,但是受害的是提供服务的企业和普通用户