Netty 如何解决 TCP 粘包、拆包的问题?
结论先行:TCP 是字节流,本身没有消息边界,粘包与拆包是必然现象而不是错误。Netty 的思路是“在字节流上重建边界”,提供固定长度、分隔符、长度字段三类解码器,工程上最常用的是长度字段方案
LengthFieldBasedFrameDecoder。
一、为什么会粘包拆包
发送方 write("hello") + write("world")
接收方可能读到:
"helloworld" ← 粘包(两次写被合并)
"hello" "world" ← 正常
"hel" "lowor" "ld" ← 拆包(受 MSS、缓冲区、Nagle 影响)
原因:TCP 只保证字节有序到达,不保证“一次读对应一次写”
| 原因 | 说明 |
|---|---|
| 发送缓冲合并 | Nagle 算法把小块数据攒起来一起发 |
| MSS 分片 | 超过最大报文段长度被 IP 层切开 |
| 接收缓冲 | 应用读取速度与到达速度不匹配 |
| 一次读多次写 | 接收缓冲区里可能已有多条完整消息 |
二、三类解码器
| 方案 | 解码器 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度 | FixedLengthFrameDecoder | 实现最简单 | 不足要补齐,浪费带宽 |
| 分隔符 | DelimiterBasedFrameDecoder | 可读性好,适合文本协议 | 内容里不能出现分隔符,需转义 |
| 长度字段 | LengthFieldBasedFrameDecoder | 二进制协议通用,效率高 | 需要协议头约定长度位置 |
三、长度字段方案的五个参数
new LengthFieldBasedFrameDecoder(
maxFrameLength, // 最大帧长,防御超大包
lengthFieldOffset, // 长度字段的起始偏移
lengthFieldLength, // 长度字段占几个字节
lengthAdjustment, // 长度值是否需要修正
initialBytesToStrip // 解码后丢弃多少字节(通常是头部)
);
协议格式:| length(4B) | type(1B) | payload(N B) |
offset=0 len=4 实际帧长 = 4 + 1 + N
参数:maxFrameLength=65535, lengthFieldOffset=0, lengthFieldLength=4,
lengthAdjustment=1, initialBytesToStrip=5
Netty 的帧长公式:
frameLength = lengthFieldValue + lengthAdjustment + (offset + lengthFieldLength)
本例:N + 1 + (0 + 4) = N + 5,正好覆盖 length + type + payload 三部分
常用组合对照:
| length 字段的含义 | lengthAdjustment | initialBytesToStrip |
|---|---|---|
| length 之后到帧尾的字节数(最常见) | 0 | 0(保留头部)或 4(去掉长度字段) |
| length 含自身,即整帧长度 | -4(负的长度字段长度) | 0 |
| length 只含业务 payload,中间隔 1 字节 type | 1 | 5 |
四、服务端装配
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(65535, 0, 4, 0, 4))
.addLast(new LengthFieldPrepender(4)) // 编码时自动补长度
.addLast(new ByteToMessageDecoder() { // 帧内再解析业务字段
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 1) return;
byte type = in.readByte();
out.add(new Frame(type, in.readRetainedSlice(in.readableBytes())));
}
})
.addLast(new BusinessHandler());
LengthFieldPrepender 与 LengthFieldBasedFrameDecoder 是配套的:一个写头,一个读头,双方约定同一套参数。
常见追问
- 追问:为什么 UDP 不用考虑粘包?UDP 是数据报协议,保留消息边界,一次
receive对应一个数据报(但可能丢包、乱序)。 - 追问:
maxFrameLength设大了有什么风险?攻击者可以声明 1GB 长度让服务端预分配内存,所以要设上限并对超限连接直接关闭。 - 追问:分隔符方案怎么处理内容里出现分隔符?做转义(如
\n转\\n),或用长度字段方案,二进制协议基本都选后者。
小结
- 粘包拆包源于 TCP 无边界,只能由应用层重建边界。
- 三类方案:定长、分隔符、长度字段;二进制协议首选长度字段。
lengthAdjustment与initialBytesToStrip是最容易配错的两个参数。- 编解码器必须成对出现,参数要在协议文档里写死。