websocket因底层tcp流特性会出现粘包/半包,需在应用层添加长度前缀协议(如4字节body长度+2字节command类型),并用lengthfieldbasedframedecoder正确配置lengthfieldoffset、lengthfieldlength、lengthadjustment和initialbytestostrip。

WebSocket 本身不解决粘包半包问题,底层走的仍是 TCP 流,所以服务端收到的 BinaryWebSocketFrame 可能包含多帧数据,也可能只有一帧的前半截——这不是 WebSocket 协议缺陷,而是 TCP 的天然特性。必须在应用层加协议头(如长度字段)才能可靠解析。
为什么 WebSocket 也会出现粘包/半包
很多人误以为 WebSocket 是“消息级”协议,其实它只是在 HTTP 握手后升级为 TCP 长连接,并封装了帧格式(FIN、opcode、mask 等),但这些帧仍受 TCP 流影响:
- TCP 层可能把多个 WebSocket 帧合并发到一个 IP 包里(粘包)
- 也可能把一个大 WebSocket 帧拆成多个 TCP 段(半包)
- Netty 的
BinaryWebSocketFrame是按 WebSocket 帧边界解出来的,但如果你在业务层又把多个业务消息塞进同一个帧里(比如批量推送),那粘包就发生在你自己的协议层
用长度前缀协议解决粘包最稳妥
在 WebSocket 帧内再定义一层二进制协议,头部固定 6 字节:int32 表示 body 长度 + int16 表示 command 类型。这是目前 IM、实时信令等场景最通用的做法。
关键点:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 长度字段必须是定长且网络字节序(Big-Endian),避免因平台差异导致解析错位
- 不要用字符串长度(如 UTF-8 字节数 ≠ Java
String.length()),要序列化后取.length - 长度字段本身不参与校验,但应设上限(如最大 4MB),防止恶意构造超长 length 导致 OOM
- Netty 中推荐用
LengthFieldBasedFrameDecoder替代手写解码逻辑,它已内置对偏移、长度字节数、是否跳过长度字段等控制
Netty 中如何配置 LengthFieldBasedFrameDecoder
直接在 pipeline 中添加,比手写 MessageToMessageDecoder 更安全、更少出错:
pipeline.addLast(new LengthFieldBasedFrameDecoder(
4 * 1024 * 1024, // maxFrameLength,防攻击
0, // lengthFieldOffset,长度字段从第 0 字节开始
4, // lengthFieldLength,int32 占 4 字节
0, // lengthAdjustment,body 前无额外字段(如 command 字段需 +2)
4 // initialBytesToStrip,解码后丢弃前 4 字节(只留 body)
));
注意:lengthAdjustment 要根据你协议头实际结构填。例如协议是 [len:4][cmd:2][body],那么 length 字段值只含 body 长度,但整个 header 占了 6 字节,此时 lengthAdjustment = 2(让 decoder 从 len 字段后偏移 2 字节开始截 body),initialBytesToStrip = 6 才对。
Protobuf 场景下容易忽略的坑
用 Protobuf 时,msg.toByteArray().length 是真实 body 长度,但必须确保所有 message class 都继承自 GeneratedMessageV3,否则 toByteArray() 可能抛 RuntimeException;另外:
- 不要在编码器里反复 new
ByteBuf,用ctx.alloc().ioBuffer()或.buffer(),由 Netty 统一管理池化内存 - 解码后得到的
ByteBuf默认是 readerIndex=0,但经LengthFieldBasedFrameDecoder处理后的 buffer 已自动调整 readerIndex,直接传给MyMsg.parseFrom(byteBuf.nioBuffer())即可 - 如果用了
@ChannelHandler.Sharable,确保MessageRecognizer是无状态的,否则多 channel 并发时getMsgCommandByMsgClazz可能返回错乱指令
真正麻烦的从来不是协议怎么设计,而是 length 字段算错、字节序搞反、strip 数量和 adjustment 对不上这三处——它们会导致解码器静默吞掉数据,或者抛 CorruptedFrameException 却没打日志。调试时务必打开 LengthFieldBasedFrameDecoder 的 DEBUG 日志,看它每次 read 到的 length 值和实际可读字节数是否匹配。










