你了解Netty中ByteBuf类中的三种模式吗

881 阅读5分钟

这是我参与8月更文挑战的第23天,活动详情查看:8月更文挑战

  网络数据的基本单位总是字节。Java NIO提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁锁。
  Netty的ByteBuffer替代器是ByteBuf,一个强大的实现,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。

一、ByteBuf的API

  Netty的数据处理API通过两个组件暴露——abstract class ByteBuf和interface ByteBufHolder。

以下是ByteBuf API的优点:
1、它可以被用户自定义的缓冲区类型扩展;
2、通过内置的复合缓冲工类型实现了透明的零拷贝;
3、容量可以按需增长(类似于JDK的StringBuilder);
4、在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;
5、读和写使用了不同的索引
6、支持方法的链式调用
7、支持引用计数
8、支持池化

二、ByteBuf类——Netty的数据容器

2.1 它是如何工作的

  ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,它的readerIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writerIndex也会被递增。下图展示了一个空ByteBuf的布局结构和状态。

image.png

  名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开关的操作则不会。可以指定ByteBuf的最大容量。(默认的限制是Integer.MAX_VALUE)。

2.2 ByteBuf的使用模式

2.2.1 堆缓冲区

  最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,如下代码所示,非常适合于有遗留的数据需要处理的情况。

ByteBuf heapBuf = Unpooled.buffer();
// 检查ByteBuf是否有一个支撑数组
if (heapBuf.hasArray()) {
    // 如果有,则获取对该数组的引用
    byte[] array = heapBuf.array();
    // 计算第一个字节的偏移量
    int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
    // 获得可读字节数
    int length = heapBuf.readableBytes();
    // 使用数组、偏移量和长度作为参数调用你的方法
    handleArray(array, offset, length);
}

2.2.2 直接缓冲区

  直接缓冲区是另外一种ByteBuf模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的——NIO在JDK1.4 中引入的ByteBuffer类允许JVM实现通过本地调用来分配内存。这主要是为了避免在每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。

  ByteBuffer的Javadoc明确指出:“直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外”。这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中。

  直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另个一个缺点:因为数据不是在堆上,所以你不得不进行一次复制。如以下代码清单:

ByteBuf directBuf = Unpooled.directBuffer();
// 检查ByteBuf是否由数组支撑。如果不是,则这是一个直接缓冲区
if (!directBuf.hasArray()) {
    // 获取可读字节数
    int length = directBuf.readableBytes();
    // 分配一个新的数组来保存具有该长度的字节数据
    byte[] array = new byte[length];
    // 将字节复制到数组
    directBuf.getBytes(directBuf.readerIndex(), array);
    // 使用数组、偏移量和长度作为参数调用你的方法
    handleArray(array, 0, length);
}

2.2.3 复合缓冲区

  复合缓冲区,它为多个ByteBuf提供一个聚合视图。在这里你可以根据需要添加或者删除ByteBuf实例,这是一个JDK的ByteBuffer实现完全缺失的特性。

  Netty通过一个ByteBuf子类——CompositeByteBuf——实现了这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。

  为了举例说明,让我们考虑一下一个帖两部分——头部和主体——组成的将通过HTTP协议传输的消息。这两部分由应用程序的不同模块产生,将会在消息被发送的时候组装。该应用程序可以选择为多个消息重用相同的消息主体。当这种情况发生时,对于每个消息都将会创建一个新的头部。

  因为我们不想为每个消息都重新分配这两个缓冲区,所以使用CompositeByteBuf是一个完美的选择。它在消除了没必要的复制的同时,暴露了通用的ByteBuf API。下图展示了生成的消息布局。

image.png

  以下代码清单展示了如何通过使用JDK的ByteBuffer来实现这一需求。创建了一个包含两个ByteBuffer的数组用来保存这些消息组件,同时创建了第三个ByteBuffer用来保存所有这些数据的副本。

ByteBuffer header = ByteBuffer.allocate(3);
ByteBuffer body = ByteBuffer.allocate(6);
ByteBuffer[] message = new ByteBuffer[] {header, body};
ByteBuffer message2 = ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();

  分配和复制操作,以及伴随着对数组管理的需要,使得这个版本的实现效率低下而且笨拙。以下代码清单展示了一个使用了CompositeByteBuf的版本。

CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = Unpooled.buffer();
ByteBuf bodyBuf = Unpooled.buffer();
// 将ByteBuf实例追加到CompositeByteBuf
messageBuf.addComponents(headerBuf, bodyBuf);
// .......
// 删除位于索引位置为0(第一个组件)的ByteBuf
messageBuf.removeComponent(0);
// 循环遍历所有的ByteBuf实例
for (ByteBuf buf : messageBuf) {
    System.out.println(buf.toString());
}

  CompositeByteBuf可能不支持访问其支撑数组,因些访问CompositeByteBuf中的数据类似于(访问)直接缓冲区的模式。