WebSocket帧层自动处理TCP黏包,但业务消息需应用层解决帧内黏包;推荐长度前缀+二进制帧方案,发送时加4字节大端长度头,接收端依头解析。

WebSocket 协议本身不解决黏包与分包问题,但它的帧层(frame layer)已由浏览器和服务器底层(如 Netty、Node.js 的 ws 库)自动处理 TCP 层的粘包/拆包。真正需要你关注和编码的,是 WebSocket 帧 payload 内部的业务消息如何切分——也就是“帧内黏包”。
浏览器端 WebSocket 默认不暴露帧边界,需自行协议设计
浏览器原生 WebSocket API 只提供 message 事件,且:
- 文本帧(
TextWebSocketFrame)会自动转成string或DOMString,但若后端拼了多个 JSON,前端收到的就是一串无分隔的字符串(如'{"id":1}{"id":2}'),无法直接JSON.parse - 二进制帧(
BinaryWebSocketFrame)传入的是ArrayBuffer或Blob,内容完全由你定义,没有内置长度或分隔符
这意味着:你不能依赖 event.data 天然对应一条业务消息;必须在应用层约定解析规则。
推荐方案:长度前缀 + 二进制帧(最健壮)
在发送前对每条业务消息加 4 字节大端序长度头(Uint32BE),再拼接 body。例如:
立即学习“Java免费学习笔记(深入)”;
// 发送端(JS)
function sendPacket(data) {
const encoder = new TextEncoder();
const body = encoder.encode(JSON.stringify(data));
const header = new ArrayBuffer(4);
const view = new DataView(header);
view.setUint32(0, body.length, false); // false = big-endian
const packet = new Uint8Array(header.byteLength + body.length);
packet.set(new Uint8Array(header), 0);
packet.set(body, 4);
ws.send(packet.buffer);
}
接收端维护一个缓冲区,按长度头逐步切包:
// 接收端(JS)
let buffer = new Uint8Array(0);
ws.onmessage = (e) => {
const chunk = new Uint8Array(e.data);
buffer = concatUint8Array(buffer, chunk);
while (buffer.length >= 4) {
const view = new DataView(buffer.buffer, buffer.byteOffset);
const len = view.getUint32(0, false); // 读长度头
if (buffer.length < 4 + len) break; // 包不完整,等下一次
const body = buffer.slice(4, 4 + len);
try {
const msg = JSON.parse(new TextDecoder().decode(body));
handleMessage(msg);
} catch (err) {
console.warn('解析失败', body);
}
buffer = buffer.slice(4 + len); // 截掉已处理部分
}
};
备选方案:分隔符(适合纯文本,慎用于二进制)
若只传 UTF-8 文本且不含换行符,可用 \n 分隔:
- 发送:
ws.send(JSON.stringify(msg) + '\n') - 接收:将所有数据按
\n切分,但注意最后可能残留半包(末尾无\n),需缓存未结束行
⚠️ 不适用于二进制数据(\n 可能出现在原始字节中),也不适合 Protobuf/MessagePack 等序列化格式。
服务端必须配合,否则前端白忙
前端加长度头,后端也得用对应解码器(如 Netty 的 LengthFieldBasedFrameDecoder)提前拆好帧内消息,再投递给业务 handler。如果后端直接把整帧当一条消息转发,前端再怎么解析也没用。
关键点:前后端必须约定一致的子协议,包括字节序、长度字段位置、是否含命令字、body 编码方式等。调试时可先用 console.log(new Uint8Array(e.data)) 打印原始字节,确认结构是否符合预期。


















