最近做了一个项目,项目中用到Netty来接受一些自定义的报文。
一、背景
tcp是以流的方式进行传输,在流里我们要判断消息的起始位置和结束位置。为了区分消息,往往采用下面的几种方式。
Netty中针对以上的方案都有已经实现好的解码器作为解决方案。
其中前三种比较简单。第四种有很多的参数比较复杂。
找到该类其中的一个构造方法,下面会根据一个实例来解释它的各个参数的用法,如下所示:
public LengthFieldBasedFrameDecoder(
int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
this(
ByteOrder.BIG_ENDIAN, maxFrameLength, lengthFieldOffset, lengthFieldLength,
lengthAdjustment, initialBytesToStrip, failFast);
}
我们以一个实际的例子,来解释下各个参数,假设报文格式如下表:
语法 |
长度 位数 |
Event_Tag |
8 |
Event Length |
32 |
Event Number |
16 |
for(i=0;i |
|
Event_id |
16 |
Event_parameters |
32 |
Event_time |
64 |
} |
|
SCID |
32 |
Event_CRC |
32 |
注:长度字段表示的长度是从长度字段以后到报文结束位置的长度。也就是长度字段的值没有把长度字段和长度字段以前的长度算进去。
下面我们希望使用Netty的LengthFieldBasedFFrameDecoder来为我们屏蔽掉底层的拆包粘包的细节。
下面拿一段真实的报文,我们试着解析一下。报文如下,16进制表示。
8e000000340003020200000065002018121114515902020000083600201812111452110202000008360020181211145247055db07a0e4d0fc0
Event_tag: 0x8e
Event length: 00000034 等于10进制的52。也就是从0x34后面的52字节是属于一条消息的。
00030202000000650020181211145159
02020000083600201812111452110202
000008360020181211145247055db07a
0e4d0fc0
上面的每一行是16字节,在加上最后的4字节正好是52个。
我们用程序测试结果来说明,各个参数设置不同对结果的影响。
服务端代码:
final EventLoopGroup parentGroup = new NioEventLoopGroup(2);
final EventLoopGroup workGroup = new NioEventLoopGroup(2);
final ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(parentGroup, workGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(final SocketChannel ch) {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(
new AudiencePacketFrameDecoder(ByteOrder.BIG_ENDIAN, MAX_FRAME_LENGTH, 1, 4, 0, 0, true));
}
}).option(ChannelOption.SO_BACKLOG, 1024);
try {
final ChannelFuture channelFuture = bootstrap.bind(9090).sync();
if (channelFuture.isSuccess()) {
LOGGER.info("server start");
}
channelFuture.channel().closeFuture().sync();
} catch (final InterruptedException e) {
LOGGER.info("", e);
} finally {
parentGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
我们的channelPipleLine中只有两个Channelhandler,第一个是打印日志的,第二个就是我们实现了LengthFiledBasedFrameDecoder的类。代码如下:
final ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (null == frame) {
return null;
}
LOGGER.info("hexString:" + ByteBufUtil.hexDump(frame));
我们的代码中只是打印了解码的结果。
当各个参数的设置为
的时候,结果是
hexString:8e000000340003020200000065002018121114515902020000083600201812111452110202000008360020181211145247055db07a0e4d0fc0
对比原始报文发现所有的报文都打印出来了。
测试一:
我们改变一下initialBytesToStrip 为5 看一下结果,其他位置不变
结果:
hexString:0003020200000065002018121114515902020000083600201812111452110202000008360020181211145247055db07a0e4d0fc0
对比原始报文发现少了8e00000034五个字节,我们从上可知,initialBytesToStrip 能帮助我们把报文的头部截取掉,或者截取一下与业务无关的数据。
测试二:
假设我们的长度字段把其本身和它之前的字段长度也包括进去了。
以前的报文:
8e000000340003020200000065002018121114515902020000083600201812111452110202000008360020181211145247055db07a0e4d0fc0
长度字段是00000034=52,它没有包含自己的长度和Event_tag的长度。下面我们稍作改变。假设它包含自己的长度。报文如下:
8e000000390003020200000065002018121114515902020000083600201812111452110202000008360020181211145247055db07a0e4d0fc0
注意长度字段变为 00000039。
下面参数不变,我们看程序的结果:
执行的时候,我们发现日志竟然没有打印。这是为什么呢?我们试着改变一下参数。我们把lengthAdjustment 设置为 --5,如下所示:
hexString:0003020200000065002018121114515902020000083600201812111452110202000008360020181211145247055db07a0e4d0fc0
我们发现结果又可以正常打印出来了。导致结果不一样的原因只有lengthAdjustment 参数的值,设置为0的时候我们打印不出结果,设置为-5的时候能正常打印出来。
看源码的过程中,发现这么一句。
报文的长度 += lengthAdjustment+ 长度字段结束位置的偏移量。
这个报文的长度就是我们返回的数据。当我们把lengthAdjustment 设置为0的时候,
报文的长度= 报文的长度(00000039=57)+0+5=62,我们的报文的总长度才为57,所以解码不出来结果。
当我们把lengthAdjustment 设置为--5的时候,
报文的长度=报文的长度(00000039=57)+(--5)+5=57。这样就能解析出结果了。
综合所述,如果报文中的长度字段的值包含了长度字段本身,那么设置补偿字段长度的时候应该把这个长度减去。
例如我们例子中的 长度字段加Tag的长度为5,长度字段的值为 00000039=57里面包含了这个长度字段的长度。所以Netty提供了这个参数来减去长度字段的长度值。
二、自定义拆包
由于业务需求,我需要把两种报文合在一起当成一个报文。这样我就没有办法用Netty提供给我们使用的解码器来出来拆包,粘包了。
思路就是,从流里读,只有读到符合自己要求的格式的数据的时候才往下传,否则就丢弃字节或等待。
代码如下:
public class UnpackingDecoder extends ByteToMessageDecoder {
private final static Logger LOGGER = LoggerFactory.getLogger(UnpackingDecoder.class);
int count=0;
@Override
protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List