WebSocket 通信说明与基于 ESP-IDF 的 WebSocket 使用

一、 WebSocket 出现的背景

最开始 客户端(Client)服务器(Server) 通信使用的是 HTTP 协议,HTTP 协议有一个的缺陷为:通信只能由客户端(Client)发起。

在一些场景下,这种单向请求的特点,注定了当 服务器(Server) 有连续的状态变化时, 客户端(Client) 要获知就非常麻烦。 客户端(Client) 只能使用轮询的方式,即每隔一段时间,就发出一个询问,来了解 服务器(Server) 有没有新的信息,最典型的场景就是聊天室。

轮询的方式导致效率很低,非常浪费资源, 客户端(Client) 必须不停地发起连接,或者 HTTP 连接始终打开。为此工程师们一直在思考更好的解决方案,因此 WebSocket 就这样诞生了。


二、WebSocket 的优缺点

WebSocket 优势:

  • 实时性: 由于 WebSocket 的持久化连接,它可以实现实时的数据传输,避免了 Web 应用程序需要不断地发送请求以获取最新数据的情况。
  • 双向通信 : WebSocket 协议支持双向通信,这意味着 服务器(Server) 可以主动向 客户端(Client) 发送数据,而不需要 客户端(Client) 发送请求。
  • 减少网络负载: 由于 WebSocket 的持久化连接,它可以减少 HTTP 请求的数量,从而减少了网络负载。

WebSocket 的劣势:

  • 需要浏览器和 服务器(Server) 都支持 : WebSocket 是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和 服务器(Server) 可能不支持 WebSocket。
  • 需要额外的开销: WebSocket 需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和 CPU。
  • 安全问题 : 由于 WebSocket 允许 服务器(Server) 主动向 客户端(Client) 发送数据,可能会存在安全问题。 服务器(Server) 必须保证只向合法的 客户端(Client) 发送数据。

三、WebSocket 协议概述

WebSocket 协议是一种基于 TCP 的协议,用于在 客户端(Client)服务器(Server) 之间建立持久连接,并且可以在这个连接上实时地交换数据。WebSocket 协议有自己的握手协议,用于建立连接,也有自己的数据传输格式。

客户端(Client) 发送一个 WebSocket 请求时,服务器(Server) 将发送一个协议响应以确认请求。在握手期间, 客户端(Client)服务器(Server) 将协商使用的协议版本、支持的子协议、支持的扩展选项等。一旦握手完成,连接将保持打开状态, 客户端(Client)服务器(Server) 就可以在连接上实时地传递数据。

WebSocket 协议使用的是 双向数据传输 ,即 客户端(Client)服务器(Server) 都可以在任意时间向对方发送数据,而不需要等待对方的请求。它支持传输 二进制数据文本数据,并可以自由地在它们之间进行转换。

WebSocket 通信过程

一个 WebSocket 连接包含以下四个主要阶段:

1、连接建立阶段(Connection Establishment)

在这个阶段, 客户端(Client)服务器(Server) 之间的 WebSocket 连接被建立。 客户端(Client) 发送一个 WebSocket 握手请求, 服务器(Server) 响应一个握手响应,然后连接就被建立了。握手过程 如下:

WebSocket 为了兼容 HTTP 协议,是在 HTTP 协议的基础之上进行升级得到的。在客户端(Client)服务器(Server) 端建立 HTTP 连接之后,客户端(Client) 会向 服务器(Server) 端发送一个升级到 WebSocket 的协议,如下所示:

c 复制代码
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

通过设置 Upgrade 和 Connection 这两个 header,表示准备升级到 WebSocket 了。

除了这里列的属性之外,其他的 HTTP 自带的 header 属性都是可以接受的。

服务器(Server) 端收到客户端(Client) 的请求之后,会返回给客户端(Client) 一个响应,告诉客户端(Client) 协议已经从 HTTP 升级到 WebSocket 了。返回的响应可能是这样的:

c 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

这里的 Sec-WebSocket-Accept 是根据客户端(Client) 请求中的 Sec-WebSocket-Key 来生成的。具体而言是将客户端(Client) 发送的 Sec-WebSocket-Key 和 字符串 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 进行连接。然后使用 SHA1 算法求得其 Hash 值,最后将 Hash 值进行 base64 编码即可,当 服务器(Server) 端返回 Sec-WebSocket-Accept 之后,客户端(Client) 可以对其进行校验,已完成整个握手过程。

2、连接开放阶段(Connection Open)

在这个阶段,WebSocket 连接已经建立并开放,客户端(Client)服务器(Server) 可以在连接上互相发送数据。

3、连接关闭阶段(Connection Closing)

在这个阶段,一个 WebSocket 连接即将被关闭。它可以被客户端(Client)服务器(Server) 发起,通过发送一个关闭帧来关闭连接。

4、连接关闭完成阶段(Connection Closed)

在这个阶段,WebSocket 连接已经完全关闭。客户端(Client)服务器(Server) 之间的任何交互都将无效。

需要注意的是,WebSocket 连接在任何时候都可能关闭,例如网络故障、 服务器(Server) 崩溃等情况都可能导致连接关闭。因此,需要及时处理 WebSocket 连接关闭的事件,以确保应用程序的可靠性和稳定性。

WebSocket 的消息格式

WebSocket 的消息格式与 HTTP 请求和响应的消息格式有所不同。WebSocket 的消息格式可以是 文本二进制数据,并且 WebSocket 消息的传输是在一个已经建立的连接上进行的,因此不需要再进行 HTTP 请求和响应的握手操作。

由图可知,WebSocket 的报文格式可以分为七大部分,分别是 1bitFIN 标志位3bitRSV 保留位4bitOpcode1bitMask 标志位7/7+16/7+64bitpayloadLen ,可选字段 masking-key,可选字段 payload。具体解释下:

  • FIN 标志位:此标志位用于指示当前的帧是消息的最后一个分段。

    • WebSocket 支持将长消息切割成若干帧发送,切分后,前边的帧的 FIN 字段均为 0,最后一个帧的 FIN 为 1
    • 当消息没有分段时,这个帧便包含所有信息,FIN 标志位为1.【1bite】
  • RSV1~3 :这是三个保留位,一般情况下为全 0

    • 客户端(Client) 、服务端协商采用 WebSocket 扩展时,这三个标志位可以 非 0,且值的含义由扩展进行定义。
    • 如果出现 非0 值但并未采用 WebSocket 扩展,连接出错。
  • Opcode : 4bit 操作码,用于指示帧类型。

    • Opcode 决定了如何解析后续的数据载荷部分,如果操作码是不认识的,接收端应该断开连接。可选的操作码如下:

      c 复制代码
      %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
      %x1:表示这是一个文本帧(frame)
      %x2:表示这是一个二进制帧(frame)
      %x3-7:保留的操作代码,用于后续定义的非控制帧。
      %x8:表示连接断开。
      %x9:表示这是一个 ping 操作。
      %xA:表示这是一个 pong 操作。
      %xB-F:保留的操作代码,用于后续定义的控制帧。
    • 其中注意 WebSocket 既可以传输文本数据,也可以传输二进制数据

  • Mask 标志位 :指示帧的 payload 是否需要使用掩码覆盖。

    • RFC6455 规定,当且仅当由客户端(Client) 向服务端发送的帧需要覆盖。

    • 掩码覆盖的作用:解决 "缓冲区溢出"(忽略)

    • Mask1 ,但服务端接收的数据没有进行过掩码操作,服务端需要断开连接

      c 复制代码
      payload length:7位/7+16位(64k)/7+64位(超级大),单位是字节
      模式区分:
      ①当7bite的payloadlength<126,此时为模式1
      ② 7bite的payloadlength=126,16bite生效为模式2
      ③ 7bite的payloadlength=126,64bite生效为模式3
    • masking-keymask 值有关,当 mask0 时,没有 masking-key; 为 1 时,有 4bitmasking-key

  • payload-data :长度可变。包含扩展数据(x 字节)和应用数据(y 字节)

    • 如果通信双方约定使用了 WebSocket 扩展,则扩展数据也存放于此,并声明扩展长度。

    • 如果没有约定使用,则扩展数据为 0 字节

WebSocket 的报文格式中最重要的便是 Opcodepayload length(三种模式)payload data


四、基于 esp-idf 如何使用 webSocket

如何使用

  • 对于 esp-idf v5.0 以下版本, 有对应的 websocket 示例, 用户可以直接进行测试。

  • 对于 esp-idf v5.0 以上版本, 提供了 esp_websocket_client 组件, 直接在对应的示例下面添加组件即可。

    c 复制代码
    idf.py add-dependency "espressif/esp_websocket_client^1.2.3"

基于 ESP-IDF SDK 使用 Websocket 的案例:

使用 esp-idf v4.2.2 版本, 服务器(Server) 有时会异常断开, 模块会收到 服务器(Server) 发过来的 opcode=0x08 关闭帧, 这这种情况下设备要如何重新连接?这个机制是怎样的呢?

首先,op_code0x08 是一个断开帧, 表示对端主动断开的, 我们是不需要在 WEBSOCKET_EVENT_DATA 里去判断 op_code 等于 0x08 的情况。在 WEBSOCKET CLOSED 事件中,内部会进行断开的处理,如果直接在 WEBSOCKET_EVENT_CLOSED 调用 esp_websocket_client_start(client) 接口重新连接 服务器(Server) , 则实际测试下来会进入到 close 事件里面, 但是并没有重新连接, 日志如下:

c 复制代码
2024-09-27 12:10:14.783]# RECV ASCII>
[0;32mI (27116) WEBSOCKET: ====================Received opcode=1==================[0m
[0;33mW (27116) WEBSOCKET: ------> [3, "410212051", {}] <------

[2024-09-27 12:10:21.160]# RECV ASCII>
[0;32mI (33506) uart_events: netStatus:7
[2024-09-27 12:10:22.466]# RECV ASCII>
[0;32mI (34796) WEBSOCKET: ====================Received opcode=8==================[0m
[0;32mI (34796) WEBSOCKET: WEBSOCKET_EVENT_CLOSED[0m
[2024-09-27 12:10:26.185]# RECV ASCII>
[0;31mE (38486) TRANSPORT_WS: Error read response for Upgrade header GET /HBE-123456 HTTP/1.1Connection: UpgradeHost: 0c6eeb3d0d512aa2.octt.openchargealliance.org:21128User-Agent: ESP32 Websocket ClientUpgrade: websocketSec-WebSocket-Version: 13Sec-WebSocket-Key: VMy5tg1Rv1Gr/P7HhLCTVw==Sec-WebSocket-Protocol: ocpp1.6[0m
[0;31mE (38506) WEBSOCKET_CLIENT: Error transport connect[0m
[0;31mE (38516) WEBSOCKET_CLIENT: esp_websocket_client_abort_connection(160): Websocket already stop
[2024-09-27 12:10:29.403]# RECV ASCII>
[0;32mI (41746) uart_events: netStatus:6
[2024-09-27 12:10:37.633]# RECV ASCII>
[0;32mI (49976) uart_events: netStatus:6
[2024-09-27 12:10:45.848]# RECV ASCII>
[0;32mI (58196) uart_events: netStatus:6
[2024-09-27 12:10:51.190]# RECV ASCII>
[0;31mE (63456) task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:[0m
[0;31mE (63456) task_wdt:  - IDLE (CPU 0)[0m
[0;31mE (63456) task_wdt: Tasks currently running:[0m
[0;31mE (63456) task_wdt: CPU 0: websocket_task[0m
[0;31mE (63456) task_wdt: CPU 1: IDLE[0m
[0;31mE (63456) task_wdt: Print CPU 0 (current core) backtrace
Backtrace: 0x4013BF6A:0x3FFBE920 0x40082A71:0x3FFBE940 0x4000BFED:0x3FFF1030 0x40093AAD:0x3FFF1040 0x40091698:0x3FFF1060 0x400917A8:0x3FFF10A0 0x400DD000:0x3FFF10C0 0x400938D9:0x3FFF10F0
[0;31mE (63456) task_wdt: Print CPU 1 backtrace[0m
Backtrace: 0x4008BFF1:0x3FFBEF2

再进一步调试发现是因为客户端(Client) 如果收到 0x08opcode , 它不仅仅只是断开之前的连接, 还会释放掉之前创建的 handle,所以用户需要在 WEBSOCKET_EVENT_CLOSED 事件里创建好 handle , 然后直接去做重新连接的操作, 参考如下代码:

c 复制代码
case WEBSOCKET_EVENT_CLOSED:
        ESP_LOGI(TAG, "WEBSOCKET_EVENT_CLOSED");
        esp_websocket_client_config_t websocket_cfg = {};

    shutdown_signal_timer = xTimerCreate("Websocket shutdown timer", NO_DATA_TIMEOUT_SEC * 1000 / portTICK_PERIOD_MS,
                                         pdFALSE, NULL, shutdown_signaler);
    shutdown_sema = xSemaphoreCreateBinary();

    websocket_cfg.task_stack = 8192;
    websocket_cfg.uri = "ws://0c6eeb3d0d512aa2.octt.openchargealliance.org:21128/HBE-123456";
	websocket_cfg.subprotocol="ocpp1.6";

    ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri);

    esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
    esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);

        esp_websocket_client_start(client);
        break;
    }

修改代码之后, 在 服务器(Server) 收到 0x08opcode 之后, 可以重新连接成功。

相关推荐
MC何失眠1 小时前
vulnhub靶场【哈利波特】三部曲之Fawkes
网络·python·学习·网络安全
小袁顶风作案1 小时前
Ubuntu桥接模式设置静态IP
网络·tcp/ip·桥接模式
闲人-闲人1 小时前
CIA安全属性简介
网络·安全
搬砖的果果5 小时前
HTTP代理有那些常见的安全协议?
服务器·python·网络协议·tcp/ip
Looper03315 小时前
【Shell 脚本实现 HTTP 请求的接收、解析、处理逻辑】
网络·网络协议·http
eddieHoo5 小时前
HTTP、RPC
网络协议·http·rpc
m0_689618285 小时前
亚毫米级纤维机器人,如何在腔内“大显身手”
网络·笔记·机器人
7ACE6 小时前
TCP Analysis Flags 之 TCP Spurious Retransmission
网络协议·tcp/ip·wireshark
夜星辰20236 小时前
网络相关问题
服务器·网络
Jackey_Song_Odd7 小时前
[node.js] [HTTP/S] 实现 requests 发起 HTTP/S/1.1/2.0 请求
网络协议·http·node.js