有一点我们需要知道的是,ByteBuf的jar包,是可以单独使用的。比如某个项目中有一个场景,需要处理某个自定义的协议,那么我们在解析协议时,就可以将接收到的将字节内容写入一个ByteBuf,然后从ByteBuf中慢慢的将内容读取出来。下面让我们用一个例子简单的了解下ByteBuf的使用。
要想使用ByteBuf,首先肯定是要创建一个ByteBuf,更确切的说法就是要申请一块内存,后续可以在这块内存中执行写入数据读取数据等等一系列的操作。
那么如何创建一个ByteBuf呢?Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。
具体的层级关系如下图所示:
有了Allocator之后,Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。
我们以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但最终都是以下这几种,只是参数不一样而已:
// 在堆上分配一个ByteBuf,并指定初始容量和最大容量
public static ByteBuf buffer(int initialCapacity, int maxCapacity) {
return ALLOC.heapBuffer(initialCapacity, maxCapacity);
}
// 在堆外分配一个ByteBuf,并指定初始容量和最大容量
public static ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
return ALLOC.directBuffer(initialCapacity, maxCapacity);
}
// 使用包装的方式,将一个byte[]包装成一个ByteBuf后返回
public static ByteBuf wrappedBuffer(byte[] array) {
if (array.length == 0) {
return EMPTY_BUFFER;
}
return new UnpooledHeapByteBuf(ALLOC, array, array.length);
}
// 返回一个组合ByteBuf,并指定组合的个数
public static CompositeByteBuf compositeBuffer(int maxNumComponents){
return new CompositeByteBuf(ALLOC, false, maxNumComponents);
}
其中包装方法除了上述这个方法之外,还有一些其他常用的包装方法,比如参数是一个ByteBuf的包装方法,比如参数是一个原生的ByteBuffer的包装方法,比如指定一个内存地址和大小的包装方法等等。
另外还有一些copy*开头的方法,实际是调用了buffer(int initialCapacity, int maxCapacity)或directBuffer(int initialCapacity, int maxCapacity)方法,然后将具体的内容write进生成的ByteBuf中返回。
以上所有的这些方法都实际通过一个叫ALLOC的静态变量进行了调用,来实现具体的ByteBuf的创建,而这个ALLOC实际是一个ByteBufAllocator:
private static final ByteBufAllocator
ALLOC = UnpooledByteBufAllocator.DEFAULT;
ByteBufAllocator是一个专门负责ByteBuf分配的接口,对应的Unpooled实现类就是UnpooledByteBufAllocator。在UnpooledByteBufAllocator类中可以看到UnpooledByteBufAllocator.DEFAULT变量是一个final类型的静态变量
/**
* Default instance which uses leak-detection for direct buffers.
* 默认的UnpooledByteBufAllocator实例,并且会对堆外内存进行泄漏检测
*/public static final UnpooledByteBufAllocator
DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
涉及的设计模式
ByteBuf和ByteBufAllocator之间是一种相辅相成的关系,ByteBufAllocator用来创建一个ByteBuf,而ByteBuf亦可以返回创建他的Allocator。ByteBuf和ByteBufAllocator之间是一种 抽象工厂模式,具体可以用一张图描述如下:
下面我来用一个实际的例子来说明ByteBuf的使用,并通过观察在不同阶段ByteBuf的读写指针的值和ByteBuf的容量变化来更加深入的了解ByteBuf的设计,为了方便,我会用非池化的分配器来创建ByteBuf。
我构造了一个demo,来演示在ByteBuf中插入数据、读取数据、清空读写指针、数据清零、扩容等等方法,具体的代码如下:
private static void simpleUse() {
// 1.创建一个非池化的ByteBuf,大小为10个字节
ByteBuf buf = Unpooled.buffer(10);
System.out.println("原始ByteBuf为====================>" + buf.toString());
System.out.println("1.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
// 2.写入一段内容
byte[] bytes = { 1, 2, 3, 4, 5 };
buf.writeBytes(bytes);
System.out.println("写入的bytes为====================>" + Arrays.toString(bytes));
System.out.println("写入一段内容后ByteBuf为===========>" + buf.toString());
System.out.println("2.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
// 3.读取一段内容
byte b1 = buf.readByte();
byte b2 = buf.readByte();
System.out.println("读取的bytes为====================>" + Arrays.toString(new byte[] { b1, b2 }));
System.out.println("读取一段内容后ByteBuf为===========>" + buf.toString());
System.out.println("3.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
// 4.将读取的内容丢弃
buf.discardReadBytes();
System.out.println("将读取的内容丢弃后ByteBuf为(读写指针都向前移动)===>" + buf.toString());
System.out.println("4.ByteBuf中的内容为(可读数据向前移动)===>" + Arrays.toString(buf.array()));
buf.writeByte(9);
buf.writeByte(9);
System.out.println("再写入2个9后的ByteBuf为========>" + buf.toString());
System.out.println("ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
// 5.清空读写指针
buf.clear();
System.out.println("将读写指针清空后ByteBuf为==========>" + buf.toString());
System.out.println("5.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
// 6.再次写入一段内容,比第一段内容少
byte[] bytes2 = { 1, 2, 3 };
buf.writeBytes(bytes2);
System.out.println("写入的bytes为====================>" + Arrays.toString(bytes2));
System.out.println("写入一段内容后ByteBuf为===========>" + buf.toString());
System.out.println("6.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
// 7.将ByteBuf清零
buf.setZero(0, buf.capacity());
System.out.println("将内容清零后ByteBuf为==============>" + buf.toString());
System.out.println("7.ByteBuf中的内容为================>" + Arrays.toString(buf.array()) + "\n");
// 8.再次写入一段超过容量的内容
byte[] bytes3 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
buf.writeBytes(bytes3);
System.out.println("写入的bytes为====================>" + Arrays.toString(bytes3));
System.out.println("写入一段内容后ByteBuf为===========>" + buf.toString());
System.out.println("8.ByteBuf中的内容为===============>" + Arrays.toString(buf.array()) + "\n");
}
执行结果如下:
原始ByteBuf为====================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
1.ByteBuf中的内容为===============>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
写入的bytes为====================>[1, 2, 3, 4, 5]
写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)
2.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
读取的bytes为====================>[1, 2]
读取一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 2, widx: 5, cap: 10)
3.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
将读取的内容丢弃后ByteBuf为(读写指针都向前移动)===>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
4.ByteBuf中的内容为(可读数据向前移动)===>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]
再写入2个9后的ByteBuf为========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)
ByteBuf中的内容为===============>[3, 4, 5, 9, 9, 0, 0, 0, 0, 0]
将读写指针清空后ByteBuf为==========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
5.ByteBuf中的内容为===============>[3, 4, 5, 9, 9, 0, 0, 0, 0, 0]
写入的bytes为====================>[1, 2, 3]
写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
6.ByteBuf中的内容为===============>[1, 2, 3, 9, 9, 0, 0, 0, 0, 0]
将内容清零后ByteBuf为==============>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)
7.ByteBuf中的内容为================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
写入的bytes为====================>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 14, cap: 64)
8.ByteBuf中的内容为===============>[0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
下面让我们来仔细的研究下执行的过程,并分析下为什么会产生这样的执行结果。
刚初始化的ByteBuf对象,容量为10,读写指针都为0,且每个字节的值都为0,并且这些字节都是“可写”的,我们用红色来表示。
当写入一段内容后(这里写入的是5个字节),写指针向后移动了5个字节,写指针的值变成了5,而读指针没有发生变化还是0,但是读指针和写指针之间的字节现在变成了“可读”的状态了,我们用紫色来表示。
接着我们有读取了2个字节的内容,这时读指针向后移动了2个字节,读指针的值变成了2,写指针不变,此时0和读指针之间的内容变成了“可丢弃”的状态了,我们用粉色来表示。
紧接着,我们将刚刚读取完的2个字节丢弃掉,这时ByteBuf把读指针与写指针之间的内容(即 3、4、5 三个字节)移动到了0的位置,并且将读指针更新为0,写指针更新为原来写指针的值减去原来读指针的值。但是需要注意的是,第4和第5个字节的位置上,还保留的原本的内容,只是这两个字节由原来的“可读”变成了现在的“可写”。
这时再写入两个9,可以看到输出是34599。writerIndex=3和writerIndex=4的两个数字4和5变成了两个9
然后,我们执行了一个 clear 方法,将读写指针同时都置为0了,此时所有的字节都变成“可写”了,但是需要注意的是,clear方法只是更改的读写指针的值,每个位置上原本的字节内容并没有发生改变。
然后再次写入一段内容123后,读指针不变,写指针向后移动了具体的字节数,这里是向后移动了三个字节。且写入的这三个字节变成了“可读”状态。
清零(setZero)和清空(clear)的方法是两个概念完全不同的方法,“清零”是把指定位置上的字节的值设置为0,除此之外不改变任何的值,所以读写指针的值和字节的“可读写”状态与上次保持一致,而“清空”则只是将读写指针都置为0,并且所有字节都变成了“可写”状态。
最后我们往ByteBuf中写入超过ByteBuf容量的内容,这里是写入了11个字节,此时ByteBuf原本的容量不足以写入这些内容了,所以ByteBuf发生了扩容。其实只要写入的字节数超过可写字节数,就会发生扩容了。
那么扩容是怎么扩的呢,为什么容量从10扩容到64呢?我们从源码中找答案。
扩容肯定发生在写入字节的时候,让我们找到 writeBytes(byte[] bytes) 方法,具体如下:
@Override
public ByteBuf writeBytes(byte[] src) {
writeBytes(src, 0, src.length);
return this;
}@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
// 该方法检查是否有足够的可写空间,是否需要进行扩容
ensureWritable(length);
setBytes(writerIndex, src, srcIndex, length);
writerIndex += length;
return this;
}
在进入 ensureWritable(length) 方法内部查看,具体的代码如下:
@Override
public ByteBuf ensureWritable(int minWritableBytes) {
if (minWritableBytes < 0) {
throw new IllegalArgumentException(String.format(
"minWritableBytes: %d (expected: >= 0)", minWritableBytes));
}
ensureWritable0(minWritableBytes);
return this;
}
final void ensureWritable0(int minWritableBytes) {
// 检查该ByteBuf对象的引用计数是否为0,保证该对象在写入之前是可访问的
ensureAccessible();
if (minWritableBytes <= writableBytes()) {
return;
}
if (minWritableBytes > maxCapacity - writerIndex) {
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}
// Normalize the current capacity to the power of 2.
// 计算新的容量,即为当前容量扩容至2的幂次方大小
int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);
// Adjust to the new capacity.
// 设置扩容后的容量
capacity(newCapacity);
}
从上面的代码中可以很清楚的看出来,计算新的容量的方法是调用的 ByteBufAllocator 的 calculateNewCapacity() 方法,继续跟进去该方法,这里的 ByteBufAllocator 的实现类是 AbstractByteBufAllocator ,具体的代码如下:
@Override
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
if (minNewCapacity < 0) {
throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");
}
if (minNewCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format(
"minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
minNewCapacity, maxCapacity));
}
// 扩容的阈值,4兆字节大小
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
if (minNewCapacity == threshold) {
return threshold;
}
// If over threshold, do not double but just increase by threshold.
// 如果要扩容后新的容量大于扩容的阈值,那么扩容的方式改为用新的容量加上阈值,
// 否则将新容量改为双倍大小进行扩容
if (minNewCapacity > threshold) {
int newCapacity = minNewCapacity / threshold * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
// Not over threshold. Double up to 4 MiB, starting from 64.
// 如果要扩容后新的容量小于4兆字节,则从64字节开始扩容,每次双倍扩容,
// 直到小于指定的新容量位置
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
return Math.min(newCapacity, maxCapacity);
}
到这里就很清楚了,每次扩容时,有一个阈值t(4MB),计划扩容的大小为c,扩容后的值为n。
扩容的规则可以用下面的逻辑表示:
来源:简书