Netty系列教程(六)Netty组件之ByteBuf

387 阅读7分钟

Netty的ByteBuf优势

NIO的ByteBuffer存在的问题:

  • ByteBuffer长度固定,不能动态扩容和收缩
  • ByteBuffer需要手动调用flip()方法进行读写模式切换
  • API功能有限,需要手动去实现

JDK NIO ByfteBuffer常见用法:

final ByteBuffer allocate = ByteBuffer.allocate(1024);
String value = "Hi Netty!";
//写
allocate.put(value.getBytes());
//读写切换
allocate.flip();
final byte[] bytes = new byte[allocate.remaining()];
//读
final ByteBuffer byteBuffer = allocate.get(bytes);
System.out.println(new String(bytes));

Netty的ByteBuf的优势:

  • Pooling池化技术,减少了内存复制和GC,提升了效率
  • 复合缓冲区类型,支持零复制
  • 不需要手动调用flip()方法切换读写模式
  • 可扩展性好
  • 可以自定义缓冲区类型
  • 读取和写入索引分开
  • 方法的链式调用
  • 可以进行计数,方便重复使用

其中池化技术是什么?为什么不需要手动调用flip()方法也能完成读写模式的切换,在下一个部分会详细展开。

ByteBuf的组成

ByteBuf通过三个重要的整数类型的属性有效地区分可读数据和可写数据的索引,使得读写直之间不会互相冲突,这三个属性被定义在类AbstractByteBuf中,如下图所示:

编辑切换为居中

ByteBuf的三个重要属性

三个字段对应的解释如下:

  • readerIndex(读指针)指示读取的起始位置,每读取一个字节,readerIndex自动加1。一旦readerIndex与writerIndex相等,则表示ByteBuf不可读了
  • writerIndex(写指针):指示写入的起始位置,每写一个字节,writerIndex自动增加1,一旦增加到与capacity()容量相等,则表示ByteBuf不可写了,capacity()返回的是ByteBuf中可以写入到容量,而它的值不一定是最大容量值
  • maxCapacity(最大容量):表示ByteBuf可扩容的最大容量,当向ByteBuf写数据的时候,如果容量不足,可以进行扩容。扩容最大限度由maxCapacity来设置,超过maxCapacity就会报错。

这三个属性的关系如下图所示:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

Netty提供了readerIndex和writerIndex这两个指针变量用于支持顺序读取和写入功能,分别用来标识读取索引指针和写入索引指针。readerIndex和writerIndex把ByteBuf缓冲区分隔成三个区域:

  • Discardable Bytes:读取过被丢弃的字节区域
  • Readable Bytes:可读取的字节区域
  • Writable Bytes :可写的字节区域

当调用read时,从readerIndex所在位置开始读取,readerIndex和writerIndex之间的空间为可读的字节缓冲区

  • 从writerIndex到capacity之间的叫可写字节缓冲区
  • 0到readerIndex之间的是已经读过的缓冲区,可以调用discardReadBytes操作来重用这部分的空间,以节约内存,防止ByteBuf的动态扩张

discardable bytes:可以实现内存的复用,但是缺点是会发生内存复制,频繁的调用会导致性能下降,使用的时候需要权衡性能和内存空间的平衡。

ByteBuf的动态扩容:

ByteBuffer在写入数据之前,为了避免空间不够导致内存溢出,需要对可用空间进行判断,这会导致大量的代码冗余,并且稍有不慎可能会出现问题。ByteBuf为了解决这个问题,对write()写入方法进行了封装,当空间不够时,会自动进行扩容。

其实现源码如下:

public CompositeByteBuf writeByte(int value) {
    this.ensureWritable0(1);
    this._setByte(this.writerIndex++, value);
    return this;
}

其中的 ensureWritable0 方法就是检查空间是否足够。

ByteBuf分类

从内存分配角度来看,ByteBuf类型有:

  • 堆内存(HeapByteBuf):特点是内存分配和回收速度快,可以交由JVM进行回收,缺点是如果进行Socke的I/O读写,需要额外进行一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能有一定的下降
  • 直接内存(DirectByteBuf):内存在堆外进行分配,它的内存分配和回收速度会慢一些,但是进行Scoket I/O读写时,减少了一内存复制,速度比堆内存快。
  • 多个缓冲区的组合表示(CompositeBuffer):方便一次操作多个缓冲实例

HeapByteBuf和DirectByteBuf这两种内存分配方式各有利弊,比较推荐的做法是在I/O通信线程的读写缓冲区使用直接内存,在业务消息的编解码模块使用对内存分配方式。它们都可以设置为池化或者非池化方式

从内存回收角度来看,可以将ByteBuf分为如下几类:

  • 基于对象池的ByteBuf:可以重用ByteBuf对象,自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存使用效率,降低由于高负载导致的频繁GC
  • 普通的ByteBuf

在服务端启动时候,可以设置缓冲区分配器是普通的缓冲区还是池化缓冲区:

//启动服务端引导
ServerBootstrap bootstrap = new ServerBootstrap();
//设置父通道的缓冲区分配器为池化的缓冲区分配器
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
//设置子通道的缓冲区分配器为池化的缓冲区分配器
bootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

为了快速创建ByteBuf,Netty提供了一个非常方便获取缓冲区的类-Unpooled,用它可以创建和使用非池化的缓冲区,使用示例如下:

//创建堆缓冲区
 final ByteBuf heapBuffer = Unpooled.buffer(8);
 //创建直接内存缓冲区
 final ByteBuf directBuffer = Unpooled.directBuffer(16);
 //创建复合缓冲区
 final CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();

ByteBuf的主要API功能介绍

可以将针对ByteBuf的操作主要分为容量获取、写入、读取、其他四大类

  • 获取容量:

  • capacity():ByteBuf缓冲区的容量,等于废弃字节数+ 可读字节数+可写字节数

  • maxCapacity():表示ByteBuf能够容纳的最大字节数,当向ByteBuf写入数据的时候,如果发现容量不够,则进行扩容,直到扩容到maxCapacity设定的上限

  • 写入:

  • isWritable():表示ByteBuf是否可写。如果writerIndex指针位置小于capacity()容量,则表示可写,否则不可写

  • writableBytes():取得可写的字节数,它的值等于容量capacity()-writerIndex

  • maxWritableBytes():取得最大的可写字节数,它的值等于最大容量maxCapacity减去writerIndex

  • writeBytes(byte[] src):把src字节数组中的数据全部写入ByteBuf中

  • writeTYPE(TYPE value):写入基础数据类型的数据。包括writeByte()、writeBoolean()、writeLong()等

  • markWriterIndex()与resetWriterIndex():前一个方法表示把当前的写指针writerIndex属性的值保存在markedWriterIndex标记属性中,后一个方法表示把之前保存的markedWriterIndex值恢复到写指针writerIndex属性中,其中markedWriterIndex相当于一个写指针的暂存属性

  • 读取

  • isReadable():返回ByteBuf是否可读。如果readerIndex指针的值小于writerIndex指针的值,则表示可读,否则为不可读

  • readableBytes():返回表示ByteBuf当前可读取的字节数,它的值等于writerIndex减去readerIndex

  • readBytes(byte[] dst):将数据从ByteBuf读取到dst目标字节数组中,这里dst字节数组的大小通常等于readableBytes()可读字节数。这个方法也是最为常用的方法之一

  • readTYPE():读取基础数据类型。可以读取八大基础数据类型:readByte()、readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble()

  • getTYPE():读取基础数据类型,并且不改变readerIndex读指针的值

  • 其他

  • clear():清除。clear并不会清空缓冲区内容本身,而是将位置指针重置到初始值,包括position、limit和mark。对于ByteBuf,它也是从来操作readerIndex和writerIndex,将他们还原为初始值,下图展示了在调用clear之后读写指针被重置的场景:

编辑切换为居中

添加图片注释,不超过 140 字(可选)

  • 查找元素:如果需要从byteBuf中查找某个元素,可以通过ByteBuf提供的API方法来实现,例如:indexOf()、bytesBefore()、forEachByte()等

  • 将ByteBuf转换为ByteBuffer:可以通过下面两个方法来实现转换:

  • ByteBuffer nioBuffer()

  • ByteBuffer nioBuffer(int index, int length):将当前ByteBuf从index开始,长度为length的内容转换为ByteBuffer

  • 随机读写(get和set):可以指定随机读写的位置