水军大提督 发表于 2025-3-19 18:13:05

怎样在 Arduino IDE 中利用 WebSockets 创建 ESP32 Web 服务器

https://i-blog.csdnimg.cn/direct/8a6d2f65360640128cb80ea5022b6f39.webp#pic_center
假设我们必要创建一个利用 ESP32 通过 WiFi 控制 LED 的项目。实现非常简朴:将 ESP32 设置为软 AP 或 STA 模式,答应它提供一个网页,将 LED 开关的状态表现为“开”或“关”。当用户在浏览器中切换 LED 开关时,ESP32 将收到一个 HTTP 请求。作为响应,它将相应地调整 LED 的状态,并将响应发送回浏览器。这正是我之前完成的小项目《基于 Arduino IDE 搭建一个轻量的 ESP32 Web 服务器》所实现的功能。
但是,此小项目存在一个问题。Web 是一个多用户体系,其他的设备也可以同时连接到同一个 Web 服务器。换句话说,服务器是一种共享资源。如果两个用户尝试同时打开或关闭 LED,这两个浏览器界面并不同步,且不会正确表现 LED 的实际状态。因此,它不是此类体系的符合办理方案。
当然,这个小问题我们可以用 WebSocket 通信协议的双向功能来构建一个响应式体系,其中单击 LED 开关会立即中继到服务器并广播到所有连接的浏览器,以便所有用户的 LED 开关状态始终同步。
本篇博客正是描述了怎样利用 WebSocket 通信协议在 ESP32 搭建 Web 服务器。
什么是 WebSocket?

WebSocket 是一种通信协议的名称,它支持客户端和 Web 服务器之间的双向(更正确地说是全双工)通信。简朴地说,WebSocket 是一种答应客户端和服务器创建连接的技术,通过该连接,任何一方都可以随时向另一方发送消息。
这与常规 HTTP 连接不同,在常规 HTTP 连接中,客户端发起请求,服务器发送响应,然后连接终止。事实上,WebSocket 是一种完全不同的通信协议,当客户端与服务器创建连接时,连接的两端都可以发送和接收数据。因此,就像服务器正在侦听新消息一样,所有连接的客户端也在积极侦听。因此,服务器可以将数据发送到特定客户端或将其广播到所有客户端,而无需请求。最重要的是,连接将保持活动状态,直到客户端或服务器关闭它,从而答应它们之间进行持续通信。
https://i-blog.csdnimg.cn/direct/00c99940147b4d51b393ee51f0172c4b.png#pic_center
   [!NOTE]
本文的重点在于怎样搭建 WedSocket 的 Web 服务器,而不是在科普 WedSocket,以是关于 WedSocket 的解释不会深入。如果想要比较生动形象的相识 WedSocket 相关知识,我在 B 站找到了一个相对不错的视频,大家可以移步去看一下。
视频链接:websocket是什么?和HTTP是什么区别?长轮询是什么?服务器推是什么?_哔哩哔哩_bilibili
基于 WedSocket 的小项目概述

接下来,我将利用 ESP32 构建一个 WebSocket 服务器,通过 websocket 连接长途控制 ESP32 的板载 LED。
我会设计一个网页,其中包含一个拨动开关,答应用户更改 ESP32 的 GPIO 2 引脚的状态(这个 GPIO 引脚对应的就是板载的 LED 的引脚),Web 界面还将表现 LED 的当前状态。
ESP32 将在网络端口 80 上自动侦听传入的 WebSocket 连接和消息。当用户在网页上切换 LED 开关时,将向 ESP32 发送 “toggle” 消息。当 ESP32 收到此消息时,它将切换 LED 的状态,并通过发送 “1”(表现 LED 亮起)或 “0”(表现 LED 熄灭)来关照所有连接的客户端(浏览器)。因此,所有活动客户端将立即在其网页上更新 LED 的状态。
该小项目的大抵工作原理,如下图:
https://i-blog.csdnimg.cn/direct/5f054a0ac8d045ef8000c83df798b04f.gif#pic_center
项目实现

安装 WebSocket 库与其他依赖库

想要在 ESP32 搭建 WebSocket 服务器,就要利用到 ESPAsyncWebServer 库,该库依赖于 AsyncTCP 库。Arduino IDE 2 现已上架这些库,可以直接在 “Library Manager” 中直接搜索并安装。
https://i-blog.csdnimg.cn/direct/0770ac8bdba442ac9e97928100ba3c12.png#pic_center
还有 AsyncTCP 库也一并安装上。
https://i-blog.csdnimg.cn/direct/7b3a92d05b4f4f43827828e797472265.png#pic_center
   [!IMPORTANT]
如果你利用的 Arduino IDE 1.8 或更低的版本,可以在库管理中找不到这些库。我个人更倾向于不再利用低版本的 IDE,而是利用最新版本的 IDE。
如果你觉得卸载 IDE 再重新下载新的 IDE 并安装比较贫苦,那你只能下载这些库,再手动导入 IDE 了。
下载链接:


[*]ESPAsyncWebServer 库:https://github.com/me-no-dev/ESPAsyncWebServer/archive/master.zip
[*]AsyncTCP 库:https://github.com/me-no-dev/AsyncTCP/archive/master.zip
下载后,在 Arduino IDE 中,按 Sketch -> Include Library -> Add .ZIP Library 的次序,然后选择刚刚下载的库导入。由于我没有安装低版本的 Arduino IDE,以是我用 Arduino IDE 2.3 的版本截一个图参考一下:
https://i-blog.csdnimg.cn/direct/e5a47b98b4044b4aa1f43a864c013a0e.png#pic_center
代码分析说明

本示例代码实现了一个基于 ESP32 的 WebSocket 服务器,通过 Web 浏览器控制板载 LED,并及时将 LED 的状态关照其他接入 WebSocket 服务器的设备。该项目利用了非壅闭的异步通信模型,确保了高效的资源管理和响应速度。
完整的代码已经推送至我的代码堆栈,链接在文章结尾,必要的小伙伴可以自取(不会 git 命令的小伙伴,也可以在找我的小助理要代码)。以下是对代码的分段剖析:
库和界说常量

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

const char* ssid   = "Grayson";
const char* password = "Zhengxinyu13@";

bool led_state = 0;
const int led_pin = 2;

AsyncWebServer server(80);
AsyncWebSocket ws("/ws");


[*]包含库:导入了用于 WiFi 连接、异步 TCP 通信以及异步 Web 服务器的库。
[*]常量界说:设定了 WiFi 的 SSID 和密码,以及初始化 LED 状态和引脚。
[*]对象实例化:创建了 AsyncWebServer 和 AsyncWebSocket 对象,分别用于 HTTP 服务和 WebSocket 通信。
HTML 页面模板

const char index_html[] PROGMEM = R"rawliteral(
    <!DOCTYPE html>
    <html>
    <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
      <title>ESP32 WebSocket Server</title>
      <style>
            html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center; }
            body { margin-top: 50px; }
            h1 { color: #444444; margin: 50px auto; }
            p { font-size: 19px; color: #888; }
            #state { font-weight: bold; color: #444; }
            .switch { margin: 25px auto; width: 80px; }
            .toggle { display: none; }
            .toggle + label { display: block; position: relative; cursor: pointer; outline: 0; user-select: none; padding: 2px; width: 80px; height: 40px; background-color: #ddd; border-radius: 40px; }
            .toggle + label:before, .toggle + label:after { display: block; position: absolute; top: 1px; left: 1px; bottom: 1px; content: ""; }
            .toggle + label:before { right: 1px; background-color: #f1f1f1; border-radius: 40px; transition: background .4s; }
            .toggle + label:after { width: 40px; background-color: #fff; border-radius: 20px; box-shadow: 0 2px 5px rgba(0, 0, 0, .3); transition: margin .4s; }
            .toggle:checked + label:before { background-color: #4285f4; }
            .toggle:checked + label:after { margin-left: 42px; }
            .footer-text { font-size: 30px; }
            .bold-text { font-weight: bold !important; }
      </style>
    </head>
    <body>
      <h1>ESP32 WebSocket Server</h1>
      <div class="switch">
            <input id="toggle-btn" class="toggle" type="checkbox" %CHECK%>
            <label for="toggle-btn"></label>
      </div>
      <p>On-board LED: <span id="state">%STATE%</span></p>

      <script>
            window.addEventListener('load', function() {
                var websocket = new WebSocket(`ws://${window.location.hostname}/ws`);
                websocket.onopen = function(event) {
                  console.log('Connection established');
                }
                websocket.onclose = function(event) {
                  console.log('Connection died');
                }
                websocket.onerror = function(error) {
                  console.log('error');
                };
                websocket.onmessage = function(event) {
                  if (event.data == "1") {
                        document.getElementById('state').innerHTML = "ON";
                        document.getElementById('toggle-btn').checked = true;
                  } else {
                        document.getElementById('state').innerHTML = "OFF";
                        document.getElementById('toggle-btn').checked = false;
                  }
                };

                document.getElementById('toggle-btn').addEventListener('change', function() { websocket.send('toggle'); });
            });
      </script>

      <div class="footer">
            <p class="footer-text">This software was written by <span class="bold-text">Grayson Zheng</span>.</p>
      </div>
    </body>
    </html>
)rawliteral";


[*]HTML 页面:界说了一个静态 HTML 页面,包含了样式表和 JavaScript 代码,用于表现一个开关按钮来控制 LED,而且通过 WebSocket 与 ESP32 进行通信。整个代码将在客户端执行。
[*]PROGMEM:利用 PROGMEM 宏,将内容存储在 ESP32 的程序内存(flash memory)而不是 SRAM 中,以优化内存。
WebSocket 消息处置惩罚函数

void handle_websocket_message(void *arg, uint8_t *data, size_t len)
{
    AwsFrameInfo *info = (AwsFrameInfo*)arg;
    if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
      data = 0;
      if (strcmp((char*)data, "toggle") == 0) {
            led_state = !led_state;
            ws.textAll(String(led_state));
      }
    }
}


[*]消息处置惩罚:当收到 “toggle” 消息时,切换 LED 状态,并向所有连接的客户端发送新的 LED 状态。
WebSocket 事件处置惩罚器

void event_handler(AsyncWebSocket *server, AsyncWebSocketClient *client,
                  AwsEventType type, void *arg, uint8_t *data, size_t len)
{
    switch (type) {
      case WS_EVT_CONNECT:
      Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
      break;
      case WS_EVT_DISCONNECT:
      Serial.printf("WebSocket client #%u disconnected\n", client->id());
      break;
      case WS_EVT_DATA:
      handle_websocket_message(arg, data, len);
      digitalWrite(led_pin, led_state);
      break;
      case WS_EVT_PONG:
      case WS_EVT_ERROR:
      break;
    }
}


[*]事件处置惩罚:根据不同的 WebSocket 事件范例(如连接、断开、数据接收等)执行相应操作。具体如下:

[*]WS_EVT_CONNECT:当新客户端连接到 WebSocket 服务器时
[*]WS_EVT_DISCONNECT:当客户端与 WebSocket 服务器断开连接时
[*]WS_EVT_DATA:当 WebSocket 服务器从客户端接收数据时
[*]WS_EVT_PONG:当 WebSocket 收到 ping 请求时
[*]WS_EVT_ERROR:当客户端报告错误时

变量替换处置惩罚器

String processor(const String& var)
{
    if(var == "STATE")
      return led_state ? "ON" : "OFF";

    if(var == "CHECK")
      return led_state ? "checked" : "";

    return String();
}


[*]变量替换:此函数用于在发送 HTML 页面之前,用实际的 LED 状态值替换 HTML 中的占位符(如 %STATE% 和 %CHECK%)。
初始化设置

void setup()
{
    Serial.begin(115200);
    pinMode(led_pin, OUTPUT);
    digitalWrite(led_pin, LOW);

    Serial.print("Connecting to ");
    Serial.println(ssid);
   
    WiFi.begin(ssid, password);
   
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
   
    Serial.println("");
    Serial.println("Connected..!");
    Serial.print("Got IP: ");Serial.println(WiFi.localIP());

    ws.onEvent(event_handler);
    server.addHandler(&ws);

    // Route for root / web page
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send_P(200, "text/html", index_html, processor);
    });

    // Start server
    server.begin();
}


[*]串口配置:开始串口通信,波特率为 115200bps。
[*]GPIO 配置:设置 LED 引脚为输出模式,并将其初始状态设为低电平。
[*]WiFi 连接:尝试连接到指定的 WiFi 网络,并等待直到连接乐成。
[*]WebSocket 和服务器配置:注册 WebSocket 事件处置惩罚器,添加 WebSocket 处置惩罚器到服务器,并为根路径设定 HTTP GET 请求处置惩罚器。
[*]启动服务器:启动异步 Web 服务器。
主循环

void loop()
{
    ws.cleanupClients();
}


[*]清理客户端:定期查抄并清除不再活泼的 WebSocket 客户端连接,保持服务器健康运行。
结果演示

把代码下载到程序后,可以在串口监督器中看到信息输出(如果没有输出,可以按一下复位按钮),ESP32 如果连接 WiFi 乐成,路由器会分配一个 IP,其实就是用了 STA 模式入网。
https://i-blog.csdnimg.cn/direct/c6309f67a052488ab432a793eb4d8f65.png#pic_center
在浏览器输入 ESP32 的 IP 地址并回车,就可以看到对应的 Web 界面了。
https://i-blog.csdnimg.cn/direct/1407b4248982428d90e1e3bdf93b6f73.png#pic_center
具体的演示结果如下:

   

代码链接

GitHub:Simple_ESP32_Web_Server/ESP32_WebSocket at main · zhengxinyu13/Simple_ESP32_Web_Server
Gitee:ESP32_WebSocket · Grayson_Zheng/Simple_ESP32_Web_Server
GitCode:Simple_ESP32_Web_Server - GitCode
不会利用 git 命令克隆代码的小伙伴,可以在我主页找到我助理的地球号,大概直接看下面,找我的助理领取。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 怎样在 Arduino IDE 中利用 WebSockets 创建 ESP32 Web 服务器