用户登录
用户注册

分享至

Arduino for ESP8266&ESP32适用库ESPAsyncWebServer:WebSocket和EventSource

  • 作者: 一撸定乾坤
  • 来源: 51数据库
  • 2021-07-08

文章目录

  • 目的
  • WebSocket
    • 使用方法
    • 使用演示
    • 更多说明
  • EventSource
    • 使用方法
    • 使用演示
    • 更多说明
  • 总结

目的

WebSocket和EventSource是HTML5开始提供的功能。WebSocket可以在单个TCP连接上进行全双工通讯;EventSource可以由服务器主动向客户端推送消息。两个功能可以大大提升web应用的数据交互的性能。这篇文章将介绍ESPAsyncWebServer库中这两个功能的使用方式。

本文中各例程演示均在ESP32中进行。

WebSocket

使用方法

WebSocket使用并不复杂,除了正常的声明与初始化AsyncWebServer对象外,只需下面几步即可:

  • 声明 AsyncWebSocket 对象与URL;
  • 绑定 AsyncWebSocket 对象事件回调函数;
  • AsyncWebSocket 对象添加到服务器中;

使用演示

使用下面代码进行测试:

#include <WiFi.h>
#include <ESPAsyncWebServer.h> //引入相应库

const char *ssid = "********";
const char *password = "********";

// 以下为网页文件
String indexhtml = String("") +
                   "<!DOCTYPE html>\n" +
                   "<head>\n" +
                   "    <meta charset="UTF-8">\n" +
                   "    <title>WebSocket Test</title>\n" +
                   "    <script>\n" +
                   "        var ws;\n" +
                   "        if ("WebSocket" in window) {\n" +
                   "            ws = new WebSocket("ws://" + window.location.host + "/"); // 建立WebSocket连接\n" +
                   "            ws.onopen = function () { // 连接建立成功时触发\n" +
                   "                document.getElementById("info").innerHTML += "WebSocket连接成功!" + "<br>";\n" +
                   "                ws.send("connect ok!"); // 向服务器发送数据\n" +
                   "            };\n" +
                   "            ws.onmessage = function (evt) { // 收到服务器数据时触发\n" +
                   "                document.getElementById("info").innerHTML += evt.data + "<br>";\n" +
                   "            };\n" +
                   "            ws.onerror = function () { // 发生错误时触发\n" +
                   "                document.getElementById("info").innerHTML += "通讯发送错误!" + "<br>";\n" +
                   "            };\n" +
                   "            ws.onclose = function () { // 连接关闭时触发\n" +
                   "                document.getElementById("info").innerHTML += "WebSocketTest连接已关闭!" + "<br>";\n" +
                   "            };\n" +
                   "        }\n" +
                   "        else {\n" +
                   "            document.getElementById("info").innerHTML = "浏览器不支持 WebSocket!";\n" +
                   "        }\n" +
                   "        function sned() {\n" +
                   "            ws.send("hahaha~~~"); // 向服务器发送数据\n" +
                   "        }\n" +
                   "    </script>\n" +
                   "</head>\n" +
                   "<body>\n" +
                   "    <button type="button" οnclick=sned()>点击向服务器发送数据</button>\n" +
                   "    <div id="info"></div>\n" +
                   "</body>\n" +
                   "</html>\n";

AsyncWebServer server(80); // 声明WebServer对象

AsyncWebSocket ws("/"); // WebSocket对象,url为/

// WebSocket事件回调函数
void onEventHandle(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{
  if (type == WS_EVT_CONNECT) // 有客户端建立连接
  {
    Serial.printf("ws[%s][%u] connect\n", server->url(), client->id());
    client->printf("Hello Client %u !", client->id()); // 向客户端发送数据
    client->ping();                                    // 向客户端发送ping
  }
  else if (type == WS_EVT_DISCONNECT) // 有客户端断开连接
  {
    Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id());
  }
  else if (type == WS_EVT_ERROR) // 发生错误
  {
    Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t *)arg), (char *)data);
  }
  else if (type == WS_EVT_PONG) // 收到客户端对服务器发出的ping进行应答(pong消息)
  {
    Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char *)data : "");
  }
  else if (type == WS_EVT_DATA) // 收到来自客户端的数据
  {
    AwsFrameInfo *info = (AwsFrameInfo *)arg;
    Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT) ? "text" : "binary", info->index, info->index + len);
    data[len] = 0;
    Serial.printf("%s\n", (char *)data);
  }
}

void setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  ws.onEvent(onEventHandle); // 绑定回调函数
  server.addHandler(&ws);    // 将WebSocket添加到服务器中

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { //注册链接"/lambda"与对应回调函数(匿名函数形式声明)
    request->send(200, "text/html", indexhtml);                //向客户端发送响应和内容
  });

  server.begin(); //启动服务器

  Serial.println("Web server started");
}

void loop()
{
  delay(2000);
  ws.textAll("lalala~~~"); // 向所有建立连接的客户端发送数据
  ws.cleanupClients(); // 关闭过多的WebSocket连接以节省资源
}


上面就是一个使用WebSocket进行双向通讯的例子,所有的关键步骤都在上面代码中写了注释。可以看到使用WebSocket进行数据交互比传统的用Ajax的方式要方便多了。

更多说明

WebSocket用于双向通讯,服务器端接收来自客户端的消息上面已经进行了演示,是在事件回调函数中进行的,同时在回调函数中也可以通过AsyncWebSocketClient对象发送消息,主要方式如下:

  • 格式化输出方式:
    size_t printf(const char *format, ...) __attribute__ ((format (printf, 2, 3)));
    size_t printf_P(PGM_P formatP, ...) __attribute__ ((format (printf, 2, 3))); // ESP32特有
  • 文本数据输出方式:
    void text(const char * message, size_t len);
    void text(const char * message);
    void text(uint8_t * message, size_t len);
    void text(char * message);
    void text(const String &message);
    void text(const __FlashStringHelper *data);
    void text(AsyncWebSocketMessageBuffer *buffer);
  • 二进制数据输出方式:
    void binary(const char * message, size_t len);
    void binary(const char * message);
    void binary(uint8_t * message, size_t len);
    void binary(char * message);
    void binary(const String &message);
    void binary(const __FlashStringHelper *data, size_t len);
    void binary(AsyncWebSocketMessageBuffer *buffer);

AsyncWebSocketClient对象除了可以用来发送数据,还可以获得一些信息 IPAddress remoteIP();uint16_t remotePort();,可以使用 void ping(uint8_t *data=NULL, size_t len=0); 方法向客户端发送ping信息,可以使用 void keepAlivePeriod(uint16_t seconds) 设置定时自动发送ping。最后可以用 void close(uint16_t code=0, const char * message=NULL); 方法关闭当前建立的连接。

除了上面客户端对象可以用来发送数据,AsyncWebSocket对象自身也可以发送数据,发送方法和客户端对象差不多,只不过第一个参数新增了客户端id:

  • size_t printf(uint32_t id, const char *format, ...) __attribute__ ((format (printf, 3, 4)));
    ……
  • void text(uint32_t id, const char * message, size_t len);
    ……
  • void binary(uint32_t id, const char * message, size_t len);
    ……

此外AsyncWebSocket对象还可以同时向全体已连接的客户端发送消息,方法和上面的差不多,主要是不需要id,然后方法名后跟上All:

  • size_t printfAll(const char *format, ...) __attribute__ ((format (printf, 2, 3)));
    ……
  • void textAll(const char * message, size_t len);
    ……
  • void binaryAll(const char * message, size_t len);
    ……

AsyncWebSocket对象除了上面这些发送消息的功能外还有一些其它方法,可以用 size_t count() const; 获取已连接客户端数量,可以用 bool hasClient(uint32_t id){ return client(id) != NULL; } 判断是否有指定客户端,可以用 void ping(uint32_t id, uint8_t *data=NULL, size_t len=0);void pingAll(uint8_t *data=NULL, size_t len=0); 发送ping信息。最后可以用 void close(uint32_t id, uint16_t code=0, const char * message=NULL);void closeAll(uint16_t code=0, const char * message=NULL); 关闭客户端连接。
另外对于AsyncWebSocket对象还有一个特殊的方法:void cleanupClients(uint16_t maxClients = DEFAULT_MAX_WS_CLIENTS);。该方法上面演示中也有用到,依据建立事件,从最早的开始关闭超出设定数量的客户端连接。(主要是芯片性能有限,无限制的建立长连接可能就无法正常工作了。默认情况下ESP32可以建立32个连接、ESP8266可以建立8个连接。)

EventSource

使用方法

EventSource的使用比较简单,除了正常的声明与初始化AsyncWebServer对象外,只要下面几步就可以使用了:

  • 声明 AsyncEventSource 对象与URL;
  • 绑定 AsyncEventSource 对象事件回调函数; (可选)
  • AsyncEventSource 对象添加到服务器中;

使用演示

使用下面代码进行测试:

#include <WiFi.h>
#include <ESPAsyncWebServer.h> //引入相应库

const char *ssid = "********";
const char *password = "********";

// 以下为网页文件
String indexhtml = String("") +
                   "<!DOCTYPE html>\n" +
                   "<head>\n" +
                   "    <meta charset="UTF-8">\n" +
                   "    <title>EventSource Test</title>\n" +
                   "</head>\n" +
                   "<body>\n" +
                   "    <div id="info"></div>\n" +
                   "    <script>\n" +
                   "        if (typeof (EventSource) !== "undefined") {\n" +
                   "            var source = new EventSource("/es"); // 建立EventSource连接\n" +
                   "            source.onopen = function () { // 连接建立成功时触发\n" +
                   "                document.getElementById("info").innerHTML += "EventSource连接成功!" + "<br>";\n" +
                   "            };\n" +
                   "            source.onmessage = function (event) { // 收到服务器数据时触发\n" +
                   "                document.getElementById("info").innerHTML += event.data + "<br>";\n" +
                   "            };\n" +
                   "            source.onerror = function () { // 发生错误时触发\n" +
                   "                document.getElementById("info").innerHTML += "通讯发送错误!" + "<br>";\n" +
                   "            };\n" +
                   "        }\n" +
                   "        else {\n" +
                   "            document.getElementById("info").innerHTML = "浏览器不支持 EventSource!";\n" +
                   "        }\n" +
                   "    </script>\n" +
                   "</body>\n" +
                   "</html>\n";

AsyncWebServer server(80); // 声明WebServer对象

AsyncEventSource events("/es"); // EventSource对象,url为/es; 注意EventSource和Http是共用url的

// EventSource事件回调函数
void onEventHandle(AsyncEventSourceClient *client)
{
  Serial.printf("Client reconnected! Last message ID that it gat is: %u\n", client->lastId());
  // 向客户端发送hello!
  // events.count()为已经结客户端数,这里用作给客户端的id号
  // 1000表示告诉客户端如果连接断开则再1000毫秒后尝试重新连接
  client->send("hello!", NULL, events.count(), 1000);
}

void setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  events.onConnect(onEventHandle); // 绑定当有客户端连接时的回调函数
  server.addHandler(&events);      // 将EventSource添加到服务器中

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { //注册链接"/lambda"与对应回调函数(匿名函数形式声明)
    request->send(200, "text/html", indexhtml);                 //向客户端发送响应和内容
  });

  server.begin(); //启动服务器

  Serial.println("Web server started");
}

void loop()
{
  delay(2000);
  events.send("lalala~~~"); // 向所有已连接的客户端推送消息 // 也可以用events.send("lalala~~~", NULL, events.count(), 1000);
}


上面就是一个使用EventSource进行通讯的例子,所有的关键步骤都在上面代码中写了注释。可以看到使用EventSource功能服务器可以主动向客户端推送数据。

更多说明

EventSource相比WebSocket简单很多,在完成基本的初始化工作后就只有一个向全体客户端发送消息的功能:
void send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0);
另外AsyncEventSource对象还有 void close();size_t count() const; 等少数几个不常用的方法。

在AsyncEventSource对象的onConnect事件回调函数中我们也可以用下面的方法向触发该事件的客户端发送消息:
client->write(const char * message, size_t len);
client->send(const char *message, const char *event=NULL, uint32_t id=0, uint32_t reconnect=0);

总结

ESPAsyncWebServer的WebSocket和EventSource主要就是上面这些内容,更多内容可以查看项目官方的文档和例程,或者我的其他ESPAsyncWebServer的文章:
ESPAsyncWebServer项目地址:https://github.com/me-no-dev/ESPAsyncWebServer
《Arduino for ESP8266&ESP32适用库ESPAsyncWebServer:快速入门》
《Arduino for ESP8266&ESP32适用库ESPAsyncWebServer:事件处理绑定》
《Arduino for ESP8266&ESP32适用库ESPAsyncWebServer:请求与响应》
《Arduino for ESP8266&ESP32适用库ESPAsyncWebServer:静态文件和模板引擎》
《Arduino for ESP8266&ESP32适用库ESPAsyncWebServer:WebSocket和EventSource》

软件
前端设计
程序设计
Java相关