ESP32-Web-Server编程- WebSocket 编程
概述
在前述 ESP32-Web-Server 实战编程-通过网页控制设备的 GPIO 中,我们创建了一个基于 HTTP 协议的 ESP32 Web 服务器,每当浏览器向 Web 服务器发送请求,我们将 HTML/CSS 文件提供给浏览器。
使用 HTTP 服务器库的缺点是,**如果多个客户端连接到 Web 服务器,当 A 浏览器改变了网页的内容(比如点餐系统),它不会自动更新网页上的内容到所有客户端B、C。我们可以通过使用 WebSocket 通信协议来解决此问题。**例如,如果多个客户端连接到 Web 服务器,并且任何一个客户端更改了设备的 GPIO 引脚的状态,则它将自动向所有连接的客户端通知该更改状态。
相比 HTTP 协议,WebSocket 通信协议除了可以双向通信、并且向多个客户端同时发送通知信息外,还可以提供持久连接,并且由于没有为每个请求重新建立连接的开销,因此延迟较低。
需求及功能解析
本节演示如何在 ESP32 上实现一个WebSocket 服务器。示例仍旧以在网页控制一个 GPIO 为例子。
与前述不同的是,通过 WebSocket 服务器,当多个浏览器在访问该服务器时,不同的浏览器之间可以及时接收到网页更新的信息。
示例解析
前端代码
示例中的 ESP32 WebSocket 服务器前端代码在 data\index.html
文件中,其主要提供两个功能:
-
显示 LED(对应设备的一个 GPIO)的状态,并为网页提供一个按钮用于切换 LED 的状态。
<div class="topnav"> <h1>ESP32 WebSocket Server</h1> </div> <div class="content"> <div class="card"> <h2>ONBOARD LED GPIO2</h2> <p><button id="button" class="button">Toggle LED</button></p> <p class="state">State: <span id="state">%s</span></p> </div> </div> </div>
-
在
script
中,每当 LED 的状态发生更新时,将 LED 状态作为 WebSocket 消息发送到所有连接的客户端。// 当网页加载时将自动调用该函数 function onLoad(event) { initWebSocket(); initButton(); } // 此函数负责初始化页面上的按钮元素并向其附加事件侦听器。单击该按钮时,它会通过 WebSocket 协议向 ESP32 发送消息,以切换 LED 的状态。 function initButton() { document.getElementById('button').addEventListener('click', toggle); }
-
在该 JavaScript 代码中,其定义了一个变量“gateway”,即 WebSocket 的端点,其相当于 HTTP 中的 URL。
-
点击“切换 LED”按钮后,会通过 WebSocket 向 ESP32 发送消息,切换 LED 的状态,并更新页面上的状态文本以反映 LED 的当前状态。
-
该代码中还利用了 console.log() 函数,该函数会将消息输出到浏览器的开发人员控制台。这对于调试和理解代码流非常有用。
后端代码
通过 HTTP 建立 wrbsocket 握手
WebSocket 服务器通过 HTTP 协议握手,然后开始使用 WebSocket 通信协议进行数据通信。因此,我们需要设置一个HTTP GET请求处理程序来完成最初始的建立握手的环节。
get_req_handler()
函数用于响应该握手阶段的 HTTP 请求。
esp_err_t get_req_handler(httpd_req_t *req)
{
int response;
if(led_state)
{
sprintf(response_data, index_html, "ON");
}
else
{
sprintf(response_data, index_html, "OFF");
}
response = httpd_resp_send(req, response_data, HTTPD_RESP_USE_STRLEN);
return response;
}
handle_ws_req(httpd_req_t *req) 负责处理从所有 Web 客户端发送到服务器的 WebSocket 请求。
WebSocket 接收客户端的数据
函数 handle_ws_req(httpd_req_t *req) 负责处理从所有 Web 客户端发送到服务器的 WebSocket 请求(包括打开、关闭和处理通过 websocket 连接发送的数据)。
static esp_err_t handle_ws_req(httpd_req_t *req)
该函数首先检查请求的方法是否是 HTTP 协议的 HTTP_GET,如果是,它将打印一条消息,指示 WebSocket 握手阶段(因为 WebSocket Web 服务器通过 HTTP 握手开始初始通信,然后遵循 WebSocket 通信协议)已完成,WebSocket 连接已打开,函数返回。
if (req->method == HTTP_GET)
{
ESP_LOGI(TAG, "Handshake done, the new connection was opened");
return ESP_OK;
}
如果请求的方法不是HTTP_GET,则在该示例中表示客户端请求正在发送 WebSocket 数据帧。将调用函数 httpd_ws_recv_frame() 来接收 WebSocket 数据帧并将其存储在 ws_pkt 变量中。
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret);
return ret;
}
如果接收到的数据帧长度不为零,则该函数将调用 calloc(1, ws_pkt.len + 1) 为 buf 变量分配内存,该变量将用于存储数据帧的有效负载。然后,它再次调用 httpd_ws_recv_frame() 来检索数据框的有效负载并将其存储在 buf 变量中。该函数记录收到的消息和帧的长度。
if (ws_pkt.len)
{
buf = calloc(1, ws_pkt.len + 1);
if (buf == NULL)
{
ESP_LOGE(TAG, "Failed to calloc memory for buf");
return ESP_ERR_NO_MEM;
}
ws_pkt.payload = buf;
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret);
free(buf);
return ret;
}
ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload);
}
ESP_LOGI(TAG, "frame len is %d", ws_pkt.len);
最后,该函数检查收到的消息是否为“切换”,如果是,则调用 trigger_async_send(req->handle, req) 函数来通知连接的其他客户端。
if (ws_pkt.type == HTTPD_WS_TYPE_TEXT &&
strcmp((char *)ws_pkt.payload, "toggle") == 0)
{
free(buf);
return trigger_async_send(req->handle, req);
}
return ESP_OK
总之,此函数通过处理 WebSocket 数据帧、接收请求中发送的消息以及通过向所有连接的客户端发送异步消息来处理消息来处理 WebSocket 请求。
WebSocket 发送方函数
以下两个函数响应 WebSocket 并将帧发送到所有连接的客户端:
static void ws_async_send(void *arg)
static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req)
trigger_async_send()负责调用 ws_async_send(),通过使用 httpd_queue_work()**对ws_async_send()进行排队并传递服务器句柄:
static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req)
{
struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg));
resp_arg->hd = req->handle;
resp_arg->fd = httpd_req_to_sockfd(req);
return httpd_queue_work(handle, ws_async_send, resp_arg);
}
ws_async_send() 执行以下功能:
- 切换 led_state 变量的状态,该变量跟踪 LED 的状态。
- 根据led_state更新指示灯的状态。
- 使用当前 led_state 格式化要发送的字符串,以生成 Web 套接字数据包的有效负载。
- 使用 httpd_ws_send_frame_async 函数将数据包发送到所有连接的客户端。
- 最后,释放为 resp_arg 分配的内存。
示例效果
通过 WebSocket 实现多个浏览器客户端连接到 Web 服务器时可以同步更新同一个网页的内容:
讨论
1)HTTP 传输协议与 WebSocket 在使用场景上有哪些不同?
HTTP在处理静态数据且不定期更新的应用程序中更可取。
WebSocket在处理实时数据的应用程序中更为可取。比如使用动态数据并期望持续和频繁更新的应用程序,游戏应用程序、社交软件必须与多个用户建立联系,这种类型的应用程序可以选择WebSocket来处理实时数据。
2)WebSocket 在前端中的 onload() 事件怎么理解?
即在网页加载时,自动触发的函数,这是浏览器默认的行为,是一个标准。还有其他称为“onOpen()“,“onClose()”,“onMessage()" 等的函数,它们处理WebSocket 上可能发生的不同事件。
总结
1)本节主要是介绍在 ESP32 上实现 WebSocket 服务器。相比 HTTP 协议,WebSocket 通信协议除了可以双向通信、并且向多个客户端同时发送通知信息外,还可以提供持久连接,并且由于没有为每个请求重新建立连接的开销,因此延迟较低。
资源链接
1)ESP32-Web-Server ESP-IDF系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)
3)下一篇:ESP32-Web-Server编程- 使用SSE 实时更新设备信息
(码字不易感谢点赞或收藏)