自顶向下深入分析Netty(九)--ByteBuf源码分析

9.4 ByteBuf源码分析

9.4.1 类图

ByteBuf的子类实现非常多,其中关键的实现类如下:


ByteBuf类图

可以使用两种方式对ByteBuf进行分类:按底层实现方式和按是否使用对象池。

  1. 按底层实现
    1. HeapByteBuf
      HeapByteBuf的底层实现为JAVA堆内的字节数组。堆缓冲区与普通堆对象类似,位于JVM堆内存区,可由GC回收,其申请和释放效率较高。常规JAVA程序使用建议使用该缓冲区。
    2. DirectByteBuf
      DirectByteBuf的底层实现为操作系统内核空间的字节数组。直接缓冲区的字节数组位于JVM堆外的NATIVE堆,由操作系统管理申请和释放,而DirectByteBuf的引用由JVM管理。直接缓冲区由操作系统管理,一方面,申请和释放效率都低于堆缓冲区,另一方面,却可以大大提高IO效率。由于进行IO操作时,常规下用户空间的数据(JAVA即堆缓冲区)需要拷贝到内核空间(直接缓冲区),然后内核空间写到网络SOCKET或者文件中。如果在用户空间取得直接缓冲区,可直接向内核空间写数据,减少了一次拷贝,可大大提高IO效率,这也是常说的零拷贝。
    3. CompositeByteBuf
      CompositeByteBuf,顾名思义,有以上两种方式组合实现。这也是一种零拷贝技术,想象将两个缓冲区合并为一个的场景,一般情况下,需要将后一个缓冲区的数据拷贝到前一个缓冲区;而使用组合缓冲区则可以直接保存两个缓冲区,因为其内部实现组合两个缓冲区并保证用户如同操作一个普通缓冲区一样操作该组合缓冲区,从而减少拷贝操作。
  2. 按是否使用对象池
    1. UnpooledByteBuf
      UnpooledByteBuf为不使用对象池的缓冲区,不需要创建大量缓冲区对象时建议使用该类缓冲区。
    2. PooledByteBuf
      PooledByteBuf为对象池缓冲区,当对象释放后会归还给对象池,所以可循环使用。当需要大量且频繁创建缓冲区时,建议使用该类缓冲区。Netty4.1默认使用对象池缓冲区,4.0默认使用非对象池缓冲区。

9.4.2 关键类分析

9.4.2.1 ByteBuf

ByteBuf被定义为抽象类,但其中并未实现任何方法,故可看做一个接口,该接口扩展了ReferenceCounted实现引用计数。该类最重要的方法如下:

    int getInt(int index);
    ByteBuf setInt(int index, int value);
    int readInt();
    ByteBuf writeInt(int value);

这些方法从缓冲区取得或设置一个4字节整数,区别在于getInt()setInt()不会改变索引readInt()writeInt()分别会将读索引和写索引增加4,因为int占4个字节。该类方法有大量同类,可操作布尔数Boolean,字节Byte,字符Char,2字节短整数Short,3字节整数Medium,4字节整数Int,8字节长整数Long,4字节单精度浮点数Float,8字节双精度浮点数Double以及字节数组ByteArray

该类的一些方法遵循这样一个准则:空参数的方法类似常规getter方法,带参数的方法类似常规setter方法。比如capacity()表示缓冲区当前容量,capacity(int newCapacity)表示设置新的缓冲区容量。在此列出这些方法的带参数形式:

    ByteBuf capacity(int newCapacity); // 设置缓冲区容量
    ByteBuf order(ByteOrder endianness); // 设置缓冲区字节序
    ByteBuf readerIndex(int readerIndex); // 设置缓冲区读索引
    ByteBuf writerIndex(int writerIndex); // 设置缓冲区写索引

上述后两个方法,可操作读写索引,除此之外,还有以下方法可以操作读写索引:

    ByteBuf setIndex(int readerIndex, int writerIndex); // 设置读写索引
    ByteBuf markReaderIndex();  // 标记读索引,写索引可类比
    ByteBuf resetReaderIndex(); // 重置为标记的读索引
    ByteBuf skipBytes(int length); // 略过指定字节(增加读索引)
    ByteBuf clear(); // 读写索引都置0

缓冲区可写可读性判断,以可读性为例:

    int readableBytes(); // 可读的字节数
    boolean isReadable(); // 是否可读
    boolean isReadable(int size); // 指定的字节数是否可读

堆缓冲区和直接缓冲区的判断,有以下方法:

    boolean hasArray(); // 判断底层实现是否为字节数组
    byte[] array(); // 返回底层实现的字节数组
    int arrayOffset();  // 底层字节数组的首字节位置
    
    boolean isDirect(); // 判断底层实现是否为直接ByteBuffer
    boolean hasMemoryAddress(); // 底层直接ByteBuffer是否有内存地址
    long memoryAddress(); // 直接ByteBuffer的首字节内存地址

这一组方法主要用于区分缓冲区的底层实现,前3个方法用于对底层实现为字节数组的堆缓冲区进行操作,后三个方法用于底层为NIO Direct ByteBuffer的缓冲区进行操作。
使用缓冲区时,经常需要在缓冲区查找某个特定字节或对某个字节进行操作,为此ByteBuf提供了一系列方法:

    // 首个特定字节的绝对位置
    int indexOf(int fromIndex, int toIndex, byte value);
    // 首个特定字节的相对位置,相对读索引
    int bytesBefore(byte value);
    int bytesBefore(int length, byte value);
    int bytesBefore(int index, int length, byte value);
    
    // processor返回false时的首个位置
    int forEachByte(ByteBufProcessor processor);
    int forEachByte(int index, int length, ByteBufProcessor processor);
    int forEachByteDesc(ByteBufProcessor processor);
    int forEachByteDesc(int index, int length, ByteBufProcessor processor);

接口ByteBufProcessorprocess(byte)方法定义的不符合常规,返回true时表示希望继续处理下一个字节,返回false时表示希望停止处理返回当前位置。由此,当我们希望查找到换行符时,代码如下:

    public boolean process(byte value) throws Exception {
        return value != '\r' && value != '\n';
    }

这正是Netty默认实现的一个ByteBufProcessor FIND_CRLF,可见当我们期望查找换行符时,process的代码却要表达非换行符,此处设计不太合理,不人性化。作为折中,ByteBufProcessor中提供了大量默认的处理器,可满足大量场景的需求。如果确有必要实现自己的处理器,一定注意实现方法需要反义。
复制或截取缓冲区也是一个频繁操作,ByteBuf提供了以下方法:

    ByteBuf copy();
    ByteBuf copy(int index, int length);
    ByteBuf slice();
    ByteBuf slice(int index, int length);
    ByteBuf duplicate();

其中copy()方法生成的ByteBuf完全独立于原ByteBuf,而slice()duplicate()方法生成的ByteBuf与原ByteBuf共享相同的底层实现,只是各自维护独立的索引和标记,使用这两个方法时,特别需要注意结合使用场景确定是否调用retain()增加引用计数。
有关JAVA NIO中的ByteBuffer的方法:

    int nioBufferCount();
    ByteBuffer nioBuffer();
    ByteBuffer nioBuffer(int index, int length);
    ByteBuffer internalNioBuffer(int index, int length);
    ByteBuffer[] nioBuffers();
    ByteBuffer[] nioBuffers(int index, int length);

这些方法在进行IO的写操作时会大量使用,一般情况下,用户很少调用这些方法。
最后需要注意的是toString()方法:

    String toString();
    String toString(Charset charset);
    String toString(int index, int length, Charset charset);

不带参数的toString()方法是JAVA中Object的标准重载方法,返回ByteBuf的JAVA描述;带参数的方法则返回使用指定编码集编码的缓冲区字节数据的字符形式。
不再列出ByteBuf继承自ReferenceCounted接口的方法,可见ByteBuf是一个含有过多的抽象方法的抽象类(接口)。此处,Netty使用了聚合的设计方法,将ByteBuf的子类可能使用的方法都集中到了基类。再加上使用工厂模式生成ByteBuf,给用户程序员带来了极大便利:完全不用接触具体的子类,只需要使用顶层接口进行操作。

了解了ByteBuf的所有方法,那么我们逐个击破,继续分析AbstractByteBuf

9.4.2.2 AbstractByteBuf

抽象基类AbstractByteBuf中定义了ByteBuf的通用操作,比如读写索引以及标记索引的维护、容量扩增以及废弃字节丢弃等等。首先看其中的私有变量:

    int readerIndex; // 读索引
    int writerIndex; // 写索引
    private int markedReaderIndex; // 标记读索引
    private int markedWriterIndex; // 标记写索引

    private int maxCapacity; // 最大容量

与变量相关的setter和getter方法不再分析,分析第一个关键方法:计算容量扩增的方法calculateNewCapacity(minNewCapacity),其中参数表示扩增所需的最小容量:

    private int calculateNewCapacity(int minNewCapacity) {
        final int maxCapacity = this.maxCapacity;
        final int threshold = 1048576 * 4; // 4MB的阈值

        if (minNewCapacity == threshold) {
            return threshold;
        }

        // 所需的最小容量超过阈值4MB,每次增加4MB
        if (minNewCapacity > threshold) {
            int newCapacity = (minNewCapacity / threshold) * threshold;
            if (newCapacity > maxCapacity - threshold) {
                newCapacity = maxCapacity; // 超过最大容量不再扩增
            } else {
                newCapacity += threshold; // 增加4MB
            }
            return newCapacity;
        }

        // 此时所需的最小容量小于阈值4MB,容量翻倍
        int newCapacity = 64;
        while (newCapacity < minNewCapacity) {
            newCapacity <<= 1; // 使用移位运算表示*2
        }

        return Math.min(newCapacity, maxCapacity);
    }

可见ByteBuf的最小容量为64B,当所需的扩容量在64B和4MB之间时,翻倍扩容;超过4MB之后,则每次扩容增加4MB,且最终容量(小于maxCapacity时)为4MB的最小整数倍。容量扩增的具体实现与ByteBuf的底层实现紧密相关,最终实现的容量扩增方法capacity(newCapacity)由底层实现。
接着分析丢弃已读字节方法discardReadBytes()

    public ByteBuf discardReadBytes() {
        if (readerIndex == 0) {
            return this;
        }

        if (readerIndex != writerIndex) {
            // 将readerIndex之后的数据移动到从0开始
            setBytes(0, this, readerIndex, writerIndex - readerIndex);
            writerIndex -= readerIndex; // 写索引减少readerIndex
            adjustMarkers(readerIndex); // 标记索引对应调整
            readerIndex = 0; // 读索引置0
        } else {
            // 读写索引相同时等同于clear操作
            adjustMarkers(readerIndex);
            writerIndex = readerIndex = 0;
        }
        return this;
    }

只需注意其中的setBytes(),从一个源数据ByteBuf中复制数据到ByteBuf中,在本例中数据源ByteBuf就是它本身,所以是将readerIndex之后的数据移动到索引0开始,也就是丢弃readerIndex之前的数据。adjustMarkers()重新调节标记索引,方法实现简单,不再进行细节分析。需要注意的是:读写索引不同时,频繁调用discardReadBytes()将导致数据的频繁前移,使性能损失。由此,提供了另一个方法discardSomeReadBytes(),当读索引超过容量的一半时,才会进行数据前移,核心实现如下:

    if (readerIndex >= capacity() >>> 1) {
        setBytes(0, this, readerIndex, writerIndex - readerIndex);
        writerIndex -= readerIndex;
        adjustMarkers(readerIndex);
        readerIndex = 0;
    }

当然,如果并不想丢弃字节,只期望读索引前移,可使用方法skipBytes():

    public ByteBuf skipBytes(int length) {
        checkReadableBytes(length);
        readerIndex += length;
        return this;
    }

接下来以getInt()readInt()为例,分析常用的数据获取方法。

    public int getInt(int index) {
        checkIndex(index, 4);   // 索引正确性检查
        return _getInt(index);
    }

    protected abstract int _getInt(int index);

该方法对索引进行正确性检查,然后将实际操作交给子类负责具体实现的_getInt()方法。

    public int readInt() {
        checkReadableBytes0(4); // 检查索引
        int v = _getInt(readerIndex);
        readerIndex += 4;   // 读索引增加
        return v;
    }

可见readIntgetInt的最大区别在于是否自动维护读索引,readInt将增加读索引,getInt则不会对索引产生任何影响。
数据设置方法setInt()writeInt()的实现可对应类比,代码如下:

    public ByteBuf setInt(int index, int value) {
        checkIndex(index, 4);
        _setInt(index, value);
        return this;
    }

    protected abstract void _setInt(int index, int value);
    
    public ByteBuf writeInt(int value) {
        ensureAccessible();
        ensureWritable0(4);
        _setInt(writerIndex, value);
        writerIndex += 4;
        return this;
    }

此外,在AbstractByteBuf中实现了ByteBuf中很多和索引相关的无参方法,比如copy():

    public ByteBuf copy() {
        return copy(readerIndex, readableBytes());
    }

具体实现只对无参方法设定默认索引,然后委托给有参方法由子类实现。
最后再分析一下检索字节的方法,比如indexOf()bytesBefore()forEachByte()等,其中的实现最后都委托给forEachByte(),核心代码如下:

    private int forEachByteAsc0(int start, int end, 
            ByteBufProcessor processor) throws Exception {
        for (; start < end; ++start) {
            if (!processor.process(_getByte(start))) {
                return start;
            }
        }

        return -1;
    }

原理也很简单,从头开始使用_getByte()取出每个字节进行比较。if语句中的逻辑非!符号取反,这个设计并不好,容易让人产生误解。比如,indexOf()查找一个字节的ByteBufProcessor实现如下:

    private static class IndexOfProcessor implements ByteBufProcessor {
        private final byte byteToFind;

        public IndexOfProcessor(byte byteToFind) {
            this.byteToFind = byteToFind;
        }

        @Override
        public boolean process(byte value) {
            // 期望找到某个字节,但此处需要使用!=
            return value != byteToFind;
        }
    }

此外的其他方法不再分析,接着分析与引用计数相关的AbstractReferenceCountedByteBuf

9.4.2.3 AbstractReferenceCountedByteBuf

从名字可以推断,该抽象类实现引用计数相关的功能。引用计数的功能简单理解就是:当需要使用一个对象时,计数加1;不再使用时,计数减1。如何实现计数功能呢?考虑到引用计数的多线程使用情形,一般情况下,我们会选择简单的AtomicInteger作为计数,使用时加1,释放时减1。这样的实现是没有问题的,但Netty选择了另一种内存效率更高的实现方式:volatile + FieldUpdater
首先看使用的成员变量:

    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
            AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");

    private volatile int refCnt = 1;    // 实际的引用计数值,创建时为1

增加引用计数的方法:

    private ByteBuf retain0(int increment) {
        for (;;) {
            int refCnt = this.refCnt;
            final int nextCnt = refCnt + increment;

            if (nextCnt <= increment) {
                throw new IllegalReferenceCountException(refCnt, increment);
            }
            if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
                break;
            }
        }
        return this;
    }
    
    public ByteBuf retain() {
        return retain0(1);
    }

实现较为简单,只需注意compareAndSet(obj, expect, update)。这是一个原子操作,当前字段的实际值如果与expect相同,则会将字段值更新为update;否则,更新失败返回false。所以,代码中使用for(;;)循环直到该字段的新值被更新。
减少引用计数的方法也类似:

    private boolean release0(int decrement) {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt < decrement) {
                throw new IllegalReferenceCountException(refCnt, -decrement);
            }

            if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
                if (refCnt == decrement) {
                    deallocate();
                    return true;
                }
                return false;
            }
        }
    }
    
    public boolean release() {
        return release0(1);
    }

至此,ByteBuf的抽象层代码分析完毕。稍作休整,进入正题:ByteBuf的具体实现子类。

推荐阅读更多精彩内容