一、为什么会有粘包/拆包问题
1.1 根本原因:TCP 是面向流的协议
TCP 是一个面向字节流(byte-stream)的协议,不是面向消息的协议。它没有消息边界的概念,只保证:
- 数据有序到达
- 数据不丢失
- 数据不重复
但完全不关心你的应用层数据是怎么划分消息的。
应用层视角: [Message A] [Message B] [Message C] ← 离散的、有边界的消息
| | |
v v v
TCP 层视角: [byte][byte][byte][byte][byte][byte] ← 连续的、无边界的字节流
1.2 具体触发因素
| 因素 | 导致的现象 | 说明 |
|---|---|---|
| 发送缓冲区 | 粘包 | 多次快速 send() 的数据被 OS 合并到同一个 TCP 段 |
| Nagle 算法 | 粘包 | 默认开启,会将小包攒成大包再发送 |
| MSS/MTU 限制 | 拆包 | 数据超过 MSS(通常 1460 字节)会被拆分 |
| 接收缓冲区 | 粘包 | 应用读取慢,多个段在接收缓冲区累积 |
| 网络延迟波动 | 拆包 | 数据被路由器分片、重传导致到达顺序与发送不完全对应 |
1.3 一个具体的例子
发送方连续调用两次 send():
send("Hello") // 调用1: 5 bytes
send("World") // 调用2: 5 bytes
接收方调用 recv() 时可能遇到以下任意情况:
场景1 - 正常: recv() → "Hello" recv() → "World"
场景2 - 完全粘包: recv() → "HelloWorld"
场景3 - 拆包: recv() → "Hel" recv() → "loWorld"
场景4 - 混合: recv() → "HelloWor" recv() → "ld"
场景5 - 任意碎片: recv() → "He" recv() → "lloWo" recv() → "rld"
关键结论:send() 的调用次数和 recv() 的返回次数之间没有任何对应关系。TCP 保证的是字节顺序,不是消息边界。
1.4 UDP 为什么没有这个问题?
UDP 是面向消息(message-oriented)的协议。每次 sendto() 都产生一个独立的数据报,一次 sendto() 严格对应一次 recvfrom(),消息边界由协议本身保证。代价是不保证可靠、有序,所以不存在粘包问题。
举个例子:有三个数据包,大小分别为2k、4k、6k,
- 如果采用UDP发送的话,不管接受方的接收缓存有多大,我们必须要进行至少三次以上的发送才能把数据包发送完;
- 但是使用TCP协议发送的话,我们只需要接受方的接收缓存有12k的大小,就可以一次把这3个数据包全部发送完毕。
二、对日常协议设计的影响
既然 TCP 不提供消息边界,应用层协议必须自己定义消息的边界。这就是所谓的"消息成帧(framing)"。常见的成帧策略:
策略 A:定长消息
每条消息固定 N 字节,不足的用填充符补齐。
[ 10 bytes ][ 10 bytes ][ 10 bytes ]
- 优点:实现最简单
- 缺点:浪费带宽,不灵活
应用场景:固定大小的二进制结构体通信
策略 B:分隔符
用特殊字符/字节序列标记消息结束,如 \r\n、\0。
Hello\r\nWorld\r\n
- 优点:人类可读,适合文本协议
- 缺点:需要转义,不适合二进制数据
应用场景:HTTP 头部、Redis RESP 协议、SMTP、FTP
策略 C:长度前缀(Length Field)—— 最推荐
每条消息前面加一个固定长度的字段表示消息体长度。
+--------+------------------+
| Length | Content |
| 4字节 | N字节 |
+--------+------------------+
- 优点:高效、无需转义、原生支持二进制
- 缺点:需要缓冲管理,需要校验恶意长度值
应用场景:WebSocket、gRPC/HTTP2、MQTT、TLS Record、Thrift 以及绝大多数现代二进制协议
策略 D:TLV(Type-Length-Value)
+------+--------+---------+
| Type | Length | Value |
| 1字节 | 4字节 | N字节 |
+------+--------+---------+
- 优点:可扩展、支持可选字段、前/后向兼容
- 缺点:每个字段都有额外开销
应用场景:ASN.1、DHCP Options、Protobuf 线格式、IPv6 扩展头
设计建议
| 关注点 | 建议 |
|---|---|
| 最大消息大小 | 必须设上限,防止内存耗尽攻击(OOM) |
| 字节序 | 明确定义,网络字节序(大端)是标准 |
| 部分读取 | TCP 可能只传来半条消息 → 必须缓冲累积 |
| 魔数/版本号 | 协议头加入魔数用于快速识别,版本号便于升级 |
三、Netty 如何解决——源码分析
Netty 提供了一套完整的解码器框架来解决粘包/拆包问题。核心架构分两层:
ByteToMessageDecoder ← 字节累积器(框架层,解决"不够一帧就攒着"的问题)
├── FixedLengthFrameDecoder ← 定长解码
├── LineBasedFrameDecoder ← 换行符解码
├── DelimiterBasedFrameDecoder ← 自定义分隔符解码
├── LengthFieldBasedFrameDecoder ← 长度字段解码(最常用)
└── 自定义 Decoder 数据被路由器分片、重传导致到达顺序与发送不完全对应← 你自己写的解码逻辑
3.1 框架层:ByteToMessageDecoder —— 字节累积器
源文件:
codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java
这是所有解码器的基类,核心职责:把每次收到的零散 ByteBuf 累积起来,然后循环调用子类的 decode() 方法,直到不够组成一帧为止。
关键字段
// ByteToMessageDecoder.java:78-192
public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {
// 累积策略1:内存拷贝合并(默认)
// 将新数据 copy 到累积缓冲区的尾部
public static final Cumulator MERGE_CUMULATOR = ...;
// 累积策略2:零拷贝合并
// 使用 CompositeByteBuf 将多个 ByteBuf 逻辑上拼接,无需物理拷贝
public static final Cumulator COMPOSITE_CUMULATOR = ...;
// 状态机:INIT → CALLING_CHILD_DECODE → HANDLER_REMOVED_PENDING
private static final byte STATE_INIT = 0;
private static final byte STATE_CALLING_CHILD_DECODE = 1;
private static final byte STATE_HANDLER_REMOVED_PENDING = 2;
private Queue<Object> inputMessages; // 重入调用时的消息队列
ByteBuf cumulation; // ★ 核心字段:累积缓冲区
private Cumulator cumulator = MERGE_CUMULATOR;
private boolean singleDecode; // 是否每次只解码一条消息(用于协议升级)
private boolean first;
private byte decodeState = STATE_INIT;
private int discardAfterReads = 16; // 读 16 次后尝试丢弃已读字节
private int numReads;
}
MERGE_CUMULATOR 累积策略(默认)
// ByteToMessageDecoder.java:83-116
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
@Override
public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
if (cumulation == in) {
in.release();
return cumulation;
}
if (!cumulation.isReadable() && in.isContiguous()) {
// 累积区为空且输入是连续的 → 直接复用输入缓冲区,零分配
cumulation.release();
return in;
}
try {
final int required = in.readableBytes();
if (required > cumulation.maxWritableBytes() ||
required > cumulation.maxFastWritableBytes() && cumulation.refCnt() > 1 ||
cumulation.isReadOnly()) {
// 容量不够或缓冲区被共享 → 扩容(新分配一个更大的缓冲区)
return expandCumulation(alloc, cumulation, in);
}
// 容量足够 → 直接将 in 写入 cumulation 尾部
cumulation.writeBytes(in, in.readerIndex(), required);
in.readerIndex(in.writerIndex());
return cumulation;
} finally {
in.release(); // 写完后释放输入缓冲区
}
}
};
核心方法:channelRead() — 入口
// ByteToMessageDecoder.java:286-341
@Override
public void channelRead(ChannelHandlerContext ctx, Object input) throws Exception {
if (decodeState == STATE_INIT) {
do {
if (input instanceof ByteBuf) {
selfFiredChannelRead = true;
CodecOutputList out = CodecOutputList.newInstance();
try {
first = cumulation == null;
// ★ 关键步骤1:将新到达的数据累积到 cumulation 缓冲区
cumulation = cumulator.cumulate(ctx.alloc(),
first ? EMPTY_BUFFER : cumulation, (ByteBuf) input);
// ★ 关键步骤2:尝试从累积数据中循环解码出完整消息
callDecode(ctx, cumulation, out);
} catch (DecoderException e) {
throw e;
} catch (Exception e) {
throw new DecoderException(e);
} finally {
try {
if (cumulation != null && !cumulation.isReadable()) {
// 累积区已全部读完 → 释放内存
numReads = 0;
try {
cumulation.release();
} catch (IllegalReferenceCountException e) {
throw new IllegalReferenceCountException(
getClass().getSimpleName() +
"#decode() might have released its input buffer, " +
"or passed it down the pipeline without a retain() call, " +
"which is not allowed.", e);
}
cumulation = null;
} else if (++numReads >= discardAfterReads) {
// 读太多次还没消耗完 → 丢弃已读部分,防止 OOM
// 参见 https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
firedChannelRead |= out.insertSinceRecycled();
// ★ 关键步骤3:将解码出的消息传递给 Pipeline 下游
fireChannelRead(ctx, out, size);
} finally {
out.recycle();
}
}
} else {
// 不是 ByteBuf,直接透传
ctx.fireChannelRead(input);
}
} while (inputMessages != null && (input = inputMessages.poll()) != null);
} else {
// 重入调用(decode() 中触发了 fireChannelRead,又回到了 channelRead)
// 消息暂存到队列,由原始调用处理
if (inputMessages == null) {
inputMessages = new ArrayDeque<>(2);
}
inputMessages.offer(input);
}
}
核心方法:callDecode() — 循环解码引擎
// ByteToMessageDecoder.java:464-517
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) { // ★ 只要还有数据可读就循环
final int outSize = out.size();
if (outSize > 0) {
// 上一轮解码出了消息 → 先传递下去
fireChannelRead(ctx, out, outSize);
out.clear();
// 安全检查:decode() 可能移除了自身 handler
if (ctx.isRemoved()) {
break;
}
}
int oldInputLength = in.readableBytes();
// ★ 调用子类的 decode() 方法(带重入保护)
decodeRemovalReentryProtection(ctx, in, out);
if (ctx.isRemoved()) {
break;
}
if (out.isEmpty()) {
if (oldInputLength == in.readableBytes()) {
// 子类既没读数据也没产出消息 → 数据不够组成一帧,退出循环等下次
break;
} else {
// 子类读了数据但没产出消息(比如跳过了一些非法字节)→ 继续循环
continue;
}
}
if (oldInputLength == in.readableBytes()) {
// 子类产出了消息但没读任何数据 → 实现有 bug,抛异常
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
// 单次解码模式(用于协议升级等场景),只解码一条就退出
break;
}
}
} catch (DecoderException e) {
throw e;
} catch (Exception cause) {
throw new DecoderException(cause);
}
}
callDecode 的精髓:这个循环逻辑完美解决了粘包/拆包问题:
-
拆包(数据不够):子类
decode()返回 null,不添加到 out,out.isEmpty()为 true 且没读数据 →break,数据留在cumulation里等下次 -
粘包(数据多了):子类
decode()成功提取一帧,out不为空 → 回到while(in.isReadable())继续尝试提取下一帧
3.2 FixedLengthFrameDecoder —— 定长解码
源文件:
codec-base/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java
// FixedLengthFrameDecoder.java:41-79
/**
* A decoder that splits the received {@link ByteBuf}s by the fixed number
* of bytes. For example, if you received the following four fragmented packets:
*
* +---+----+------+----+
* | A | BC | DEFG | HI |
* +---+----+------+----+
*
* A {@link FixedLengthFrameDecoder}{@code (3)} will decode them into the
* following three packets with the fixed length:
*
* +-----+-----+-----+
* | ABC | DEF | GHI |
* +-----+-----+-----+
*/
public class FixedLengthFrameDecoder extends ByteToMessageDecoder {
private final int frameLength;
public FixedLengthFrameDecoder(int frameLength) {
checkPositive(frameLength, "frameLength");
this.frameLength = frameLength;
}
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
// ★ 核心逻辑:极简实现
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (in.readableBytes() < frameLength) {
return null; // 不够一帧,等下次
} else {
return in.readRetainedSlice(frameLength); // 够了,切出 frameLength 字节
}
}
}
精髓:只做一件事——判断 readableBytes() < frameLength。不够就返回 null,够了就切。所有累积、循环、内存管理的复杂度都由父类 ByteToMessageDecoder 处理了。
3.3 LengthFieldBasedFrameDecoder —— 长度字段解码(最常用)
源文件:
codec-base/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java
这是工业界使用最广泛的解码器,支持高度灵活的协议格式配置。
构造参数
// LengthFieldBasedFrameDecoder.java:269-329
public LengthFieldBasedFrameDecoder(
ByteOrder byteOrder, // 字节序(默认大端)
int maxFrameLength, // ★ 最大帧长度,防止恶意大帧导致 OOM
int lengthFieldOffset, // ★ 长度字段在帧中的偏移量(前面可能有其他头部)
int lengthFieldLength, // ★ 长度字段本身的字节数(1/2/3/4/8)
int lengthAdjustment, // 长度补偿值(长度字段值不包含自身头部时需要补偿)
int initialBytesToStrip, // 解码后跳过前面多少字节(通常用来跳过长度字段头)
boolean failFast // 超过最大长度时是否立即抛异常(true=立即,false=读完再抛)
)
关键字段
// LengthFieldBasedFrameDecoder.java:189-200
public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
private final ByteOrder byteOrder;
private final int maxFrameLength; // 最大允许帧长度
private final int lengthFieldOffset; // 长度字段起始偏移
private final int lengthFieldLength; // 长度字段字节数
private final int lengthFieldEndOffset; // = lengthFieldOffset + lengthFieldLength
private final int lengthAdjustment; // 长度补偿值
private final int initialBytesToStrip; // 解码后跳过的字节数
private final boolean failFast;
private boolean discardingTooLongFrame; // 是否正在丢弃超长帧
private long tooLongFrameLength;
private long bytesToDiscard;
private int frameLengthInt = -1; // ★ 缓存当前帧长度,-1表示正在解析新帧
}
核心方法:decode() — 帧解析
// LengthFieldBasedFrameDecoder.java:331-444
@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
throws Exception {
Object decoded = decode(ctx, in);
if (decoded != null) {
out.add(decoded);
}
}
// ★ 核心解析逻辑
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
long frameLength = 0;
if (frameLengthInt == -1) { // -1 表示需要解析新的帧
// 如果上一帧超长,先丢弃
if (discardingTooLongFrame) {
discardingTooLongFrame(in);
}
// ★ 步骤1:检查是否有足够字节读取整个 length field
if (in.readableBytes() < lengthFieldEndOffset) {
return null; // 连长度字段都不完整,等下次
}
// ★ 步骤2:读取长度字段的值
int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset;
frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset,
lengthFieldLength, byteOrder);
// 步骤3:校验长度字段不为负
if (frameLength < 0) {
failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset);
}
// ★ 步骤4:计算完整帧长度
// 完整帧 = lengthFieldEndOffset(长度字段及其之前的部分) + frameLength(长度字段指示的大小) + lengthAdjustment(补偿)
frameLength += lengthAdjustment + lengthFieldEndOffset;
// 步骤5:校验帧长度合理性
if (frameLength < lengthFieldEndOffset) {
failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset);
}
// ★ 步骤6:安全校验——防止恶意超大帧
if (frameLength > maxFrameLength) {
exceededFrameLength(in, frameLength);
return null;
}
// 缓存帧长度,下次进入不用重新解析 length field
frameLengthInt = (int) frameLength;
}
// ★ 步骤7:检查是否有足够字节组成完整帧
if (in.readableBytes() < frameLengthInt) {
return null; // 数据不够完整帧,等下次
}
// 步骤8:校验 initialBytesToStrip 不能超过帧长度
if (initialBytesToStrip > frameLengthInt) {
failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip);
}
// ★ 步骤9:跳过不需要的字节(通常是长度字段头)
in.skipBytes(initialBytesToStrip);
// ★ 步骤10:提取完整帧
int readerIndex = in.readerIndex();
int actualFrameLength = frameLengthInt - initialBytesToStrip;
ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength);
in.readerIndex(readerIndex + actualFrameLength);
frameLengthInt = -1; // 重置,准备解析下一帧
return frame;
}
超长帧丢弃逻辑
// LengthFieldBasedFrameDecoder.java:364-378
private void exceededFrameLength(ByteBuf in, long frameLength) {
long discard = frameLength - in.readableBytes();
tooLongFrameLength = frameLength;
if (discard < 0) {
// 缓冲区数据比 frameLength 还多 → 直接跳过 frameLength 字节
in.skipBytes((int) frameLength);
} else {
// 缓冲区不够 → 进入丢弃模式,后续数据继续丢弃直到够数
discardingTooLongFrame = true;
bytesToDiscard = discard;
in.skipBytes(in.readableBytes());
}
failIfNecessary(true);
}
官方注释中的配置示例
示例1:基础 — 2字节长度字段,不剥离头部
lengthFieldOffset = 0, lengthFieldLength = 2, lengthAdjustment = 0, initialBytesToStrip = 0
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
示例2:剥离头部 — 只拿内容
lengthFieldOffset = 0, lengthFieldLength = 2, lengthAdjustment = 0, initialBytesToStrip = 2
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
示例3:长度字段包含自身 — 需要负补偿
lengthFieldOffset = 0, lengthFieldLength = 2, lengthAdjustment = -2, initialBytesToStrip = 0
// 长度字段值 0x000E(14) 包含了自身2字节,所以补偿 -2
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
示例4:复杂头部 — HDR1 + Length + HDR2 + Content
lengthFieldOffset = 1, lengthFieldLength = 2, lengthAdjustment = 1, initialBytesToStrip = 3
// offset=1 跳过 HDR1, adjustment=1 补偿 HDR2, strip=3 剥离 HDR1+LEN
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
3.4 LineBasedFrameDecoder —— 换行符解码
源文件:
codec-base/src/main/java/io/netty/handler/codec/LineBasedFrameDecoder.java
以 \n 或 \r\n 为分隔符,是 DelimiterBasedFrameDecoder 的优化特例。
// LineBasedFrameDecoder.java:42-187
public class LineBasedFrameDecoder extends ByteToMessageDecoder {
private final int maxLength; // 最大允许帧长度
private final boolean failFast; // 超长时是否立即失败
private final boolean stripDelimiter; // 是否剥离分隔符
private boolean discarding; // 是否正在丢弃超长帧
private int discardedBytes; // 已丢弃的字节数
private int offset; // 上次扫描位置(增量扫描优化)
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
final int eol = findEndOfLine(buffer); // ★ 查找换行符位置
if (!discarding) {
if (eol >= 0) {
// ★ 找到换行符 → 提取一帧
final ByteBuf frame;
final int length = eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1; // \r\n=2字节, \n=1字节
if (length > maxLength) {
buffer.readerIndex(eol + delimLength);
fail(ctx, length);
return null;
}
if (stripDelimiter) {
frame = buffer.readRetainedSlice(length); // 读内容(不含分隔符)
buffer.skipBytes(delimLength); // 跳过分隔符
} else {
frame = buffer.readRetainedSlice(length + delimLength); // 内容+分隔符一起
}
return frame;
} else {
// 没找到换行符 → 检查是否已超长
final int length = buffer.readableBytes();
if (length > maxLength) {
// 超长了还没遇到换行符 → 进入丢弃模式
discardedBytes = length;
buffer.readerIndex(buffer.writerIndex());
discarding = true;
offset = 0;
if (failFast) {
fail(ctx, "over " + discardedBytes);
}
}
return null; // 不够一帧,等下次
}
} else {
// 丢弃模式:继续丢弃直到找到换行符
if (eol >= 0) {
final int length = discardedBytes + eol - buffer.readerIndex();
final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
buffer.readerIndex(eol + delimLength);
discardedBytes = 0;
discarding = false;
if (!failFast) {
fail(ctx, length);
}
} else {
// 还没找到 → 继续丢弃
discardedBytes += buffer.readableBytes();
buffer.readerIndex(buffer.writerIndex());
offset = 0;
}
return null;
}
}
// ★ 增量扫描优化:从上次停止的位置继续找 \n
private int findEndOfLine(final ByteBuf buffer) {
int totalLength = buffer.readableBytes();
int i = buffer.indexOf(buffer.readerIndex() + offset,
buffer.readerIndex() + totalLength, (byte) '\n');
if (i >= 0) {
offset = 0;
if (i > 0 && buffer.getByte(i - 1) == '\r') {
i--; // \r\n → 指向 \r 的位置
}
} else {
offset = totalLength; // 记住已扫描的位置,下次从这里继续
}
return i;
}
}
关键设计点:
-
增量扫描:
offset字段记录上次扫描位置,下次只扫描新增数据,避免重复扫描已扫描过的字节 - 丢弃模式:超长帧不会导致 OOM,而是进入丢弃模式,后续数据直接跳过直到找到下一个分隔符
四、整体工作流程图
TCP 字节流到达
│
▼
┌─────────────────────┐
│ ByteToMessageDecoder│
│ channelRead() │
│ │
│ 1. cumulate: │
│ 新数据追加到 │
│ cumulation 缓冲区│
│ │
│ 2. callDecode: │
│ while(可读) { │
│ 子类.decode() │
│ 返回null → break(等下次)│
│ 返回frame → 继续(粘包处理)│
│ } │
│ │
│ 3. 内存管理: │
│ 全部读完 → release│
│ 读16次还没完 → discardSomeReadBytes│
└─────────────────────┘
│
┌──────────┼──────────┐──────────┐
▼ ▼ ▼ ▼
FixedLength LineBased Delimiter LengthField
定长判断 找\n 找自定义 读length字段
┌──┘ 分隔符 等够完整帧
│ ┌──┘ ┌──┘
▼ ▼ ▼
所有策略统一返回 null(等数据)或 ByteBuf(一帧完整消息)
五、总结
┌──────────────────────────────────────────────────────┐
│ 粘包/拆包的完整图景 │
├──────────────────────────────────────────────────────┤
│ │
│ 根因:TCP 是字节流协议,没有消息边界 │
│ │
│ 对协议设计的影响: │
│ → 应用层必须自己定义消息边界(消息成帧) │
│ → 策略:定长 / 分隔符 / 长度前缀 / TLV │
│ │
│ Netty 的解决方案:两层架构 │
│ → 框架层 ByteToMessageDecoder: │
│ 字节累积 + 循环解码 + 内存管理 │
│ → 策略层子类实现不同成帧方式: │
│ FixedLengthFrameDecoder 定长 │
│ LineBasedFrameDecoder 换行符 │
│ DelimiterBasedFrameDecoder 自定义分隔符 │
│ LengthFieldBasedFrameDecoder 长度字段(最常用) │
│ │
│ 设计模式:模板方法模式(Template Method) │
│ 父类定义骨架流程,子类实现具体的帧判定逻辑 │
│ │
└──────────────────────────────────────────────────────┘
六、延伸思考
- 为什么 Netty 的解码器框架要设计成两层(框架层 + 策略层)? 这是什么设计模式?(提示:模板方法模式)
- 如果协议是变长头部(如 HTTP),应该怎么处理? (提示:HttpObjectDecoder 的实现方式)
- ReplayingDecoder 和 ByteToMessageDecoder 有什么区别?各适合什么场景?
- LengthFieldBasedFrameDecoder 的
frameLengthInt字段为什么要缓存?不缓存会怎样? - 如果攻击者发送一个超大 length 值但从不发送后续数据,Netty 会怎样?
七、相关源码文件索引
| 文件 | 说明 |
|---|---|
codec-base/src/main/java/io/netty/handler/codec/ByteToMessageDecoder.java |
字节累积解码器基类 |
codec-base/src/main/java/io/netty/handler/codec/FixedLengthFrameDecoder.java |
定长帧解码器 |
codec-base/src/main/java/io/netty/handler/codec/LineBasedFrameDecoder.java |
换行符帧解码器 |
codec-base/src/main/java/io/netty/handler/codec/DelimiterBasedFrameDecoder.java |
分隔符帧解码器 |
codec-base/src/main/java/io/netty/handler/codec/LengthFieldBasedFrameDecoder.java |
长度字段帧解码器 |