单片机学习小组
直播中

爱与友人

9年用户 985经验值
擅长:可编程逻辑 模拟技术 存储技术
私信 关注

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. 概述




  • ESP WebSocket客户端是用于ESP32的WebSocket协议客户端实现。

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示例,使用esp_websocket_client建立一个WebSocket连接,并通过websocket.org服务器发送/接收数据,可以在这里找到:protocols/ WebSocket

// 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] = '';
            break;
        } else if (c > 0 && c < 127) {
            line[count] = c;
            ++count;
        }
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
}
#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */

    // 是否需要手动输入uri地址,若配置中不存在则需要
#if CONFIG_WEBSOCKET_URI_FROM_STDIN
    char line[128];

    ESP_LOGI(TAG, "Please enter uri of websocket endpoint");
    get_string(line, sizeof(line));

    websocket_cfg.uri = line;
    ESP_LOGI(TAG, "Endpoint uri: %sn", line);

#else
    // 直接获取uri地址
    websocket_cfg.uri = CONFIG_WEBSOCKET_URI;

#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */

    /*
    这个函数必须是第一个调用的函数,它返回一个 esp_websocket_client_handle_t ,
    你必须把它作为接口中其他函数的输入。
    当操作完成时,这个调用必须有一个对应的 esp_websocket_client_destroy 调用。
    */
    esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
    // 注册Websocket事件。
    esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
    // 打开WebSocket连接。
    esp_websocket_client_start(client);



  • 在上一步中注册了Websocket事件。以下是处理事件的函数。

static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
    esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
    switch (event_id) {
        // 客户端与服务器成功建立连接。客户机现在可以发送和接收数据了。不包含事件数据。
    case WEBSOCKET_EVENT_CONNECTED:
        ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED");
        break;
        // 由于传输层读取数据失败(例如服务器不可用),客户端已经终止连接。不包含事件数据。
    case WEBSOCKET_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED");
        break;
        // 客户端已经成功接收并解析了一个`WebSocket`帧。
        // 事件数据包含一个指向有效载荷数据的指针,有效载荷数据的长度以及接收帧的操作码。
        // 如果长度超过缓冲区大小,则消息可能被分割成多个事件。
        // 此事件也将被发布为非有效载荷帧,例如pong或连接关闭帧。
    case WEBSOCKET_EVENT_DATA:
        ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA");
        ESP_LOGI(TAG, "Received opcode=%d", data->op_code);
        ESP_LOGW(TAG, "Received=%.*s", data->data_len, (char *)data->data_ptr);
        ESP_LOGW(TAG, "Total payload length=%d, data_len=%d, current payload offset=%drn", data->payload_len, data->data_len, data->payload_offset);

        xTimerReset(shutdown_signal_timer, portMAX_DELAY);
        break;
        // 在客户端的当前实现中未使用。
    case WEBSOCKET_EVENT_ERROR:
        ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR");
        break;
    }
}



  • 启动定时器,循环查询接收数据。





  • 表现结果:一直循环(1s间隔)查询连接状态,如果连接上,就连续发送10次数据,然后退出。
  • 不然就一直查询连接状态。如果没有发送满10次,也没有收到数据,定时器就会每10s打印一次报错,且释放一次信号量。
  • 如果一直没连接上,试验现象就是每隔10秒打印。

// 启动定时器
xTimerStart(shutdown_signal_timer, portMAX_DELAY);
char data[32];
int i = 0;
while (i < 10)
{
    // 检查WebSocket客户端连接状态。
    if (esp_websocket_client_is_connected(client))
    {
        int len = sprintf(data, "hello %04d", i++);
        ESP_LOGI(TAG, "Sending %s", data);
        // 将文本数据写入WebSocket连接
        esp_websocket_client_send_text(client, data, len, portMAX_DELAY);
    }
    vTaskDelay(1000 / portTICK_RATE_MS);
}



  • 已经发送完数据,等待是否没有数据接收,然后关闭程序,退出。没有收到数据的判断依据是信号量。只要进入一次定时器超时就会满足。

// 宏获取信号量
xSemaphoreTake(shutdown_sema, portMAX_DELAY);
/*
停止WebSocket连接而没有WebSocket关闭握手。
此API停止ws客户端并直接关闭TCP连接,而不发送关闭帧。
使用 esp_websocket_client_close() 以一种干净的方式关闭连接是一个很好的实践。
*/
esp_websocket_client_stop(client);
ESP_LOGI(TAG, "Websocket Stopped");
/*
销毁 WebSocket 连接并释放所有资源。
这个函数必须是会话调用的最后一个函数。
它与 esp_websocket_client_init 函数相反,
调用时必须使用与 esp_websocket_client_init 调用返回的输入相同的句柄。
这可能会关闭该句柄使用过的所有连接。
*/
esp_websocket_client_destroy(client);

四、试验总结


1. 查看握手协议




  • 我使用网络调试助手上位机,连接esp32。注意,该上位机没有Websocket服务端功能,我只是用来看esp32发送的tcp数据到底是什么。
  • esp32配置中的URI设置为ws://192.168.1.101:8080,连接到我的上位机,然后上位机就接收到了Websocket客户端的握手数据。





  • 但是这个上位机不具有Websocket服务端的功能,所以不会回应握手数据。esp32也就连接不上,发送不了10次数据,也接收不到数据,就会每10s进入一次超时函数打印错误。从接收信息上看,客户端好像在重复发送握手,尝试链接。我在例程里没看到具体写出来的步骤,应该是打包在api中实现的功能。





  • 对比上一个笔记,tcp例程的实验。加深理解: 传输层就是发送应用层的数据 。不同层之间的数据是层层打包的关系。

2. 连接 Websocket服务器




  • 例程中自带的python脚本运行不了,搜索不到Websocket服务器的小工具,基本都是Websocket客户端的小工具。真的是奇了怪了。
  • 在看Websocket概念时明白到可以用node.js写一个服务器,恰巧之前为了学上位机安装了node.js。可以用上了。

【小插曲】使用 node.js 编写简易 Websocket服务器




  • 第一步应该先安装nodejs,然后安装npm,设定npm软件包的安装目录:Nodejs+npm详细安装
  • 第二步要注意是否导入了npm安装包的路径,以免在编写程序导入包时报错找不到:nodejs require模块找不到的两种解决办法
  • 在编写程序之前,先安装模块npm install nodejs-websocket (-g)。要注意安装模块的方法:【nodejs】使用 npm安装模块方法。本地安装就是指直接安装在读取目录的node_modules文件夹下,如果没有就会新建。直接到全局目录下使用本地安装,得到的效果和全局安装一样。
  • 编写参考教程,nodejs-websocket创建websocket服务器。注意教程中用的是本地安装,包不算大,所以本地安装也没关系。不过已经全局安装就不用再安装了。
  • 然后我直接拷贝了教程中的代码,仅把端口改成了8080。运行,成功(这个程序好像有点bug,无论如何都会打印“连接成功”)。好了,剩下的之后学node.js时再深究。

3. 实验现象




  • 半完整的实验现象如下图,还有一半是打印接收内容。因为服务器没有写发送内容,所以就没有。



注意事项:连接前记得查看自己电脑的IP地址,然后输入正确的URI,不然连接不上。我刚刚重启了一下路由器,我的ip地址就变了……要重新修改。






  • 客户端在连续发送10次数据,且进入了一次定时器超时函数后,就关闭了Websocket客户端功能。
  • 另外,不知道为什么看同步效果好差,怀疑是不是服务器小工具的问题。居然肉眼可见的不同步,达到1s以上(即客户端发送2次数据,服务器才打印一次)。暂时留下疑问,日后解答。

4. 总结的总结




以上没了。下一步继续按着教程系列学习。话说这节的教程系列的内容的Websocket服务器,而且用到的api好像都是再次宏定义后的接口,在官方api里找不到(我去没看源码证实猜想)。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分