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()
:返回一个包装了给定数据的ByteBuf
-
copiedBuffer()
:返回一个复制了给定数据的 ByteBuf
派生缓冲区
当需要使用多个视图去操作内存时,可以使用以下方法获取:
-
duplicate()
-
slice()
-
Unpooled.unmodifiableBuffer()
-
order()
-
readSlice()
这几个方法可以返回一个具有单独的读索引、写索引和标记索引实例,但是实际数据是共享的。如果需要复制一个全新的缓冲区对象,可以使用
copy()
方法。
其他可能用到的操作
ByteBufUtil类
:ByteBufUtil提供了用于操作ByteBuf的一些方法。包括
hexdump()
,
equals()
等
release()
:进行内存回收。
readableBytes()
:返回可读取的字节数
writeableBytes()
:返回可写入的字节数
isReable()
:是否至少有一个字节可读取
isWriteable
:是否至少有一个字节可写入
array()
:返回一个字节数组