2026-04-16 TCP粘包/拆包问题与 Netty 解决方案

一、为什么会有粘包/拆包问题

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)                │
│    父类定义骨架流程,子类实现具体的帧判定逻辑              │
│                                                      │
└──────────────────────────────────────────────────────┘

六、延伸思考

  1. 为什么 Netty 的解码器框架要设计成两层(框架层 + 策略层)? 这是什么设计模式?(提示:模板方法模式)
  2. 如果协议是变长头部(如 HTTP),应该怎么处理? (提示:HttpObjectDecoder 的实现方式)
  3. ReplayingDecoder 和 ByteToMessageDecoder 有什么区别?各适合什么场景?
  4. LengthFieldBasedFrameDecoder 的 frameLengthInt 字段为什么要缓存?不缓存会怎样?
  5. 如果攻击者发送一个超大 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 长度字段帧解码器

八、参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容