NodeJS与ESP32通过WebSocket传输时如何识别二进制与文本消息
解决方案:WebSocket消息类型区分(ESP32-Cam + Node.js ws模块)
方案1:自定义二进制消息头部(通用高效)
通过在二进制数据前添加固定长度的标识头部,直接把类型信息嵌入消息中,无需额外发送标记消息,效率高且无同步问题。
ESP32-Cam端代码修改
发送帧数据前,拼接4字节的类型标识(比如STRM表示视频流,THUM表示缩略图),再发送拼接后的二进制数据:
void liveCam(uint8_t num){ camera_fb_t * fb = esp_camera_fb_get(); if (!fb) { Serial.println("Frame buffer could not be acquired"); return; } // 4字节ASCII标识,自定义类型 uint8_t header[] = {'S', 'T', 'R', 'M'}; // STRM = 视频流 size_t totalLen = sizeof(header) + fb->len; // 分配临时缓冲区存储头部+帧数据 uint8_t *combinedBuf = (uint8_t*)malloc(totalLen); if (!combinedBuf) { esp_camera_fb_return(fb); Serial.println("Failed to allocate buffer for combined message"); return; } // 复制头部和帧数据到缓冲区 memcpy(combinedBuf, header, sizeof(header)); memcpy(combinedBuf + sizeof(header), fb->buf, fb->len); // 发送拼接后的二进制消息 webSocket.sendBIN(num, combinedBuf, totalLen); // 释放资源 free(combinedBuf); esp_camera_fb_return(fb); } // 发送缩略图示例 void sendThumbnail(uint8_t num, uint8_t* thumbBuf, size_t thumbLen){ uint8_t header[] = {'T', 'H', 'U', 'M'}; // THUM = 缩略图 size_t totalLen = sizeof(header) + thumbLen; uint8_t *combinedBuf = (uint8_t*)malloc(totalLen); if (!combinedBuf) return; memcpy(combinedBuf, header, sizeof(header)); memcpy(combinedBuf + sizeof(header), thumbBuf, thumbLen); webSocket.sendBIN(num, combinedBuf, totalLen); free(combinedBuf); }
Node.js端(ws模块)处理代码
收到消息后先判断类型,二进制消息读取前4字节标识再处理对应数据:
this.socket.on("connection", (ws) => { ws.on("message", (msg) => { if (typeof msg === 'string') { // 处理JSON文本指令,按原有tag字段区分 const data = JSON.parse(msg); switch(data.tag) { case 'blink': // 执行闪烁逻辑 break; case 'fetch_temp': // 执行温度获取逻辑 break; default: console.log("Unknown text command:", data); } } else if (msg instanceof ArrayBuffer) { const uint8Arr = new Uint8Array(msg); // 读取前4字节类型标识 const typeTag = String.fromCharCode(...uint8Arr.slice(0, 4)); // 提取实际二进制 payload const payload = uint8Arr.slice(4); switch(typeTag) { case 'STRM': // 转发视频流到前端或存储 // 例如:broadcastToAllClients(payload); break; case 'THUM': // 处理缩略图数据 break; default: console.log("Unknown binary type:", typeTag); } } }); });
方案2:利用WebSocket自定义Opcode(零额外开销)
WebSocket协议保留了部分Opcode(0x3-0x7、0xB-0xF)可用于自定义消息类型,无额外数据开销,效率最高,但需要ESP端WebSocket库支持自定义Opcode发送。
ESP32-Cam端代码修改
如果WebSocket库提供指定Opcode的发送方法(如sendFrame),直接用自定义Opcode标记:
void liveCam(uint8_t num){ camera_fb_t * fb = esp_camera_fb_get(); if (!fb) { Serial.println("Frame buffer could not be acquired"); return; } // 用Opcode 0x3标记视频流,0x4标记缩略图 webSocket.sendFrame(num, 0x3, fb->buf, fb->len); esp_camera_fb_return(fb); } void sendThumbnail(uint8_t num, uint8_t* thumbBuf, size_t thumbLen){ webSocket.sendFrame(num, 0x4, thumbBuf, thumbLen); }
Node.js端(ws模块)处理代码
ws模块的message事件返回flags参数,包含消息Opcode,直接通过Opcode判断类型:
this.socket.on("connection", (ws) => { ws.on("message", (msg, flags) => { if (flags.opcode === 1) { // 文本消息 const data = JSON.parse(msg); switch(data.tag) { case 'blink': // 处理逻辑 break; case 'fetch_temp': // 处理逻辑 break; } } else if (flags.opcode === 3) { // 自定义Opcode:视频流 // msg为ArrayBuffer,处理视频流 } else if (flags.opcode === 4) { // 自定义Opcode:缩略图 // 处理缩略图 } else if (flags.opcode === 2) { // 普通二进制消息(如果有) // 处理普通二进制 } }); });
方案3:前置标记消息(兼容旧库,需处理同步)
如果上述方案无法实现,可先发送文本标记、再发送对应二进制,但需处理消息同步(如用序列号匹配),缺点是增加网络开销,可能影响帧率。
ESP32-Cam端代码
static uint32_t streamSeq = 0; void liveCam(uint8_t num){ camera_fb_t * fb = esp_camera_fb_get(); if (!fb) { Serial.println("Frame buffer could not be acquired"); return; } // 先发送标记消息,包含类型和序列号 String tagMsg = "{\"type\":\"stream\",\"seq\":" + String(streamSeq) + "}"; webSocket.sendTXT(num, tagMsg.c_str()); // 发送视频帧 webSocket.sendBIN(num, fb->buf, fb->len); streamSeq++; esp_camera_fb_return(fb); }
Node.js端代码
this.socket.on("connection", (ws) => { let nextMsgInfo = null; // 存储下一个二进制消息的类型和序列号 ws.on("message", (msg) => { if (typeof msg === 'string') { const data = JSON.parse(msg); if (data.type && data.seq) { // 记录下一个二进制消息的信息 nextMsgInfo = { type: data.type, seq: data.seq }; } else { // 处理普通JSON指令 switch(data.tag) { case 'blink': // 处理逻辑 break; case 'fetch_temp': // 处理逻辑 break; } } } else if (msg instanceof ArrayBuffer && nextMsgInfo) { if (nextMsgInfo.type === 'stream') { // 处理视频流 } else if (nextMsgInfo.type === 'thumbnail') { // 处理缩略图 } // 重置标记,准备接收下一组消息 nextMsgInfo = null; } }); });
方案选择建议
- 优先选方案2:ESP端支持自定义Opcode时,零额外开销,效率最高。
- 其次选方案1:兼容性强,开销极小(仅4字节),无同步问题。
- 方案3仅作为备选:适合无法修改二进制消息结构的场景,但需注意网络丢包可能导致的消息错位。
内容的提问来源于stack exchange,提问作者matez
相关产品推荐
相关产品推荐

