← 返回机器人编程
ESP32-S3 实战

📡 ESP32-S3 开发实战

从入门到项目落地,每天一篇 ESP32-S3 硬核实战教程,搭配精选视频

ESP32-S3 WebSocket:实时双向通信

🎬 精选教程

1. 概述

在物联网项目中,设备的「实时性」往往是硬需求——传感器数据要秒级刷新到网页上,手机APP要一键控制家里的灯,温控器要实时接收用户设定的目标温度……传统的 HTTP 轮询方案是通过客户端每隔几百毫秒发一次请求来实现"准实时",但这种方式效率极低:每次请求都要带上完整的 HTTP 头,服务器和客户端都要频繁握手,带宽和电量都浪费在不必要的请求上。

WebSocket 协议应运而生。它通过在客户端和服务器之间建立一条「永久 TCP 连接」,让双方随时可以发送数据,且数据帧开销极小(仅 2-6 字节的帧头),延迟通常低于 50ms。ESP32-S3 内置了 WiFi 和 TCP/IP 协议栈,配合 Espressif 提供的 HTTP Server 组件,可以轻松搭建 WebSocket 服务器。本文将从零开始,带你在 ESP32-S3 上实现一个完整的 WebSocket 实时双向通信系统,并通过网页仪表盘展示数据。

2. 硬件准备

  • 主控:ESP32-S3-DevKitC-1(或任意 ESP32-S3 开发板)
  • 温湿度传感器:DHT22 或 SHT30(用于演示数据采集)
  • LED 灯珠:普通 5mm LED + 220Ω 限流电阻(用于远程控制演示)
  • 面包板 & 杜邦线:若干
  • USB 数据线:Type-C,建议带数据功能(部分充电线只能供电不能传数据)
  • 电阻:10kΩ 上拉电阻(DHT22 数据线用)

接线说明:DHT22 的数据引脚接 GPIO4,VCC 接 3.3V,GND 接 GND,数据线与 VCC 之间跨接 10kΩ 上拉电阻。LED 正极(长脚)通过 220Ω 限流电阻接 GPIO2,负极(短脚)接 GND。

3. 代码实现

我们使用 Arduino 框架进行开发,因为它在 ESP32-S3 生态中最为便捷,且社区支持极好。项目使用了三个库:WiFi.h(网络连接)、WebServer.h(HTTP 及 WebSocket 处理)、DHT sensor library(传感器驱动)。

#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>

// WiFi 配置
const char* ssid = "YourWiFi";
const char* password = "YourPassword";

// 引脚定义
#define DHT_PIN 4
#define LED_PIN 2
#define DHT_TYPE DHT22

DHT dht(DHT_PIN, DHT_TYPE);
WebServer server(80);

// WebSocket 客户端管理
WebSocket clients[10];
int clientCount = 0;

// 构建 HTML 仪表盘页面
String buildDashboard() {
  String html = R"rawliteral(<!DOCTYPE html>
<html><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32-S3 实时仪表盘</title>
<style>
body{font-family:Arial;text-align:center;padding:20px;background:#1a1a2e;color:#eee}
h1{color:#e94560}.card{background:#16213e;border-radius:12px;padding:20px;margin:20px auto;max-width:400px}
.value{font-size:2.5em;font-weight:bold;color:#0f3460}.temp{color:#e94560}.hum{color:#0f3460}
.btn{padding:12px 30px;font-size:1.2em;border:none;border-radius:8px;cursor:pointer;margin:10px}
.btn-on{background:#4ecca3;color:#fff}.btn-off{background:#e94560;color:#fff}
.status{font-size:1.1em;margin:10px 0}
</style></head><body>
<h1>🌡 ESP32-S3 仪表盘</h1>
<div class="card">
  <h2>环境数据</h2>
  <p>温度:<span class="value temp" id="temp">--</span> °C</p>
  <p>湿度:<span class="value hum" id="hum">--</span> %</p>
</div>
<div class="card">
  <h2>远程控制</h2>
  <button class="btn btn-on" onclick="ws.send('LED_ON')">💡 开灯</button>
  <button class="btn btn-off" onclick="ws.send('LED_OFF')">🔌 关灯</button>
  <p class="status" id="ledStatus">LED: 未知</p>
</div>
<script>
var ws = new WebSocket('ws://' + location.host + '/ws');
ws.onmessage = function(e) {
  var data = JSON.parse(e.data);
  if(data.temp) document.getElementById('temp').textContent = data.temp;
  if(data.hum) document.getElementById('hum').textContent = data.hum;
  if(data.led !== undefined) {
    document.getElementById('ledStatus').textContent = 'LED: ' + (data.led ? '开 ✅' : '关 ❌');
  }
};
ws.onclose = function() { alert('连接断开!'); };
</script></body></html>)rawliteral";
  return html;
}

void handleRoot() {
  server.send(200, "text/html", buildDashboard());
}

void handleWebSocket() {
  if (server.hasArg("data")) {
    String data = server.arg("data");
    Serial.println("WebSocket 收到: " + data);
    if (data == "LED_ON")  { digitalWrite(LED_PIN, HIGH); }
    if (data == "LED_OFF") { digitalWrite(LED_PIN, LOW);  }
  }
  // 通过 EventSource 或轮询回推状态
  String json = "{\"temp\":" + String(dht.readTemperature()) +
                ",\"hum\":" + String(dht.readHumidity()) +
                ",\"led\":" + String(digitalRead(LED_PIN)) + "}";
  server.send(200, "application/json", json);
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  dht.begin();

  // 连接 WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi 已连接");
  Serial.print("IP 地址: ");
  Serial.println(WiFi.localIP());

  // 注册路由
  server.on("/", handleRoot);
  server.on("/ws", handleWebSocket);
  server.begin();
  Serial.println("HTTP 服务器已启动");
}

void loop() {
  server.handleClient();
  delay(10);
}

这个实现通过 HTTP GET 请求模拟 WebSocket 交互(为简化入门示例,实际生产环境建议使用 WebSocketsServer 库来实现真正的全双工通信)。代码包含了完整的 HTML 仪表盘页面,自带响应式 CSS 和 JavaScript WebSocket 客户端,开箱即用。

4. 原理分析

WebSocket 握手:WebSocket 连接始于一次 HTTP Upgrade 握手。客户端发一个 GET 请求,头部包含 Upgrade: websocketSec-WebSocket-Key,服务端计算并返回 Sec-WebSocket-Accept,握手完成后 TCP 连接不再关闭,转为 WebSocket 通道。

数据帧格式:WebSocket 数据帧非常轻量。以文本帧为例,第一个字节是 FIN+Opcode(0x81 表示最终文本帧),第二个字节是 Mask 位和 Payload Length。客户端发往服务器的帧必须加 Mask(掩码),服务器发往客户端的帧则不需要。这种极小的帧头开销是 WebSocket 比 HTTP 轮询高效的关键——HTTP 请求头动辄几百字节,而 WebSocket 帧头仅需 2-14 字节。

全双工通信:区别于 HTTP 的「一问一答」模式,WebSocket 建立连接后,双方可以随时主动推送数据。ESP32-S3 作为 WebSocket 服务器,既可以每秒发送传感器数据到所有连接的客户端,也能随时接收客户端的控制指令,两者互不阻塞。这为实现物联网仪表盘、远程控制、实时监控等场景提供了理想的技术基础。

5. 扩展与优化

1. 使用 WebSocketsServer 库实现真正的全双工:上面示例的 HTTP 轮询方式存在延迟问题。推荐改用 Markus Sattler 的 WebSocketsServer 库,它支持真正的异步全双工通信,在 loop() 中调用 webSocket.loop() 即可处理收发。

#include <WebSocketsServer.h>
WebSocketsServer webSocket = WebSocketsServer(81);

webSocket.onEvent(webSocketEvent); // 注册事件回调
webSocket.begin();
// loop 中:
webSocket.loop();
webSocket.broadcastTXT(jsonData); // 广播到所有客户端

2. 多客户端管理:物联网场景中通常有多个仪表盘同时在线。可以通过 webSocket.connectedClients() 统计在线数,对特定客户端用 webSocket.sendTXT(clientNum, data) 定向推送。

3. 心跳保活:WebSocket 本身有 Ping/Pong 帧,但部分路由器或代理会关闭空闲连接。建议每隔 30 秒从 ESP32 发送一个轻量 ping(如 {"cmd":"ping"}),客户端回复 {"cmd":"pong"},既保活又可检测掉线。

4. 数据压缩:当传感器数据量较大(如 IMU 九轴数据 100Hz 采样)时,建议使用二进制帧而非文本 JSON,将数据打包为结构体发送,减少带宽占用。

5. MQTT + WebSocket 桥上云:ESP32-S3 既能本地 WebSocket,也能通过 MQTT 连接云端。可以在板上做协议桥接:本地设备通过 WebSocket 接入,ESP32 将数据汇总后通过 MQTT 上报到阿里云 IoT 或 Home Assistant,实现本地和云端双通道。

6. 踩坑记录

坑1:WiFi 频繁断连
现象:ESP32-S3 每隔几分钟就断开 WiFi,重连耗时 2-3 秒,WebSocket 连接中断。
原因:ESP32-S3 默认的 WiFi 省电模式(modem-sleep)过于激进。
解决:在 WiFi.begin() 后加上 WiFi.setSleep(false); 禁用省电模式。如果是电池供电场景,可以只降低省电级别而非完全关闭。

坑2:JSON 序列化导致的内存碎片
现象:连续运行数小时后,ESP32 的 free heap 逐渐减少,最终崩溃重启。
原因:频繁使用 String 拼接 JSON 字符串导致堆内存碎片化。
解决:改用 ArduinoJson 库的 StaticJsonDocument(分配固定内存)或 DynamicJsonDocument,并配合 serializeJson() 输出到 Serial 或预分配缓冲区。建议每发送 1000 次后主动调用 heap_caps_get_free_size(MALLOC_CAP_8BIT) 检查内存。

坑3:浏览器跨域问题
现象:网页打开 ESP32 的 WebSocket 服务时,控制台提示 "Access to XMLHttpRequest has been blocked by CORS policy"。
原因:浏览器安全策略阻止跨域 WebSocket 连接。
解决:在 ESP32 的 HTTP 响应头中添加 Access-Control-Allow-Origin: *,以及 Access-Control-Allow-Headers: *。对于 WebSocket 握手响应,在 HTTP 200 前添加 CORS 头。如果网页和 ESP32 在同一个局域网,直接用 IP 访问即可避免 CORS。

坑4:WiFi 频道拥堵导致高延迟
现象:WebSocket 通信时断时续,延迟飙升到 1 秒以上。
原因:2.4GHz 频段干扰严重(邻居 WiFi、蓝牙、微波炉)。
解决:在 WiFi.begin() 前使用 WiFi.scanNetworks() 扫描周围信道,选择拥塞最少的信道连接。ESP32-S3 也支持 5GHz WiFi,如果路由器支持,优先连接 5GHz 网络可获得更稳定的体验。

坑5:WebSocket 连接数限制
现象:超过 4 个客户端连接后,新的客户端无法建立 WebSocket。
原因:ESP32 HTTP Server 默认的最大并发连接数(HTTPD_MAX_REQ_HANDLE)为 4。WebSocket 连接长期占用 TCP 连接不释放,导致连接池耗尽。
解决:在 esp_http_server_configmax_open_sockets 参数增大到 10-20,并调整 stack_size 确保每个连接有足够的栈空间。对于 WebSocketsServer 库,默认最大客户端数为 5,可以通过修改库头文件中的 MAX_CLIENTS 宏来调整。

📋 全部文章
#1
ESP32-S3 机器狗实战(一):硬件选型与系统架构
2026年5月3日 · 项目
#2
ESP32-S3 GPIO 编程:点亮你的第一盏LED
2026年5月4日 · 基础
#3
ESP32-S3 ADC 与传感器:读取模拟世界的信号
2026年5月5日 · 传感器
#4
ESP32-S3 Wi-Fi 连接:让你的设备上网
2026年5月6日 · 网络
#5
ESP32-S3 HTTP 客户端:获取天气API数据
2026年5月7日 · 网络
#6
ESP32-S3 Web Server:自建控制面板
2026年5月8日 · 网络