本文主要内容:

  • ByteBuf:Netty的数据容器
  • API的详细信息
  • 用例
  • 内存分配

ByteBuf的API

Java NIO虽然提供了ByteBuffer作为字节容器,但是其使用过于复杂和繁琐,因此用ByteBuf来代替ByteBuffer。

优点:

  • 可以被用户自定义的缓冲区类型扩展
  • 通过内置的复合缓冲区类型实现了透明的零拷贝
  • 容量可以按需增长
  • 在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用
  • 支持引用计数
  • 支持池化

ByteBuf类

如何工作

ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,它的readIndex将会被递增已经被读取的字节数,同样,当你写入ByteBuf时,它的writerIndex也会被递增。

img

ByteBuf的使用模式

堆缓冲区

将数据存储在JVM的堆空间中,这种模式被称为支撑数组,它能在没有使用池化的情况下提供快速的分配和释放。

public static void heapBuffer() {
        ByteBuf heapBuf = BYTE_BUF_FROM_SOMEWHERE; //get reference form somewhere
        //检查 ByteBuf 是否有一个支撑数组
        if (heapBuf.hasArray()) {
            //如果有,则获取对该数组的引用
            byte[] array = heapBuf.array();
            //计算第一个字节的偏移量
            int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
            //获得可读字节数
            int length = heapBuf.readableBytes();
            //使用数组、偏移量和长度作为参数调用你的方法
            handleArray(array, offset, length);
        }
    }

直接缓冲区

直接缓冲区时另一种ByteBuf模式,它的内容将驻留在常规的会被垃圾回收的堆之外,相对于堆缓冲区,它的分配和释放都较为昂贵,另外因为数据不是在堆上,所以还要进行一次复制。

public static void directBuffer() {
        ByteBuf directBuf = BYTE_BUF_FROM_SOMEWHERE; //get reference form somewhere
        //检查 ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区
        if (!directBuf.hasArray()) {
            //获取可读字节数
            int length = directBuf.readableBytes();
            //分配一个新的数组来保存具有该长度的字节数据
            byte[] array = new byte[length];
            //将字节复制到该数组
            directBuf.getBytes(directBuf.readerIndex(), array);
            //使用数组、偏移量和长度作为参数调用你的方法
            handleArray(array, 0, length);
        }
    }

复合缓冲区

Netty通过一个ByteBuf子类CompositeByteBuf来提供一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。

public static void byteBufComposite() {
        CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
        ByteBuf headerBuf = BYTE_BUF_FROM_SOMEWHERE; // can be backing or direct
        ByteBuf bodyBuf = BYTE_BUF_FROM_SOMEWHERE;   // can be backing or direct
        //将 ByteBuf 实例追加到 CompositeByteBuf
        messageBuf.addComponents(headerBuf, bodyBuf);
        //...
        //删除位于索引位置为 0(第一个组件)的 ByteBuf
        messageBuf.removeComponent(0); // remove the header
        //循环遍历所有的 ByteBuf 实例
        for (ByteBuf buf : messageBuf) {
            System.out.println(buf.toString());
        }
    }

CompositeByteBuf可能不支持访问其支撑数组,因此访问CompositeByteBuf中的数据类似于访问直接缓冲区,代码如下:

public static void byteBufCompositeArray() {
        CompositeByteBuf compBuf = Unpooled.compositeBuffer();
        //获得可读字节数
        int length = compBuf.readableBytes();
        //分配一个具有可读字节数长度的新数组
        byte[] array = new byte[length];
        //将字节读到该数组中
        compBuf.getBytes(compBuf.readerIndex(), array);
        //使用偏移量和长度作为参数使用该数组
        handleArray(array, 0, array.length);
    }

字节级操作

随机访问索引

ByteBuf的索引也是从零开始的,访问数据的代码如下:

public static void byteBufRelativeAccess() {
        ByteBuf buffer = BYTE_BUF_FROM_SOMEWHERE; //get reference form somewhere
        for (int i = 0; i < buffer.capacity(); i++) {
            byte b = buffer.getByte(i);
            System.out.println((char) b);
        }
    }

顺序访问索引

ByteBuf被读索引和写索引划分为了三个区域:

img

可丢弃字节

可丢弃字节的分段包含了已经被读过的字节。通过调用discardReadBytes()方法,可以丢弃它们并回收空间,变成可写字节,但是不建议频繁调用discardReadBytes()方法,因为可读字节必须被移动到缓冲区的开始位置。

img

可读字节

存储了实际数据,任何名称以read 或者skip 开头的操作都将检索或者跳过位于当前readerIndex 的数据,并且将它增加已读字节数。

可写字节

拥有未定义内容的、写入就绪的内存区域,任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并将它增加已经写入的字节数。

索引管理

通过调用markReaderIndex()、markWriterIndex()、resetWriterIndex()和resetReaderIndex()来标记和重置ByteBuf 的readerIndex 和writerIndex。可以通过调用clear()方法来将readerIndex 和writerIndex 都设置为0,但是这并不会清除内存中的内容。

clear()方法调用前:

img

clear()方法调用后:

img

查找操作

  • boolean process(byte value) 检查输入值是否是正在查找的值

  • forEachByte(ByteBufProcessor.FIND_NUL) 和以NULL结尾的内容的Flash套接字集成

派生缓冲区

1.duplicate();
2.slice();
3.slice(int, int);
4.Unpooled.unmodifiableBuffer(…);
5.order(ByteOrder);
6.readSlice(int)。

以上这些方法都将返回一个新的ByteBuf实例,它具有自己的读索引、写索引和标记索引,其内部存储和JDK的ByteBuffer一样也是共享的,因此如果修改了它的内容,那么同时也就修改了其对应的源实例。

如果需要一个现有缓冲区的真实副本,使用copy()或者copy(int,int)方法。

下面代码展示了如何使用slice(int,int)方法来操作ByteBuf的一个分段:

public static void byteBufSlice() {
      Charset utf8 = Charset.forName("UTF-8");
      //创建一个用于保存给定字符串的字节的 ByteBuf
      ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
      //创建该 ByteBuf 从索引 0 开始到索引 15 结束的一个新切片
      ByteBuf sliced = buf.slice(0, 15);
      //将打印“Netty in Action”
      System.out.println(sliced.toString(utf8));
      //更新索引 0 处的字节
      buf.setByte(0, (byte)'J');
      //将会成功,因为数据是共享的,对其中一个所做的更改对另外一个也是可见的
      assert buf.getByte(0) == sliced.getByte(0);
  }

如果有可能,建议使用slice()方法来避免复制内存的开销。

读/写操作

有两种类别的读/写操作:

  • get()和set()操作,从给定的索引开始,并且保持索引不变;

  • read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。

测试代码:

public static void byteBufSetGet() {
        Charset utf8 = Charset.forName("UTF-8");
        //创建一个新的 ByteBuf以保存给定字符串的字节
        ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
        //打印第一个字符'N'
        System.out.println((char)buf.getByte(0));
        //存储当前的 readerIndex 和 writerIndex
        int readerIndex = buf.readerIndex();
        int writerIndex = buf.writerIndex();
        //将索引 0 处的字 节更新为字符'B'
        buf.setByte(0, (byte)'B');
        //打印第一个字符,现在是'B'
        System.out.println((char)buf.getByte(0));
        //将会成功,因为这些操作并不会修改相应的索引
        assert readerIndex == buf.readerIndex();
        assert writerIndex == buf.writerIndex();
    }

ByteBufHolder接口

Netty提供了ByteBufHolder来提供高级特性的支持,如存储各种属性值(HTTP响应中字节的内容,状态码,cookie等),缓冲区池化等。

ByteBufHolder只有几种用于访问底层数据和引用计数的方法:

名称 描述
content() 返回由这个ByteBufHolder所持有的ByteBuf
copy() 返回这个ByteBufHolder的一个深拷贝,包括一个其所包含的ByteBuf 的非共享拷贝
duplicate() 返回这个ByteBufHolder 的一个浅拷贝,包括一个其所包含的ByteBuf 的共享拷贝

ByteBuf 分配

按需分配:ByteBufAllocator 接口

Netty 通过ByteBufAllocator接口 实现了(ByteBuf 的)池化 ,它可以用来分配我们所描述过的任意类型的ByteBuf实例。

Netty提供了两种ByteBufAllocator 的实现: PooledByteBufAllocatorUnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片(一种称为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存).后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。

Unpooled缓冲区

提供静态的辅助方法来创建未池化的ByteBuf实例:

名称 描述
buffer()
buffer(int initialCapacity)
buffer(int initialCapacity, int maxCapacity)
返回一个未池化的基于堆内存存储的ByteBuf
directBuffer()
directBuffer(int initialCapacity)
directBuffer(int initialCapacity, int maxCapacity)
返回一个未池化的基于直接内存存储的ByteBuf
wrappedBuffer() 返回一个包装了给定数据的ByteBuf
copiedBuffer() 返回一个复制了给定数据的ByteBuf

ByteBufUtil 类

提供了用于操作ByteBuf的静态的辅助方法,有两个非常有用的方法:

  • hexdump() 以十六进制的表示形式打印ByteBuf 的内容
  • equals(ByteBuf, ByteBuf) 它被用来判断两个ByteBuf实例的相等性

引用计数

引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。

引用计数对于池化实现(如PooledByteBufAllocator)来说是至关重要的,它降低了内存分配的开销。

public static void referenceCounting(){
     Channel channel = CHANNEL_FROM_SOMEWHERE; //get reference form somewhere
     //从 Channel 获取ByteBufAllocator
     ByteBufAllocator allocator = channel.alloc();
     //...
     //从 ByteBufAllocator分配一个 ByteBuf
     ByteBuf buffer = allocator.directBuffer();
     //检查引用计数是否为预期的 1
     assert buffer.refCnt() == 1;
     //...
 }

释放引用计数的对象:

public static void releaseReferenceCountedObject(){
      ByteBuf buffer = BYTE_BUF_FROM_SOMEWHERE; //get reference form somewhere
      //减少到该对象的活动引用。当减少到 0 时,该对象被释放,并且该方法返回 true
      boolean released = buffer.release();
      //...
  }