← 返回题目列表

Netty 的编解码器(Codec)是什么?ByteToMessageDecoder、MessageToByteEncoder 怎么用?

高频 中等 第 5 / 23 题 更新于 2026/08/03
编解码器CodecByteToMessageDecoder编码器

简化版

编解码器(Codec)是 Netty 里负责「字节 ↔ 对象」转换的处理器——因为网络传输的是字节(ByteBuf),而业务代码想处理的是对象(如一个消息、一个请求),编解码器在中间做转换。两个方向:① 解码器(Decoder,入站)——把收到的字节转成对象(ByteBuf → Message),供后面的业务 Handler 处理;② 编码器(Encoder,出站)——把要发送的对象转成字节(Message → ByteBuf),发出去。核心基类ByteToMessageDecoder(字节转对象的解码器基类,重写 decode(ctx, in, out))、MessageToByteEncoder(对象转字节的编码器基类,重写 encode(ctx, msg, out))、MessageToMessageCodec(对象↔对象转换)、ByteToMessageCodec(编解码合一)。解码器最重要的职责是「解决粘包/半包」——TCP 是字节流,一次读到的字节可能是「半个消息」或「多个消息粘在一起」,ByteToMessageDecoder 帮你处理「累积字节、够一个完整消息才解码」(Netty 还提供了 LengthFieldBasedFrameDecoder 等现成的解码器)。核心:编解码器隔离「字节」和「对象」,业务 Handler 只处理对象,解码器还负责拆包。

详细版

编解码器基类

基类方向作用
ByteToMessageDecoder入站字节 → 对象(解码,含拆包)
MessageToByteEncoder出站对象 → 字节(编码)
MessageToMessageDecoder入站对象 → 对象(如 String → 业务对象)
MessageToMessageEncoder出站对象 → 对象
ByteToMessageCodec双向编解码合一(字节↔对象)
MessageToMessageCodec双向对象↔对象
// 解码器:字节 → 对象(继承 ByteToMessageDecoder)
public class MyDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        // in 是累积的字节,判断够不够一个完整消息
        if (in.readableBytes() < 4) return;      // 不够消息头,等更多字节
        in.markReaderIndex();
        int length = in.readInt();               // 读消息长度
        if (in.readableBytes() < length) {       // 消息体不完整
            in.resetReaderIndex();               // 重置,等更多字节
            return;
        }
        byte[] body = new byte[length];
        in.readBytes(body);
        out.add(new Message(body));              // 解码出一个完整消息,加到 out
    }
}

// 编码器:对象 → 字节(继承 MessageToByteEncoder)
public class MyEncoder extends MessageToByteEncoder<Message> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) {
        out.writeInt(msg.getBody().length);      // 写长度
        out.writeBytes(msg.getBody());           // 写消息体
    }
}

// pipeline 里配置:解码器在前(入站),编码器(出站)
ch.pipeline()
  .addLast(new MyDecoder())        // 入站:字节 → Message
  .addLast(new MyEncoder())        // 出站:Message → 字节
  .addLast(new BusinessHandler()); // 业务处理 Message

⚠️ 编解码器最核心的价值是「隔离字节和对象」+「解决粘包半包」,理解 ByteToMessageDecoder 怎么处理拆包是关键。TCP 是「字节流」,没有「消息边界」的概念——你 write 一个消息,对端可能一次读到「半个消息」(半包)、或「一个半消息」「两个消息粘在一起」(粘包)。ByteToMessageDecoder 帮你处理这个:它内部维护一个累积缓冲区(cumulation),每次读到新字节就追加进去,然后调你的 decode——你在 decode 里判断「累积的字节够不够一个完整消息」,够就解码一个(加到 out)、不够就返回等更多字节。关键点:不够时不要读(用 resetReaderIndex 重置),够时才读并解码out 里加了几个对象,就往后传几个(可能一次解出多个)。Netty 还提供了现成的解码器(LengthFieldBasedFrameDecoder 按长度字段、LineBasedFrameDecoder 按行、DelimiterBasedFrameDecoder 按分隔符),大多数情况不用手写。

完整版教学

一、为什么需要编解码器

先理解编解码器解决的问题:

网络传输的是字节,业务想处理的是对象:
  发送:业务有一个 Message 对象 → 要转成字节(ByteBuf)发出去
  接收:收到字节(ByteBuf)→ 要转成 Message 对象给业务处理

如果没有编解码器:
  业务 Handler 要自己处理字节:
    - 从 ByteBuf 解析出消息(还要处理粘包半包)
    - 把对象序列化成字节
  → 业务逻辑和"字节处理"混在一起,繁琐、易错

编解码器的作用:
  ① 隔离字节和对象:
     解码器:字节 → 对象(业务 Handler 只收到对象)
     编码器:对象 → 字节(业务 Handler 只写对象)
     → 业务 Handler 只处理对象,不碰字节
  ② 解决粘包半包(解码器的职责):
     TCP 字节流没有消息边界,解码器负责"拆出完整消息"

所以编解码器 = 字节↔对象的转换 + 拆包,让业务只处理对象

编解码器解决「网络传输字节、业务处理对象」的矛盾——发送时对象转字节、接收时字节转对象。没有编解码器,业务 Handler 要自己处理字节(解析消息、处理粘包半包、序列化),繁琐易错。编解码器的作用:① 隔离字节和对象(业务 Handler 只处理对象不碰字节)、② 解决粘包半包(解码器拆出完整消息)。理解「网络传字节业务处理对象、没编解码器业务要自己处理字节繁琐;编解码器隔离字节和对象(业务只处理对象)+解决粘包半包(拆出完整消息)」,就理解了编解码器的价值。

二、解码器:ByteToMessageDecoder

解码器(入站)——字节转对象:

ByteToMessageDecoder(字节 → 对象的解码器基类):
  重写 decode(ctx, ByteBuf in, List<Object> out):
    in:累积的字节(Netty 帮你累积)
    out:解码出的对象放这里(往后传)

工作方式:
  ① Netty 收到字节 → 追加到累积缓冲区(cumulation)
  ② 调你的 decode(in 是累积字节)
  ③ 你判断"够不够一个完整消息":
     够 → 读出一个消息、add 到 out(往后传)
     不够 → 直接 return(等更多字节,Netty 会保留累积字节)
  ④ decode 可能被反复调(每次读到新字节)

关键(拆包逻辑):
  在 decode 里判断消息完整性:
    if (可读字节 < 消息最小长度) return;   // 不够,等
    // 读消息头(如长度)
    if (可读字节 < 完整消息长度) { resetReaderIndex(); return; }  // 体不完整
    // 够了,读出完整消息
    out.add(message);

  ★ 不够时不要消费字节(要能"重来")
    用 markReaderIndex/resetReaderIndex 或先检查再读

一次可能解出多个消息:
  如果累积字节里有多个完整消息,可以在 decode 里循环解、add 多个
  (或 decode 被反复调,每次解一个)

所以 ByteToMessageDecoder = 累积字节 + 你判断完整性 + 解码出对象

解码器 ByteToMessageDecoder(字节转对象)——重写 decode(ctx, in, out)(in 是累积字节、out 放解码对象)。工作方式:Netty 收字节追加到累积缓冲区 → 调 decode → 你判断够不够一个完整消息(够就读出加到 out、不够 return 等更多字节)→ decode 反复调拆包逻辑关键:判断消息完整性,不够时不消费字节(要能重来,用 markReaderIndex/resetReaderIndex)、够了才读。一次可能解出多个消息(循环解或反复调)。理解「ByteToMessageDecoder 字节转对象:decode(in 累积字节/out 放对象)、Netty 累积字节调 decode、判断完整性(够读出加 out/不够 return 等)、不够时不消费字节(markReaderIndex/reset)、一次可能解多个」,就掌握了解码器。

三、编码器:MessageToByteEncoder

编码器(出站)——对象转字节:

MessageToByteEncoder<T>(对象 → 字节的编码器基类):
  泛型 T 是要编码的对象类型
  重写 encode(ctx, T msg, ByteBuf out):
    msg:要编码的对象
    out:编码后的字节写这里(发出去)

工作方式:
  业务 Handler writeAndFlush(message)(出站)
  → 经过编码器 encode(msg, out)
  → 你把 msg 转成字节写进 out
  → out 的字节被发送出去

例(长度 + 内容的编码):
  protected void encode(ctx, Message msg, ByteBuf out) {
    out.writeInt(msg.getBody().length);   // 先写长度(4字节)
    out.writeBytes(msg.getBody());        // 再写内容
  }
  → 和解码器对应(解码器先读长度、再读内容)

编码器和解码器要"配对":
  编码器怎么写(长度+内容),解码器就怎么读(读长度、读内容)
  → 协议格式一致,才能正确编解码

MessageToMessageEncoder(对象 → 对象):
  如果要 对象 → 另一种对象(如业务对象 → String)
  用 MessageToMessageEncoder(不是转字节)

所以 MessageToByteEncoder = 把对象转成字节写进 out

编码器 MessageToByteEncoder<T>(对象转字节)——泛型 T 是对象类型,重写 encode(ctx, msg, out)(msg 是对象、out 写字节)。工作方式:业务 writeAndFlush(message)(出站)→ 经过编码器 encode → 把 msg 转字节写进 out → 发送。例:先写长度再写内容(和解码器对应——编码器怎么写解码器就怎么读,协议格式一致)。MessageToMessageEncoder(对象转对象,不是转字节)。理解「MessageToByteEncoder 对象转字节:encode(msg 对象/out 写字节)、writeAndFlush 经过编码器转字节发送、和解码器配对(编码写什么解码读什么协议一致);MessageToMessageEncoder 对象转对象」,就掌握了编码器。

四、Codec:编解码合一

Codec——把编码和解码合在一个类:

Codec(编解码器合一):
  一个类同时处理编码和解码(省得写两个类)

ByteToMessageCodec<T>(字节↔对象):
  同时重写:
    encode(ctx, T msg, ByteBuf out):对象 → 字节
    decode(ctx, ByteBuf in, List<Object> out):字节 → 对象
  → 一个类搞定编解码

MessageToMessageCodec<INBOUND, OUTBOUND>(对象↔对象):
  encode 和 decode 都是对象↔对象

Codec vs 分开写:
  分开(Decoder + Encoder 两个类):
    职责单一、可复用(解码器/编码器可以单独用)
  合一(Codec 一个类):
    编解码逻辑在一起(协议相关的逻辑集中),少一个类
  → 看情况,编解码紧密相关时用 Codec,需要复用时分开

例:
  public class MyCodec extends ByteToMessageCodec<Message> {
    protected void encode(ctx, Message msg, ByteBuf out) { ... }
    protected void decode(ctx, ByteBuf in, List<Object> out) { ... }
  }

所以 Codec = 编解码合一(一个类处理双向)

Codec(编解码合一)——一个类同时处理编码和解码:ByteToMessageCodec<T>(字节↔对象,同时重写 encode(对象转字节)和 decode(字节转对象))、MessageToMessageCodec(对象↔对象)。Codec vs 分开写:分开(Decoder+Encoder 两类,职责单一可复用)、合一(Codec 一类,编解码逻辑集中、少一个类)——编解码紧密相关用 Codec、需要复用分开。理解「Codec 编解码合一:ByteToMessageCodec(字节↔对象,重写 encode+decode)/MessageToMessageCodec(对象↔对象);vs 分开:分开职责单一可复用、合一逻辑集中;紧密相关用 Codec、复用分开」,就掌握了 Codec。

五、现成的解码器

Netty 提供了现成的解码器,大多数情况不用手写:

Netty 内置的解码器(解决粘包半包的常见方案):
  ① LengthFieldBasedFrameDecoder(最常用):
     按"长度字段"拆包——消息头有一个长度字段,先读长度、再读那么多字节
     配置:长度字段偏移、长度字段字节数、长度调整等
     → 适合"长度+内容"的协议(最通用)

  ② LineBasedFrameDecoder:
     按"行"拆包——以换行符(\n、\r\n)分隔消息
     → 适合文本行协议

  ③ DelimiterBasedFrameDecoder:
     按"分隔符"拆包——以指定的分隔符分隔消息
     → 适合自定义分隔符的协议

  ④ FixedLengthFrameDecoder:
     按"固定长度"拆包——每个消息固定长度
     → 适合定长消息

  ⑤ 现成的协议编解码:
     HttpRequestDecoder/Encoder(HTTP)
     ProtobufDecoder/Encoder(Protobuf)
     StringDecoder/Encoder(字符串)

用法:
  ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(...))
  → 拆出完整帧后,再接自己的业务解码器/Handler

选择:
  长度+内容协议 → LengthFieldBasedFrameDecoder(最常用)
  文本行 → LineBasedFrameDecoder
  分隔符 → DelimiterBasedFrameDecoder
  定长 → FixedLengthFrameDecoder
  标准协议 → 用现成的(HTTP/Protobuf 等)

所以大多数拆包用现成的解码器,不用手写

Netty 内置现成的解码器(解决粘包半包):LengthFieldBasedFrameDecoder(最常用,按长度字段拆包,适合「长度+内容」协议)、② LineBasedFrameDecoder(按行拆,文本协议)、③ DelimiterBasedFrameDecoder(按分隔符)、④ FixedLengthFrameDecoder(定长)、⑤ 现成协议编解码(HTTP/Protobuf/String)。用法:pipeline 里加解码器拆出完整帧、再接业务 Handler。选择:长度+内容用 LengthFieldBasedFrameDecoder(最常用)、文本行用 LineBased、分隔符用 Delimiter、定长用 FixedLength、标准协议用现成的。理解「Netty 内置解码器:LengthFieldBasedFrameDecoder(最常用,长度字段拆包)/LineBasedFrameDecoder(按行)/DelimiterBasedFrameDecoder(分隔符)/FixedLengthFrameDecoder(定长)/HTTP-Protobuf-String;大多数拆包用现成的不用手写」,就掌握了现成的解码器。

六、pipeline 顺序与实践

编解码器在 pipeline 里的顺序和实践:

pipeline 里编解码器的顺序:
  入站(读,字节→对象):解码器在前
  出站(写,对象→字节):编码器在后(但出站是反向执行)

典型 pipeline:
  ch.pipeline()
    .addLast(new LengthFieldBasedFrameDecoder(...))  // ① 拆帧
    .addLast(new MyDecoder())                        // ② 帧→业务对象
    .addLast(new MyEncoder())                        // 编码器(出站)
    .addLast(new BusinessHandler());                 // ③ 业务处理对象

执行顺序:
  入站(读):字节 → FrameDecoder → MyDecoder → BusinessHandler(对象)
    → 入站按添加顺序执行(前面的先处理)
  出站(写):BusinessHandler → MyEncoder → 字节
    → 出站按添加逆序执行(后面的先处理)
  (见 channel-pipeline-handler 题的入站/出站传播)

实践建议:
  ① 拆包用现成的解码器(LengthFieldBasedFrameDecoder 等)
  ② 拆帧后再接业务解码器(帧 → 业务对象)
  ③ 编码器和解码器协议格式要配对
  ④ 手写解码器注意:不够时不消费字节、可能一次解多个
  ⑤ 业务 Handler 只处理对象(编解码隔离了字节)

核心总结:
  编解码器隔离字节和对象、解决粘包半包
  解码器 ByteToMessageDecoder(字节→对象,含拆包)
  编码器 MessageToByteEncoder(对象→字节)
  大多数拆包用现成解码器,业务 Handler 只处理对象

编解码器在 pipeline 的顺序:入站(读)解码器在前、出站(写)编码器(出站反向执行)。典型 pipeline:FrameDecoder(拆帧)→ MyDecoder(帧转对象)→ MyEncoder → BusinessHandler。执行:入站按添加顺序(字节→解码→业务对象)、出站按逆序(业务→编码→字节)。实践:拆包用现成解码器、拆帧后接业务解码器、编解码协议配对、手写解码器注意不够时不消费字节、业务 Handler 只处理对象。理解「pipeline 顺序:入站解码器在前、出站编码器反向执行;典型 FrameDecoder→业务 Decoder→Encoder→Business;入站按顺序出站按逆序;实践:现成解码器拆包+业务解码器+协议配对+业务只处理对象」,就掌握了 pipeline 顺序和实践。

记忆钩子:「Netty 编解码器 Codec=字节↔对象转换(网络传字节、业务处理对象)+解决粘包半包;两方向:①解码器(入站)ByteToMessageDecoder(字节→对象,重写 decode(in 累积字节/out 放对象)、判断消息完整性够读出加 out/不够 return 等、不够时不消费字节用 markReaderIndex/reset)②编码器(出站)MessageToByteEncoder(对象→字节,重写 encode)、编解码要配对(编码写什么解码读什么);Codec 合一 ByteToMessageCodec(字节↔对象)/MessageToMessageCodec(对象↔对象);★现成解码器:LengthFieldBasedFrameDecoder(最常用长度字段)/LineBasedFrameDecoder(按行)/DelimiterBasedFrameDecoder(分隔符)/FixedLengthFrameDecoder(定长),大多数不用手写;业务 Handler 只处理对象」

七、常见误区与追问

  • 误区:TCP 传输有消息边界,不用处理粘包半包。 TCP 是字节流、没有消息边界——你 write 一个消息,对端可能一次读到半个消息(半包)或多个消息粘一起(粘包);必须用解码器(ByteToMessageDecoder 或现成的 LengthFieldBasedFrameDecoder 等)拆出完整消息;这是解码器的核心职责。
  • 误区:ByteToMessageDecoder 的 decode 里不够一个消息就随便读。 不够时不能消费字节——如果读了一部分又发现不够,要用 resetReaderIndex 重置读位置(或先检查再读),然后 return 等更多字节;Netty 会保留累积的字节、下次读到新字节再调 decode;如果乱读会导致字节错乱、解码失败。
  • 误区:编码器和解码器可以用不同的协议格式。 必须配对——编码器怎么写(如先写 4 字节长度、再写内容),解码器就要怎么读(先读 4 字节长度、再读那么多内容);协议格式不一致会导致解码错误;编码器和解码器是「同一个协议的两个方向」。
  • 误区:粘包半包都要手写解码器处理。 大多数不用——Netty 提供了现成的解码器:LengthFieldBasedFrameDecoder(按长度字段,最通用)、LineBasedFrameDecoder(按行)、DelimiterBasedFrameDecoder(按分隔符)、FixedLengthFrameDecoder(定长);根据你的协议选一个现成的,拆出完整帧后再接业务解码器;很少需要从头手写。
  • 追问:Netty 的解码器怎么解决粘包半包? ByteToMessageDecoder 内部维护一个累积缓冲区,每次读到新字节就追加进去,然后调你的 decode 方法;你在 decode 里判断「累积的字节够不够一个完整消息」——够就读出一个完整消息、add 到 out(往后传),不够就 return(等更多字节,不消费已读的字节);这样无论字节怎么分片到达,都能拼出完整消息;Netty 还提供 LengthFieldBasedFrameDecoder 等现成的解码器自动处理。
  • 追问:ByteToMessageDecoder 和 MessageToMessageDecoder 有什么区别? ByteToMessageDecoder 是「字节 → 对象」(in 是 ByteBuf 字节,负责从字节流解码出对象,含拆包);MessageToMessageDecoder 是「对象 → 对象」(in 是某种对象,负责把一种对象转成另一种,如把 String 转成业务对象);通常先用 ByteToMessageDecoder 把字节解成一个中间对象(如帧),再用 MessageToMessageDecoder 把中间对象转成业务对象;一个处理字节、一个处理对象转换。
  • 追问:编解码器在 ChannelPipeline 里怎么放,执行顺序是什么? 解码器(入站)和编码器(出站)都放在业务 Handler 之前;入站(读数据)按 pipeline 添加顺序执行:字节 → FrameDecoder(拆帧)→ 业务 Decoder(帧转对象)→ 业务 Handler(处理对象);出站(写数据)按逆序执行:业务 Handler → 业务 Encoder(对象转字节)→ 发送;因为入站 Handler 从头往后传、出站 Handler 从尾往前传(见 pipeline 的入站出站传播)。

八、加强记忆

编解码器(Codec)是 Netty 负责「字节 ↔ 对象」转换的处理器——网络传输字节(ByteBuf)、业务处理对象,编解码器在中间转换,并隔离两者(业务 Handler 只处理对象)。两个方向:① 解码器(入站)ByteToMessageDecoder——字节 → 对象,重写 decode(ctx, in, out)in 是 Netty 累积的字节、out 放解码出的对象);最重要的职责是解决粘包/半包——TCP 是字节流没有消息边界,在 decode 里判断「累积字节够不够一个完整消息,够就读出加到 out、不够就 return 等更多字节(不够时不消费字节,用 markReaderIndex/resetReaderIndex)」;② 编码器(出站)MessageToByteEncoder<T>——对象 → 字节,重写 encode(ctx, msg, out)编码器和解码器要配对(编码写什么、解码读什么,协议格式一致)。Codec(合一)ByteToMessageCodec(字节↔对象)、MessageToMessageCodec(对象↔对象)。大多数拆包用现成的解码器LengthFieldBasedFrameDecoder(最常用,按长度字段)LineBasedFrameDecoder(按行)、DelimiterBasedFrameDecoder(按分隔符)、FixedLengthFrameDecoder(定长),还有 HTTP/Protobuf/String 编解码。pipeline 顺序:入站解码器在前、出站编码器逆序执行。一句话「编解码器=字节↔对象转换+解决粘包半包(TCP 字节流无边界);ByteToMessageDecoder(字节→对象,decode 判断消息完整性够读出不够 return 等,不够不消费字节)、MessageToByteEncoder(对象→字节),编解码配对;现成解码器 LengthFieldBasedFrameDecoder(最常用)等大多数不用手写;业务 Handler 只处理对象」。