ByteBuf
ByteBuf是对nio中ByteBuffer的增强。主要的增强点就是ByteBuf它可以动态调整容量大小,当要存储的数据超过了当前容量的上限就会进行扩容,扩容的上限是多少?扩容机制是什么?请跟着本文往下看。对了,还有一个增强就是byteBuf不用和ByteBuffer一样进行读写模式的切换,ByteBuffer中读与写是共用一个指针,而ByteBuf既有读指针,也有写指针。
创建
ByteBufAllocator类中有一个默认实现,我们可以使用这个默认实现来创建一个Bytebuf
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
public class ByteBufCreateTest {
public static void main(String[] args) {
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
}
}
这里可以指定ByteBuf的容量,可以传一个int,如果不指定的话默认是256。
这里比nio好的一个地方就是,netty的Bytebuf容量的可以动态扩容的,而nio的ByteBuffer指定了就不能动了。
然后往ByteBuf中添加数据,验证是否会扩容
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(读指针 写指针 容量)
添加数据前:PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
添加数据后:PooledUnsafeDirectByteBuf(ridx: 0, widx: 300, cap: 512)
上面的方式只是平时可以这样创建Bytebuf,我们一般情况下都是在Handler中创建ByteBuf,建议使用下面的方式创建ByteBuf
内存分配与池化
ByteBuf支持堆内存,也支持直接内存。
分配堆内存 ByteBufAllocator.DEFAULT.heapBuffer();
分配直接内存 ByteBufAllocator.DEFAULT.directBuffer();
堆内存的分配效率高,但是读写效率低。直接内存读写效率高。nettty默认采用的是直接内存,也就是上面创建ByteBuf时直接使用的
ByteBufAllocator.DEFAULT.buffer(); 获取的是直接内存。
netty中的ByteBuf支持一种池化的管理,netty默认情况下获取的ByteBuf都是从池中获取的,如果不想要从池中获取需要在idea启动配置的地方加一个-Dio.netty.allocator.type=upooled参数
ByteBuf组成
刚开始,读写两个指针都在0的位置。
ByteBuf有读指针和写指针,不像nio中的ByteBuffer公用一个指针 然后还要进行读写模式切换。
Bytebuf如果容量不够了可以动态扩容,但最大容量不能超过int的最大值。
上图中的四块区域分为:废弃区、可读区、可写区、可扩容区
四个属性为:读指针,写指针,容量,最大容量
扩容规则
如果写入的数据没有超过512,则选择的是下一个16的整数倍,例如现在容量是12,进行一次扩容会变为16,如果又不够了就会变为32
而如果写入后的数据大小超过了512,则选择下一个2n。例如写入后的大小为513,库容后的大小就变为210=1024,因为2^9=512不够
但是扩容不能超过max capacity,否则会报错
写入
写入的常用方法如下:
以下的方法会改变写指针位置,还有一类以set开头的方法,也可以写数据,但是不会改变写指针的位置
读取
buffer.readByte() 每次读取一个字节
buffer.readInt() 每次读取一个整数,也就是四个字节
buffer.markReaderIndex() 为读指针做一个标记,配合下面的方法可以实现重复读取某个数
buffer.resetReaderIndex() 将读指针跳到上一个标记过的地方实现重复读取某个数。
除了上面一些了read开头的方法以外,还有一系列get开头的方法也可以读取数据,只不过get开头的方法不会改变读指针位置。相当于是按索引去获取。
内存回收
ByteBuf有几种实现方式,针对这几种ByteBuf实现的机制也不同
- UnpooleHeapByteBuf 使用的是jvm内存,只需要等待GC回收内存即可
- UnPooleDirectByteBuf 使用的是直接内存,需要特殊的方法来回收内存
- PooledByteBuf 和它的子类使用了池化机制,需要复杂的规则来回收内存
不同的ByteBuf的实现,内存回收也不一样,还好netty提供了统一的接口ReferenceCounted
接口,该接口提供了通用的方法来进行上面几种ByteBuf的内存回收,该接口的工作方式是采用引用计数的规则
- 每个ByteBuf 对象的初始计数为1
- 调用release() 方法计数减1,如果计数为0则ByteBuf的内存会被回收
- 调用retain()方法计数加1,表示调用者没用完之前,其它handler即使调用了release()也不会造成回收
- 当计数为0时,底层内存会被回收,这时即使ByteBuf对象还在,其各个方法均无法正常使用
谁应该来调用release()方法嘞?
应该由最后一个使用ByteBuf的入站Handler来调用release()方法,因为可能出现前面一个handler释放掉了ByteBuf,下一个handler就使用不了了。
如果入站Handler使用的ByteBuf最后传递给了tail,tail会自动释放掉ByteBuf,但是有可能前面的handler处理了数据,然后传递给下一个handler的不是ByteBuf而是处理后的数据,这样最后的tail由于接受的参数不是ByteBuf,所以也没办法释放。出站Handler最后传递给head也会自动自动释放ByteBuf。
头尾handler释放ByteBuf的源码分析
在TailContext类中的channelRead()
,然后再跟进方法,多点几层,然后会找到如下所示的方法。
零拷贝
应用的场景是我要分别处理一个大的ByteBuf中的几段数据,也就是将这一个ByteBuf拆分为几个小的ByteBuf。当然可以直接创建介个小的ByteBuf,然后进行赋值,但是这样有数据拷贝,有效率问题。而零拷贝就是直接将一个大的ByteBuf拆分为几个小的,这里只是逻辑上的拆分,他们共用同一块内存区域。各个小的ByteBuf有自己的读写指针。
其他几个不常用的方法
duplicate() 截取原始ByteBuf的所有内容,而slice()方法只是截取一部分。底层和原始ByteBuf也是使用的同一块内存,也有自己的读写指针。
copeXXXXX 与零拷贝对应的就是一系列以cope开头的方法,也是创建新的ByteBuf,只是会进行数据拷贝,读写都与原始ByteBuf无关
上面是将一个大的ByteBuf切分为几个小的ByteBuf,下面还有将几个小的ByteBuf零拷贝组合成一个大的ByteBuf。
也就是调用compositeBuffer()创建ByteBuf,然后调用addComponents()方法进行添加。
然后运行,发现数据并没有写入新的ByteBuf中,这是因为addComponents() 可变参数 或者是空参的这个方法默认不会去跳转写指针的位置。需要在该方法的参数1位置加一个boolean类型的参数true就可以了。
buffer.addComponents(true, buf1, buf2); 这个就表示写指针会自动增长。这样就可以正确的将两个ByteBuf组合到一起。
使用这个也需要注意release()的问题。
Unpooled是一个工具类,提供了非池化的 ByteBuf创建、组合、复制等操作。这里有一个关于零拷贝相关的方法
Unpooled.wrappedBuffer(ByteBuf buf...)方法,可以将多个Bytebuf组合成一个Bytebuf,底层使用的是compositeBuffer()方法
ByteBuf优势
- 池化,可以重用池中的Bytebuf
- 自动扩容,容量不够时会自动扩容,但是不会超过int的最大值
- 读写指针分离,避免了nio中的切换读写模式
- 支持链式调用
- 很多地方体现零拷贝,比如slice、duplicate、compositeBuffer、
一个小知识点:java Socket是全双工的,在任意时刻读写都可以同时进行,即使的阻塞io,读写也可以同时进行,只要分别采用两个线程分别处理读写即可。