ESP32 IDF开发 应用篇⑳ WebSocket
别迷路-导航栏
快速导航找到你想要的(文章目录)
此篇文章如果对你有用,请点赞收藏,您的支持就是博主坚持的动力。
1、博主写这篇技术文章的目的:
(1)、掌握 Websocket 原理和工作过程;
(2)、掌握ESP32 的 WebSocket 的程序设计;
2、概述
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
3、 websocket与http
(1)、WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)。
有交集,但是并不是全部。另外Html5是指的一系列新的API,或者说新规范,新技术。
(2)、请求握手包
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
(3)、接收请求包
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~
Upgrade: websocket
Connection: Upgrade
4、Websocket建立连接
当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦
客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:17(HTTP Request)
服务端:ok,确认,已升级为Websocket协议(HTTP Protocols Switched)
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
服务端:笑死我了哈哈哈哈哈哈哈
就变成了这样,只需要经过一次HTTP请求,就可以做到源源不断的信息传送了。(在程序设计中,这种设计叫做回调,即:你有信息了再来通知我,而不是我傻乎乎的每次跑来问你 )这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx等服务器的解析下,然后再传送给相应的Handler(PHP等)来处理。简单地说,我们有一个非常快速的 接线员(Nginx) ,他负责把问题转交给相应的 客服(Handler) 。
本身接线员基本上速度是足够的,但是每次都卡在客服(Handler)了,老有客服处理速度太慢。,导致客服不够。Websocket就解决了这样一个难题,建立后,可以直接跟接线员建立持久连接,有信息的时候客服想办法通知接线员,然后接线员在统一转交给客户。
这样就可以解决客服处理速度过慢的问题了。
同时,在传统的方式上,要不断的建立,关闭HTTP协议,由于HTTP是非状态性的,每次都要重新传输 identity info (鉴别信息),来告诉服务端你是谁。
虽然接线员很快速,但是每次都要听这么一堆,效率也会有所下降的,同时还得不断把这些信息转交给客服,不但浪费客服的处理时间,而且还会在网路传输中消耗过多的流量/时间。
但是Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求,这样就解决了接线员要反复解析HTTP协议,还要查看identity info的信息。
同时由客户主动询问,转换为服务器(推送)有信息的时候就发送(当然客户端还是等主动发送信息过来的。。),没有信息的时候就交给接线员(Nginx),不需要占用本身速度就慢的客服(Handler)了
注:详细讲解参考https://www.cnblogs.com/fuqiang88/p/5956363.html
5、Websocket相关API的介绍
这里介绍大部分Websocket库的使用更详细的请参考:
更多API 参考esp-idf\components\esp_websocket_client\include\esp_websocket_client.h
Websocket API和http API的封装类似使用起来也似曾相识。
跟esp_http_client主要是对esp_websocket_client_config_t结构体的操作。
(1)、启动Websocket会话
/**
* @brief 启动Websocket会话
*此函数必须是第一个要调用的函数,
*它返回一个esp_websocket_client_handle_t句柄。
*操作完成后,此调用必须对esp_websocket_client_destroy进行相应的调用。
*
* @param [in] config配置
*
* @return
*-`esp_websocket_client_handle_t`
*-如果有任何错误,则为NULL
*/
esp_websocket_client_handle_t esp_websocket_client_init(const esp_websocket_client_config_t *config);
(2)、注册回调函数类似于wifi连接过程的回调函数,此函数使用WEBSOCKET_EVENT_ANY事件
esp_err_t esp_websocket_register_events(esp_websocket_client_handle_t client,
esp_websocket_event_id_t event,
esp_event_handler_t event_handler,
void *event_handler_arg);
(3)、启动websocket_client,创建了一个esp_websocket_client_task任务
/**
* @brief 打开WebSocket连接
*
* @param[in] client 客户端句柄
*
* @return esp_err_t
*/
esp_err_t esp_websocket_client_start(esp_websocket_client_handle_t client);
esp_err_t esp_websocket_client_start(esp_websocket_client_handle_t client)
{
。。。。
if (xTaskCreate(esp_websocket_client_task, "websocket_task", client->config->task_stack, client, client->config->task_prio, NULL) != pdTRUE) {
。。。。
return ESP_OK;
}
(4)、最后解释连接写函数,读取数据在回调函数中获取数据
/**
* @brief 检查WebSocket连接状态
*
* @param[in] client 客户端句柄
*
* @return
* - true
* - false
*/
bool esp_websocket_client_is_connected(esp_websocket_client_handle_t client);
/**
* @brief 将通用数据写入WebSocket连接; 默认为二进制发送
*
* @param[in] client The client
* @param[in] data The data
* @param[in] len The length
* @param[in] timeout Write data timeout in RTOS ticks
*
* @return
* - Number of data was sent
* - (-1) if any errors
*/
int esp_websocket_client_send(esp_websocket_client_handle_t client, const char *data, int len, TickType_t timeout);
6、软件设计
websocket_client程序的实现主要是在esp_websocket_client.c文件中实现,这里主要是使用其中的API即可,其操作也主要是围绕esp_websocket_client_config_t结构体的操作。先来看看结构体几个重要的成员描述:
/**
* MQTT client configuration structure
*/
typedef struct {
mqtt_event_callback_t event_handle; /*!< handle for MQTT events as a callback in legacy mode */
esp_event_loop_handle_t event_loop_handle; /*!< handle for MQTT event loop library */
const char *host; /*!< MQTT server domain (ipv4 as string) */
const char *uri; /*!< Complete MQTT broker URI */
uint32_t port; /*!< MQTT server port */
const char *client_id; /*!< default client id is ``ESP32_%CHIPID%`` where %CHIPID% are last 3 bytes of MAC address in hex format */
const char *username; /*!< MQTT username */
const char *password; /*!< MQTT password */
} esp_mqtt_client_config_t;
这个结构体跟esp_http_client_config_t结构体类似。
①、初始化结构体创建句柄
esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
②、注册回调函数并启动
//注册websocket回调函数用于检测执行过程中的一些状态,跟wifi连接回调函数一样
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
//启动websocket任务,开始检测回调状态
esp_websocket_client_start(client);
③、发送数据
//将通用数据写入WebSocket连接; 默认为二进制发送
esp_websocket_client_send(client, data, len, portMAX_DELAY);
④、从服务端接收数据,这个在回调函数中处理
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;
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=%d\r\n", data->payload_len, data->data_len, data->payload_offset);
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR");
break;
}
}
接收数据结构体
/**
* @brief Websocket事件数据
*/
typedef struct {
const char *data_ptr; /*!< 数据指针 */
int data_len; /*!< Data length */
uint8_t op_code; /*!< 收到操作码 */
esp_websocket_client_handle_t client; /*!< esp_websocket_client_handle_t context */
void *user_context; /*!< user_data context, from esp_websocket_client_config_t user_data */
int payload_len; /*!< 包长度 */
int payload_offset; /*!< 与此事件关联的数据的实际偏移量 */
} esp_websocket_event_data_t;
7、实例
本实例使用的url是官网提供的测试连接ws://echo.websocket.org
源码如下:
在wifi连接成功后开始调用测试函数
static void vTaskWebSocket(void *pvParameters)
{
//等待连接成功,或已经连接有断开连接,此函数会一直阻塞,只有有连接
xEventGroupWaitBits(xCreatedEventGroup_WifiConnect,// 事件标志组句柄
WIFI_CONNECTED_BIT, // 等待bit0和bit1被设置
pdFALSE, //TRUE退出时bit0和bit1被清除,pdFALSE退出时bit0和bit1不清除
pdTRUE, //设置为pdTRUE表示等待bit1和bit0都被设置,pdFALSE表示等待bit1或bit0其中一个被设置
portMAX_DELAY); //等待延迟时间,一直等待
websocket_test();
ESP_LOGI(TAG, "Finish websocket Test");
vTaskDelete(NULL);
}
static void websocket_test(void)
{
//Websocket客户端设置配置
esp_websocket_client_config_t websocket_cfg = {};
//填充url
websocket_cfg.uri = "ws://echo.websocket.org";
ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri);
//创建websocket句柄
esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
//注册websocket回调函数用于检测执行过程中的一些状态,跟wifi连接回调函数一样
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
//启动websocket任务,开始检测回调状态
esp_websocket_client_start(client);
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(client, data, len, portMAX_DELAY);
}
vTaskDelay(1000 / portTICK_RATE_MS);
}
esp_websocket_client_stop(client);
ESP_LOGI(TAG, "Websocket Stopped");
esp_websocket_client_destroy(client);
}
回调函数
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;
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=%d\r\n", data->payload_len, data->data_len, data->payload_offset);
break;
case WEBSOCKET_EVENT_ERROR:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR");
break;
}
}
8、调试结果
所有文章源代码:https://download.csdn.net/download/lu330274924/88518092