在本教程中,您将学习如何使用 WebSocket 通信协议构建具有 ESP8266 的 Web 服务器。例如,我们将向您展示如何构建一个网页来远程控制ESP8266输出。输出状态显示在网页上,并在所有客户端中自动更新。
该ESP8266将使用 Arduino IDE 和 ESPAsyncWebServer 进行编程。我们也有类似的 ESP32 WebSocket 指南。
如果您一直在关注我们以前的一些 Web 服务器项目,例如这个项目,您可能已经注意到,如果您同时打开了多个选项卡(在相同或不同的设备上),除非您刷新网页,否则状态不会在所有选项卡中自动更新。为了解决这个问题,我们可以使用 WebSocket 协议——当发生更改时,所有客户端都可以收到通知,并相应地更新网页。
目录
WebSocket 简介
WebSocket 是客户端和服务器之间的持久连接,允许双方使用 TCP 连接进行双向通信。这意味着您可以在任何给定时间将数据从客户端发送到服务器,并从服务器发送到客户端。
客户端通过称为 WebSocket 握手的过程与服务器建立 WebSocket 连接。握手以 HTTP 请求/响应开始,允许服务器在同一端口上处理 HTTP 连接以及 WebSocket 连接。建立连接后,客户端和服务器可以全双工模式发送 WebSocket 数据。
使用 WebSockets 协议,服务器(ESP8266板)可以在不请求的情况下向客户端或所有客户端发送信息。这也允许我们在发生更改时向 Web 浏览器发送信息。
这种变化可以是网页上发生的事情(您点击了一个按钮),也可以是发生在ESP8266端的事情,例如按下电路上的物理按钮。
项目概况
这是我们将为这个项目构建的网页。
- ESP8266 Web 服务器显示一个网页,其中包含一个用于切换 GPIO 2 状态的按钮;
- 为简单起见,我们控制 GPIO 2 – 板载 LED。您可以使用此示例来控制任何其他 GPIO;
- 该接口显示当前 GPIO 状态。每当GPIO状态发生变化时,接口就会立即更新;
- GPIO 状态在所有客户端中自动更新。这意味着,如果您在同一设备或不同设备上打开了多个 Web 浏览器选项卡,它们都会同时更新。
它是如何工作的?
下图描述了单击“切换”按钮时发生的情况。
当您单击“切换”按钮时,会发生什么情况:
- 点击“切换”按钮;
- 客户端(您的浏览器)通过 WebSocket 协议发送带有“切换”消息的数据;
- ESP8266(服务器)收到此消息,因此它知道它应该切换 LED 状态。如果 LED 之前熄灭,请将其打开;
- 然后,它通过 WebSocket 协议将具有新 LED 状态的数据发送到所有客户端;
- 客户端接收消息并相应地更新网页上的 LED 状态。这使我们能够在发生更改时几乎立即更新所有客户端。
准备Arduino IDE
我们将使用 Arduino IDE 对 ESP8266 板进行编程,因此请确保已将其安装在 Arduino IDE 中。
安装库 – 异步 Web 服务器
为了构建 Web 服务器,我们将使用 ESPAsyncWebServer 库。该库需要 ESPAsyncTCP 库才能正常工作。单击下面的链接下载库。
这些库无法通过Arduino库管理器进行安装,因此您需要将库文件复制到Arduino安装库文件夹。或者,在Arduino IDE中,您可以转到“程序”>“包含库”>“添加.zip库”,然后选择刚刚下载的库。
ESP8266 NodeMCU WebSocket 服务器的代码
将以下代码复制到 Arduino IDE。
在以下变量中插入您的网络凭据,代码将立即起作用。
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
代码的工作原理
继续阅读以了解代码的工作原理,或跳到演示部分。
导入库
导入必要的库以构建 Web 服务器。
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
网络凭据
在以下变量中插入网络凭据:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
GPIO输出
创建一个名为 ledState 的变量来保存 GPIO 状态,并创建一个名为 ledPin 的变量来引用要控制的 GPIO。在本例中,我们将控制板载 LED(连接到 GPIO 2)。
bool ledState = 0;
const int ledPin = 2;
AsyncWebServer 和 AsyncWebSocket
在端口 80 上创建一个 AsyncWebServer 对象。
AsyncWebServer server(80);
ESPAsyncWebServer 库包含一个 WebSocket 插件,可以轻松处理 WebSocket 连接。创建一个名为 ws 的 AsyncWebSocket 对象,以处理 /ws 路径上的连接。
AsyncWebSocket ws("/ws");
构建网页
index_html 变量包含构建网页和设置网页样式以及使用 WebSocket 协议处理客户端-服务器交互所需的 HTML、CSS 和 JavaScript。
注意:我们将构建网页所需的所有内容放在我们在Arduino程序上使用的index_html变量上。请注意,将 HTML、CSS 和 JavaScript 文件分开,然后上传到 ESP8266 文件系统并在代码中引用它们可能更实用。
推荐阅读:使用 SPIFFS ESP8266 Web 服务器(SPI 闪存文件系统)
下面是 index_html 变量的内容:
<!DOCTYPE HTML>
<html>
<head>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2{
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px;
padding-bottom:20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color:#8c8c8c;
font-weight: bold;
}
</style>
<title>ESP Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
</head>
<body>
<div class="topnav">
<h1>ESP WebSocket Server</h1>
</div>
<div class="content">
<div class="card">
<h2>Output - GPIO 2</h2>
<p class="state">state: <span id="state">%STATE%</span></p>
<p><button id="button" class="button">Toggle</button></p>
</div>
</div>
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage; // <-- add this line
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
window.addEventListener('load', onLoad);
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
</body>
</html>
CSS
在<style></style>标记之间,我们包含了使用css设置网页样式的样式。您可以随意更改它,使网页看起来像您所希望的那样。我们不会解释此网页的css是如何工作的,因为它与本WebSocket教程无关。
<style>
html {
font-family: Arial, Helvetica, sans-serif;
text-align: center;
}
h1 {
font-size: 1.8rem;
color: white;
}
h2 {
font-size: 1.5rem;
font-weight: bold;
color: #143642;
}
.topnav {
overflow: hidden;
background-color: #143642;
}
body {
margin: 0;
}
.content {
padding: 30px;
max-width: 600px;
margin: 0 auto;
}
.card {
background-color: #F8F7F9;;
box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5);
padding-top:10px;
padding-bottom:20px;
}
.button {
padding: 15px 50px;
font-size: 24px;
text-align: center;
outline: none;
color: #fff;
background-color: #0f8b8d;
border: none;
border-radius: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
.button:active {
background-color: #0f8b8d;
box-shadow: 2 2px #CDCDCD;
transform: translateY(2px);
}
.state {
font-size: 1.5rem;
color:#8c8c8c;
font-weight: bold;
}
</style>
[HTML全文]
在标签之间,我们添加用户可见的网页内容。
<div class="topnav">
<h1>ESP WebSocket Server</h1>
</div>
<div class="content">
<div class="card">
<h2>Output - GPIO 2</h2>
<p class="state">state: <span id="state">%STATE%</span></p>
<p><button id="button" class="button">Toggle</button></p>
</div>
</div>
标题 1 中包含文本“ESP WebSocket Server”。随意修改该文本。
<h1>ESP WebSocket Server</h1>
然后,有一个标题 2,其中包含“输出 – GPIO 2”文本。
<h2>Output - GPIO 2</h2>
之后,我们有一个显示当前GPIO状态的段落。
<p class="state">state: <span id="state">%STATE%</span></p>
%STATE% 是 GPIO 状态的占位符。它将被发送网页时的ESP8266替换为当前值。HTML 文本上的占位符应介于 % 符号之间。这意味着这个 %STATE% 文本就像一个变量,然后将被替换为实际值。
将网页发送到客户端后,每当 GPIO 状态发生更改时,状态都需要动态更改。我们将通过 WebSocket 协议接收该信息。然后,JavaScript 会处理如何处理接收到的信息,以相应地更新状态。为了能够使用 JavaScript 处理该文本,文本必须具有我们可以引用的 id。在本例中,id 为 state ()。
最后,有一个段落带有切换 GPIO 状态的按钮。
<p><button id="button" class="button">Toggle</button></p>
请注意,我们已经为按钮指定了一个 id (id=“button”)。
JavaScript – 处理 WebSocket
JavaScript 在标记之间移动。它负责在浏览器中完全加载 Web 界面后立即初始化与服务器的 WebSocket 连接,并通过 WebSocket 处理数据交换。
<script>
var gateway = `ws://${window.location.hostname}/ws`;
var websocket;
function initWebSocket() {
console.log('Trying to open a WebSocket connection...');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage; // <-- add this line
}
function onOpen(event) {
console.log('Connection opened');
}
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
window.addEventListener('load', onLoad);
function onLoad(event) {
initWebSocket();
initButton();
}
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
function toggle(){
websocket.send('toggle');
}
</script>
让我们来看看它是如何工作的。
网关是 WebSocket 接口的入口点。
var gateway = `ws://${window.location.hostname}/ws`;
window.location.hostname 获取当前页面地址(Web 服务器 IP 地址)。
创建一个名为 websocket 的新全局变量。
var websocket;
添加一个事件侦听器,该侦听器将在网页加载时调用 onload 函数。
window.addEventListener('load', onload);
onload() 函数调用 initWebSocket() 函数来初始化与服务器的 WebSocket 连接,并使用 initButton() 函数将事件侦听器添加到按钮。
function onload(event) {
initWebSocket();
initButton();
}
initWebSocket() 函数在前面定义的网关上初始化 WebSocket 连接。我们还分配了几个回调函数,用于打开、关闭 WebSocket 连接或接收消息时。
function initWebSocket() {
console.log('Trying to open a WebSocket connection…');
websocket = new WebSocket(gateway);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
}
当连接打开时,我们只需在控制台中打印一条消息并发送一条消息说“嗨”。ESP8266收到该消息,因此我们知道连接已初始化。
function onOpen(event) {
console.log('Connection opened');
websocket.send('hi');
}
如果由于某种原因 Web 套接字连接关闭,我们在 2000 毫秒(2 秒)后再次调用 initWebSocket() 函数。
function onClose(event) {
console.log('Connection closed');
setTimeout(initWebSocket, 2000);
}
setTimeout() 方法在指定的毫秒数后调用函数或计算表达式。
最后,我们需要处理收到新消息时发生的情况。服务器(您的 ESP 板)将发送“1”或“0”消息。根据收到的消息,我们希望在显示状态的段落上显示“ON”或“OFF”消息。还记得那个带有 id=“state” 的标签吗?我们将获取该元素并将其值设置为 ON 或 OFF。
function onMessage(event) {
var state;
if (event.data == "1"){
state = "ON";
}
else{
state = "OFF";
}
document.getElementById('state').innerHTML = state;
}
initButton() 函数通过其 id(按钮)获取按钮,并添加一个类型为“click”的事件侦听器。
function initButton() {
document.getElementById('button').addEventListener('click', toggle);
}
这意味着当您单击该按钮时,将调用切换函数。
toggle 函数使用 WebSocket 连接发送带有“toggle”文本的消息。
function toggle(){
websocket.send('toggle');
}
然后,ESP8266应处理收到此消息时发生的情况 – 切换当前 GPIO 状态。
处理 WebSocket – 服务器
之前,您已经了解了如何在客户端(浏览器)上处理 WebSocket 连接。现在,让我们来看看如何在服务器端处理它。
通知所有客户
notifyClients() 函数通过一条消息通知所有客户端,其中包含您作为参数传递的任何内容。在这种情况下,每当发生更改时,我们都会通知所有客户端当前 LED 状态。
void notifyClients() {
ws.textAll(String(ledState));
}
AsyncWebSocket 类提供了一个 textAll() 方法,用于将相同的消息发送到同时连接到服务器的所有客户端。
处理 WebSocket 消息
handleWebSocketMessage() 函数是一个回调函数,每当我们通过 WebSocket 协议从客户端接收新数据时,它就会运行。
void handleWebSocketMessage(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[len] = 0;
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
}
}
如果我们收到 “toggle” 消息,我们将切换 ledState 变量的值。此外,我们通过调用 notifyClients() 函数来通知所有客户端。这样,所有客户端都会收到更改通知,并相应地更新界面。
if (strcmp((char*)data, "toggle") == 0) {
ledState = !ledState;
notifyClients();
}
配置 WebSocket 服务器
现在,我们需要配置一个事件侦听器来处理 WebSocket 协议的不同异步步骤。可以通过定义 onEvent() 来实现此事件处理程序,如下所示:
void onEvent(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 %sn", client->id(), client->remoteIP().toString().c_str());
break;
case WS_EVT_DISCONNECT:
Serial.printf("WebSocket client #%u disconnectedn", client->id());
break;
case WS_EVT_DATA:
handleWebSocketMessage(arg, data, len);
break;
case WS_EVT_PONG:
case WS_EVT_ERROR:
break;
}
}
type 参数表示发生的事件。它可以采用以下值:
- WS_EVT_CONNECT客户端登录时;
- WS_EVT_DISCONNECT客户端注销时;
- WS_EVT_DATA从客户端接收到数据包时;
- WS_EVT_PONG响应 ping 请求;
- WS_EVT_ERROR从客户端收到错误时。
初始化 WebSocket
最后,initWebSocket() 函数初始化 WebSocket 协议。
void initWebSocket() {
ws.onEvent(onEvent);
server.addHandler(&ws);
}
processor()
processor() 函数负责在 HTML 文本上搜索占位符,并在将网页发送到浏览器之前将它们替换为我们想要的任何内容。在我们的例子中,如果 ledState 为 1,我们将 %STATE% 占位符替换为 ON。否则,请将其替换为 OFF。
String processor(const String& var){
Serial.println(var);
if(var == "STATE"){
if (ledState){
return "ON";
}
else{
return "OFF";
}
}
}
setup()
在 setup() 中,初始化串行监视器以进行调试。
Serial.begin(115200);
将 ledPin 设置为 OUTPUT,并在程序首次启动时将其设置为 LOW。
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, LOW);
初始化 Wi-Fi 并在串行监视器上打印ESP8266 IP 地址。
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP Local IP Address
Serial.println(WiFi.localIP());
通过调用之前创建的 initWebSocket() 函数来初始化 WebSocket 协议。
initWebSocket();
处理请求
当您在根/URL 上收到请求时,提供保存在 index_html 变量上的文本 – 您需要将处理器函数作为参数传递,以将占位符替换为当前 GPIO 状态。
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", index_html, processor);
});
最后,启动服务器。
server.begin();
loop()
LED 将在 loop() 上进行物理控制。
void loop() {
ws.cleanupClients();
digitalWrite(ledPin, ledState);
}
请注意,我们都调用 cleanupClients() 方法。原因如下(来自 ESPAsyncWebServer 库 GitHub 页面的解释):
浏览器有时无法正确关闭 WebSocket 连接,即使在 JavaScript 中调用 close() 函数也是如此。这最终会耗尽 Web 服务器的资源,并导致服务器崩溃。定期从 main loop() 调用 cleanupClients() 函数,通过在超过最大客户端数时关闭最旧的客户端来限制客户端数量。这可以每个周期调用,但是,如果您希望使用更少的功率,那么每秒调用一次的频率就足够了。
示范
在 ssid 和 password 变量上插入网络凭据后,您可以将代码上传到您的主板。不要忘记检查您是否选择了正确的电路板和 COM 端口。
上传代码后,以 115200 的波特率打开串行监视器,然后按下板载 EN/RST 按钮。应打印 ESP IP 地址。
在本地网络上打开浏览器并插入ESP8266 IP 地址。您应该可以访问网页以控制输出。
单击按钮以切换 LED。您可以同时打开多个 Web 浏览器选项卡或从不同的设备访问 Web 服务器,每当有更改时,LED 状态都会在所有客户端中自动更新。
总结
在本教程中,你学习了如何使用 ESP8266 设置 WebSocket 服务器。WebSocket 协议允许客户端和服务器之间进行全双工通信。初始化后,服务器和客户端可以在任何给定时间交换数据。
这非常有用,因为每当发生某些事情时,服务器都可以向客户端发送数据。例如,您可以向此设置添加一个物理按钮,按下该按钮会通知所有客户端更新 Web 界面。
在此示例中,我们向您展示了如何控制ESP8266的一个 GPIO。您可以使用此方法控制更多 GPIO。您还可以使用 WebSocket 协议在任何给定时间发送传感器读数或通知。