用Netty自己写拆包粘包解码器

最近做了一个项目,项目中用到Netty来接受一些自定义的报文。

一、背景

tcp是以流的方式进行传输,在流里我们要判断消息的起始位置和结束位置。为了区分消息,往往采用下面的几种方式。

  1. 消息有固定的长度
  2. 换行符做分隔
  3. 用一个特殊的分隔符来分隔
  4. 在消息头中增加length字段

Netty中针对以上的方案都有已经实现好的解码器作为解决方案。

  1. 针对有固定长度的消息,Netty提供了FixedlengthFrameDecoder解决。
  2. 针对以回车换行符作为消息结束符的,Netty提供了LineBasedFrameDecoder。
  3. 针对以分隔符作为消息结束符的,Netty提供了DelimiterBasedFrameDecoder。
  4. 针对消息头中的放长度字段来分隔的,Netty提供了LengthFieldBasedFrameDecoder。

其中前三种比较简单。第四种有很多的参数比较复杂。

找到该类其中的一个构造方法,下面会根据一个实例来解释它的各个参数的用法,如下所示:

public LengthFieldBasedFrameDecoder(
            int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
            int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
        this(
                ByteOrder.BIG_ENDIAN, maxFrameLength, lengthFieldOffset, lengthFieldLength,
                lengthAdjustment, initialBytesToStrip, failFast);
    }
  • 第一个参数 maxFrameLength: 指定允许的最大长度
  • 第二个参数lengthFieldOffset:长度字段在报文中的偏移量。
  • 第三个参数lengthFieldLength: 长度字段的长度。
  • 第四个参数lengthAdjustment:长度字段的补偿长度。这个不好理解,下面会根据例子解释。这里先不必深究。
  • 第五个参数initialBytesToStrip: 从报文开始位置截取的长度。
  • 第六个参数failFast:是指定ToLongFrameException该怎么报出来。当为true时即使流未读但是发现长度超过了我们设置的第一个参数就报错,当为false的时候,只有读超过我们设置的长度的时候才报错。

我们以一个实际的例子,来解释下各个参数,假设报文格式如下表:

语法

长度

位数

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来为我们屏蔽掉底层的拆包粘包的细节。

  • 第一个参数:设置成1024*1024。这个根据需要设置。
  • 第二个参数:我们的长度字段是Event Length,在报文中的位置是第二个字节。所以我们设置该值为1。如果Event_Tag的长度位数为16。那么该值就应该设置为2了。
  • 第三个参数:长度字段的长度,我们是32位的长度,是4字节,所以这个参数我们设置为4。
  • 第四个参数:长度字段的补偿字段,由于我们长度不包含长度字段本身的长度,所以我们暂且设置为0。
  • 第五个参数:从开始位置开始截取的长度。我们也暂且设置为0;
  • 第六个参数:设置为true.

下面拿一段真实的报文,我们试着解析一下。报文如下,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 out) {
        LOGGER.info("start UnpackingDecoder!");
        LOGGER.info("unpacking execute count:"+ count);
        count+=1;
        try {
            // 这里是防止出现 我们不想要的报文
            while (true) {
                if (in.readableBytes() < 2) {
                    return;
                }
                if ((in.getUnsignedByte(in.readerIndex()) ^ 0x8E)
                    == 0 || (in.getUnsignedShort(in.readerIndex()) ^ 0x020E) == 0) {
                    break;
                }
                LOGGER.warn("this frame is undefined:"+ByteBufUtil.hexDump(in,in.readerIndex(),1));
                in.skipBytes(1);
            }
            // 根据报文的开头确定是哪种报文,
            if ((in.getUnsignedByte(in.readerIndex()) ^ 0x8E) == 0) {
                LOGGER.info("this package is begin with 0x8E.");
                if (in.readableBytes() < 5) {
                    LOGGER.info("buffer size is less than 5,return");
                    return;
                }
                int businessTagByteSize = 1;
                int businessLengthFiledSize = 4;
                int sigTagByteSize = 1;
                int sigLengFiledSize = 2;
                long businessLength = in.getUnsignedInt(in.readerIndex() + businessTagByteSize);
                LOGGER.info("business frame length filed:" + ByteBufUtil
                    .hexDump(in, in.readerIndex() + businessTagByteSize, businessLengthFiledSize));
                int businessFrameLen = (int) (businessTagByteSize + businessLengthFiledSize + businessLength);
                if (in.readableBytes() < (businessFrameLen + sigTagByteSize + sigLengFiledSize)) {
                    LOGGER.info("this package is not enough,sign data is not enough.");
                    return;
                }
                int sigLen = in.getUnsignedShort(in.readerIndex() + businessFrameLen + sigTagByteSize);
                LOGGER.info("signature frame length filed:" + ByteBufUtil
                    .hexDump(in, in.readerIndex() + businessFrameLen + sigTagByteSize, sigLengFiledSize));
                int signFrameLen = sigTagByteSize + sigLengFiledSize + sigLen;
                int totalFrameSize = businessFrameLen + signFrameLen;
                LOGGER.info("total frame length" + totalFrameSize);
                if (in.readableBytes() < totalFrameSize) {
                    LOGGER.info("this package is not enough,sign data is not enough.");
                    return;
                }
                // 如果找到了需要的报文,那么放到out种,调用下面的解码器来处理。
                ByteBuf frame = in.slice(in.readerIndex(), totalFrameSize).retain();
                in.skipBytes(totalFrameSize);
                out.add(frame);
            } else {
                LOGGER.info("this package is begin with 0x020E.");
                if (in.readableBytes() < 13) {
                    return;
                }
                // 与上面相同
                ByteBuf frame = in.slice(in.readerIndex(), 13).retain();
                in.skipBytes(13);
                out.add(frame);
            }
        } catch (final Throwable t) {
            LOGGER.error("UnpackingDecoder Error!", t);
        }
        LOGGER.info("end UnpackingDecoder!");
    }
}
 
  

 

你可能感兴趣的:(Netty)