粘包拆包解码器
TCP 只保证字节流,不保证消息边界。Netty 的解码器把“字节可能不完整、一次可能有多帧、输入可能重入”集中处理,业务 Handler 只接收完整消息。
先给答案:拆包器是在 TCP 的连续字节流上重新建立消息边界
Section titled “先给答案:拆包器是在 TCP 的连续字节流上重新建立消息边界”TCP 只保证字节按序到达,不保证一次 write 对应一次 read。拆包器必须保存跨 read 的中间状态:半个头部要等待更多字节,长度字段错误要拒绝,多个完整帧则要在一次 read 中循环产出。
因此长度字段协议的核心不是“读出一个 int”,而是验证长度是否可信、帧是否完整、解码失败后是否还能恢复同步。把边界检查放在解码器而不是业务 Handler,可以让后续业务只接收完整消息,但也要求限制最大帧长,避免攻击者用一个超大长度占住内存。
read(ByteBuf input) │ ▼cumulator.cumulate(old, input) │ ▼callDecode(cumulation, out) │ 0..N frames ▼fireChannelRead(frame) │ └─ no readable bytes -> release cumulationByteToMessageDecoder#channelRead 收到 ByteBuf 后,把输入交给 cumulator,再调用 callDecode,最后批量传播输出;坐标为 codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java:285-328。累积缓冲区耗尽时会释放并置空;若长期有残留,会按 discardAfterReads 周期丢弃已读字节,避免 readerIndex 无限前移造成内存保留。
解码器必须满足两个性质:
- 输入不足时不修改到不可恢复的状态,等待下一次
channelRead。 - 一次输入包含多帧时循环解码,直到剩余字节不足。
如果解码方法把输入传给下游后没有 retain,框架在 finally 中释放累积缓冲区时会发现引用计数错误,并抛出带有具体提示的 IllegalReferenceCountException,坐标为 ByteToMessageDecoder.java:303-315。
长度字段协议
Section titled “长度字段协议”┌──── header ────┬──── payload ────┐│ length field │ length bytes │└────────────────┴─────────────────┘ ^ ^ offset adjustmentLengthFieldBasedFrameDecoder 在构造期校验最大帧长度、字段偏移、字段长度和 initialBytesToStrip,坐标为 codec-base/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java:302-329。当长度为负数、调整后小于字段尾部或超过 maxFrameLength 时,会跳过或进入 discard 模式并抛出 CorruptedFrameException,坐标为 LengthFieldBasedFrameDecoder.java:339-377。
解码过程中下游 Handler 可能同步触发新的 channelRead。ByteToMessageDecoder 在非 INIT 状态下不递归进入原解码循环,而是把输入放入 inputMessages 队列,坐标为 ByteToMessageDecoder.java:330-340。这是避免递归栈和破坏 cumulation 状态的关键。
为什么不在 TCP 层保证消息边界
Section titled “为什么不在 TCP 层保证消息边界”TCP 的职责是可靠、有序的字节流;如果传输层强行理解业务消息,会失去协议通用性。Netty 把边界规则放在 Pipeline 中,长度字段、分隔符、固定长度和自定义状态机都能组合。
maxFrameLength是安全边界,不能用“相信对端”的方式取消;超长帧会造成内存和 CPU 消耗。initialBytesToStrip、lengthAdjustment配错会导致看似随机的错位,应该用协议样本和 EmbeddedChannel 测试覆盖。- Decoder 中不能阻塞,也不能保存未 retain 的 ByteBuf。
- 连接关闭时仍可能有残余字节,
decodeLast会处理最后一批输入,相关路径见ByteToMessageDecoder.java:406-453。
“累积输入 -> 尝试推进 -> 输入不足则保留状态”是所有流式解析器的通用状态机:日志、文件、串口、WebSocket 分片和自定义 RPC 都适用。安全实现必须同时限制单帧大小和累积缓冲区增长。
面试锚点:TCP 为什么会粘包?ByteToMessageDecoder 如何处理半包、多帧和重入?长度字段解码器如何防止恶意超长帧?