一、初始ByteBuf

网络上数据的基本单位总是字节。java NIO提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂和繁琐。

netty的替代品ByteBuf,一个强大的实现。既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的api。

ByteBuf维护着两个索引,一个是读索引,一个是写索引。

*      +-------------------+------------------+------------------+
*      | discardable bytes |  readable bytes  |  writable bytes  |
*      |                   |     (CONTENT)    |                  |
*      +-------------------+------------------+------------------+
*      |                   |                  |                  |
*      0      <=      readerIndex   <=   writerIndex    <=    capacity

可以看出

  • 0到readerIndex 之间的数据是已经丢弃的数据
  • readerIndex 到 writerIndex之间的为可读数据
  • writerIndex 到capacity之间的数据为可写数据

ByteBuf API的优点:

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

二、ByteBuf类 – netty的数据容器

ByteBuf维护两个不同的索引,一个是读索引(readerIndex),一个是写索引(writerIndex)。当从ByteBuf中读取数据的时候readerIndex索引会递增已经被读取的字节数。同样,当写入ByteBuf的时候,它的writerIndex索引会被递增。下图展示了一个初始容量为16的空ByteBuf的布局和状态

在这里插入图片描述

readIndex和writeIndex均为0的16字节的ByteBuf

  • 1、如果readerIndex和WriterIndex的值一样的时候,如果继续向前读取数据。将会抛出IndexOutOf-BoundsException

  • 2、以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或get开头操作不会修改索引。

  • 3、ByteBuf具有最大的容量值,试图移动写索引超过这个值,将会触发一个异常。其默认的限制为Integer.MAX_VALUE

三、ByteBuf的几种使用方式

1、堆缓冲区

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

    /**
     * 测试堆缓冲区的使用  byteBuf分配的内存为Java 的堆内存
     */
    @Test
    public void testHeapByte(){
        ByteBuf byteBuf = Unpooled.buffer();
        //向byteBuf中写入数据
       for(int i= 65;i<65+26;i++){
          byteBuf.writeByte(i);
       }
       //从byteBuf中读取数据
        byte[] bytes = new byte[26];
       byteBuf.readBytes(bytes);
        System.out.println(new String(bytes));
    }

2、直接缓冲区

直接缓冲区是另外一种ByteBuf的模式。我们期望用于对象创建的内存分配永远都来自于堆中,NIO 在JDK1.4中引入的ByteBuffer类允许JVM实现通过本地调用来分配内存。这主要是避免在每次调用本地I/O操作之前将缓冲区的数据复制到一个中间缓冲区。使用直接缓冲区的示例代码如下:

    /**
     * 测试直接缓冲区的使用
     */
    @Test
    public void testDirectByte(){
        ByteBuf byteBuf = Unpooled.directBuffer();
        //向byteBuf中写入数据
        for(int i= 65;i<65+26;i++){
            byteBuf.writeByte(i);
        }
        //从byteBuf中读取数据
        byte[] bytes = new byte[26];
        byteBuf.readBytes(bytes);
        System.out.println(new String(bytes));

    }

3、复合缓冲区

第三种 也是最后一种模式使用的是符合缓冲区,它为多个ByteBuffer提供一个聚合视图。可以根据实际的需要添加或者删除一个ByteBuf的实例。这个功能是JDK的ByteBuffer实现完全缺失的一个特性。

ByteBuf的子类 – compositeByteBuf实现了这个模式,它提供了一个将多个缓冲区一个统一的视图。

我们考虑一下一个又两部分 — 头部和主题 — 组成的将通过http协议发送的消息。

在这里插入图片描述

示例代码如下:

 /**
     * 测试符合缓冲区的使用
     * 符合缓冲区的如果包含多个缓冲区,那么就会直接返回false
     */
    @Test
    public void testCompositeBuffer() throws IOException {
        CompositeByteBuf byteBufs = Unpooled.compositeBuffer();
        ByteBuf  heapBuf = Unpooled.buffer();


        ByteBuf directBuf = Unpooled.directBuffer();

        heapBuf.readableBytes();



        heapBuf.writeBytes("我是头部信息".getBytes());
        directBuf.writeBytes("我是尾部信息".getBytes());



        //将 buf添加到符合缓冲区中
        byteBufs.addComponents(true,heapBuf);
        byteBufs.addComponents(true,directBuf);

        //从符合缓冲区中读取数据
        byte[] bytes = new byte[byteBufs.readableBytes()];
        byteBufs.readBytes(bytes);
        System.out.println(new String(bytes));



    }

四、ByteBuf的字节级操作

1、随机访问索引

跟java的普通数组一样,ByteBuf的索引位置也是从0开始的,第一个字节的索引是0,最后一个字节的索引是capacity()-1。如下面的代码所示:

    @Test
    public void randomAccessByte(){
        ByteBuf byteBuf = Unpooled.buffer(16);
        byteBuf.writeBytes("abcdefg".getBytes());

        System.out.println((char)byteBuf.getByte(2));

    }

使用那些需要一个索引值参数的方法时不会改变readerIndex和writerIndex这两个索引。如果有需要,可以调用readerIndex(index)和writerIndex(index)手动调整。

2、顺序访问

由于JDK的ByteBuffer只有一个索引,所以其需要通过调用flip()方法来在读写模式之间进行切换,下图展示了ByteBuf如何被两个索引划分为3个区域。
在这里插入图片描述

3、可丢弃字节

可丢弃字节的分段包含了已经被读过的字节。通过调用 方法,可以丢弃他们并回收空间。这个分段的初始大小为0,存储在readIndex中,随着readerIndex的值不断增加而增大。

*  BEFORE discardReadBytes()
*
*      +-------------------+------------------+------------------+
*      | discardable bytes |  readable bytes  |  writable bytes  |
*      +-------------------+------------------+------------------+
*      |                   |                  |                  |
*      0      <=      readerIndex   <=   writerIndex    <=    capacity
*
*
*  AFTER discardReadBytes()
*
*      +------------------+--------------------------------------+
*      |  readable bytes  |    writable bytes (got more space)   |
*      +------------------+--------------------------------------+
*      |                  |                                      |
* readerIndex (0) <= writerIndex (decreased)        <=        capacity

上述是摘录自ByteBuf的注释片段。我们可以清楚的看到,执行完discardBytes()方法后,readIndex的值变为0,且可写区域增大 。

调用discardReadBytes()可以确保可写分段的最大化,但是这里面伴随着内存的复制。建议还是在只有真正需要的时候才那么做。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐