Netty之ByteBuf

110 阅读7分钟

​1. Netty的数据容器    

我们知道,网络数据的基本单位是字节,在Java NIO中使用ByteBuffer作为它的字节容器,但是这个类使用起来过于繁琐,Netty提供了ByteBuf作为ByteBuffer的替代,下面是ByteBuf的一些优点:

  • 可以被用户自定义的缓冲区类型扩展;

  • 通过内置的复合缓冲区类型是实现了透明的零拷贝;

  • 容量可以按需增长;

  • 在读写两种模式之间切换不需要调用flip()方法;

  • 读和写使用了不同的索引;

  • 支持方法的链式调用;

  • 支持引用计数;

  • 支持池化;

2.ByteBuf的使用模式

ByteBuf有三种常见的使用模式,分别是堆缓冲区、直接缓冲区和复合缓冲区。

2.1 堆缓冲区

    堆缓冲区是将数据存储在JVM的堆空间内,这种模式被称为支撑数组,它能在没有池化的情况下提供快速的分配和释放,适合于有遗留数据需要处理的情况。

/**
 * 堆缓冲区
 */
private static void createHeapBuf(){
    //创建缓冲区
    ByteBuf heapBuf = Unpooled.buffer(16);
    //向缓冲区中写入数据
    heapBuf.writeByte(1);
    heapBuf.writeByte(2);
    //判断是否有一个支撑数组
    if (heapBuf.hasArray()){
        //获得对该数组的引用
        byte[] array = heapBuf.array();
        System.out.println("array length:"+array.length);//16
        System.out.println("array[0]:"+array[0]);//1
        System.out.println("array[1]:"+array[1]);//2
        //获得可读字节数
        int length = heapBuf.readableBytes();
        System.out.println("可读字节数:"+length);//2
    }
}

2.2 直接缓冲区

    直接缓冲区是在堆之外开辟的空间,它对于网络数据传输是最理想的选择,因为如果是堆缓冲区发送数据,在发送之前JVM内部需要将数据复制到一个直接缓冲区中,如果是直接缓冲区则免去了这一步;但是直接缓冲区相对来说分配和释放会更昂贵,并且如果你有遗留代码需要处理的话,由于数据不是在堆上,你可能需要将数据复制一份,如果这样的话倒不如一开始就使用堆缓冲区。

/**
 * 创建直接缓冲区
 */
private static void createDirectBuf(){
    //创建缓冲区
    ByteBuf directBuf = Unpooled.directBuffer(16);
    directBuf.writeByte(1);
    directBuf.writeByte(2);
    //如果不是数组支撑,则是一个直接缓冲区
    if (!directBuf.hasArray()){
        //byte[] array = directBuf.array(); UnsupportedOperationException
        System.out.println(directBuf.getByte(0));
    }
}

2.3 复合缓冲区

    复合缓冲区为多个缓冲区提供了一个聚合视图,我们可以根据需要添加或者移除缓冲区。复合缓冲区适用于需要发送多个不同消息的时候,为了避免多余的复制(创建多个相同的消息头),我们可以选择使用复合缓冲区。

    /**
     * 复合缓冲区
     */
    private static void compositeBuf(){
        //创建复合缓冲区
        CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
        //创建堆缓冲区
        ByteBuf heapBuf = Unpooled.buffer(16);
        heapBuf.writeByte(1);
        heapBuf.writeByte(2);
        heapBuf.writeByte(3);
        //创建直接缓冲区
        ByteBuf directBuf = Unpooled.directBuffer(16);
        directBuf.writeByte(4);
        directBuf.writeByte(5);
        directBuf.writeByte(6);
        //将堆缓冲区和直接缓冲区追加到复合缓冲区
        compositeByteBuf.addComponents(heapBuf,directBuf);
        //移除缓冲区
//        compositeByteBuf.removeComponent(0);
        //遍历内部的缓冲区
        Iterator<ByteBuf> iterator = compositeByteBuf.iterator();
        while (iterator.hasNext()){
            ByteBuf buf = iterator.next();
            int length = buf.readableBytes();
            byte[] array = new byte[length];
            buf.getBytes(buf.readerIndex(),array);
            for (byte b : array) {
                System.out.println(b);
            }
        }
    }

3.ByteBuf的相关操作

3.1 随机访问索引

ByteBuf的索引和普通的字节数组一样,第一个是0,最后一个是capacity()-1。

/**
 * 随机访问索引
 */
private static void randomAccessIndex(){
    ByteBuf buf = Unpooled.buffer(16);
    buf.writeByte(1);
    buf.writeByte(2);
    buf.writeByte(3);
    buf.writeByte(4);
    buf.writeByte(5);
    buf.writeByte(6);
    for (int i=0;i<buf.capacity();i++){
        System.out.println(buf.getByte(i));
    }
}

3.2 顺序访问索引

我们知道,ByteBuf同时具有读索引和写索引,这也是为什么它无需调用flip()方法来切换读写模式的原因。下图展示了ByteBuf的内部分段:

3.2.1 可丢弃字节

可丢弃字节包含了已经读过的字节,这个分段的初始大小为0,会随着read操作的执行而增加。可以通过调用discardReadBytes()方法来丢弃它们并回收空间,不过这有可能会导致内存复制,因为可读字节需要被移动到缓冲区的开始位置,所以建议只有在真正需要的地方使用,例如内存非常宝贵的时候。

3.2.2 可读字节

可读字节分段存储了实际可读的数据,新分配的缓冲区默认readerIndex为0,任何read和skip开头的操作都将使readerIndex增加。

/**
 * 可读字节
 */
private static void bufRead(){
    ByteBuf buf = Unpooled.buffer(16);
    buf.writeByte(1);
    buf.writeByte(2);
    buf.writeByte(3);
    buf.writeByte(4);
    buf.writeByte(5);
    buf.writeByte(6);
    while (buf.isReadable()){
        System.out.println(buf.readByte());
    }
}

3.2.3 可写字节

可写字节分段是一个未定义内容、写入就绪的区域,同样新分配的缓冲区默认的writerIndex为0,任何write操作都将使writerIndex增加。

/**
 * 可写字节
 */
private static void bufWrite(){
    ByteBuf buf = Unpooled.buffer(16);
    buf.writeByte(1);
    buf.writeByte(2);
    System.out.println("可写字节数:"+buf.writableBytes());
    buf.writeByte(3);
    buf.writeByte(4);
    System.out.println("可写字节数:"+buf.writableBytes());
}

3.3 索引管理

我们可以通过调用markReaderIndex、resetReaderIndex、markWriterIndex和resetWriterIndex来标记和重置readerIndex和writerIndex的位置。

    /**
     * 标记和重置索引
     */
    private static void markAndReset(){
        ByteBuf buf = Unpooled.buffer(16);
        buf.writeByte(1);
        buf.writeByte(2);
        buf.writeByte(3);
        buf.markWriterIndex();
        buf.writeByte(4);
        buf.writeByte(5);
        buf.writeByte(6);
//        System.out.println("可写字节数:"+buf.writableBytes());//10
//        buf.resetWriterIndex();
//        System.out.println("可写字节数:"+buf.writableBytes());//13
        System.out.println(buf.readByte());
        System.out.println(buf.readByte());
        System.out.println(buf.readByte());
        buf.markReaderIndex();
        System.out.println(buf.readByte());
        System.out.println(buf.readByte());
        System.out.println(buf.readByte());
        System.out.println("可读字节数:"+buf.readableBytes());//0
        buf.resetReaderIndex();
        System.out.println("可读字节数:"+buf.readableBytes());//3
    }

我们也可以使用readerIndex和writerIndex方法来完成移动索引的目的。

/**
 * 移动索引
 */
private static void readerAndWriterIndex(){
    ByteBuf buf = Unpooled.buffer(16);
    buf.writeByte(1);
    buf.writeByte(2);
    buf.writeByte(3);
    buf.writeByte(4);
    buf.writeByte(5);
    buf.writeByte(6);
    System.out.println("可写字节数:"+buf.writableBytes());//10
    buf.writerIndex(3);
    System.out.println("可写字节数:"+buf.writableBytes());//13
    System.out.println("可读字节数:"+buf.readableBytes());//3,可读字节数=writerIndex-readerIndex
    System.out.println(buf.readByte());
    System.out.println(buf.readByte());
    System.out.println(buf.readByte());
    System.out.println("可读字节数:"+buf.readableBytes());//0
    buf.readerIndex(0);
    System.out.println("可读字节数:"+buf.readableBytes());//3
}

我们可以使用clear方法来将readerIndex和writerIndex都设置为0.

private static void clearIndex(){
    ByteBuf buf = Unpooled.buffer(16);
    buf.writeByte(1);
    buf.writeByte(2);
    buf.writeByte(3);
    buf.writeByte(4);
    buf.writeByte(5);
    buf.writeByte(6);
    System.out.println("可写字节数:"+buf.writableBytes());//10
    System.out.println(buf.readByte());
    System.out.println(buf.readByte());
    System.out.println(buf.readByte());
    System.out.println("可读字节数:"+buf.readableBytes());//3
    buf.clear();
    System.out.println("可写字节数:"+buf.writableBytes());//16
    System.out.println("可读字节数:"+buf.readableBytes());//0
}

3.4 派生缓冲区

派生缓冲区为ByteBuf提供了以专门的方式呈现其内容的视图。我们可以通过duplicate和slice等方法来创建此类视图,它们会返回一个ByteBuf实例,具有自己的读写索引,但是数据是和源缓冲区共享的,也就是一旦我们修改了派生缓冲区的内容,源缓冲区的内容也会同步改变。

/**
 * 缓冲区切片
 */
private static void sliceBuf(){
    ByteBuf buf = Unpooled.copiedBuffer("Hello Netty!", Charset.forName("UTF-8"));
    ByteBuf sliceBuf = buf.slice(0, 5);
    System.out.println(sliceBuf.toString(Charset.forName("UTF-8")));//hello
    sliceBuf.setByte(0,(byte)'G');
    assert buf.getByte(0) == sliceBuf.getByte(0);
}

如果想要使返回的ByteBuf具有独立的数据副本,我们可以使用copy方法。

/**
 * 复制缓冲区
 */
private static void copyBuf(){
    Charset charset = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Hello Netty!",charset);
    ByteBuf copyBuf = buf.copy(0, 5);
    System.out.println(copyBuf.toString(charset));
    copyBuf.setByte(0,(byte)'G');
    assert buf.getByte(0) != copyBuf.getByte(0);
}

3.5 ByteBuf分配

ByteBuf有两种分配方式,一种是前面使用的Unpooled工具类,它是用来创建未池化的ByteBuf实例;还有一种就是使用ByteBufAllocator接口。

3.5.1 ByteBufAllocator接口

    为了降低内存分配和释放的开销,Netty通过ByteBufAllocator接口实现了ByteBuf的池化。Netty提供了ByteBufAllocator的两种实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf实例以提高性能并最大限度地减少内存碎片,它使用了一种名为jemalloc的被大量现代操作系统采用的高效方法来分配内存。后者不池化ByteBuf实例,每次都是返回一个新的实例。

    /**
     * ByteBuf分配
     */
    private static void allocBuf(){
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16);
        buf.writeByte(1);
        buf.writeByte(2);
        buf.writeByte(3);
        buf.writeByte(4);
        buf.writeByte(5);
        while (buf.isReadable()){
            System.out.println(buf.readByte());
        }
    }

3.5.2 Unpooled缓冲区

这是我们前面使用到的分配ByteBuf的方法,它提供了一些静态方法来创建未池化的ByteBuf实例。

    /**
     * ByteBuf分配
     */
    private static void allocBuf(){
        /**
         * 注意:
         * ByteBufAllocator的buffer()方法是返回一个基于堆或者直接内存的缓冲区,
         * 而Unpooled的buffer()方法是返回一个基于堆内存的缓冲区,如果只想使用ByteBufAllocator
         * 创建堆缓冲区的话,可以使用heapBuffer()方法。
         */
//        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16);
        ByteBuf buf = Unpooled.buffer(16);
        buf.writeByte(1);
        buf.writeByte(2);
        buf.writeByte(3);
        buf.writeByte(4);
        buf.writeByte(5);
        while (buf.isReadable()){
            System.out.println(buf.readByte());
        }
    }

3.6 引用计数

    引用计数是一种通过一个对象持有的资源不再被其它对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty在第四版中引入了引用计数技术,ByteBuf和ByteBufHolder类都实现了ReferenceCounted接口。一般ReferenceCounted的实现的引用计数都是从1开始,当减少为0时该对象就会被释放。

/**
 * 引用计数
 */
private static void referenceCount(){
    ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16);
    //输出引用计数量
    System.out.println(buf.refCnt());
    //引用计数减一,为0时该方法返回true
    boolean released = buf.release();
    System.out.println("是否释放:"+released);
}