websocket帧本身完整独立,黏包/半包源于业务消息在帧内未按协议解析;前端需用长度前缀+缓冲区管理,严格校验长度并统一处理arraybuffer类型。

WebSocket 协议本身不解决应用层的黏包和半包问题,但它的帧机制(FIN、opcode、mask、length)已由底层(如浏览器或 Netty 的 WebSocket13FrameDecoder)严格保障——你收到的每个 BinaryWebSocketFrame 或 message 事件触发的数据,都是一个**完整、独立的 WebSocket 帧**。真正出问题的地方,是你在帧 payload 内部塞了多条业务消息(黏包),或一条消息被拆成多个帧发送(半包),而前端没按协议解析。
明确帧与业务消息的区别
浏览器 WebSocket API 的 onmessage 回调中:
- 若发送的是文本帧(
send("a")、send("b")),每次回调拿到的是一个完整字符串,不会黏连 —— 这是规范保证的; - 若发送的是二进制帧(
send(arrayBuffer)或send(blob)),且你在 ArrayBuffer 里手动拼了两条消息(如new Uint8Array([...lenA, ...cmdA, ...bodyA, ...lenB, ...cmdB, ...bodyB])),那回调里收到的就是这一整块 Buffer,必须靠你自己定义的协议去切分; - 如果后端用了自定义二进制子协议(比如 4 字节长度 + 2 字节命令 + body),但前端没做长度解析,直接
new DataView(buf)读取,就会读错偏移、解出乱码或崩溃。
前端常用处理方式:长度前缀 + 缓冲区管理
这是最健壮、无歧义的做法,尤其适合高频、二进制通信场景(如 IM、信令、实时音视频控制):
- 约定消息格式为:[len:Uint32BE][cmd:Uint16BE][body],其中 len 是 body 字节数(不含头),网络字节序;
- 维护一个全局
let buffer = new Uint8Array(0)作为接收缓冲区; - 在
ws.onmessage中,将新数据追加到 buffer:buffer = concatUint8Array(buffer, event.data); - 循环检查 buffer 是否足够读取一个完整消息头(至少 6 字节)→ 读出 len → 判断剩余长度是否 ≥ len → 若够,截取并解析该条消息,更新 buffer;若不够,等待下一次 onmessage。
避免常见陷阱
这些细节容易被忽略,却直接导致解析失败:
-
不要依赖 onmessage 调用次数等于发送次数:即使后端发了 10 次
send(),浏览器可能合并触发 1 次回调(尤其小数据+高频率时); -
event.data 类型需统一处理:可能是
ArrayBuffer、Blob或string,二进制场景建议设ws.binaryType = 'arraybuffer'并始终转为Uint8Array; -
不要在异步回调里直接修改共享 buffer:如果
processData()是 async 函数,需确保 buffer 状态在 await 前已提取完毕,否则并发消息会互相覆盖; - 长度字段必须校验上限:例如限制最大消息体为 2MB,若读出 len > 2 * 1024 * 1024,应丢弃并重置 buffer,防止内存溢出或恶意攻击。
简单示例(同步解析逻辑)
以下代码可直接嵌入项目,支持连续黏包、半包自动重组:
let buffer = new Uint8Array(0);
ws.binaryType = 'arraybuffer';
ws.onmessage = (e) => {
const newData = new Uint8Array(e.data);
buffer = concatUint8Array(buffer, newData);
while (buffer.length >= 6) { // 至少有 len(4)+cmd(2)
const view = new DataView(buffer.buffer, buffer.byteOffset);
const len = view.getUint32(0, false); // BE
if (len > 2 * 1024 * 1024) throw new Error('msg too large');
if (buffer.length Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











