ESP WebSocket 客户端
ESP WebSocket客户端是什么? WebSocket与http相比有何特点呢?
回帖 (1)
2022-2-22 13:43:29
单片机学习笔记 - 08 - WebSocket客户端
一、应用层协议 科普概念
在看例程之前先补补概念,我现在还是一脸懵逼,不知道这个是什么的状态。明明上一层的tcp已经能够通讯了,怎么又加多了一层。
对Http和WebSocket初步的认知。总结以下几点:
http有1.0和1.1版本,现在基本1.1版本。http属于无状态、无连接、单向 的应用层协议。即通讯请求只能有客户端发起,服务端只负责响应请求,不能主动发起。在应用中表现为 通过频繁的请求实现长轮询 。
WebSocket对比http,最大的特点就是可以主动向客户端发起请求。两者属于交集关系,有相同的地方,但不一样(所以应该是并列关系?)。
二者的握手协议也很相识,下图中,左图是WebSocket右图是http,可发现格式差不多。
总结回顾之前三层内容(链路层、网络层、传输层)。梳理一下之前学到的知识(按我个人的理解):
wifi和Ethernet协议 ,本身有通讯协议。类比uart底层发送,定义电信号0/1的层面,有自身的校验位、起始位结束位等。属于链路层 (也称网络接口层),主要是物理层面,保证了字节数据的正确性。
IP协议 ,本身有通讯协议,规定了数据要发送给网络中的哪个设备。类比片选等功能。属于网络层 ,主要是指定设备,保证传输对象的正确性。
TCP/UDP协议 ,也是有协议,用链路层得到的信息都是0/1的字节信息,打包了一些通讯数据,保证这些通讯数据的正确性。类比我用uart通讯时也写了一个帧头帧尾校验位的协议,如果一个数据包不符合设定要求,我就认为错误不可用。这时已经能初步得到想要的通讯数据了,不是八位的0/1或是单个字符,而是按我自己设定的16位、32位数据读取。属于传输层 ,主要是数据包的传输,保证设备间数据包传输的正确性。
通过上图就能清晰明了的理解,数据是如何被层层打包的。再上一层,就是应用层 —— HTTP/WebSocket协议 ,用传输层得到的数据包为基础,进一步规定两个设备的通讯(对话)方式。是一问一答还是多问多答,还是只答不问、只问不答……这样的规定是很重要的,因为网络通讯中要同时访问多个服务器/设备。应用层的协议有很多,适用不同的应用场景。
二、编程指南 翻译
1. 概述
2. 特点
支持基于TCP的WebSocket,带有mbedtls的TLS。
易于设置URI。
多个实例(一个应用程序中的多个客户机)。
3. 配置
1)URI
支持ws, wss方案。
WebSocket样本:①ws://echo.websocket.org: WebSocket通过TCP,默认端口80,②wss://echo.websocket.org: WebSocket通过SSL,默认端口443。
// 最小的配置:
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://echo.websocket.org",
};
// WebSocket客户端支持在URI中同时使用路径和查询。示例:
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://echo.websocket.org/connectionhandler?id=104",
};
// 如果在 esp_websocket_client_config_t 中有任何与URI相关的选项,则URI定义的选项将被覆盖。示例:
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://echo.websocket.org:123",
.port = 4567, //WebSocket客户端将使用端口4567连接到websocket.org
};
2)TLS
如果需要验证服务器端,需要提供PEM格式的证书,并在“websocket_client_config_t”中提供cert_pem。如果没有提供证书,那么TLS连接将默认不需要验证。
// 配置
const esp_websocket_client_config_t ws_cfg = {
.uri = "wss://echo.websocket.org",
.cert_pem = (const char *)websocket_org_pem_start,
};
3)子协议
客户端对服务器响应中的子协议字段无关,并且无论服务器响应什么都将接受连接。
// 配置结构中的子协议字段可用于请求子协议
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://websocket.org",
.subprotocol = "soap",
};
4. 事件
WEBSOCKET_EVENT_CONNECTED:客户端与服务器成功建立连接。客户机现在可以发送和接收数据了。不包含事件数据。
WEBSOCKET_EVENT_DISCONNECTED:由于传输层读取数据失败(例如服务器不可用),客户端已经终止连接。不包含事件数据。
WEBSOCKET_EVENT_DATA:客户端已经成功接收并解析了一个WebSocket帧。事件数据包含一个指向有效载荷数据的指针,有效载荷数据的长度以及接收帧的操作码。如果长度超过缓冲区大小,则消息可能被分割成多个事件。此事件也将被发布为非有效载荷帧,例如pong或连接关闭帧。
WEBSOCKET_EVENT_ERROR:在客户端的当前实现中未使用。
// 如果客户端句柄需要在事件处理程序中,它可以通过传递给事件处理程序的指针访问:
esp_websocket_client_handle_t client = (esp_websocket_client_handle_t)handler_args;
5. 限制和已知问题
客户端可以在握手期间请求服务器使用子协议,但是不会对来自服务器的响应进行任何与子协议相关的检查。
6. 应用举例
// WebSocket客户端支持以文本数据帧的形式发送数据,这告知应用层有效载荷数据是编码为UTF-8的文本数据。例子:
esp_websocket_client_send_text(client, data, len, portMAX_DELAY);
三、例程解析
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("WEBSOCKET_CLIENT", ESP_LOG_DEBUG);
esp_log_level_set("TRANS_TCP", ESP_LOG_DEBUG);
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
初始化联网,开启联网,记得打开项目配置菜单(idf.py menuconfig)修改配置。
/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
* Read "Establishing Wi-Fi or Ethernet Connection" section in
* examples/protocols/README.md for more information about this function.
*/
ESP_ERROR_CHECK(example_connect());
例程创建了一个定时器和信号用于调试,如果定时器超时,就表示已经10s没有收到信息,同时释放信号量。如果有收到信息,会触发事件重置定时器。
// 定时器超时函数
static void shutdown_signaler(TimerHandle_t xTimer)
{
ESP_LOGI(TAG, "No data received for %d seconds, signaling shutdown", NO_DATA_TIMEOUT_SEC);
// 宏定义 释放信号量
xSemaphoreGive(shutdown_sema);
}
// 创建一个新的软件计时器实例,并返回一个句柄,通过这个句柄可以引用创建的软件计时器。
shutdown_signal_timer = xTimerCreate("Websocket shutdown timer", // 只是一个文本名称,不被内核使用。
NO_DATA_TIMEOUT_SEC * 1000 / portTICK_PERIOD_MS, // 计时器周期(单位是tick)。
pdFALSE, // 计时器将在到期时自动重新加载。(不会)
NULL, // 为每个计时器分配一个唯一的id等于它的数组索引。
shutdown_signaler); // 每个计时器在到期时调用同一个回调。
// 创建一个新的二进制信号量实例,并返回一个句柄,通过这个句柄可以引用新的信号量。
shutdown_sema = xSemaphoreCreateBinary();
除了联网的内容需要配置,还有WebSocket客户端的URI需要配置,如果第一个选项设定了From stdin,例程就会开启WEBSOCKET_URI_FROM_STDIN宏定义。在联网成功后,连接服务器的URI需要手动输入(在监视器中)。如果设定为From string,会开启CONFIG_WEBSOCKET_URI宏定义,直接配置URI设置。
// 打包函数,用于获取uri字符串
#if CONFIG_WEBSOCKET_URI_FROM_STDIN
static void get_string(char *line, size_t size)
{
int count = 0;
while (count < size) {
int c = fgetc(stdin);
if (c == 'n') {
line[count] = '