长话短说 WebSocket

995 阅读7分钟

本文旨在用最简短的语言描述 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-AcceptSec-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位,用于拓展,如果没有拓展,必须是0
  • opcode,占4位,操作码,表示帧类型
    • %x0 表示一个延续帧
    • %x1 文本类型
    • %x2 二进制类型
    • %x3-7 保留的非控制帧
    • %x8 连接关闭
    • %x9 ping 消息(心跳)
    • %xA pong 消息(心跳)
    • %xB-F 保留的控制帧
  • MASK 占1位,1表示使用掩码,客户端发给服务端必须使用掩码
  • Payload lenExtended payload length 占 7 或 7+16 或 7+64 位,表示数据部分的长度(单位:字节),因为数据有长有短,使用变长的 Payload len 可以节省传输的字节,具体计算方式下面会说
  • Masking-key 占 32 位(如果 MASK 为 1)或 0 位(如果 MASK 为 0),掩码,用于数据部分的解析
  • 以上统称为帧的首部,剩下的都是帧的数据载荷部分,其长度由 Payload lenExtended 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小节

长话短说,看图:

我们假设客户端不使用掩码,且服务器不检查掩码

缓存代理服务器污染攻击.png

虽然被攻击的是缓存代理服务器,但是受害的是提供服务的企业和普通用户

引用及参考文献