09.Netty之ByteBuf介绍(一)
一、概述
ByteBuf是对NIO中ByteBuffer的增强。相比于ByteBuffer,Netty 的 ByteBuf 具有卓越的功能性和灵活性。
ByteBuf API 的优点
- 可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现透明的零拷贝
- 容量可以按需增长
- 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip() 方法
- 在读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
二、工作原理
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入,当你从 ByteBuf 读取时,readIndex 会递增已经被读取的字节数。同样的,当你写入 ByteBuf 时,它的 writeIndex 也会递增。
readIndex 和 writeIndex 的起始位置都为 0。
如果 readIndex 和 writeIndex 的值相等,也即此时已经到了可读取数据的末尾,就如同达到数组末尾一样,试图读取超出该点的数据将触发 IndexOutOfBoundsException。
名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开关的操作则不会。可以指定ByteBuf的最大容量。(默认的限制是Integer.MAX_VALUE)。
三、ByteBuf创建
可以通过ByteBuf Allocator类中的默认实现来创建一个Bytebuf
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
public class ByteBufCreateDemo {
public static void main(String[] args) {
//在传参时可以指定ByteBuf的容量,如果不指定的默认是256
// ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10);
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
}
}
- 在创建ByteBuf时可以指定容量大小,如果不指定的默认是256
相比于NIO中的ByteBuffer,Netty中的Bytebuf可以动态扩容。
- 验证动态扩容
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
public class ByteBufCreateDemo {
public static void main(String[] args) {
// 创建一个Bytebuf,默认创建的容量是256
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
System.out.println("添加数据前:" + buffer);
// 往Bytebuf中写数据
StringBuilder stringBuilder = new StringBuilder();
// 故意超过初始容量,验证是否会自动扩容
for (int i = 0; i < 300; i++) {
stringBuilder.append("a");
}
// 将数据写入ByteBuf
buffer.writeBytes(stringBuilder.toString().getBytes());
System.out.println("添加数据后:" + buffer);
}
}
运行结果
添加数据前:PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
添加数据后:PooledUnsafeDirectByteBuf(ridx: 0, widx: 300, cap: 512)
说明
- ridx:表示read index,即读取位置;
- widx:表示write index,即写入位置;
- cap:表示容量
从程序运行结果可知,将数据写入ByteBuf后容量由之前的256扩容到512,扩大了一倍。
四、ByteBuf的使用模式
ByteBuf共有三种模式:
-
堆缓冲区模式(Heap Buffer)
//可以使用下面的代码来创建池化基于堆的 ByteBuf ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
-
直接缓冲区模式(Direct Buffer)
//通过该方法创建的ByteBuf,使用的是基于直接内存的ByteBuf ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
-
复合缓冲区模式(Composite Buffer)
4.1 堆缓冲区模式(Heap Buffer)
堆缓冲区模式又称为支撑数组(backing array)。将数据存放在JVM的堆空间,通过将数据存储在数组中实现。
- 优点: 由于数据存储在JVM堆中可以快速创建和快速释放,并且提供了数组直接快速访问的方法
- 缺点: 每次数据与I/O进行传输时,都需要将数据拷贝到直接缓冲区
public class ByteBufHeapBufferDemo {
public static void main(String[] args) {
// 创建一个堆缓冲区
ByteBuf buffer = Unpooled.buffer(10);
String s = "waylau";
buffer.writeBytes(s.getBytes());
// 检查是否是支撑数组
if (buffer.hasArray()) {
// 获取支撑数组的引用
byte[] array = buffer.array();
// 计算第一个字节的偏移量
int offset = buffer.readerIndex() + buffer.arrayOffset();
// 可读字节数
int length = buffer.readableBytes();
printBuffer(array, offset, length);
}
}
//打印出Buffer的信息
private static void printBuffer(byte[] array, int offset, int len) {
System.out.println("array:" + array);
System.out.println("array->String:" + new String(array));
System.out.println("offset:" + offset);
System.out.println("len:" + len);
}
}
运行结果
array:[B@5b37e0d2
array->String:waylau
offset:0
len:6
4.2 直接缓冲区模式(Direct Buffer)
Direct Buffer属于堆外分配的直接内存,不会占用堆的容量。适用于套接字传输过程,避免了数据从内部缓冲区拷贝到直接缓冲区的过程,性能较好。
- 优点: 使用Socket传递数据时性能很好,避免了数据从Jvm堆内存拷贝到直接缓冲区的过程。提高了性能
- 缺点: 相对于堆缓冲区而言,Direct Buffer分配内存空间和释放更为昂贵
public class ByteBufDirectBufferDemo {
public static void main(String[] args) {
// 创建一个直接缓冲区
ByteBuf buffer = Unpooled.directBuffer(10);
String s = "waylau";
buffer.writeBytes(s.getBytes());
// 检查是否是支撑数组.
// 不是支撑数组,则为直接缓冲区
if (!buffer.hasArray()) {
// 计算第一个字节的偏移量
int offset = buffer.readerIndex();
// 可读字节数
int length = buffer.readableBytes();
// 获取字节内容
byte[] array = new byte[length];
buffer.getBytes(offset, array);
printBuffer(array, offset, length);
}
}
//打印出Buffer的信息
private static void printBuffer(byte[] array, int offset, int len) {
System.out.println("array:" + array);
System.out.println("array->String:" + new String(array));
System.out.println("offset:" + offset);
System.out.println("len:" + len);
}
}
运行结果
array:[B@6d5380c2
array->String:waylau
offset:0
len:6
对于涉及大量I/O的数据读写,建议使用Direct Buffer。而对于用于后端的业务消息编解码模块建议使用Heap Buffer。
4.3 复合缓冲区模式(Composite Buffer)
Composite Buffer是Netty特有的缓冲区。本质上类似于提供一个或多个ByteBuf的组合视图,可以根据需要添加和删除不同类型的ByteBuf。
- 优点:提供了一种访问方式让使用者自由地组合多个
ByteBuf
,避免了复制和分配新的缓冲区。 - 缺点:不支持访问其支撑数组。因此如果要访问,需要先将内容复制到堆内存中,再进行访问。
以下示例是复合缓冲区将堆缓冲区和直接缓冲区组合在一起,没有进行任何复制过程,仅仅创建了一个视图而已。
public class ByteBufCompositeBufferDemo {
public static void main(String[] args) {
// 创建一个堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(3);
String way = "way";
heapBuf.writeBytes(way.getBytes());
// 创建一个直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(3);
String lau = "lau";
directBuf.writeBytes(lau.getBytes());
// 创建一个复合缓冲区
CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer(10);
compositeBuffer.addComponents(heapBuf, directBuf); // 将缓冲区添加到符合缓冲区
// 检查是否是支撑数组.
// 不是支撑数组,则为复合缓冲区
if (!compositeBuffer.hasArray()) {
for (ByteBuf buffer : compositeBuffer) {
// 计算第一个字节的偏移量
int offset = buffer.readerIndex();
// 可读字节数
int length = buffer.readableBytes();
// 获取字节内容
byte[] array = new byte[length];
buffer.getBytes(offset, array);
printBuffer(array, offset, length);
}
}
}
//打印出Buffer的信息
private static void printBuffer(byte[] array, int offset, int len) {
System.out.println("array:" + array);
System.out.println("array->String:" + new String(array));
System.out.println("offset:" + offset);
System.out.println("len:" + len);
}
}
运行结果
array:[B@4d76f3f8
array->String:way
offset:0
len:3
array:[B@2d8e6db6
array->String:lau
offset:0
len:3
CompositeByteBuf
是一个虚拟的缓冲区,其用途是将多个缓冲区显示为单个合并缓冲区,类似数据库中的视图。
五、池化与非池化
池化的最大意义在于可以重用 ByteBuf,优点有
- 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
- 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
- 4.1 之前,池化功能还不成熟,默认是非池化实现
对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
- 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
- 4.1 之前,池化功能还不成熟,默认是非池化实现