Netty4.0源码解析:TCP二进制流的截取方案

一、引言

TCP是一个基于流的协议,TCP作为传输层协议并不知道应用层协议的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在应用层上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和半包问题。

Netty提供了多个进站处理器来处理这个问题:

  • LineBasedFrameDecoder:通过换行符来区分每个包
  • DelimiterBasedFrameDecoder:通过特殊分隔符来区分每个包
  • FixedLengthFrameDecoder:通过定长的报文来分包
  • LengthFieldBasedFrameDecoder:跟据包头部定义的长度来区分包

这几个类都拥有一个共同的父类:ByteToMessageDecoder
在这里插入图片描述

需要注意的是,ByteToMessageDecoder的子类不允许使用@Sharable注解(因为每个Decoder都应当有各自的缓冲区,从逻辑上也不能够被共享),否则在构造阶段会抛出IllegalStateException异常。

二、ByteToMessageDecoder

ByteToMessageDecoder提供了最基本的字节转换为可识别消息的功能,也就是将多个直接从套接字读取的ByteBuf转换为一个方便识别的ByteBuf。一般放在ChannelPipeline管道的头部。

ByteToMessageDecoder持有以下成员变量:

//每次和其它ByeBuf消息碎片合并后的缓冲区
ByteBuf cumulation;
//合并策略,这里默认为通过一次内存复制操作来完成cumulation和读入的ByteBuf的合并
private Cumulator cumulator = MERGE_CUMULATOR;
//是否仅解码一条消息
private boolean singleDecode;
//是否在没有字节可读时尝试进行获取更多的字节进行解码
private boolean decodeWasNull;
//cumulation是否为null
private boolean first;
//本次解码的状态
private byte decodeState = STATE_INIT;
//当读到多少个零碎的ByteBuf时就将当前cumulation作为坏包丢弃
private int discardAfterReads = 16;
//本次解码读到的ByteBuf的数量
private int numReads;
1、channelRead方法

要了解ByteToMessageDecoder解码机制,我们可以从它的channelRead方法开始分析:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
   
   
    if (msg instanceof ByteBuf) {
   
    //接收Channel读到的数据,此时的ByteBuf数据可能是不可读的
    	//构造一个List,用于存放每个ByteBuf解码的结果
        CodecOutputList out = CodecOutputList.newInstance();
        try {
   
   
            ByteBuf data = (ByteBuf) msg;
            first = cumulation == null;
            if (first) //如果cumulation为null,就无需进行ByteBuf的合并
                cumulation = data;
            else //否则调用Cumulator的cumulate方法将当前ByteBuf和cumulation合并
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
   
   
            throw e;
        } catch (Exception e) {
   
   
            throw new DecoderException(e);
        } finally {
   
   
        	//如果cumulation中的字节已经全部解码成功,那么将当前ByteToMessageDecoder复位
            if (cumulation != null && !cumulation.isReadable()) {
   
   
                numReads = 0;
                cumulation.release();
                cumulation = null;
            //否则cumulation还仍有零散的消息碎片
            } else if (++ numReads >= discardAfterReads) {
   
   
                numReads = 0;
                discardSomeReadBytes(); //丢弃已经读取到的字节
            }
            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();
        }
    } else {
   
    //如果不是ByteBuf,就忽略这个消息传递给下一个管道
        ctx.fireChannelRead(msg);
    }
}

channelRead方法的执行可以分为以下几个步骤:

  • 获取一个CodecOutputList,用于存放每次channelRead方法调用完成后的解码结果
    CodecOutputList实现了java.util.List接口,并通过FastThreadLocal存放在InternalThreadLocalMap中,每个线程都默认持有16个CodecOutputList实例,通过CodecOutputListCodecOutputLists来维护,如果所需的CodecOutputList超出16个,那么会默认实例化一个新的CodecOutputList实例。

  • 将当前ByteBuf与之前读到的ByteBuf(成员变量cumulation)进行合并
    如果cumulation为空,那么直接将当前ByteBuf引用赋给cumulation
    如果cumulation不为空,那么将根据成员变量cumulator定义的合并策略进行ByteBuf的合并。
    Cumulator是ByteToMessageDecoder的内部接口,定义了一个方法:

    ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in);
    

    参数allocByteBuf分配器,用于分配一个新的ByteBuf,可以通过调用ChannelHandlerContextalloc方法获得。cumulation为原ByteBuf缓冲区,in为需要被合并的ByteBuf缓冲区,其返回值为合并后的ByteBuf缓冲区。

ByteToMessageDecoder默认定义了2种Cumulator实现类:ByteToMessageDecoder.MERGE_CUMULATORByteToMessageDecoder.COMPOSITE_CUMULATOR

MERGE_CUMULATOR的合并策略是通过ByteBufAllocator分配一个大小为cumulation加上in的可读字节数,然后将cumulationin的数据复制到缓冲区中,所以MERGE_CUMULATOR需要一次内存复制操作。ByteToMessageDecoder默认采用这种策略合并缓冲区。

COMPOSITE_CUMULATOR的合并策略是通过CompositeByteBuf来完成ByteBuf的合并,它可以持有多个ByteBuf实例,所以不需要进行内存复制操作。但是CompositeByteBuf的索引算法实现较为复杂,可能会比MERGE_CUMULATOR要慢。

  • 合并完成后,对合并后的ByteBuf缓冲区(cumulation)的数据进行解码
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
   
   
    try {
   
   
        while (in.isReadable()) {
   
   
            int outSize = out.size(); //注:在第一次循环中outSize为0
            if (outSize > 0) {
   
    //如果List的长度大于0,说明已经有解码好的消息
            	//产生一个ChannelRead事件,并将集合out的每个元素传播到管道
                fireChannelRead(ctx, out, outSize);
                out.clear(); //清空这个List
                if (ctx.isRemoved()) //如果已经从管道中移除,那么退出循环结束
                    break;
                outSize = 0;
            }
            //获取可读字节数量
            int oldInputLength = in.readableBytes();
            decodeRemovalReentryProtection(ctx, in, out);
            if (ctx.isRemoved()) //如果this已经从管道移除,那么退出循环
                break;
            if (outSize == out.size()) {
   
    //如果集合out元素数量在本次循环中没有改变
                //如果在decodeRemovalReentryProtection没有处理任何数据
                if (oldInputLength == in.readableBytes())
                    break;
                else
                    continue;
            }
            if (oldInputLength == in.readableBytes()) //一般不会发生
                throw new DecoderException(StringUtil.simpleClassName(getClass()) +
                                ".decode() did not read anything but decoded a message.");
            if (isSingleDecode()) //如果仅解码一条消息,那么退出循环
                break;
        }
    } catch (DecoderException e
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值