1
0
0

Netty:ByteToMessageDecoder 与编解码 Pipeline

2026-10-31
2026-10-31
文章摘要
|

一、ByteToMessageDecoder 是什么?

ByteToMessageDecoder 不是接口,而是抽象类,继承自
ChannelInboundHandlerAdapter,属于入站 Handler。

它提供字节累积、循环解码等通用能力。开发者可以继承它实现自己的协议解码器,也可以使用
Netty 提供的内置解码器。

常见实现关系:

ByteToMessageDecoder(抽象类)
    ├── FixedLengthFrameDecoder       固定长度解码器
    ├── DelimiterBasedFrameDecoder    分隔符解码器
    ├── LineBasedFrameDecoder         换行符解码器
    └── LengthFieldBasedFrameDecoder  长度字段解码器

注意:LengthFieldBasedFrameDecoder
是根据消息头中的长度字段确定消息边界,不是"文本长度解码器"。

二、常见 Handler 的作用

  • ByteToMessageDecoder:解码基类,提供数据累积和解码调度机制。

  • FixedLengthFrameDecoder:每固定 N 个字节切出一条消息。

  • DelimiterBasedFrameDecoder:根据指定分隔符切出消息。

  • LineBasedFrameDecoder:根据换行符切出消息。

  • LengthFieldBasedFrameDecoder:根据消息头中的长度字段切出消息。

  • StringDecoder:将完整的 ByteBuf 转换成 String。

  • StringEncoder:将 String 转换成 ByteBuf。

  • LengthFieldPrepender:在出站消息前面添加长度字段。

固定长度、分隔符、换行符、长度字段解码器解决的是消息边界识别;StringDecoder
解决的是数据类型转换,两者不是同一类工作。

三、ByteToMessageDecoder 如何处理粘包拆包?

假设协议格式:

| 4 字节正文长度 | 正文 |

客户端发送:

00 00 00 05 68 65 6C 6C 6F

表示正文长度为 5,正文是 hello。

如果服务端分三次收到:

第一次:00 00
第二次:00 05 68 65
第三次:6C 6C 6F

处理过程:

收到数据
   ↓
累积到内部 ByteBuf
   ↓
调用具体解码器的 decode()
   ↓
数据不完整:保留数据,等待后续数据
   ↓
后续数据到达:继续累积并解码
   ↓
完整消息:加入 out 集合
   ↓
传递给 Pipeline 下一个入站 Handler

如果一次收到两条完整消息,解码器也可以连续解析,分别输出两条消息。

核心区别:

  • ByteToMessageDecoder:提供累积缓冲区和解码调度机制。

  • 具体解码器:负责判断协议格式、消息长度以及如何提取完整消息。

四、什么时候使用哪种解码器?

  • 固定长度协议:使用 FixedLengthFrameDecoder。例如每条消息固定 8
    字节。

  • 指定分隔符协议:使用 DelimiterBasedFrameDecoder。例如 hello##。

  • 换行符协议:使用 LineBasedFrameDecoder。例如 hello\n。

  • 长度字段协议:使用 LengthFieldBasedFrameDecoder。例如
    长度(4字节) + 正文。

  • 复杂自定义协议:继承 ByteToMessageDecoder,例如魔数 + 版本 +
    类型 + 长度 + 正文。

这些解码器不需要全部添加到
Pipeline,应根据实际通信协议选择合适的消息边界解码方式。

五、LengthFieldBasedFrameDecoder 五个参数

构造方法:

new LengthFieldBasedFrameDecoder(
    maxFrameLength,
    lengthFieldOffset,
    lengthFieldLength,
    lengthAdjustment,
    initialBytesToStrip
);

假设协议格式:

| 4 字节正文长度 | 正文 |

长度字段只记录正文长度,正文是 hello:

00 00 00 05 68 65 6C 6C 6F

配置:

new LengthFieldBasedFrameDecoder(
    1024, // 最大完整帧长度
    0,    // 长度字段从第 0 字节开始
    4,    // 长度字段占 4 字节
    0,    // 长度字段只记录正文长度,无需调整
    4     // 输出时跳过前 4 字节长度字段
);

五个参数:

  1. maxFrameLength:最大完整帧长度,超出限制会触发异常。

  2. lengthFieldOffset:长度字段在帧中的起始偏移。

  3. lengthFieldLength:长度字段占用的字节数。

  4. lengthAdjustment:对长度字段的值进行调整,以计算完整帧长度。

  5. initialBytesToStrip:完整帧输出前,从开头跳过的字节数。

完整帧长度计算公式:

frameLength =
    lengthFieldOffset
    + lengthFieldLength
    + lengthAdjustment
    + lengthFieldValue

本例:

0 + 4 + 0 + 5 = 9 字节

如果长度字段记录的是整条帧长度 9 字节,而不是正文长度 5
字节,则参数需要调整:

offset = 0
length = 4
adjustment = -4
lengthValue = 9

frameLength = 0 + 4 - 4 + 9 = 9

initialBytesToStrip 只控制输出时跳过多少字节,不参与完整帧长度计算。

六、什么时候需要自己继承 ByteToMessageDecoder?

协议简单时,直接使用内置解码器即可。

如果协议包含魔数、版本号、消息类型、长度、校验码等复杂字段,需要自行校验并转换成业务对象,就可以继承
ByteToMessageDecoder。

简化示例:协议为"4 字节正文长度 + 正文"。

public class CustomLengthDecoder extends ByteToMessageDecoder {

    private static final int HEADER_LENGTH = 4;
    private static final int MAX_BODY_LENGTH = 1024 * 1024;

    @Override
    protected void decode(
            ChannelHandlerContext ctx,
            ByteBuf in,
            List<Object> out) {

        // 1. 长度字段不足 4 字节,等待后续数据
        if (in.readableBytes() < HEADER_LENGTH) {
            return;
        }

        // 2. 查看正文长度,不移动 readerIndex
        int bodyLength = in.getInt(in.readerIndex());

        // 3. 校验长度,防止异常数据
        if (bodyLength < 0 || bodyLength > MAX_BODY_LENGTH) {
            throw new CorruptedFrameException(
                "非法消息长度:" + bodyLength
            );
        }

        // 4. 完整消息未到达,等待后续数据
        if (in.readableBytes() < HEADER_LENGTH + bodyLength) {
            return;
        }

        // 5. 跳过 4 字节长度字段
        in.skipBytes(HEADER_LENGTH);

        // 6. 提取正文并输出给下一个 Handler
        out.add(in.readRetainedSlice(bodyLength));
    }
}

关键方法说明:

  • getInt(index):按下标读取整数,不移动 readerIndex。

  • readInt():读取整数,并移动 readerIndex。

  • skipBytes(n):跳过指定字节数,同时移动 readerIndex。

  • out.add(msg):将解码结果放入输出集合,之后交给下一个入站 Handler。

  • readRetainedSlice(n):截取共享底层内存的视图,并增加引用计数。

readRetainedSlice()
输出的对象需要由下游正确释放。若下游使用默认自动释放的
SimpleChannelInboundHandler,通常不需要再次手动释放。

自定义 ByteToMessageDecoder 不要随意添加
@Sharable,因为它内部维护累积缓冲区状态,通常应为每个 Channel
创建独立实例。

七、Decoder 与 Encoder 在 Pipeline 中如何传播?

入站和出站方向相反:

入站 Inbound:Head → Tail
出站 Outbound:Tail → Head

入站流程:

TCP 收到字节
    ↓
LengthFieldBasedFrameDecoder:识别完整消息
    ↓
StringDecoder:ByteBuf 转 String
    ↓
业务 Handler:处理业务

出站流程:

业务 Handler 调用 writeAndFlush(String)
    ↓
StringEncoder:String 转 ByteBuf
    ↓
LengthFieldPrepender:添加长度字段
    ↓
Socket 发送字节

完整配置示例:

@Override
protected void initChannel(SocketChannel ch) {
    ChannelPipeline pipeline = ch.pipeline();

    // 入站:根据长度字段切出完整帧
    pipeline.addLast(new LengthFieldBasedFrameDecoder(
            1024 * 1024, 0, 4, 0, 4));

    // 出站:添加 4 字节长度字段
    pipeline.addLast(new LengthFieldPrepender(4));

    // 入站:ByteBuf 转 String
    pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));

    // 出站:String 转 ByteBuf
    pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));

    // 业务处理
    pipeline.addLast(new SimpleChannelInboundHandler<String>() {
        @Override
        protected void channelRead0(
                ChannelHandlerContext ctx, String msg) {
            System.out.println("收到消息:" + msg);
            ctx.writeAndFlush("服务端回复:" + msg);
        }
    });
}

传播规则:

  • 入站只经过支持入站事件的 Handler,按 Head 到 Tail 的方向传播。

  • 出站只经过支持出站事件的 Handler,按 Tail 到 Head 的方向传播。

  • ctx.writeAndFlush() 从当前 Handler 所在位置向前寻找出站 Handler。

  • channel.writeAndFlush() 通常从 Pipeline 尾部开始向前传播。

  • StringDecoder 负责类型转换,不负责识别 TCP 消息边界。

  • LengthFieldPrepender 负责添加长度字段,不负责处理入站数据。

八、实际开发怎么选择?

  • 直接接收原始 TCP 数据:在业务 Handler 中自行解析
    ByteBuf,或添加自定义 Decoder。

  • 固定长度协议:使用 FixedLengthFrameDecoder。

  • 换行符协议:使用 LineBasedFrameDecoder。

  • 长度字段协议:使用 LengthFieldBasedFrameDecoder。

  • 复杂二进制协议:自定义 ByteToMessageDecoder。

  • 需要 String 入站转换:使用 StringDecoder。

  • 发送长度字段协议:使用 LengthFieldPrepender 或自定义 Encoder。

如果项目已经在公共 ChannelInitializer 中配置了解码器,业务 Handler
就可能直接收到解码后的对象,不需要自己再次解析 ByteBuf。

九、常见问题与答案

1. ByteToMessageDecoder 是接口吗?

不是。它是抽象类,继承自 ChannelInboundHandlerAdapter,属于入站
Handler。

2. 固定长度、分隔符、长度字段解码器与它是什么关系?

它们是 ByteToMessageDecoder 的具体子类,实现不同的消息边界识别方式。

3. ByteToMessageDecoder 自己知道消息格式吗?

不知道。它提供累积缓冲区和解码调度机制,具体消息格式由子类的 decode()
实现决定。

4. 这些解码器需要全部添加到 Pipeline 吗?

不需要。根据协议选择合适的帧解码器;需要类型转换时,再添加对应 Decoder。

5. 没有配置 Decoder,业务 Handler 收到什么?

通常收到 Netty 底层读取产生的 ByteBuf,需要自行解析字节和消息边界。

6. StringDecoder 能解决粘包拆包吗?

不能。它负责将 ByteBuf 转成
String,不负责识别一条业务消息从哪里开始、在哪里结束。

7. 为什么要同时配置 Decoder 和 Encoder?

Decoder 负责接收方向的解析;Encoder
负责发送方向的编码。双方必须遵循同一套通信协议。

十、重点总结

  1. ByteToMessageDecoder 是抽象类,不是接口。

  2. 固定长度、分隔符、行分隔符、长度字段解码器都是它的具体实现。

  3. ByteToMessageDecoder
    负责累积数据和调度解码,具体子类负责识别消息边界。

  4. LengthFieldBasedFrameDecoder 通过长度字段确定完整帧。

  5. StringDecoder 只负责 ByteBuf 到 String 的转换,不负责拆包。

  6. 入站方向 Head → Tail;出站方向 Tail → Head。

  7. 简单协议优先使用内置解码器,复杂协议再自定义 Decoder。

  8. Netty 不会自动替开发者选择协议解码器,需要在 Pipeline 中按协议配置。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论