Netty解码器
ByteToMessageDecoder
一个标准的解码器将输入类型为ByteBuf缓冲区的数据进行解码,输出一个一个的Java POJO对象。Netty内置了这个解码器,叫作ByteToMessageDecoder,位在Netty的io.netty.handler.codec包中。所有的Netty中的解码器,都是Inbound入站处理器类型,都直接或者间接地实现了ChannelInboundHandler接口。
ByteToMessageDecoder解码的流程,大致具体可以描述为:
- 首先,它将上一站传过来的输入到Bytebuf中的数据进行解码,解码出一个List<Object>对象列表;
- 然后,迭代List<Object>列表,逐个将Java POJO对象传入下一站Inbound入站处理器。
如果要实现一个自己的ByteBuf解码器,流程大致如下:
(1)首先继承ByteToMessageDecoder抽象类。
(2)然后实现其基类的decode抽象方法。将ByteBuf到POJO解码的逻辑写入此方法。将Bytebuf二进制数据,解码成一个一个的Java POJO对象。
(3)在子类的decode方法中,需要将解码后的Java POJO对象,放入decode的List<Object>实参中。这个实参是ByteTo-MessageDecoder父类传入的,也就是父类的结果收集列表。在流水线的过程中,ByteToMessageDecoder调用子类decode方法解码完成后,会将List<Object>中的结果,一个一个地分开传递到下一站的Inbound入站处理器。
自定义整数解码器:
public class Byte2IntegerDecoder extends ByteToMessageDecoder {
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in,List<Object> out) {
while (in.readableBytes() >= 4) {
int i = in.readInt();
Logger.info("解码出一个整数: " + i);
out.add(i);
}
}
}
ByteToMessageDecoder传递给下一站的是解码之后的Java POJO对象,不是ByteBuf缓冲区。
(1)ByteBuf缓冲区由谁负责进行引用计数和释放管理的呢?基类ByteToMessageDecoder负责解码器的ByteBuf缓冲区的释放工作,它会调用ReferenceCountUtil.release(in)方法,将之前的ByteBuf缓冲区的引用数减1。
(2)如果这个ByteBuf被释放了,在后面还需要用到,怎么办呢?可以在decode方法中调用一次ReferenceCountUtil .retain(in)来增加一次引用计数。
ReplayingDecoder
ReplayingDecoder类是ByteToMessageDecoder的子类。其作用是:在读取ByteBuf缓冲区的数据之前,需要检查缓冲区是否有足够的字节。若ByteBuf中有足够的字节,则会正常读取;反之,如果没有足够的字节,则会停止解码。
public class Byte2IntegerDecoder extends ByteToMessageDecoder { @Override public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { int i = in.readInt(); Logger.info("解码出一个整数: " + i); out.add(i); }}
|
8 ReplayingDecoder基类的关键技术就是偷梁换柱,在将外部传入的ByteBuf缓冲区传给子类之前,换成了自己装饰过的ReplayingDecoderBuffer缓冲区。
- ReplayingDecoderBuffer类型的读取方法与ByteBuf类型的读取方法相比,做了什么样的功能增强呢?主要是进行二进制数据长度的判断,如果长度不足,则抛出异常。这个异常会反过来被ReplayingDecoder基类所捕获,将解码工作停止。
- ReplayingDecoder的作用,远远不止于进行长度判断,它更重要的作用是用于分包传输的应用场景。
- ReplayingDecoder类型和所有的子类都需要保存状态信息,都有状态,不适合在不同的通道之间共享。
一个案例解析两个整数,然后求和最为解码结果:
public class IntegerAddDecoder extends ReplayingDecoder<IntegerAddDecoder.Status> { enum Status { PARSE_1, PARSE_2 } private int first; private int second; public IntegerAddDecoder() { //构造函数中,需要初始化父类的state 属性,表示当前阶段 super(Status.PARSE_1); } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { switch (state()) { case PARSE_1: //从装饰器ByteBuf 中读取数据 first = in.readInt(); //第一步解析成功, // 进入第二步,并且设置“读指针断点”为当前的读取位置 checkpoint(Status.PARSE_2); break; case PARSE_2: second = in.readInt(); Integer sum = first + second; out.add(sum); checkpoint(Status.PARSE_1); break; default: break; } } } |
checkpoint(Status)方法有两个作用:
(1)设置state属性的值,更新一下当前的状态。
(2)还有一个非常大的作用,就是设置“读断点指针”。“读断点指针”是ReplayingDecoder类的另一个重要的成员,它保存着装饰器内部ReplayingDecoderBuffer成员的起始读指针,有点儿类似于mark标记。当读数据时,一旦可读数据不够,ReplayingDecoderBuffer在抛出ReplayError异常之前,ReplayingDecoder会把读指针的值还原到之前的checkpoint(IntegerAddDecoder.Status)方法设置的“读断点指针”(checkpoint)。于是乎,在ReplayingDecoder下一次读取时,还会从之前设置的断点位置开始。
如何获取字符串的长度信息呢?
这个问题和程序所使用的具体传输协议是强相关的。一般来说,在Netty中进行字符串的传输,可以采用普通的Header-Content内容传输协议: (1)在协议的Head部分放置字符串的字节长度。Head部分可以用一个整型int来描述即可。(2)在协议的Content部分,放置的则是字符串的字节数组。
通过ReplayingDecoder解码器,可以正确地解码分包后的ByteBuf数据包。但是,在实际的开发中,不太建议继承这个类,原因是:
(1)不是所有的ByteBuf操作都被ReplayingDecoderBuffer装饰类所支持,可能有些ByteBuf操作在ReplayingDecoder子类的decode实现方法中被使用时就会抛出ReplayError异常。
(2)在数据解析逻辑复杂的应用场景,ReplayingDecoder在解析速度上相对较差。
MessageToMessageDecoder<I>
将POJO解解码为另一个POJO的解码器基类MessageToMessageDecoder<I>。
NETTY中开箱即用的Decoder:
(1)固定长度数据包解码器——FixedLengthFrameDecoder 适用场景:每个接收到的数据包的长度,都是固定的,例如100个字节。
(2)行分割数据包解码器——LineBasedFrameDecoder 适用场景:每个ByteBuf数据包,使用换行符(或者回车换行符)作为数据包的边界分割符。
(3)自定义分隔符数据包解码器——DelimiterBasedFrameDecoder。DelimiterBasedFrameDecoder是LineBasedFrameDecoder按照行分割的通用版本。
(4)自定义长度数据包解码器——LengthFieldBasedFrameDecoder 这是一种基于灵活长度的解码器。在ByteBuf数据包中,加了一个长度字段,保存了原始数据包的长度。
Netty编译器
首先,编码器是一个Outbound出站处理器,负责处理“出站”数据;其次,编码器将上一站Outbound出站处理器传过来的输入(Input)数据进行编码或者格式转换,然后传递到下一站ChannelOutboundHandler出站处理器。编码器是ChannelOutboundHandler出站处理器的实现类。
public class Integer2ByteEncoder extends MessageToByteEncoder<Integer> { @Override public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception { out.writeInt(msg); Logger.info("encoder Integer = " + msg); } } |
具有相互配套逻辑的编码器和解码器能否放在同一个类中呢?
答案是肯定的:这就要用到Netty的新类型——Codec类型。ByteToMessageCodec同时包含了编码encode和解码decode两个抽象方法。都需要自己实现。编码器和解码器如果要结合起来,除了继承的方法之外,还可以通过组合的方式实现。与继承相比,组合会带来更大的灵活性:编码器和解码器可以捆绑使用,也可以单独使用。Netty提供了一个新的组合器——CombinedChannelDuplexHandler基类。
|
|
对于对性能要求不是太高的服务器程序,可以选择JSON系列的序列化框架;对于性能要求比较高的服务器程序,则应该选择传输效率更高的二进制序列化框架,目前的建议是Protobuf。Netty也提供了相应的编解码器,为Protobuf解决了有关Socket通信中“半包、粘包”等问题。
底层网络是以二进制字节报文的形式来传输数据的。读数据的过程大致为:当IO可读时,Netty会从底层网络将二进制数据读到ByteBuf缓冲区中,再交给Netty程序转成Java POJO对象。写数据的过程大致为:这中间编码器起作用,是将一个Java类型的数据转换成底层能够传输的二进制ByteBuf缓冲数据。解码器的作用与之相反,是将底层传递过来的二进制ByteBuf缓冲数据转换成Java能够处理的Java POJO对象。
在Netty中,分包的方法,主要有两种方法:
(1)可以自定义解码器分包器:基于ByteToMessageDecoder或者ReplayingDecoder,定义自己的进程缓冲区分包器。
(2)使用Netty内置的解码器。如使用Netty内置的LengthFieldBasedFrameDecoder自定义分隔符数据包解码器,对进程缓冲区ByteBuf进行正确的分包。