Netty编码器深度解析
一、为什么需要编码器?
编码器和解码器是一对相反的操作。解码器是把网络传输过来的字节流转换成我们的Java对象,那编码器就是把Java对象转换成字节流发送到网络。
解码器:字节流 → Java对象(入站)
编码器:Java对象 → 字节流(出站)
在网络通信中,所有数据最终都要以字节的形式传输,编码器就是负责这个转换工作的。
二、MessageToByteEncoder 深度解析
从名字就能看出来,MessageToByteEncoder是将消息对象(Message)编码为字节(Byte)的编码器,也就是说它的输入是我们自定义的消息对象,输出是ByteBuf。这是最基础也是最常用的编码器类型。
2.1 核心工作流程
当我们调用ctx.write(msg)发送消息时,编码器的工作流程是这样的:
public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
ByteBuf buf = null;
try {
// 1. 检查消息类型是否匹配
if (acceptOutboundMessage(msg)) {
I cast = (I) msg;
// 2. 分配ByteBuf
buf = allocateBuffer(ctx, cast, preferDirect);
try {
// 3. 调用encode方法编码
encode(ctx, cast, buf);
} finally {
// 4. 释放原始消息
ReferenceCountUtil.release(cast);
}
// 5. 传递编码后的ByteBuf
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
// 类型不匹配,直接传递
ctx.write(msg, promise);
}
} finally {
if (buf != null) {
buf.release();
}
}
}
protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
}
整个流程很清晰:检查类型 -> 分配缓冲区 -> 编码 -> 释放原消息 -> 传递ByteBuf。
2.2 类型匹配机制
编码器通过泛型来指定它能处理的消息类型:
public class IntegerEncoder extends MessageToByteEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
out.writeInt(msg);
}
}
// 使用时
ctx.write(123); // ✓ 匹配,会调用encode()
ctx.write("Hello"); // ✗ 不匹配,直接传递给下一个Handler
这个机制和解码器的类型匹配是一样的,都是通过泛型来判断。
2.3 ByteBuf的分配
编码器会自动分配ByteBuf,默认使用直接内存:
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, I msg, boolean preferDirect) {
return preferDirect ?
ctx.alloc().ioBuffer() : // 直接内存
ctx.alloc().heapBuffer(); // 堆内存
}
直接内存适合网络传输,因为可以零拷贝直接发送到网络,而堆内存需要先拷贝到直接内存才能发送。所以默认使用直接内存是合理的。
2.4 简单示例
我们来看一个简单的例子,将Integer编码成4个字节:
public class IntegerEncoder extends MessageToByteEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
out.writeInt(msg);
}
}
数据流演示:
输入: Integer 值 = 258 (0x00000102)
encode() 执行:
out.writeInt(258)
ByteBuf 状态:
索引: 0 1 2 3
┌─────┬─────┬─────┬─────┐
数据: │ 00 │ 00 │ 01 │ 02 │
└─────┴─────┴─────┴─────┘
输出: ByteBuf [0x00, 0x00, 0x01, 0x02]
2.5 自定义协议编码器
假设我们要实现这个协议:[长度(4字节)][类型(1字节)][数据(N字节)]
public class CustomProtocolEncoder extends MessageToByteEncoder<CustomMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, CustomMessage msg, ByteBuf out) {
byte[] data = msg.getData();
byte type = msg.getType();
// 写入长度
out.writeInt(data.length);
// 写入类型
out.writeByte(type);
// 写入数据
out.writeBytes(data);
}
}
数据流演示:
输入: CustomMessage{type=1, data="Hello"}
encode() 执行过程:
1. data.length = 5
2. type = 0x01
3. data = [0x48, 0x65, 0x6C, 0x6C, 0x6F]
编码后的 ByteBuf:
索引: 0 1 2 3 4 5 6 7 8 9
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
数据: │ 00 │ 00 │ 00 │ 05 │ 01 │ 48 │ 65 │ 6C │ 6C │ 6F │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
└────────长度=5────────┘ └类型┘ └──────"Hello"──────┘
三、MessageToMessageEncoder 深度解析
从名字可以看出,MessageToMessageEncoder是将一种消息对象(Message)转换为另一种消息对象(Message)的编码器。它的输入和输出都是消息对象,而不是字节流。这个编码器通常用在第一层编码,也就是在MessageToByteEncoder之前,对业务对象做预处理。
3.1 与 MessageToByteEncoder 的区别
MessageToByteEncoder: 消息对象 → ByteBuf
MessageToMessageEncoder: 消息对象A → 消息对象B
可以看到,MessageToByteEncoder的输出是字节流,而MessageToMessageEncoder的输出还是消息对象,需要后续的编码器继续处理。
3.2 典型应用场景
最常见的场景是多层编码,比如先把业务对象转成协议对象,再把协议对象转成字节流:
// 第一层:业务对象 → 协议对象
public class UserToProtocolEncoder extends MessageToMessageEncoder<User> {
@Override
protected void encode(ChannelHandlerContext ctx, User user, List<Object> out) {
ProtocolMessage msg = new ProtocolMessage();
msg.setType(MessageType.USER_INFO);
msg.setData(user.toBytes());
out.add(msg);
}
}
// 第二层:协议对象 → ByteBuf
public class ProtocolToByteEncoder extends MessageToByteEncoder<ProtocolMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, ProtocolMessage msg, ByteBuf out) {
out.writeInt(msg.getType());
out.writeBytes(msg.getData());
}
}
// Pipeline 配置
pipeline.addLast(new UserToProtocolEncoder());
pipeline.addLast(new ProtocolToByteEncoder());
数据流:
User对象
↓ UserToProtocolEncoder
ProtocolMessage对象
↓ ProtocolToByteEncoder
ByteBuf
↓
网络传输
3.3 一对多编码
MessageToMessageEncoder还有一个特点,就是可以一对多编码,一个输入可以产生多个输出:
public class MessageSplitEncoder extends MessageToMessageEncoder<LargeMessage> {
private static final int MAX_SIZE = 1024;
@Override
protected void encode(ChannelHandlerContext ctx, LargeMessage msg, List<Object> out) {
byte[] data = msg.getData();
// 将大消息拆分成多个小消息
for (int i = 0; i < data.length; i += MAX_SIZE) {
int length = Math.min(MAX_SIZE, data.length - i);
byte[] chunk = new byte[length];
System.arraycopy(data, i, chunk, 0, length);
SmallMessage small = new SmallMessage();
small.setData(chunk);
small.setSequence(i / MAX_SIZE);
out.add(small); // 添加多个输出
}
}
}
数据流演示:
输入: LargeMessage (3000字节)
encode() 执行:
拆分成3个 SmallMessage
- SmallMessage[0]: 1024字节
- SmallMessage[1]: 1024字节
- SmallMessage[2]: 952字节
输出: List<SmallMessage> (3个对象)
Pipeline 后续处理:
每个 SmallMessage 会被单独传递给下一个Handler
四、编码器的内存管理
4.1 自动释放机制
编码器会自动释放原始消息,避免内存泄漏:
try {
encode(ctx, cast, buf);
} finally {
// 自动释放原始消息
ReferenceCountUtil.release(cast);
}
这个机制和解码器是一样的,编码完成后,原始消息就没用了,需要释放掉。
4.2 何时需要 retain()?
如果你在encode方法中需要异步处理消息,就需要手动retain:
// ❌ 错误:异步处理会导致问题
@Override
protected void encode(ChannelHandlerContext ctx, CustomMessage msg, ByteBuf out) {
executor.submit(() -> {
// 此时 msg 可能已经被释放!
byte[] data = msg.getData(); // 可能抛出异常
out.writeBytes(data);
});
}
// ✅ 正确:同步处理,不需要 retain
@Override
protected void encode(ChannelHandlerContext ctx, CustomMessage msg, ByteBuf out) {
byte[] data = msg.getData();
out.writeBytes(data);
}
一般情况下,encode方法都是同步执行的,不需要考虑retain的问题。
五、编码器与解码器的配合使用
5.1 完整的编解码链
编码器和解码器通常成对出现,一个负责编码,一个负责解码:
// 服务端 Pipeline
pipeline.addLast("decoder", new CustomProtocolDecoder()); // 入站
pipeline.addLast("encoder", new CustomProtocolEncoder()); // 出站
pipeline.addLast("handler", new BusinessHandler());
// 客户端 Pipeline
pipeline.addLast("encoder", new CustomProtocolEncoder()); // 出站
pipeline.addLast("decoder", new CustomProtocolDecoder()); // 入站
pipeline.addLast("handler", new ClientHandler());
数据流向:
客户端发送:
User对象
↓ CustomProtocolEncoder (出站)
ByteBuf
↓ 网络传输
ByteBuf
↓ CustomProtocolDecoder (入站)
User对象
↓ BusinessHandler
服务端响应:
Response对象
↓ CustomProtocolEncoder (出站)
ByteBuf
↓ 网络传输
ByteBuf
↓ CustomProtocolDecoder (入站)
Response对象
↓ ClientHandler
5.2 对称的编解码器实现
编码器和解码器的实现应该是对称的,编码器怎么写,解码器就怎么读:
// 编码器
public class MessageEncoder extends MessageToByteEncoder<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) {
byte[] data = msg.getData();
out.writeInt(data.length); // 写入长度
out.writeByte(msg.getType()); // 写入类型
out.writeBytes(data); // 写入数据
}
}
// 解码器(对称实现)
public class MessageDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (in.readableBytes() < 5) return;
in.markReaderIndex();
int length = in.readInt(); // 读取长度
byte type = in.readByte(); // 读取类型
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
byte[] data = new byte[length];
in.readBytes(data); // 读取数据
Message msg = new Message();
msg.setType(type);
msg.setData(data);
out.add(msg);
}
}
可以看到,编码器写什么,解码器就读什么,顺序完全一致。
六、常见问题和最佳实践
6.1 问题1:忘记写入数据
// ❌ 错误示例
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
// 什么都不做
}
// 结果:buf.isReadable() = false,会发送空缓冲区
// ✅ 正确做法
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
out.writeInt(msg); // 必须写入数据
}
6.2 问题2:编码器顺序错误
// ❌ 错误的 Pipeline 配置
pipeline.addLast(new StringEncoder()); // 期望 String
pipeline.addLast(new IntegerToStringEncoder()); // 输出 String
// 问题:出站Handler的执行顺序是从后往前!
// 实际执行顺序:IntegerToStringEncoder → StringEncoder
// 但 IntegerToStringEncoder 在后面,它的输出无法被 StringEncoder 处理
// ✅ 正确的配置
pipeline.addLast(new IntegerToStringEncoder()); // Integer → String
pipeline.addLast(new StringEncoder()); // String → ByteBuf
// 执行顺序:StringEncoder → IntegerToStringEncoder
// 但因为类型匹配,实际是:
// 1. IntegerToStringEncoder 处理 Integer,输出 String
// 2. StringEncoder 处理 String,输出 ByteBuf
记住:出站Handler的执行顺序是从后往前,但因为类型匹配机制,实际上每个Handler只处理它能处理的类型。
6.3 问题3:ByteBuf 泄漏
// ❌ 错误示例
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
ByteBuf temp = ctx.alloc().buffer();
temp.writeInt(msg);
out.writeBytes(temp);
// 忘记释放 temp!
}
// ✅ 正确做法
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
ByteBuf temp = ctx.alloc().buffer();
try {
temp.writeInt(msg);
out.writeBytes(temp);
} finally {
temp.release(); // 释放临时缓冲区
}
}
// ✅ 更好的做法:直接写入,不需要临时缓冲区
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) {
out.writeInt(msg);
}
七、Netty 内置编码器
Netty提供了一些常用的编码器,可以直接使用:
// 1. 字符串编码器
pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
// 2. Base64 编码器
pipeline.addLast(new Base64Encoder());
// 3. 长度字段编码器(自动添加长度前缀)
pipeline.addLast(new LengthFieldPrepender(4));
// 4. 对象序列化编码器
pipeline.addLast(new ObjectEncoder());
LengthFieldPrepender特别有用,它会自动在数据前面添加长度字段:
// 配置
pipeline.addLast(new LengthFieldPrepender(4));
pipeline.addLast(new StringEncoder());
// 发送
ctx.write("Hello");
// 编码过程:
// 1. StringEncoder: "Hello" → [0x48, 0x65, 0x6C, 0x6C, 0x6F]
// 2. LengthFieldPrepender: 添加长度 → [0x00, 0x00, 0x00, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F]
八、总结
8.1 编码器选择指南
三种编码器的核心区别在于它们处理的数据类型不同:
- MessageToByteEncoder:消息对象 → 字节流,这是最基础的编码器,直接将对象编码成网络可传输的字节数据
- MessageToMessageEncoder:消息对象 → 消息对象,用于对业务对象做预处理,比如将User对象转成ProtocolMessage对象
| 场景 | 推荐编码器 | 原因 |
|---|---|---|
| 对象 → 字节 | MessageToByteEncoder | 直接编码为网络数据 |
| 对象 → 对象 | MessageToMessageEncoder | 协议转换、数据转换 |
| 字符串发送 | StringEncoder | Netty内置,开箱即用 |
| 自定义协议 | MessageToByteEncoder | 灵活控制字节格式 |
8.2 核心要点
- 编码器处理出站数据,继承
ChannelOutboundHandlerAdapter - 自动释放原始消息,避免内存泄漏
- preferDirect 控制内存类型,默认使用直接内存
- MessageToMessageEncoder 可以一对多,一个输入产生多个输出
- 注意 Pipeline 顺序,出站Handler从后往前执行,但类型匹配机制会确保正确的处理顺序
8.3 与解码器的对比
| 特性 | 解码器 | 编码器 |
|---|---|---|
| 数据方向 | 入站(Inbound) | 出站(Outbound) |
| 基类 | ChannelInboundHandlerAdapter | ChannelOutboundHandlerAdapter |
| 核心方法 | channelRead() | write() |
| 累积机制 | 有(cumulation) | 无 |
| 典型用途 | 字节 → 对象 | 对象 → 字节 |
179



被折叠的 条评论
为什么被折叠?



