自定义协议是解决TCP拆包/粘包最通用、最可控的方式,需严格对齐总长度、版本号、消息类型三字段;Netty内置解码器依赖数据特征,不适用于无分隔符或含特征的业务协议,且不支持校验加密等扩展。

直接说结论:自定义协议是解决 TCP 拆包/粘包最通用、最可控的方式,但必须在收发两端严格对齐消息结构,否则比用 FixedLengthFrameDecoder 还容易出错。
为什么不能只靠 Netty 解码器?
Netty 提供的 LineBasedFrameDecoder、DelimiterBasedFrameDecoder 和 FixedLengthFrameDecoder 都是“被动适配型”解码器——它们依赖数据本身带有的特征(换行符、分隔符、固定长度)来切分。一旦业务协议里没有这些特征,或特征可能出现在消息体中(比如 JSON 字段值含 \r\n),就会误切、漏切甚至阻塞。
- 用
LineBasedFrameDecoder解析含换行的 log 日志可以,但解析含\n的 JSON RPC 请求就不可靠 -
FixedLengthFrameDecoder要求每个包严格补足长度,实际中字段长度浮动大时,带宽和解析开销都明显上升 - 所有内置解码器都不处理校验、压缩、加密等扩展需求,而这些在真实服务中几乎必选
自定义协议必须包含哪三个核心字段?
一个能落地的自定义协议头至少要定义:总长度、版本号、消息类型。其中 totalLength 是解决粘包/拆包的关键——接收方据此知道“还要等多少字节才算一帧完整”。
-
totalLength:4 字节 int,表示整个包(含头部 + 体)字节数;必须放在最前面,且用网络字节序(big-endian) -
version:1 字节,用于后续协议升级兼容,避免新老服务混连时解析崩溃 -
msgType:2 字节 short,标识请求/响应/心跳等语义,方便 pipeline 分发
示例二进制布局(共 7 字节 header):
0x00 0x00 0x00 0x1b | 0x01 | 0x00 0x01 ↑ totalLength=27 ↑ver ↑msgType=1
后面紧跟 20 字节(27−7)的消息体。接收端读到前 4 字节后,就知道还需等待 23 字节才能组装出完整帧。
Netty 中怎么写一个靠谱的 LengthFieldBasedFrameDecoder?
别手写 ByteToMessageDecoder,优先用 Netty 自带的 LengthFieldBasedFrameDecoder,它专为这种“头里带长度”的协议设计,但参数极易配错。
-
lengthFieldOffset = 0:长度字段从包头第 0 字节开始(即紧贴开头) -
lengthFieldLength = 4:长度字段占 4 字节(对应 int) -
lengthAdjustment = 0:如果长度字段只表示 body 长度,这里要填headerLength;但若totalLength已含 header,则填 0 -
initialBytesToStrip = 0:先不剥离 header,留给后续ByteBuf处理器解析;设为 7 就会直接把 header 切掉,body 交给下一个 handler
典型初始化写法:
new LengthFieldBasedFrameDecoder(
1024 * 1024, // maxFrameLength
0, // lengthFieldOffset
4, // lengthFieldLength
0, // lengthAdjustment
0 // initialBytesToStrip
)最容易被忽略的两个实操细节
一是发送端没做 writeAndFlush 的显式 flush,导致多个小包被内核合并发出去;二是接收端没校验 totalLength 上限,攻击者发个 0xffffffff 就会让服务端尝试分配 4GB 内存。
- 发送端每次构造完
ByteBuf后,务必调用ctx.writeAndFlush(msg),不要只write - 在
LengthFieldBasedFrameDecoder的maxFrameLength参数里设硬上限(如 2MB),超出直接抛异常丢弃,不进业务逻辑 - 协议头里的
totalLength必须大于等于 header 长度,小于maxFrameLength,这个校验要在解码后、反序列化前做一次
真正难的不是定义字段,而是让每个环节都按协议走——发端不越界、收端不信任、中间不跳步。


















