如何对方便地字节数据进行操作?(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:值为最大长度
数据写入(或读取) :每写入(或读取)一个值,position加一(图中是写入两个数据之后的位置)。
准备读取(或写入) :使用flip()
方法翻转准备数据读取(或写入),进行读取(或写入)时,不能超过limit限制,读超出限制报错BufferUnderflowException
(写超出限制报错BufferOverflowException
)
清除数据:回到初始状态可以调用clear()
方法,但是数据并不会删除,当写入时会直接覆盖对应位置的值。
标记位置:当需要进行标记时,可以使用mark()
方法,即mark=position
;进行读取后,可调用reset()
方法直接回到mark标记的位置,即position=mark
。
ByteBuf
相比于ByteBuffer,ByteBuf对其提升主要体现在以下几个方面:
- 读和写的下标索引采用了不同的两个值进行操作,读写模式切换无需进行
flip
操作 - 支持堆内存和直接内存的池化以及零拷贝
- 可以按照需要进行容量的扩展
- 支持方法的链式调用
ByteBuf的读写
下面一个ByteBuf数字的内部结构,其中会有一个readIndex和writeIndex索引来记录读取和写入的位置。当你从ByteBuf 读取时,它的readerIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writerIndex也会被递增。读取超出writerIndex会触发IndexOutOfBoundsException
。在所有方法中,名称以read或者write开头的方法,将会推进其对应的索引,而名称以set或者get为开头的操作则不会。
对于已经读取过不需要的字段,可以通过discardReadBytes()
方法进行回收,它会把可读字节复制到字节数组的前面,回收过的ByteBuf会变成下图的样子。
零拷贝
对于一般的网络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()
:返回一个包装了给定数据的ByteBufcopiedBuffer()
:返回一个复制了给定数据的 ByteBuf
派生缓冲区
当需要使用多个视图去操作内存时,可以使用以下方法获取:
duplicate()
slice()
Unpooled.unmodifiableBuffer()
order()
readSlice()
这几个方法可以返回一个具有单独的读索引、写索引和标记索引实例,但是实际数据是共享的。如果需要复制一个全新的缓冲区对象,可以使用copy()
方法。
其他可能用到的操作
ByteBufUtil类:ByteBufUtil提供了用于操作ByteBuf的一些方法。包括hexdump()
,equals()
等
release():进行内存回收。
readableBytes():返回可读取的字节数
writeableBytes():返回可写入的字节数
isReable():是否至少有一个字节可读取
isWriteable:是否至少有一个字节可写入
array():返回一个字节数组
转载自:https://juejin.cn/post/7216631742085791799