尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ESP32 WebSocket开发:C/C++选型、服务端搭建与调试实战

ESP32 WebSocket开发:C/C++选型、服务端搭建与调试实战 简介一份面向ESP32开发者的WebSocket通信实现源码包覆盖基于C/C建立WebSocket客户端与服务器的完整流程适合物联网项目或需要实时双向通信的开发者参考。资源共8个文件包含C源码、头文件、Makefile配置、sdkconfig以及Markdown说明压缩包仅15KB轻量易用便于对照代码与配置快速上手。内容以基础应用为核心从引入WebSocket库开始到回调函数处理、连接管理与数据收发均有涉及。开发者可直接参考或修改示例代码快速验证双向通信功能配置文件能减少环境搭建时间Markdown说明则梳理了关键步骤与注意事项。目前已有826人学习。无论是刚接触ESP32的初学者还是希望完善项目实时性的进阶用户都能从中获得可落地的参考思路。1. ESP32 WebSocket 开发指南选 C 还是 C 之前先看链路设备端凡是做「状态实时上收 指令下推」的功能第一反应往往是 HTTP 轮询。轮询在 ESP32 上很快会碰到两个硬问题一是 RAM 和 WiFi 功耗撑不住 1 秒一次的 GET二是服务器侧要等断路器超时、要维护大量半开连接。WebSocket 在 ESP32 上的价值不是省几条消息而是把连接从「每次重建」变成「一条长连接上的双向帧」握手只发生在建立时后续数据走数据帧这正是嵌入式设备配网页、配 App 时最常用的实时通道。用 C 还是 C 取决于你手里的 SDK。ESP-IDF 的官方组件本来就是 C 接口纯 C 写服务端和客户端都顺如果项目里已有 C 业务层把 WebSocket 封装成类也不难。真正决定开发体验的是库的选型和帧处理的边界条件。这篇文章就沿着「库选择 → 最小服务端 → 客户端直连 → 调试手段」这条路线把 ESP32 上 WebSocket 的建立过程完整过一遍。2. C/C 场景下 ESP32 的 WebSocket 选型HTTP Server、官方客户端还是 Mongoose2.1 ESP-IDF 自带的 WebSocket 能力到底覆盖到什么程度ESP-IDF 从 4.x 开始就把 WebSocket 拆成两块服务端挂在esp_http_server组件下走的是httpd_ws_*接口客户端在esp_websocket_client组件下走的是esp_websocket_client_*接口。注意这两个是独立组件服务端不需要额外引入客户端组件反过来也一样。很多刚接触的人会误以为 WebSocket 是一个独立协议栈其实它在 ESP-IDF 里仍然是 LwIP 之上的一层应用协议。握手阶段用 HTTP Upgrade之后帧传输直接走 TCP所以选型时不需要引入额外协议栈只要把下面这个关系想清楚角色组件核心 API依赖服务端esp_http_serverhttpd_register_uri_handlerhttpd_ws_recv_framembedTLSwss 时客户端esp_websocket_clientesp_websocket_client_start 事件回调mbedTLSwss 时双端通用Mongoosemg_ws_connect/mg_wss_listen自备 TCP 栈如果你的需求是「ESP32 自己开一个 /ws 端点网页或手机 App 来连」esp_http_server是首选因为它和 HTTP 文件服务、REST API 共用同一个 server 句柄不用额外管理端口和 socket。如果需求是「ESP32 作为客户端去连云端 WebSocket 网关」那就用esp_websocket_client它内部已经把重连、心跳、TLS 握手封装好了。2.2 第三方库 Mongoose 什么时候才值得用Mongoose 在嵌入式 WebSocket 圈子里热度很高它的优势是跨平台同一套 C 代码能在 ESP32、Linux、MCU 裸机上跑。如果你项目里有部分模块跑在非 ESP32 平台上或者你不想依赖 IDF 的组件版本节奏Mongoose 可以保证协议行为一致。但用它要付成本。Mongoose 的 socket 事件循环和 IDF 的事件循环是两套机制接入时你得把mg_mgr_poll放到一个任务里循环调用这相当于自己维护一个网络线程而用esp_websocket_client时事件分发走 IDF 的 event loop和系统其它部分天然衔接。我的建议是只有双端代码需要复用、或者现有代码已经是 Mongoose 体系时才引入否则 IDF 自带方案足够。2.3 工程配置里必须动的那几个开关无论哪条路线menuconfig里有几个配置直接影响 WebSocket 能不能建立CONFIG_HTTPD_MAX_OPEN_SOCKETSesp_http_server 最大并发 socket 数默认值偏小开了 WebSocket 后每个连接要占一个 socket连接数不够会握手失败。CONFIG_HTTPD_WS_SUPPORT老版本 IDF 需要显式打开 WebSocket 支持新版本默认开但升级 SDK 后最好确认一次。CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN只用 ws:// 不用管用 wss:// 时这个值影响 TLS 握手阶段可一次性读入的数据长度。设置完这些之后编译环境里要注意components路径。用idf.py create-project建立工程时WebSocket 相关头文件都在默认组件里不需要额外EXTRA_COMPONENT_DIRS。3. 用 C 在 ESP32 上实现最小 WebSocket 服务端帧接收与发送3.1 注册 WebSocket URI Handler 的完整 C 代码先写一个能跑起来的服务端骨架。假设你已经通过esp_netif_init和esp_wifi_start拿到了 IP接下来在httpd_start之后注册一个/ws端点#include esp_http_server.h #include esp_log.h static const char *TAG ws_server; static esp_err_t ws_handler(httpd_req_t *req) { if (req-method HTTP_GET) { // GET 到达这里时说明是 WebSocket 握手请求 // esp_http_server 内部会完成 Upgrade 应答此处只做日志 ESP_LOGI(TAG, WebSocket handshake from %s, req-host); return ESP_OK; } // 走数据帧处理 httpd_ws_frame_t ws_pkt {0}; size_t buf_len 0; // 第一次调用不传 payload只拿帧头里的长度 esp_err_t ret httpd_ws_recv_frame(req, ws_pkt, buf_len); if (ret ! ESP_OK) { ESP_LOGE(TAG, recv frame header failed: %s, esp_err_to_name(ret)); return ret; } if (buf_len 0) { // 空帧可能是 ping回一个空 pong 保持连接 httpd_ws_frame_t pong { .fin true, .type HTTPD_WS_TYPE_PONG, .payload NULL, .len 0 }; return httpd_ws_send_frame(req, pong); } // 再分配实际 payload 缓冲 char *buf calloc(1, buf_len 1); if (buf NULL) { return ESP_ERR_NO_MEM; } ws_pkt.payload (uint8_t *)buf; ret httpd_ws_recv_frame(req, ws_pkt, buf_len); if (ret ! ESP_OK) { free(buf); ESP_LOGE(TAG, recv frame payload failed: %s, esp_err_to_name(ret)); return ret; } // 这里 buf 里就是完整帧内容ws_pkt.type 区分 TEXT / BINARY ESP_LOGI(TAG, got ws frame type%d len%d payload%.*s, ws_pkt.type, (int)buf_len, (int)buf_len, buf); // 回显一帧 httpd_ws_frame_t resp { .fin true, .type ws_pkt.type, .payload (uint8_t *)buf, .len buf_len }; ret httpd_ws_send_frame(req, resp); free(buf); return ret; } static void start_ws_server(void) { httpd_handle_t server NULL; httpd_config_t cfg HTTPD_DEFAULT_CONFIG(); cfg.server_port 80; cfg.max_open_sockets 4; if (httpd_start(server, cfg) ESP_OK) { httpd_uri_t ws_uri { .uri /ws, .method HTTP_GET, .handler ws_handler, .user_ctx NULL, .is_websocket true }; httpd_register_uri_handler(server, ws_uri); } }这段代码的核心逻辑在两次httpd_ws_recv_frame调用上。第一次传payload NULL和len 0内核只解析帧头收到实际长度后再分配内存第二次调用填充 payload。很多写出stream disconnected报错的人问题就出在第一次调用时急着给了固定缓冲导致帧超过缓冲长度时直接断开连接。is_websocket true这个字段不能漏。它告诉esp_http_server对GET /ws不要按普通 HTTP 处理而是执行 WebSocket Upgrade 握手。否则浏览器会收到 400 应答WebSocketonclose直接报code: 1006。3.2 关于文本帧和二进制帧的取舍上例中回显类型用的是ws_pkt.type也就是客户端发什么就回什么。实际产品里这是有讲究的文本帧适合 JSON 命令、状态描述二进制帧适合传感器原始数据、OTA 分包、音频 PCM。在 WebSocket 协议里文本帧必须按 UTF-8 解析客户端 JavaScript 端拿到Blob和ArrayBuffer的处理逻辑完全不同。因此建议服务端在发送时固定好类型控制命令一律HTTPD_WS_TYPE_TEXT大块数据一律HTTPD_WS_TYPE_BINARY不要在两者之间摇摆。fin位也是容易踩坑的点。上面代码里帧的fin true表示这是完整帧没有分片。如果某个协议栈实现里分片了fin false的帧需要和后续帧拼起来而esp_http_server不会自动拼分片。嵌入式设备自己收发时最好所有帧都单帧发送不要让 payload 超过 MTU 后依赖分片。4. 让 ESP32 WebSocket 真正跑起来客户端直连、App 断连与心跳处理4.1 用 esp_websocket_client 连上自己的服务端上面服务端建好之后下一步就是拿客户端连它。最简单的客户端就是浏览器new WebSocket(ws://192.168.1.100/ws)。想用 C 验证就写一个esp_websocket_client例子在 ESP32 上连另一个 ESP32#include esp_websocket_client.h static void ws_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { esp_websocket_event_data_t *data (esp_websocket_event_data_t *)event_data; switch (event_id) { case WEBSOCKET_EVENT_CONNECTED: ESP_LOGI(TAG, connected); break; case WEBSOCKET_EVENT_DATA: ESP_LOGI(TAG, recv: %.*s,>wscat -c ws://192.168.1.100/ws连上后输入{action:ping}能看到 ESP32 日志打印出收到 text 帧随后原样回显说明握手和帧收发链路是通的。要验证二进制帧可以配合wscat -b或直接用 Python 的websockets库写个几行脚本import asyncio, websockets, json async def test(): async with websockets.connect(ws://192.168.1.100/ws) as ws: await ws.send(json.dumps({cmd: status})) recv await asyncio.wait_for(ws.recv(), timeout3) print(recv) asyncio.run(test())asyncio.wait_for加上超时是为了防止 ESP32 侧不回复时脚本卡死。这个脚本可以稳定复现问题也能作为 CI 里的冒烟测试。5.2 排查 1006 和 stream disconnected 的实际顺序WebSocket onclose code 1006的意思是连接异常关闭客户端没收到服务端的 Close 帧。排查顺序我一般固定为先看手机上是不是ws://与wss://混用再看 ESP32 日志里有没有httpd_ws_recv_frame返回失败最后抓包确认。热词里那条stream disconnected before completion: failed to send websocket request: io常见于把ws://地址传给了esp_http_client或者esp_websocket_client的 URI 里路径写错。esp_websocket_client对 URI 的解析严格区分ws:和http:前缀前缀错误时 TLS 层或 HTTP 层会在握手阶段直接中断报 io 错误。处理办法是把.uri改回ws://开头并确认路径里没有拼写大小写问题。Internet 上另一个高频报错是websocket onclose code 1006 reason: reconnect: true这其实不是错误而是客户端自己发起的重连日志。esp_websocket_client的reconnect_timeout_ms配置决定了断线后多久重连如果这个值设得太小比如几百毫秒会看到不断重连、把日志刷掉干扰判断。先把它调到 3 秒以上再抓现场。5.3 帧边界与内存占用最后要盯的两个指标WebSocket 调试的最后一道关是看帧格式。抓包时在 Wireshark 里过滤websocket协议重点看帧头第一个字节的FIN和opcode。如果抓到的帧 opcode 是0x0说明是分片消息的中间段ESP32 端没拼直接当业务数据用会出现乱码opcode 是0x8是 Close 帧之后连接就该收尾。内存方面esp_http_server模式下每个 WebSocket 连接要额外占约 6-8KB RAM包含发送缓冲和 TLS 上下文。一个 ESP32 模块上开 4 个连接已经是比较激进的配置。在menuconfig里把CONFIG_HTTPD_SESS_COUNT调小的同时记得代码里free()每个业务帧的 payload 缓冲否则连续收发几十帧后堆碎片会直接让calloc失败表现就是连接越用越卡、最终复位。本文还有配套的精品资源点击获取
返回列表