LineBasedFrameDecoder
LineBasedFrameDecoder是回车换行解码器,如果用户发送的消息以回车换行符作为消息结束的标识,则可以直接使用Netty的LineBasedFrameDecoder对消息进行解码,只需要在初始化Netty服务端或者客户端时将LineBasedFrameDecoder正确的添加到ChannelPipeline中即可,不需要自己重新实现一套换行解码器。
LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n”或者“\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。防止由于数据报没有携带换行符导致接收到ByteBuf无限制积压,引起系统内存溢出。
它的使用效果如下:
解码之前:
±-----------------------------------------------------------------+
接收到的数据报
“This is a netty example for using the nio framework.\r\n When you“
±-----------------------------------------------------------------+
解码之后的ChannelHandler接收到的Object如下:
±-----------------------------------------------------------------+
解码之后的文本消息
“This is a netty example for using the nio framework.“
±-----------------------------------------------------------------+
通常情况下,LineBasedFrameDecoder会和StringDecoder配合使用,组合成按行切换的文本解码器,对于文本类协议的解析,文本换行解码器非常实用,例如对HTTP消息头的解析、FTP协议消息的解析等。
下面我们简单给出文本换行解码器的使用示例:
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new LineBasedFrameDecoder(1024));
arg0.pipeline().addLast(new StringDecoder());
arg0.pipeline().addLast(new UserServerHandler());
}
初始化Channel的时候,首先将LineBasedFrameDecoder添加到ChannelPipeline中,然后再依次添加字符串解码器StringDecoder,业务Handler。
DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder是分隔符解码器,用户可以指定消息结束的分隔符,它可以自动完成以分隔符作为码流结束标识的消息的解码。回车换行解码器实际上是一种特殊的DelimiterBasedFrameDecoder解码器。
分隔符解码器在实际工作中也有很广泛的应用,笔者所从事的电信行业,很多简单的文本私有协议,都是以特殊的分隔符作为消息结束的标识,特别是对于那些使用长连接的基于文本的私有协议。
分隔符的指定:与大家的习惯不同,分隔符并非以char或者string作为构造参数,而是ByteBuf,下面我们就结合实际例子给出它的用法。
假如消息以“$_”作为分隔符,服务端或者客户端初始化ChannelPipeline的代码实例如下:
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("$_"
.getBytes());
ch.pipeline().addLast(
new DelimiterBasedFrameDecoder(1024,
delimiter));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new UserServerHandler());
}
首先将“$_”转换成ByteBuf对象,作为参数构造DelimiterBasedFrameDecoder,将其添加到ChannelPipeline中,然后依次添加字符串解码器(通常用于文本解码)和用户Handler,请注意解码器和Handler的添加顺序,如果顺序颠倒,会导致消息解码失败。
FixedLengthFrameDecoder
FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包等问题,非常实用。
对于定长消息,如果消息实际长度小于定长,则往往会进行补位操作,它在一定程度上导致了空间和资源的浪费。但是它的优点也是非常明显的,编解码比较简单,因此在实际项目中仍然有一定的应用场景。
利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报,它都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下个包到达后进行拼包,直到读取到一个完整的包。
假如单条消息的长度是20字节,使用FixedLengthFrameDecoder解码器的效果如下:
解码前:
±-----------------------------------------------------------------+
接收到的数据报
“HELLO NETTY FOR USER DEVELOPER“
±-----------------------------------------------------------------+
解码后:
±-----------------------------------------------------------------+
解码后的数据报
“HELLO NETTY FOR USER“
±-----------------------------------------------------------------+
LengthFieldBasedFrameDecoder解码器
了解TCP通信机制的读者应该都知道TCP底层的粘包和拆包,当我们在接收消息的时候,显示不能认为读取到的报文就是个整包消息,特别是对于采用非阻塞I/O和长连接通信的程序。
如何区分一个整包消息,通常有如下4种做法:
固定长度,例如每120个字节代表一个整包消息,不足的前面补位。解码器在处理这类定常消息的时候比较简单,每次读到指定长度的字节后再进行解码;
通过回车换行符区分消息,例如HTTP协议。这类区分消息的方式多用于文本协议;
通过特定的分隔符区分整包消息;
通过在协议头/消息头中设置长度字段来标识整包消息。
前三种解码器之前的章节已经做了详细介绍,下面让我们来一起学习最后一种通用解码器-LengthFieldBasedFrameDecoder。
大多数的协议(私有或者公有),协议头中会携带长度字段,用于标识消息体或者整包消息的长度,例如SMPP、HTTP协议等。由于基于长度解码需求的通用性,以及为了降低用户的协议开发难度,Netty提供了LengthFieldBasedFrameDecoder,自动屏蔽TCP底层的拆包和粘包问题,只需要传入正确的参数,即可轻松解决“读半包“问题。
下面我们看看如何通过参数组合的不同来实现不同的“半包”读取策略。第一种常用的方式是消息的
解码前的字节缓冲区(14字节)
使用以下参数组合进行解码:
lengthFieldOffset = 0;
lengthFieldLength = 2;
lengthAdjustment = 0;
解码后的字节缓冲区(14字节)
通过ByteBuf.readableBytes()方法我们可以获取当前消息的长度,所以解码后的字节缓冲区可以不携带长度字段,由于长度字段在起始位置并且长度为2,所以将initialBytesToStrip设置为2,参数组合修改为:
lengthFieldOffset = 0;
lengthFieldLength = 2;
lengthAdjustment = 0;
initialBytesToStrip = 2。
解码后的字节缓冲区丢弃了长度字段,仅仅包含消息体,对于大多数的协议,解码之后消息长度没有用处,因此可以丢弃。
在大多数的应用场景中,长度字段仅用来标识消息体的长度,这类协议通常由消息长度字段+消息体组成,如上图所示的几个例子。但是,对于某些协议,长度字段还包含了消息头的长度。在这种应用场景中,往往需要使用lengthAdjustment进行修正。由于整个消息(包含消息头)的长度往往大于消息体的长度,所以,lengthAdjustment为负数。图2-6展示了通过指定lengthAdjustment字段来包含消息头的长度:
lengthFieldOffset = 0;
lengthFieldLength = 2;
lengthAdjustment = -2;
initialBytesToStrip = 0。
解码之后的码流:
由于协议种类繁多,并不是所有的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,需要使用lengthFieldOffset字段进行标识,下面的参数组合给出了如何解决消息长度字段不在首位的问题:
lengthFieldOffset = 2;
lengthFieldLength = 3;
lengthAdjustment = 0;
initialBytesToStrip = 0。
其中lengthFieldOffset表示长度字段在消息头中偏移的字节数,lengthFieldLength 表示长度字段自身的长度,解码效果如下:
由于消息头1的长度为2,所以长度字段的偏移量为2;消息长度字段Length为3,所以lengthFieldLength值为3。由于长度字段仅仅标识消息体的长度,所以lengthAdjustment和initialBytesToStrip都为0。
最后一种场景是长度字段夹在两个消息头之间或者长度字段位于消息头的中间,前后都有其它消息头字段,在这种场景下如果想忽略长度字段以及其前面的其它消息头字段,则可以通过initialBytesToStrip参数来跳过要忽略的字节长度,它的组合配置示意如下:
lengthFieldOffset = 1;
lengthFieldLength = 2;
lengthAdjustment = 1;
initialBytesToStrip = 3。
解码之前的码流(16字节):
由于HDR1的长度为1,所以长度字段的偏移量lengthFieldOffset为1;长度字段为2个字节,所以lengthFieldLength为2。由于长度字段是消息体的长度,解码后如果携带消息头中的字段,则需要使用lengthAdjustment进行调整,此处它的值为1,代表的是HDR2的长度,最后由于解码后的缓冲区要忽略长度字段和HDR1部分,所以lengthAdjustment为3。解码后的结果为13个字节,HDR1和Length字段被忽略。
事实上,通过4个参数的不同组合,可以达到不同的解码效果,用户在使用过程中可以根据业务的实际情况进行灵活调整。
由于TCP存在粘包和组包问题,所以通常情况下用户需要自己处理半包消息。利用LengthFieldBasedFrameDecoder解码器可以自动解决半包问题,它的习惯用法如下:
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65536,0,2));
pipeline.addLast("UserDecoder", new UserDecoder());
在pipeline中增加LengthFieldBasedFrameDecoder解码器,指定正确的参数组合,它可以将Netty的ByteBuf解码成整包消息,后面的用户解码器拿到的就是个完整的数据报,按照逻辑正常进行解码即可,不再需要额外考虑“读半包”问题,降低了用户的开发难度。