Netty的ByteBuff使用

有一点我们需要知道的是,ByteBuf的jar包,是可以单独使用的。比如某个项目中有一个场景,需要处理某个自定义的协议,那么我们在解析协议时,就可以将接收到的将字节内容写入一个ByteBuf,然后从ByteBuf中慢慢的将内容读取出来。下面让我们用一个例子简单的了解下ByteBuf的使用。

ByteBuf的创建

要想使用ByteBuf,首先肯定是要创建一个ByteBuf,更确切的说法就是要申请一块内存,后续可以在这块内存中执行写入数据读取数据等等一系列的操作。

那么如何创建一个ByteBuf呢?Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。
具体的层级关系如下图所示:

 

有了Allocator之后,Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。

不同的创建方法

我们以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但最终都是以下这几种,只是参数不一样而已:

// 在堆上分配一个ByteBuf,并指定初始容量和最大容量

public static ByteBuf buffer(int initialCapacity, int maxCapacity) {

    return ALLOC.heapBuffer(initialCapacity, maxCapacity);

}

// 在堆外分配一个ByteBuf,并指定初始容量和最大容量

public static ByteBuf directBuffer(int initialCapacity, int maxCapacity) {

    return ALLOC.directBuffer(initialCapacity, maxCapacity);

}

// 使用包装的方式,将一个byte[]包装成一个ByteBuf后返回

public static ByteBuf wrappedBuffer(byte[] array) {

    if (array.length == 0) {

        return EMPTY_BUFFER;

    }

    return new UnpooledHeapByteBuf(ALLOC, array, array.length);

}

// 返回一个组合ByteBuf,并指定组合的个数

public static CompositeByteBuf compositeBuffer(int maxNumComponents){

    return new CompositeByteBuf(ALLOC, false, maxNumComponents);

}

其中包装方法除了上述这个方法之外,还有一些其他常用的包装方法,比如参数是一个ByteBuf的包装方法,比如参数是一个原生的ByteBuffer的包装方法,比如指定一个内存地址和大小的包装方法等等。
另外还有一些copy*开头的方法,实际是调用了buffer(int initialCapacity, int maxCapacity)或directBuffer(int initialCapacity, int maxCapacity)方法,然后将具体的内容write进生成的ByteBuf中返回。
以上所有的这些方法都实际通过一个叫ALLOC的静态变量进行了调用,来实现具体的ByteBuf的创建,而这个ALLOC实际是一个ByteBufAllocator:

private static final ByteBufAllocator

ALLOC = UnpooledByteBufAllocator.DEFAULT;

ByteBufAllocator是一个专门负责ByteBuf分配的接口,对应的Unpooled实现类就是UnpooledByteBufAllocator。在UnpooledByteBufAllocator类中可以看到UnpooledByteBufAllocator.DEFAULT变量是一个final类型的静态变量

/**

 * Default instance which uses leak-detection for direct buffers.

 * 默认的UnpooledByteBufAllocator实例,并且会对堆外内存进行泄漏检测

 */public static final UnpooledByteBufAllocator

DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());

涉及的设计模式

ByteBuf和ByteBufAllocator之间是一种相辅相成的关系,ByteBufAllocator用来创建一个ByteBuf,而ByteBuf亦可以返回创建他的Allocator。ByteBuf和ByteBufAllocator之间是一种 抽象工厂模式,具体可以用一张图描述如下:

 

下面我来用一个实际的例子来说明ByteBuf的使用,并通过观察在不同阶段ByteBuf的读写指针的值和ByteBuf的容量变化来更加深入的了解ByteBuf的设计,为了方便,我会用非池化的分配器来创建ByteBuf。

使用示例

我构造了一个demo,来演示在ByteBuf中插入数据、读取数据、清空读写指针、数据清零、扩容等等方法,具体的代码如下:

private static void simpleUse() {
        // 1.创建一个非池化的ByteBuf,大小为10个字节
        ByteBuf buf = Unpooled.buffer(10);
        System.out.println("原始ByteBuf为====================>" + buf.toString());
        System.out.println("1.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");

        // 2.写入一段内容
        byte[] bytes = { 1, 2, 3, 4, 5 };
        buf.writeBytes(bytes);
        System.out.println("写入的bytes为====================>" + Arrays.toString(bytes));
        System.out.println("写入一段内容后ByteBuf为===========>" + buf.toString());
        System.out.println("2.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");

        // 3.读取一段内容
        byte b1 = buf.readByte();
        byte b2 = buf.readByte();
        System.out.println("读取的bytes为====================>" + Arrays.toString(new byte[] { b1, b2 }));
        System.out.println("读取一段内容后ByteBuf为===========>" + buf.toString());
        System.out.println("3.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");

        // 4.将读取的内容丢弃
        buf.discardReadBytes();
        System.out.println("将读取的内容丢弃后ByteBuf为(读写指针都向前移动)===>" + buf.toString());
        System.out.println("4.ByteBuf中的内容为(可读数据向前移动)===>" + Arrays.toString(buf.array()));
        buf.writeByte(9);
        buf.writeByte(9);
        System.out.println("再写入2个9后的ByteBuf为========>" + buf.toString());
        System.out.println("ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");

        // 5.清空读写指针
        buf.clear();
        System.out.println("将读写指针清空后ByteBuf为==========>" + buf.toString());
        System.out.println("5.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");

        // 6.再次写入一段内容,比第一段内容少
        byte[] bytes2 = { 1, 2, 3 };
        buf.writeBytes(bytes2);
        System.out.println("写入的bytes为====================>" + Arrays.toString(bytes2));
        System.out.println("写入一段内容后ByteBuf为===========>" + buf.toString());
        System.out.println("6.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");

        // 7.将ByteBuf清零
        buf.setZero(0, buf.capacity());
        System.out.println("将内容清零后ByteBuf为==============>" + buf.toString());
        System.out.println("7.ByteBuf中的内容为================>" + Arrays.toString(buf.array()) + "\n");

        // 8.再次写入一段超过容量的内容
        byte[] bytes3 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
        buf.writeBytes(bytes3);
        System.out.println("写入的bytes为====================>" + Arrays.toString(bytes3));
        System.out.println("写入一段内容后ByteBuf为===========>" + buf.toString());
        System.out.println("8.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
    }

执行结果如下:

原始ByteBuf为====================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
1.ByteBuf中的内容为===============>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

写入的bytes为====================>[1, 2, 3, 4, 5]
写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)
2.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

读取的bytes为====================>[1, 2]
读取一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 2, widx: 5, cap: 10)
3.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]

将读取的内容丢弃后ByteBuf为(读写指针都向前移动)===>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
4.ByteBuf中的内容为(可读数据向前移动)===>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]
再写入2个9后的ByteBuf为========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)
ByteBuf中的内容为===============>[3, 4, 5, 9, 9, 0, 0, 0, 0, 0]

将读写指针清空后ByteBuf为==========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
5.ByteBuf中的内容为===============>[3, 4, 5, 9, 9, 0, 0, 0, 0, 0]

写入的bytes为====================>[1, 2, 3]
写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
6.ByteBuf中的内容为===============>[1, 2, 3, 9, 9, 0, 0, 0, 0, 0]

将内容清零后ByteBuf为==============>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
7.ByteBuf中的内容为================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

写入的bytes为====================>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 14, cap: 64)
8.ByteBuf中的内容为===============>[0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

执行过程分析

下面让我们来仔细的研究下执行的过程,并分析下为什么会产生这样的执行结果。

  • 1.初始化一个大小为10的ByteBuf

刚初始化的ByteBuf对象,容量为10,读写指针都为0,且每个字节的值都为0,并且这些字节都是“可写”的,我们用红色来表示。

  • 2.写入一段内容

 

当写入一段内容后(这里写入的是5个字节),写指针向后移动了5个字节,写指针的值变成了5,而读指针没有发生变化还是0,但是读指针和写指针之间的字节现在变成了“可读”的状态了,我们用紫色来表示。

  • 3.读取一段内容

 

接着我们有读取了2个字节的内容,这时读指针向后移动了2个字节,读指针的值变成了2,写指针不变,此时0和读指针之间的内容变成了“可丢弃”的状态了,我们用粉色来表示。

  • 4.将读取的内容丢弃

 

紧接着,我们将刚刚读取完的2个字节丢弃掉,这时ByteBuf把读指针与写指针之间的内容(即 3、4、5 三个字节)移动到了0的位置,并且将读指针更新为0,写指针更新为原来写指针的值减去原来读指针的值。但是需要注意的是,第4和第5个字节的位置上,还保留的原本的内容,只是这两个字节由原来的“可读”变成了现在的“可写”。

 

     

这时再写入两个9,可以看到输出是34599。writerIndex=3和writerIndex=4的两个数字4和5变成了两个9

  • 5.将读写指针清空

 

然后,我们执行了一个 clear 方法,将读写指针同时都置为0了,此时所有的字节都变成“可写”了,但是需要注意的是,clear方法只是更改的读写指针的值,每个位置上原本的字节内容并没有发生改变

  • 6.再次写入一段较少的内容

然后再次写入一段内容123后,读指针不变,写指针向后移动了具体的字节数,这里是向后移动了三个字节。且写入的这三个字节变成了“可读”状态。

  • 7.将ByteBuf中的内容清零

 

清零(setZero)和清空(clear)的方法是两个概念完全不同的方法,“清零”是把指定位置上的字节的值设置为0,除此之外不改变任何的值,所以读写指针的值和字节的“可读写”状态与上次保持一致,而“清空”则只是将读写指针都置为0,并且所有字节都变成了“可写”状态。

  • 8.写入一段超过容量的内容

 

最后我们往ByteBuf中写入超过ByteBuf容量的内容,这里是写入了11个字节,此时ByteBuf原本的容量不足以写入这些内容了,所以ByteBuf发生了扩容。其实只要写入的字节数超过可写字节数,就会发生扩容了。

扩容分析

那么扩容是怎么扩的呢,为什么容量从10扩容到64呢?我们从源码中找答案。

扩容肯定发生在写入字节的时候,让我们找到 writeBytes(byte[] bytes) 方法,具体如下:

@Override
public ByteBuf writeBytes(byte[] src) {

    writeBytes(src, 0, src.length);

    return this;

}@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {

    // 该方法检查是否有足够的可写空间,是否需要进行扩容

    ensureWritable(length);

    setBytes(writerIndex, src, srcIndex, length);

    writerIndex += length;

    return this;

}

在进入 ensureWritable(length) 方法内部查看,具体的代码如下:

@Override
public ByteBuf ensureWritable(int minWritableBytes) {

    if (minWritableBytes < 0) {

        throw new IllegalArgumentException(String.format(

            "minWritableBytes: %d (expected: >= 0)", minWritableBytes));

    }

    ensureWritable0(minWritableBytes);

    return this;

}

final void ensureWritable0(int minWritableBytes) {

    // 检查该ByteBuf对象的引用计数是否为0,保证该对象在写入之前是可访问的

    ensureAccessible();

    if (minWritableBytes <= writableBytes()) {

        return;

    }

    if (minWritableBytes > maxCapacity - writerIndex) {

        throw new IndexOutOfBoundsException(String.format(

            "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",

            writerIndex, minWritableBytes, maxCapacity, this));

    }

    // Normalize the current capacity to the power of 2.

    // 计算新的容量,即为当前容量扩容至2的幂次方大小

    int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);

    // Adjust to the new capacity.

    // 设置扩容后的容量

    capacity(newCapacity);

}

从上面的代码中可以很清楚的看出来,计算新的容量的方法是调用的 ByteBufAllocator 的 calculateNewCapacity() 方法,继续跟进去该方法,这里的 ByteBufAllocator 的实现类是 AbstractByteBufAllocator ,具体的代码如下:

@Override

public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {

    if (minNewCapacity < 0) {

        throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");

    }

    if (minNewCapacity > maxCapacity) {

        throw new IllegalArgumentException(String.format(

            "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",

            minNewCapacity, maxCapacity));

    }

    // 扩容的阈值,4兆字节大小

    final int threshold = CALCULATE_THRESHOLD; // 4 MiB page

    if (minNewCapacity == threshold) {

        return threshold;

    }

    // If over threshold, do not double but just increase by threshold.

    // 如果要扩容后新的容量大于扩容的阈值,那么扩容的方式改为用新的容量加上阈值,

    // 否则将新容量改为双倍大小进行扩容

    if (minNewCapacity > threshold) {

        int newCapacity = minNewCapacity / threshold * threshold;

        if (newCapacity > maxCapacity - threshold) {

            newCapacity = maxCapacity;

        } else {

            newCapacity += threshold;

        }

        return newCapacity;

    }



    // Not over threshold. Double up to 4 MiB, starting from 64.

    // 如果要扩容后新的容量小于4兆字节,则从64字节开始扩容,每次双倍扩容,

    // 直到小于指定的新容量位置

    int newCapacity = 64;

    while (newCapacity < minNewCapacity) {

        newCapacity <<= 1;

    }



    return Math.min(newCapacity, maxCapacity);

}

到这里就很清楚了,每次扩容时,有一个阈值t(4MB),计划扩容的大小为c,扩容后的值为n。

扩容的规则可以用下面的逻辑表示:

  • 如果c<t,则n从阈值t(4MB)开始,以每次增加2倍的方式扩容,直到双倍后的大小小于c;
  • 如果c>t,则n=c/t*t+t

得出的结论

  • ByteBuf有读readerIndex和写writerIndex两个指针,用来标记“可读”、“可写”、“可丢弃”的字节
  • 调用write*方法写入数据后,写指针将会向后移动
  • 调用read*方法读取数据后,读指针将会向后移动
  • 写入数据或读取数据时会检查是否有足够多的空间可以写入和是否有数据可以读取
  • 写入数据之前,会进行容量检查,当剩余可写的容量小于需要写入的容量时,需要执行扩容操作
  • 扩容时有一个4MB的阈值,需要扩容的容量小于阈值或大于阈值所对应的扩容逻辑不同
  • clear等修改读写指针的方法,只会更改读写指针位置的值,并不会影响ByteBuf中已有的内容
  • setZero等修改字节值的方法,只会修改对应字节的值,不会影响读写指针的值以及字节的可读写状态




来源:简书
 

 

展开阅读全文

没有更多推荐了,返回首页