如何对方便地字节数据进行操作?(ByteBuffer与ByteBuf)

ByteBuffer与ByteBuf通常用于字节数据的操作,比如对网络IO Channel进行读取或者写入,其中封装了一些操作byte数组的方法,还是很实用的。ByteBuf是对ByteBuffer的封装,由Netty提供,提供了更方便、更丰富的byte数组功能。

ByteBuffer

ByteBuffer的几个基本属性:

  • position:表示进行下一个读写操作的下标位置
  • limit:表示进行读写操作时的结束位置;
  • capacity:表示存储的容量
  • mark: 对数据进行标记

初始化:对ByteBuffer进行初始化,可以使用静态方法wrap(byte[] data)封装数组,也可以通过另一个静态方法allocate(int size)初始化指定长度的ByteBuffer。

初始状态position:0,limit:值为最大长度,capacity:值为最大长度

bytebuffer-init.png

数据写入(或读取) :每写入(或读取)一个值,position加一(图中是写入两个数据之后的位置)。

bytebuffer-write.png

准备读取(或写入) :使用flip()方法翻转准备数据读取(或写入),进行读取(或写入)时,不能超过limit限制,读超出限制报错BufferUnderflowException(写超出限制报错BufferOverflowException

bytebuffer-flip.png

清除数据:回到初始状态可以调用clear()方法,但是数据并不会删除,当写入时会直接覆盖对应位置的值。

bytebuffer-clear.png

标记位置:当需要进行标记时,可以使用mark()方法,即mark=position;进行读取后,可调用reset()方法直接回到mark标记的位置,即position=mark

ByteBuf

相比于ByteBuffer,ByteBuf对其提升主要体现在以下几个方面:

  1. 读和写的下标索引采用了不同的两个值进行操作,读写模式切换无需进行flip操作
  2. 支持堆内存和直接内存的池化以及零拷贝
  3. 可以按照需要进行容量的扩展
  4. 支持方法的链式调用

ByteBuf的读写

下面一个ByteBuf数字的内部结构,其中会有一个readIndex和writeIndex索引来记录读取和写入的位置。当你从ByteBuf 读取时,它的readerIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writerIndex也会被递增。读取超出writerIndex会触发IndexOutOfBoundsException。在所有方法中,名称以read或者write开头的方法,将会推进其对应的索引,而名称以set或者get为开头的操作则不会。 bytebuf.png 对于已经读取过不需要的字段,可以通过discardReadBytes()方法进行回收,它会把可读字节复制到字节数组的前面,回收过的ByteBuf会变成下图的样子。

bytebuf-discard.png

零拷贝

对于一般的网络IO读写,需要到端口的缓冲区进行读取之后,切换内核态写入,然后用户程序从用户态切换为内核态,拷贝数据到用户内存中,才能进行数据的处理。这里面需要几次的上下文切换拷贝数据。而对于零拷贝,则是直接访问相应位置的系统内存,节省了多次上下文切换拷贝的开销,大大提高了数据IO的读写效率。

ByteBuf的分配

ByteBuf分配主要有两种方式:池化与非池化。池化操作的内存不需要自己释放内存,它会自己回收复用,通常用于长时间存储的数据。而非池化操作内存则适用于临时存储的数据,一般用于低延迟、高性能场景。对于缓冲区的内存分配的位置主要有堆内存和直接内存两种,堆内存用的是JVM中堆的内存位置,直接内存用的是系统内存(堆外内存),直接内存由于零拷贝的方式读写数据效率更高,但是容易造成系统内存不足,需要用完立即回收。

池化分配 对于池化的分配,我们可以使用PooledByteBufAllocator进行创建。

ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
ByteBuf buf = allocator.buffer(512); 
复制代码

也使用ByteBufAllocator类创建一个池化的ByteBuf。

ByteBufAllocator allocator = new ByteBufAllocator() { 
    @Override public ByteBuf buffer() { 
        return PooledByteBufAllocator.DEFAULT.buffer(); 
    } 
    @Override public ByteBuf buffer(int initialCapacity) { 
        return PooledByteBufAllocator.DEFAULT.buffer(initialCapacity);
    }
}; 
ByteBuf buf = allocator.buffer(512);
复制代码

ByteBufAllocator类主要有以下几种方法:

  • buffer():创建一个基于堆的缓冲区
  • derectBuffer():创建一个基于直接内存的缓冲区
  • compositeBuffer():创建一个由堆内存或直接内存的复合缓冲区
  • ioBuffer():创建一个用于套接字的I/O操作的ByteBuf

非池化分配 对于非池化的分配,可以采用Unpooled的工具类,它提供了一些静态方法来创建未池化的 ByteBuf实例。

ByteBuf buf = Unpooled.buffer(512);
复制代码

Unpooled类的方法主要有以下几种方法:

  • buffer():创建一个基于堆的缓冲区
  • derectBuffer():创建一个基于直接内存的缓冲区
  • wrappedBuffer():返回一个包装了给定数据的ByteBuf
  • copiedBuffer():返回一个复制了给定数据的 ByteBuf

派生缓冲区

当需要使用多个视图去操作内存时,可以使用以下方法获取:

  • duplicate()
  • slice()
  • Unpooled.unmodifiableBuffer()
  • order()
  • readSlice()

这几个方法可以返回一个具有单独的读索引、写索引和标记索引实例,但是实际数据是共享的。如果需要复制一个全新的缓冲区对象,可以使用copy()方法。

其他可能用到的操作

ByteBufUtil类:ByteBufUtil提供了用于操作ByteBuf的一些方法。包括hexdump()equals()

release():进行内存回收。

readableBytes():返回可读取的字节数

writeableBytes():返回可写入的字节数

isReable():是否至少有一个字节可读取

isWriteable:是否至少有一个字节可写入

array():返回一个字节数组

猜你喜欢

转载自juejin.im/post/7216631742085791799