Netty 的编解码器(Codec)是什么?ByteToMessageDecoder、MessageToByteEncoder 怎么用?
简化版
编解码器(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 只处理对象」。