ByteBuf 是 Netty 的数据容器,所有网络通信中字节流的传输都是通过 ByteBuf 完成的。不同与 NIO 包中的 ByteBuffer 类,ByteBuf 是 Netty 自己实现的类,其使用相比于 ByteBuffer 更方便。

NIO 中的 ByteBuffer

我们先来介绍一下 NIO 中的 ByteBuffer,这样才能知道 ByteBuffer 在使用上有什么痛点微信截图_20211114005503.png
从上图可以得知,ByteBuffer 中有四个基本属性:

  • mark:为某个读取过的关键位置做标记,方便回退到该位置
  • position:当前 ByteBuffer 中下一个读取/写入数据的位置
  • limit:ByteBuffer 中有效数据长度
  • capacity:ByteBuffer 初始化时的容量

ByteBuffer使用上的缺陷

  1. ByteBuffer 长度固定

ByteBuffer 分配的长度是固定的,无法动态扩缩容,所以很难控制需要分配多大的容量。如果分配太大容量,容易造成内存浪费;如果分配太小,存放太大的数据会抛出 BufferOverflowException 异常。在使用 ByteBuffer 时,为了避免容量不足问题,你必须每次在存放数据的时候对容量大小做校验,如果超出 ByteBuffer 最大容量,那么需要重新开辟一个更大容量的 ByteBuffer,将已有的数据迁移过去。整个过程相对烦琐,对开发者而言是非常不友好的。

  1. 读写共用同一个指针

ByteBuffer 只能通过 position 获取当前可操作的位置,因为读写共用的 position 指针,所以需要频繁调用 flip、rewind 方法切换读写状态,开发者必须很小心处理 ByteBuffer 的数据读写,稍不留意就会出错。

如下,NIO 中通过 position 属性控制每次读取/写入Buffer数组的位置

  1. public static void main(String[] args) {
  2. IntBuffer buffer = IntBuffer.allocate(10);
  3. for(int i=0; i<7; i++){
  4. buffer.put(i);
  5. }
  6. //重置position
  7. buffer.flip();
  8. while(buffer.hasRemaining()){
  9. System.out.println(buffer.get());
  10. }
  11. }

往buffer中写入数据
微信截图_20210721220900.png
这时如果不调用 flip() 方法重置 position,则后续从 buffer 获取数据是从 position 开始往后获取,读写切换时要将 position 重置
微信截图_20210721221325.png

Netty 中的 ByteBuf

ByteBuffer 作为网络通信中高频使用的数据载体,其使用对于开发者来说非常不友好,Netty 重新实现了一个性能更高、易用性更强的 ByteBuf,相比于 ByteBuffer 它提供了很多非常酷的特性:

  • 容量可以按需动态扩展,类似于 StringBuffer;
  • 读写采用了不同的指针,读写模式可以随意切换,不需要调用 flip 方法;
  • 通过内置的复合缓冲类型可以实现零拷贝;
  • 支持引用计数;
  • 支持 ByteBuf 缓存池

微信截图_20211114123701.png
ByteBuf 主要由4部分组成:

  • 废弃字节:表示已经丢弃的无效字节。
  • 可读字节:表示 ByteBuf 中可以被读取的字节内容,可以通过 writeIndex - readerIndex 计算得出。从 ByteBuf 读取 N 个字节,readerIndex 就会自增 N,readerIndex 不会大于 writeIndex,当 readerIndex == writeIndex 时,表示 ByteBuf 已经不可读。
  • 可写字节:向 ByteBuf 中写入数据都会存储到可写字节区域。向 ByteBuf 写入 N 字节数据,writeIndex 就会自增 N,当 writeIndex 超过 capacity,表示 ByteBuf 容量不足,需要扩容。
  • 可扩容字节:表示 ByteBuf 最多还可以扩容多少字节,当 writeIndex 超过 capacity 时,会触发 ByteBuf 扩容,最多扩容到 maxCapacity 为止,超过 maxCapacity 再写入就会出错。

ByteBuf的分类

ByteBuf 有多种实现类,每种都有不同的特性,下图是 ByteBuf 的类图,可以划分为三个不同的维度:Heap/Direct、Pooled/Unpooled和Unsafe/非 Unsafe
微信截图_20211114125608.png
Heap/Direct 就是堆内和堆外内存。Heap 指的是在 JVM 堆内分配,底层依赖的是字节数据;Direct 则是堆外内存,不受 JVM 限制,分配方式依赖 JDK 底层的 ByteBuffer。

Pooled/Unpooled 表示池化还是非池化内存。Pooled 是从预先分配好的内存中取出,使用完可以放回 ByteBuf 内存池,等待下一次分配。而 Unpooled 是直接调用系统 API 去申请内存,确保能够被 JVM GC 管理回收。

Unsafe/非 Unsafe 的区别在于操作方式是否安全。 Unsafe 表示每次调用 JDK 的 Unsafe 对象操作物理内存,依赖 offset + index 的方式操作数据。非 Unsafe 则不需要依赖 JDK 的 Unsafe 对象,直接通过数组下标的方式操作数据。

ByteBuf 引用计数

ByteBuf 是基于引用计数设计的,它实现了 ReferenceCounted 接口,ByteBuf 的生命周期是由引用计数所管理。只要引用计数大于 0,表示 ByteBuf 还在被使用;当 ByteBuf 不再被其他对象所引用时,引用计数为 0,那么代表该对象可以被释放,可以将其放入到ByteBuf 缓存池中。

当新创建一个 ByteBuf 对象时,它的初始引用计数为 1。当调用 retain() 方法后,引用计数加1;当 ByteBuf 调用 release() 后,引用计数减 1。所以不要误以为调用了 release() 就会保证 ByteBuf 对象一定会被回收。你可以结合以下的代码示例做验证:

  1. ByteBuf buffer = ctx.alloc().directbuffer();
  2. assert buffer.refCnt() == 1;
  3. buffer.release();
  4. assert buffer.refCnt() == 0;

引用计数对于 Netty 设计缓存池化有非常大的帮助,当引用计数为 0,该 ByteBuf 可以被放入到对象缓存池中,避免每次使用 ByteBuf 都重复创建,对于实现高性能的内存管理有着很大的意义。

此外 Netty 可以利用引用计数的特点实现内存泄漏检测工具。JVM 并不知道 Netty 的引用计数是如何实现的,当 ByteBuf 对象不可达时,一样会被 GC 回收掉,但是如果此时 ByteBuf 的引用计数不为 0,那么该对象就不会释放或者被放入对象池,从而发生了内存泄漏。Netty 会对分配的 ByteBuf 进行抽样分析,检测 ByteBuf 是否已经不可达且引用计数大于 0,判定内存泄漏的位置并输出到日志中,你需要关注日志中 LEAK 关键字。

ByteBuf读写指针操作

在 ByteBuf 中维护了两个不同的索引:一个用于读取,一个用于写入。

  1. public abstract class AbstractByteBuf extends ByteBuf {
  2. .....
  3. int readerIndex;
  4. int writerIndex;
  5. }

当你从 ByteBuf 读取时, 它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的 writerIndex 也会被递增

下面代码中,创建一个长度为10的ByteBuf数组,capacity = 10

  1. public static void main(String[] args) {
  2. // 创建byteBuf对象,该对象内部包含一个字节数组byte[10]
  3. ByteBuf byteBuf = Unpooled.buffer(10);
  4. System.out.println("byteBuf=" + byteBuf);
  5. for (int i = 0; i < 8; i++) {
  6. byteBuf.writeByte(i);
  7. }
  8. System.out.println("byteBuf=" + byteBuf);
  9. for (int i = 0; i < 5; i++) {
  10. System.out.println(byteBuf.readByte());
  11. }
  12. System.out.println("byteBuf=" + byteBuf);
  13. for (int i = 0; i < 5; i++) {
  14. System.out.println(byteBuf.getByte(i));
  15. }
  16. System.out.println("byteBuf=" + byteBuf);
  17. }

首先往ByteBuf中写入数据写入数据,写入后 writerIndex=8
微信截图_20210721211741.png
接着往ByteBuf中读取数据,经过一轮读取,readerIndex = 5
微信截图_20210721212009.png
此时通过readerindex和writerIndex和capacity,将buffer分成三个区域
[0, readerIndex):表示已读区域
[readerIndex, writerIndex):表示可读取区域
[writerIndex, capacity):表示可写区域

如果读取[writerIndex, capacity) 区间的数据,会直接报错
微信截图_20210721214134.png

调用clear()方法可以重置读、写索引

  1. public ByteBuf clear() {
  2. this.readerIndex = this.writerIndex = 0;
  3. return this;
  4. }

ByteBuf 中有如下比较常用的API:

  • isReadable()

isReadable() 用于判断 ByteBuf 是否可读,如果 writerIndex 大于 readerIndex,那么 ByteBuf 是可读的,否则是不可读状态。

  • readableBytes()

readableBytes() 可以获取 ByteBuf 当前可读取的字节数,可以通过 writerIndex - readerIndex 计算得到。

  • readBytes(byte[] dst)、writeBytes(byte[] src)

readBytes() 和 writeBytes() 是两个最为常用的方法。readBytes() 是将 ByteBuf 的数据读取相应的字节到字节数组 dst 中,readBytes() 经常结合 readableBytes() 一起使用,dst 字节数组的大小通常等于 readableBytes() 的大小。writeBytes() 则是将 src 数组中的数据写入到 ByteBuf 中。

  • readByte()、writeByte(int value)

readByte() 是从 ByteBuf 中读取一个字节,相应的 readerIndex + 1;同理 writeByte 是向 ByteBuf 写入一个字节,相应的 writerIndex + 1。类似的 Netty 提供了 8 种基础数据类型的读取和写入,例如 readChar()、readShort()、readInt()、readLong()、writeChar()、writeShort()、writeInt()、writeLong() 等。

  • getByte(int index)、setByte(int index, int value)

与 readByte() 和 writeByte() 相对应的还有 getByte() 和 setByte(),get/set 系列方法也提供了 8 种基础类型的读写,那么这两个系列的方法有什么区别呢?read/write 方法在读写时会改变readerIndex 和 writerIndex 指针,而 get/set 方法则不会改变指针位置

  • release()、retain()

之前已经介绍了引用计数的基本概念,每调用一次 release() 引用计数减 1,每调用一次 retain() 引用计数加 1。

  • slice()、duplicate()

slice() 等同于 slice(buffer.readerIndex(), buffer.readableBytes()),默认截取 readerIndex 到 writerIndex 之间的数据,最大容量 maxCapacity 为原始 ByteBuf 的可读取字节数,底层分配的内存、引用计数都与原始的 ByteBuf 共享。

duplicate() 与 slice() 不同的是,duplicate()截取的是整个原始 ByteBuf 信息,底层分配的内存、引用计数也是共享的。如果向 duplicate() 分配出来的 ByteBuf 写入数据,那么都会影响到原始的 ByteBuf 底层数据。

  • copy()

copy() 会从原始的 ByteBuf 中拷贝所有信息,所有数据都是独立的,向 copy() 分配的 ByteBuf 中写数据不会影响原始的 ByteBuf。

通过一下例子可以加深对 ByteBuf 的使用:

  1. public class ByteBufTest {
  2. public static void main(String[] args) {
  3. ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(6, 10);
  4. printByteBufInfo("ByteBufAllocator.buffer(5, 10)", buffer);
  5. buffer.writeBytes(new byte[]{1, 2});
  6. printByteBufInfo("write 2 Bytes", buffer);
  7. buffer.writeInt(100);
  8. printByteBufInfo("write Int 100", buffer);
  9. buffer.writeBytes(new byte[]{3, 4, 5});
  10. printByteBufInfo("write 3 Bytes", buffer);
  11. byte[] read = new byte[buffer.readableBytes()];
  12. buffer.readBytes(read);
  13. printByteBufInfo("readBytes(" + buffer.readableBytes() + ")", buffer);
  14. printByteBufInfo("BeforeGetAndSet", buffer);
  15. System.out.println("getInt(2): " + buffer.getInt(2));
  16. buffer.setByte(1, 0);
  17. System.out.println("getByte(1): " + buffer.getByte(1));
  18. printByteBufInfo("AfterGetAndSet", buffer);
  19. }
  20. private static void printByteBufInfo(String step, ByteBuf buffer) {
  21. System.out.println("------" + step + "-----");
  22. System.out.println("readerIndex(): " + buffer.readerIndex());
  23. System.out.println("writerIndex(): " + buffer.writerIndex());
  24. System.out.println("isReadable(): " + buffer.isReadable());
  25. System.out.println("isWritable(): " + buffer.isWritable());
  26. System.out.println("readableBytes(): " + buffer.readableBytes());
  27. System.out.println("writableBytes(): " + buffer.writableBytes());
  28. System.out.println("maxWritableBytes(): " + buffer.maxWritableBytes());
  29. System.out.println("capacity(): " + buffer.capacity());
  30. System.out.println("maxCapacity(): " + buffer.maxCapacity());
  31. }
  32. }