Skip to content

Netty 粘包拆包

TCP 是字节流协议,不保留应用层消息边界。一次 write 不一定对应一次 read,所以会出现粘包和拆包。

为什么会发生

mermaid
flowchart TD
    A[应用发送消息A和消息B] --> B[TCP缓冲区]
    B --> C{接收端读取时机}
    C -- 一次读到A+B --> D[粘包]
    C -- 只读到A的一部分 --> E[拆包]
    C -- 刚好读到A --> F[正常]

解决方案

方案思路优点缺点
固定长度每个消息固定 N 字节简单浪费空间,不灵活
分隔符用特殊字符表示结束易读body 中要转义
长度字段头部记录 body 长度通用稳定协议稍复杂
行协议按换行切分适合文本命令不适合二进制大包

推荐做法

业务系统通常使用长度字段协议,并配合 LengthFieldBasedFrameDecoder。它可以在消息未完整时继续等待,在消息完整后再交给后续解码器。

mermaid
flowchart TD
    A["ByteBuf 字节流"] --> B["长度字段解码器"]
    B --> C{"当前帧是否完整"}
    C -- "否" --> D["等待更多字节"]
    C -- "是" --> E["输出完整帧"]
    E --> F["业务协议解码器"]
    F --> G["请求对象"]

核心原理

TCP 只保证字节顺序可靠,不保证应用层“一次发送就是一次接收”。应用层如果想知道一条消息在哪里结束,就必须自己定义边界。长度字段协议就是在消息头中写入 body 长度,接收端先读长度,再按长度等待完整 body。

mermaid
flowchart TD
    A["读取前 4 字节 length"] --> B{"ByteBuf 中剩余字节 >= length ?"}
    B -- "否" --> C["resetReaderIndex<br/>等待下一次网络读"]
    B -- "是" --> D["读取 length 个字节"]
    D --> E["得到完整业务消息"]

如果不定义边界,业务 Handler 看到的就只是随机分段后的字节流:有时两个请求连在一起,有时一个请求只到了一半。问题会很隐蔽,因为它不是每次都发生,通常在网络抖动、消息变大或并发升高时才暴露。

参数理解

参数含义
maxFrameLength单个包最大长度
lengthFieldOffset长度字段起始位置
lengthFieldLength长度字段占几个字节
lengthAdjustment长度字段之后还要修正的长度
initialBytesToStrip交给后续处理器前跳过的字节数

排查建议

  1. 先抓包确认发送端真实字节结构。
  2. 检查长度字段是否包含消息头。
  3. 检查大小端是否一致。
  4. 给最大包长设置合理上限,防止异常客户端撑爆内存。

代码 Demo:长度字段解码器

协议格式:

text
4 字节 body 长度 + body 字节

Pipeline 配置:

java
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
    1024 * 1024,
    0,
    4,
    0,
    4
));
ch.pipeline().addLast(new StringDecoder(StandardCharsets.UTF_8));
ch.pipeline().addLast(new BizHandler());

客户端发送时先写长度,再写内容:

java
byte[] body = "hello netty".getBytes(StandardCharsets.UTF_8);
ByteBuf buf = Unpooled.buffer();
buf.writeInt(body.length);
buf.writeBytes(body);
channel.writeAndFlush(buf);

这样接收端只有在 body 完整到达后,才会把完整消息交给后续 Handler。