Netty解读源码ByteToMessageDecoder

感慨

纸上得来终觉浅,源码阅读是进一步提高自身水平的手段。但源码无数,并不是什么样的源码都值得一读。
须知任何技术都是为了解决特定问题的,先针对问题进行思考,然后再读源码,会事半功倍。
本文按照一定的阅读源码思路来逐步解析ByteToMessageDecoder源码。

ByteToMessageDecoder

外围信息解析

继承关系:

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter

关键方法:

protected abstract void decode(ChannelHandlerContext var1, ByteBuf var2, List<Object> var3) throws Exception;

使用方式:

public class A extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        // byteBuf中,信息完整且能处理掉的,处理结果放到list中;信息不完整的,不消耗byteBuf,等下次完整了再处理;不能处理的,丢弃掉
    }
}

从上述内容入手,可以解析到信息如下:

  1. 继承了ChannelInboundHandlerAdapter,必然继承其channelRead方法,该方法是我们阅读源码的入口。
  2. 提供了新的decode方法,将变化的部分交给开发人员自由实现
  3. 不变的部分,主要是byteBuf有保留上一次处理结果的能力,并不是一个一次性消耗的对象,用于针对TCP拆包问题,这是ByteToMessageDecoder的关键

思考:

  1. 必然存在某个变量,用于保留处理不完的ByteBuf
  2. 必然有某个方式,将处理不完的ByteBuf和当前的ByteBuf拼接到一起

解析深入源码

channelRead外层结构

	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		if (msg instanceof ByteBuf) {
           // (略)这里是第二层结构
        } else {
            ctx.fireChannelRead(msg);
        }
    }

说明:解析源码的目的不仅仅是在于了解其实现,实际上,在了解问题场景的情况下,一个合格的程序员,花点功夫也能实现。源码往往是精心设计的,不仅是其准确性,其可读性、组织规范、容错性等方面也有很高的参考价值。
解析:其表达的意思很明确,判断传入类型,类型符合则处理,不符合则透传。这是一个组织规范上的问题,在自写处理时,要套用该模式。同时也说明了,设计上可以按照类型去做不同类型的处理,如下:

// 解析多种不同的输入格式。
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		if (msg instanceof ByteBuf) {
           // (略)
        } else if (msg instanceof String) {
           // (略)
        } else {
            ctx.fireChannelRead(msg);
        }
    }

channelRead第二层内容

		    CodecOutputList out = CodecOutputList.newInstance();
            boolean var10 = false;

            try {
                var10 = true;
                ByteBuf data = (ByteBuf)msg;
                this.first = this.cumulation == null;
                if (this.first) {
                    this.cumulation = data;
                } else {
                    this.cumulation = this.cumulator.cumulate(ctx.alloc(), this.cumulation, data);
                }

                this.callDecode(ctx, this.cumulation, out);
                var10 = false;
            } catch (DecoderException var11) {
                throw var11;
            } catch (Exception var12) {
                throw new DecoderException(var12);
            } finally {
                if (var10) {
                    if (this.cumulation != null && !this.cumulation.isReadable()) {
                        this.numReads = 0;
                        this.cumulation.release();
                        this.cumulation = null;
                    } else if (++this.numReads >= this.discardAfterReads) {
                        this.numReads = 0;
                        this.discardSomeReadBytes();
                    }

                    int size = out.size();
                    this.decodeWasNull = !out.insertSinceRecycled();
                    fireChannelRead(ctx, out, size);
                    out.recycle();
                }
            }
            if (this.cumulation != null && !this.cumulation.isReadable()) {
                this.numReads = 0;
                this.cumulation.release();
                this.cumulation = null;
            } else if (++this.numReads >= this.discardAfterReads) {
                this.numReads = 0;
                this.discardSomeReadBytes();
            }

            int size = out.size();
            this.decodeWasNull = !out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();

解析:在这里,我们找到了用于拼接的cumulation,如下:

                ByteBuf data = (ByteBuf)msg;
                this.first = this.cumulation == null;
                if (this.first) {
                    this.cumulation = data;
                } else {
                    this.cumulation = this.cumulator.cumulate(ctx.alloc(), this.cumulation, data);
                }

解析:并且,这个对象为null时,直接被替换为当前输入的数据,cumulate就是其处理两个buff拼接的代码。
接着看整体代码组织结构

            
			// (略)一些临时变量
            try {
                var10 = true;
                // (略)这个地方会抛异常
                var10 = false;
            } catch (DecoderException var11) {
                throw var11;
            } catch (Exception var12) {
                throw new DecoderException(var12);
            } finally {
                if (var10) {
                  // (略)某处理1
                }
            }
            // (略)某处理2

解析:从这个结构看,并没有什么需要特别关注的,但是,其中的处理1和处理2代码是完全一致的!!我们来思考一下这里为什么要以这种方式组织代码,暗藏何种玄机。
将代码抽象如下:

         			if (条件1) {
	                    处理1,一旦执行,将不会再满足条件1
                    } else if (条件2) {
                        处理2
                    }
					处理3

结合上述流程组织,在不抛异常时,处理1和处理2只能进入其中一个;而在抛异常时,处理1和处理2可能都会执行。这是这个组织结构想表达的意思。
那么,为什么不将其抽取成一个方法进行调用呢?本人理解如下:

  1. 代码简短,必要性不高,更何况没有其他方法需要调用类似的代码
  2. 凑到一起,并不影响阅读理解,不必太过拘泥于代码规范
  3. 实际上,没有什么特别的理由,只是开发人员觉得无所谓,怎么方便怎么搞

代码块

            if (this.cumulation != null && !this.cumulation.isReadable()) {
                this.numReads = 0;
                this.cumulation.release();
                this.cumulation = null;
            } else if (++this.numReads >= this.discardAfterReads) {
                this.numReads = 0;
                this.discardSomeReadBytes();
            }

            int size = out.size();
            this.decodeWasNull = !out.insertSinceRecycled();
            fireChannelRead(ctx, out, size);
            out.recycle();

解析:

  1. cumulation有数据时,清空掉
  2. cumulation无数据时,累加numReads,numReads超过某个阈值时,清空已读字节,假如缺少清理数据的操作,那么有很大概率导致cumulation只增不减的情况。体现内存管理细节
  3. 下半段则是处理输出数据

Cumulator

接口

    public interface Cumulator {
        ByteBuf cumulate(ByteBufAllocator var1, ByteBuf var2, ByteBuf var3);
    }

实现1(ByteToMessageDecoder使用)

    public static final ByteToMessageDecoder.Cumulator MERGE_CUMULATOR = new ByteToMessageDecoder.Cumulator() {
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            ByteBuf buffer;
            if (cumulation.writerIndex() <= cumulation.maxCapacity() - in.readableBytes() && cumulation.refCnt() <= 1 && !cumulation.isReadOnly()) {
                buffer = cumulation;
            } else {
                buffer = ByteToMessageDecoder.expandCumulation(alloc, cumulation, in.readableBytes());
            }

            buffer.writeBytes(in);
            in.release();
            return buffer;
        }
    };

解析:

  1. 首先是一些容量判断,容量充足则无需扩容,不足则调用expandCumulation进行扩容
  2. buffer.writeBytes(in),将内容拼接
    static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
        ByteBuf oldCumulation = cumulation;
        cumulation = alloc.buffer(cumulation.readableBytes() + readable);
        cumulation.writeBytes(oldCumulation);
        oldCumulation.release();
        return cumulation;
    }

实现2(ByteToMessageDecoder无使用)

   public static final ByteToMessageDecoder.Cumulator COMPOSITE_CUMULATOR = new ByteToMessageDecoder.Cumulator() {
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            Object buffer;
            if (cumulation.refCnt() > 1) {
                buffer = ByteToMessageDecoder.expandCumulation(alloc, cumulation, in.readableBytes());
                ((ByteBuf)buffer).writeBytes(in);
                in.release();
            } else {
                CompositeByteBuf composite;
                if (cumulation instanceof CompositeByteBuf) {
                    composite = (CompositeByteBuf)cumulation;
                } else {
                    composite = alloc.compositeBuffer(2147483647);
                    composite.addComponent(true, cumulation);
                }

                composite.addComponent(true, in);
                buffer = composite;
            }

            return (ByteBuf)buffer;
        }
    };

解析:

  1. 当有多个引用时,直接扩容
  2. 只有1个引用时,采用CompositeByteBuf去组合数据,相比MERGE_CUMULATOR,composite.addComponent并非值复制,其效率更高。

疑问:

  1. 为啥不用COMPOSITE_CUMULATOR而是用MERGE_CUMULATOR?
    因为TCP粘包拆包问题没那么频繁,往往是凑齐一个完整数据包的,采用MERGE_CUMULATOR并没有什么严重问题。同时拆包问题往往是初始Buff容量不足才导致拆包,而这里将Buff扩容后,后续发生拆包的可能性就低了。
  2. 为啥MERGE_CUMULATOR不用判断cumulation.refCnt() > 1?
    因为确保了这个条件不会发生,所以不必写。
  3. 为啥COMPOSITE_CUMULATOR不用判断容量?
    因为传进来的buff肯定是刚刚好的一小段,不会有多余的容量,直接扩容即可。
  4. 既然COMPOSITE_CUMULATOR没有用到,为啥还要实现?
    因为setCumulator可以更改实现,某些特殊情形下,或许会可以考虑更改。
    上文提到的单元测试中,加入setCumulator,同样是可以正常运行的。
  public TestDecoder() {
        setCumulator(COMPOSITE_CUMULATOR);
    }

https://blog.csdn.net/a215095167/article/details/104905170

总结

阅读源码,最主要一点是不要怕。精心设计的源码,可读性是比较强的。如果可读性差,要么是自身还需要历练,学习更多知识,要么是选用的源码没有什么学习价值。因此,阅读源码也是一个自我认识的过程。

发布了16 篇原创文章 · 获赞 1 · 访问量 3586

猜你喜欢

转载自blog.csdn.net/a215095167/article/details/104916624
今日推荐