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 | 交给后续处理器前跳过的字节数 |
排查建议
- 先抓包确认发送端真实字节结构。
- 检查长度字段是否包含消息头。
- 检查大小端是否一致。
- 给最大包长设置合理上限,防止异常客户端撑爆内存。
代码 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。
