本文仅针对 ESP32。
ESP32 使用 WebSocket 进行通信的场景比较少,大部分使用的应用层协议是 MQTT 和 HTTP。不过对于 WebSocket,基本的了解还是要的。最近正好有时间,就撸一下 WebSocket。
本文只是简单的对 WebSocket 进行分析,如果想深入了解的话,建议还是参考 RFC6455。
WebSocket 是一种网络通信协议。跟 MQTT、HTTP 协议一样,它也属于 TCP/IP 四层模型中的应用层协议。
WebSocket 协议在 2008 年诞生,2011 年成为国际标准。HTML5 开始提供的一种浏览器与服务器进行全双工通讯的网络技术。它基于 TCP 传输,并复用 HTTP 的握手。
对于 HTTP,我们都知道它具有以下两个特性:
这两个特性在理论上大大提高了服务器的执行效率和响应速度。但也掩盖不了 HTTP 协议的一个弊端,那就是通信只能由客户端发起。这在以前倒是没什么问题,可现在的应用对实时性的要求越来越高,典型的应用场景就是直播,网页游戏,股票交易等。虽然可以通过轮询的方式加以解决,但轮询的效率非常低,不停的轮询在无形之中又加重了服务器的负担,这与 HTTP 的设计初衷又背道而驰。
看到这里,肯定有读者想到了 HTTP 的长连接。没错,当一个 HTTP 的请求被服务器响应后,HTTP 基于的 TCP 连接不会被关闭。客户端如果想再次发送 HTTP 的请求,可以直接在该 TCP 连接上发送。相比较轮询的方式,长连接算是一种比较优雅的方式。HTTP 的长连接是在响应头中加入头部字段 Connection:keep-alive
来实现。但长连接不会永久保持连接,也有一个保持的时间。HTTP 的长连接本质上就是 TCP 的长连接。
相同点:
80
和 443
。不同点:
HTTP 是单向的,只能由客户端发起 Request 请求;WebSocket 是双向的。
联系:
WebSocket 仅仅只是借 HTTP 来完成握手。握手成功之后,WebSocket 有其自有的帧进行通信。
对于 Socket,我们都知道它仅仅是一个抽象概念,它将 TCP/IP 层操作抽象为几个简单的接口供应用程序调用。对于用户来说,仅仅需要调用几个 socket 接口就可以完成主机之间的通信。
Socket 也可以用来表示网络中一个连接的两端。WebSocket 仅仅是借用了这个概念来表示网络中一个连接的两端,而这两端可以等视作浏览器和服务器。
Espressif 已经提供了 WebSocket 的 demo。不过在该 demo 中使用的服务器已经不在提供服务,所以可以使用公共的服务器或者在本地自己搭建一个服务器。本文选择后者。
本文在 Windows 环境下搭建基于 nodejs 平台实现的 WebSocket 测试工具 wscat。
参考官方说明:wscat
安装步骤:
npm install -g wscat
安装完成之后,输入以下命令在本地 8080
端口启动 WebSocket 服务。
wscat -l 8080
在 WebSocket 目录下执行以下命令进行配置(设置 WebSocket 的 URL):
idf.py menuconfig
将固件下载到 ESP32 上并运行,就可以看到在 WebSocket 服务端不断接收到从 ESP32 上发送来的文本数据。
结合 Wireshark 抓包,可以简单的对 WebSocket 协议进行分析。
建立 TCP 连接
这个不用过多的介绍,TCP 建立连接需要 3
次握手。包 23545 (SYN),包 23547 (SYN, ACK) 和包 24983 (ACK) 分别表示 TCP 的 3
次握手包。
握手
WebSocket 借助 HTTP 来完成握手。客户端首先需要发送握手包,握手包以 HTTP GET Request 的结构发送给服务端,服务端解析了之后以 HTTP Response 的结构发送给客户端,其中 Response 中的状态码为 101
表示握手成功。
在 WebSocket 协议文档中对客户端发送的握手请求包有如下要求:
1. 握手请求包必须为一个有效的 HTTP Request
2. Request Method 必须为 Get,Request Version 必须至少为 1.1 版本
3. 请求头部中必须包含一个 Host
键值对,如果未在请求头部中使用 port
键值对的话,那在 Host
所对应的值中必须指明端口号
4. 请求头部中必须包含一个 Upgrade
键值对,值必须为 websocket
5. 请求头部中必须包含一个 Connection
键值对,值必须为 Upgrade
6. 请求头部中必须包含一个 Sec-WebSocket-Key
键值对,值必须是随机产生的 16 字节,以 base64 编码
7. 如果客户端是浏览器,则请求头部中必须包含一个 Origin
键值对
8. 请求头部中必须包含一个 Sec-WebSocket-Version
键值对,值必须为 13
9. 请求头部中可以包含一个 Sec-WebSocket-Protocol
键值对
10. 请求头部中可以包含一个 Sec-WebSocket-Extensions
键值对
我们来看一下 ESP32 发送的用于握手的 GET Request 包,基本满足协议中对握手请求包的要求。
如果服务端选择接收连接,它必须回复一个有效的 HTTP Response 包。
在 WebSocket 协议文档中对服务端回复的 HTTP Response 有如下要求:
101
,原因短语可以为 Switching Protocols
Upgrade
键,值为 websocket
Connection
键,值为 Upgrade
Sec-WebSocket-Accept
键,值是根据客户端 Get Request 中的Sec-WebSocket-Key
键值生成,流程是先经过 SHA-1 加密成 20 字节的数据,在将这些数据以 base64 编码。我们来看一下 wscat 回复的 HTTP Response 包,基本满足协议中的要求。
客户端和服务端要进行数据通信,首先要了解的就是 WebSocket 中的数据格式。WebSocket 中的数据帧比较简单,格式如下:
FIN
: 1 bit
用来指示该帧是否是整个要发送的消息的最后一帧。如果整个消息能通过一帧发送,则也要置位。
RSV1
, RSV2
, RSV3
: 1 bit
保留。必须为 0。
opcode
: 4 bits
操作码。是对 Payload Data
域的解释。 如果接收端接收到未定义的操作码,则必须结束 WebSocket 连接。
值 | 定义 |
---|---|
%x0 | 表明这一帧数据是上一帧数据的延续,这一帧是一个连续帧 |
%x1 | 表明这一帧数据是一个文本帧 |
%x2 | 表明这一帧数据是一个二进制数据帧 |
%x3 - 7 | 保留,适用于未来的非控制帧 |
%x8 | 表明这一帧是个断开连接帧 |
%x9 | ping 帧 |
%xA | pong 帧 |
%xB - F | 保留,适用于未来的控制帧 |
MASK
: 1 bit
掩码位。指明 Payload Data
域中的数据是否经过 XOR
(异或) 运算。如果值为 1
,则 Masking-key
域中的值用来还原经过 XOR
(异或) 过的数据。所有从客户端发给服务端的帧必须置 1
。
Payload len
: 7 bits, 7 + 16 bits, 7 + 64 bits
负载长度位,必须结合 Extended payload length
域一起指明 Payload Data
域中数据的长度,以字节为单位 。注意 Payload len
域和 Extended payload length
域中均采用网络字节序(大端模式)。
Payload len
域中的值在 [0, 125] 中时,该域中的值就代表实际的数据长度,此时 Extended payload length
域消失。Payload len
域后为 Masking-key
域。Payload len
域中的值在 126 时,该域中的值就表明后续的 Extended payload length
域为 16 bits。Payload Data
域中数据的长度由 Extended payload length
域指明。Payload len
域中的值在 127 时,该域中的值就表明后续的 Extended payload length
域为 64 bits。Payload Data
域中数据的长度由 Extended payload length
域指明。Masking-key
: 0 bit 或 32 bits
掩码钥匙位。 MASK
域为 1
时有效。所有从客户端发给服务端的帧 MASK
域必须置 1
。换句话说 Masking-key
域就是专门用来服务服务端的。服务端需要该域中的 key 用来解码 Payload len
域中的数据。所有服务端发送给客户端的帧设置 MASK
域为 0
,此时帧中的 Masking-key
域省略。
未完待续…