Netty学习之ByteBuf
前言
在网络中传输的基本单元是字节byte,虽然在NIO中提供了一个ByteBuffer作为字节的容器,不过由于ByteBuffer比较难使用,所以Netty自己实现了一个,ByteBuf,并且提供了比较灵活的操作方式以及操作工具,本节我们将详细学习这一部分的知识。
ByteBuf
在Netty中,数据通过ByteBuf
以及ByteBufHolder
来进行操作,ByteBuf具有众多优秀的特性
- 易于扩展
- 通过内置的composite buffer类型可以实现zero-copy
- 根据需要扩展容量,类似于JDK中的(StringBuilder)
- 自动在读写模式进行切换,相比ByteBuffer需要调用
filp()
更加方便 - 采用读写两个指针
- 支持链式调用
- 支持引用计数
- 支持池化技术
工作原理
ByteBuf可以理解为一个字节数组,并且维护两个不同的指针:读指针以及写指针,当从ByteBuf中读取数据(read开头的函数)的时候,读指针增加,当写入数据(write开头的函数)的时候,写指针增加,当读写指针相同时,表示已经没有数据可以读取,继续读取会抛出IndexOutOfBoundException
,set以及get开头的函数不会影响指针,默认的最大容量是Integer.MAX_VALUE
,当写入超过容量时,会触发异常。
ByteBuf类型
基于堆的ByteBuf,将数据存储在JVM的堆内存中,并且其内部是一个字节数组,但没有进行缓存的时候,可以快速地申请以及回收内存,比较适合处理常规数据。
基于直接内存的ByteBuf,ByteBuf的内存空间是通过直接内存申请的(本地方法调用分配的内存),好处在于可以避免在发生I/O调用的时候,将数据从堆空间拷贝到直接内存中(节省一次拷贝),比较适合于进行网络数据传输,由于数据没有在堆中,所以操作的时候需要先拷贝到堆空间中(需要手动操作),缺点是空间的申请以及回收比较消耗资源,而且不受gc的管理。
上面两个可以通过hasArray()
进行区分,基于堆的返回true,基于直接内存的返回false
组合ByteBuf(CompositeByteBuf
),提供了多个ByteBuf的聚合视图,可以往其中添加或者删除ByteBuf实例,可能包含上面两种类型的ByteBuf,所以使用的时候需要注意。
获取ByteBuf
通过ByteBufAllocator
Netty通过ByteBufAllocator接口,来提供获取ByteBuf的操作
buffer();
buffer(initCapacity);
buffer(initCapacity, maxCapacity);
heapBuffer();
heapBuffer(initCapacity);
heapBuffer(initCapacity, maxCapacity);
directBuffer();
directBuffer(..);
directBuffer(.., ..);
compositeBuffer();
compositeBuffer(..);
compositeDirectBuffer();
compositeDirectBuffer(..);
compositeHeapBuffer();
compositeHeapBuffer(..);
ioBuffer(); // for i/o in socket
可以从Channel或者ChannelHanderContext中获取ByteBuffAllocator实例
public void testAllocator() {
NioServerSocketChannel channel = new NioServerSocketChannel();
ByteBufAllocator alloc = channel.alloc();
ByteBuf byteBuf = alloc.heapBuffer();
System.out.println(byteBuf.hasArray());
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator = ctx.alloc();
}
同时,Netty提供了两种ByteBufAllocator的默认实现:PooledByteBufAllocator
以及UnpooledByteBufAllocator
// true表示调用buffer()时,使用直接内存,false表示堆内存
ByteBufAllocator allocator = new UnpooledByteBufAllocator(true);
ByteBufAllocator allocator = new PooledByteBufAllocator(true);
true跟false的区别仅在于buffer()
,如果是调用heapBuffer()
,那还是堆内存,跟true/false无关
通过Unpooled
在有一些情况,我们没有办法获取ByteBuffAllocator
,则可以通过Unpooled
工具来创建未缓存的ByteBuf
实例
buffer(); // 基于堆的ByteBuff
buffer(..); // 同上
directBuffer(); // 同上
wrappedBuffer(); // 包装给定内容
copiedBuffer(); // 拷贝给定内容
ByteBuf操作
随机访问
ByteBuf支持类似于数组的访问形式,并且其下标从0开始,最后一个Byte为capacity() - 1
ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes());
for (int i = 0; i < buffer.capacity(); i++) {
System.out.print((char)buffer.getByte(i));
}
连续访问
ByteBuf由三个部分组成,如下图所示
+----------+------------+----------------+
| | | |
| 已经读取 | 可以读取 | 可以写入 |
| | | |
+----------+------------+----------------+
0------readerIndex---writeIndex-----capacity
其中已经读取的数据不可能再被读取(指的是当读指针已经移动后,读指针之前的数据),读指针与写指针之间的数据则可以被继续读取,写指针与容量之间的空间可以继续写入。
当调用ByteBuf#discardReadByytes()
后,后面的数据会移动到前面,使得读指针归为0,注意该操作会比较消耗资源,一般只在内存资源比较紧张的时候才进行该操作。
移动指针
在ByteBuf中,存在读写指针,所以可以根据需要移动指针,当调用read开头的函数时,读指针会移动,write开头的函数时,write指针会移动,同时,可以调用readIndex(int)
将读指针设置到指定位置,超过可读位置会抛出异常,writeIndex(int)
同理。
可以调用clear()
将读写指针均设置为0,该操作并没有清空内容,也即数据依旧可以读取出来,该操作比discardReadBytes()
消耗更低,因为只是重置了指针。
搜索操作
用于查找某个字节的下标,可以使用indexOf()
,也可以使用复杂的操作ByteBufProcessor#process(byte value)
,同时ByteBufProcessor定义了一系列公用操作,如ByteBufProcessor.FIND_CR
、ByteBufProcessor.FIND_LF
衍生操作
衍生操作提供了一些可以从当前ByteBuf中获取ByteBuf的操作,其底层公用一个ByteBuff,但是具有自己的读写指针
public void testDerived() {
ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes());
// 影子拷贝,底层其实是同一个
ByteBuf duplicate = buffer.duplicate();
buffer.setByte(0, 'a');
// a
System.out.println((char)duplicate.getByte(0));
// 影子切片,可以带参数
ByteBuf slice = buffer.slice();
buffer.setByte(0, 'a');
// a
System.out.println((char) slice.getByte(0));
ByteBuf byteBuf = buffer.readSlice(buffer.readableBytes());
for (int i = 0; i < byteBuf.capacity(); i++) {
System.out.print((char) byteBuf.getByte(i));
}
}
如果是要拷贝数据,则应该使用copy()
或者copy(int, int)
操作
ByteBufUtils
在Netty中,同时还提供了ByteBufUtils工具类来操作ByteBuf,如hexDump()
可以用于打印ByteBuf的内容,更多关于ByteBufUtils,可以参考API即可。
引用计数
Netty为ByteBuf以及ByteBufHolder引入了引用计数,两者均实现了ReferenceCounted
接口,可以用于提高性能。
当引用计数的值大于0的时候,对应的资源不会被释放,当计数值等于0时,资源会被释放掉。
通常来说,最后一个使用资源的对象需要释放掉该资源,即调用其release()
方法
总结
本小节主要详细学习了Netty中的数据容器,ByteBuf,在Netty中,所有的数据都是存放在ByteBuf中,所以,对Netty中数据的操作,其实就是对ByteBuf的操作,ByteBuf有三种不同的类型,基于堆的,基于直接内存的,组合类型的,在使用的时候需要根据情况选择合适的容器,同时,为了提高性能,Netty中引入了引用计数的概念,所以,当资源不需要使用的时候,需要显示释放掉对应的资源。