Netty-8 ByteBuf及相关辅助类

本文详细探讨了Netty中的ByteBuf,对比了它与JDK的ByteBuffer。ByteBuf通过双指针解决了ByteBuffer的读写复杂性,并支持动态扩展。文章介绍了ByteBuf的读写操作、扩容机制、功能特性以及其丰富的继承体系,包括堆内存和直接内存的实现。此外,还提及了辅助类如ByteBufAllocator和ByteBufUtil的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、ByteBuffer

当我们进行数据传输的时候,往往用到数据缓冲区,常用的缓冲区是JDK NIO提供的java.nio.Buffer。
在NIO中,7种基本数据类型(boolean除外)都有自己的缓冲区实现。对于NIO编程而言,主要使用的是ByteBuffer。
然而,ByteBuffer有自己的一些缺陷:

  1. 长度固定。一旦分配完成,容量不能进行扩展和收缩;

  2. 只有一个标识位置的指针position,读写的时候需要手动调用flip()和rewind()等方法;

  3. API功能有限。一些高级和使用的特性都不支持;

    为了弥补这些缺陷,Netty提供了自己的缓冲区ByteBuffer类ByteBuf。

二、ByteBuffer工作原理

JDK ByteBuffer由于只有一个位置指针用来处理读写操作,所以每次读写之后都需要额外调用flip()方法和clear()等方法用来重置指针,如以下使用场景:

    String str = "Hello,World";
    ByteBuffer buffer = ByteBuffer.allocate(50);
    buffer.put(str.getBytes());
    buffer.flip();
    byte[] newArray = new byte[buffer.remaining()];
    buffer.get(newArray);
    System.out.println(new String(newArray));

这里如果没有调用flip()方法,将得不到正确的结果,原因如下:

  1. 在调用flip方法之前,数组效果如下:
    在这里插入图片描述
    读取到的是position到limit之间的数据,那么将什么也读取不到
  2. 当我们调用了flip()之后,数据的标识会发生变化。
    flip()方法:
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

所以调用完了之后,数组情况如下:
在这里插入图片描述
这时候再读取position和limit之间的内容,就能正确读取了。

三、ByteBuf工作原理

首先需要说明的是,ByteBuf是通过以下两种方式进行实现的:

  1. 参考JDK ByteBuffer的实现,增加额外的功能,解决原ByteBuffer的缺点;
  2. 聚合JDK ByteBuffer,通过Facade模式对其进行包装。
    所以具体的ByteBuf的实现,也有多种。如直接基于byte数组的,也有继承自java.nio.ByteBuffer的

(一)ByteBuf的改进

针对ByteBuffer单个指针维护的复杂问题,ByteBuf进行了如下优化:

  1. ByteBuf通过两个指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex;
  2. 开始的时候,readerIndex和writerIndex的值都是0;
  3. 随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。
  4. 在读取之后,0~readerIndex之间的数据会被视为discard的,调用discardReadBytes会释放空间,他的作用类似于ByteBuffer的compact方法。
  5. readerIndex和writerIndex之间的数据是可以读取的,等价于ByteBuffer的position和limit之间的数据;
  6. weiterIndex和capacity之间的空间是可写的,等价于ByteBuffer的limit和capacity之间的可用空间;
  7. 由于读操作不会改变writerIndex,写操作不会改变readerIndex, 因此读写操作之间不需要再调整指针,大大的简化了读写操作,避免了漏调flip()方法引起的问题;
  8. writableBytes()方法会返回可以写入的空间大小,readableBytes()方法

(二)ByteBuf的操作指针变化

  1. 初始分配的ByteBuf
    在这里插入图片描述2. 写入N个字节后的ByteBuf
    在这里插入图片描述
    3.读取M(M<N)个字节后的ByteBuf
    在这里插入图片描述
    4.调用discardReadBytes之后
    discartReadBytes之后
    当调用了clear()方法之后,会再次回到初始状态。

(三)ByteBuf的动态扩展

1. 扩容过程分析

ByteBuffer一已经分配,大小就固定了,不能动态的扩展和收缩,ByteBuf却可以,那么他是如何实现的呢?
跟踪AbstractByteBuf类的writeBytes方法(writeByte、writeDouble等等原理类似)如下:

  public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
    this.ensureWritable(length); //确保能够写入
    this.setBytes(this.writerIndex, src, srcIndex, length);
    this.writerIndex += length;
    return this;
  }

  public ByteBuf writeBytes(byte[] src) {
    this.writeBytes((byte[])src, 0, src.length);
    return this;
  }

这里通过ensureWritable方法来保证字节数组能够写入,内容如下:

   public ByteBuf ensureWritable(int minWritableBytes) {
    if (minWritableBytes < 0) {
      throw new IllegalArgumentException(String.format("minWritableBytes: %d (expected: >= 0)", minWritableBytes));
    } else {
      this.ensureWritable0(minWritableBytes); //确保容量ok
      return this;
    }
  }

再次通过ensureWritable0来确保数组容量是够的

  final void ensureWritable0(int minWritableBytes) {
    this.ensureAccessible();
    if (minWritableBytes > this.writableBytes()) {
      if (minWritableBytes > this.maxCapacity - this.writerIndex) {
        throw new IndexOutOfBoundsException(String.format("writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", this.writerIndex, minWritableBytes, this.maxCapacity, this));
      } else {
        int newCapacity = this.alloc().calculateNewCapacity(this.writerIndex + minWritableBytes, this.maxCapacity); //计算出新的数组容量
        this.capacity(newCapacity); //进行扩容
      }
    }
  }

调用了capacity方法进行了扩容

public ByteBuf capacity(int newCapacity) {
    this.checkNewCapacity(newCapacity);
    int oldCapacity = this.array.length;
    byte[] oldArray = this.array;
    byte[] newArray;
    if (newCapacity > oldCapacity) { //扩容
      newArray = this.allocateArray(newCapacity); //根据新的容量创建出一个空的数组
      System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); //把旧数组元素拷贝到新数组
      this.setArray(newArray); //设置存储数据的数组为新创建的数组
      this.freeArray(oldArray); //释放旧的数组
    } else if (newCapacity < oldCapacity) { // 收缩
      newArray = this.allocateArray(newCapacity);
      int readerIndex = this.readerIndex();
      if (readerIndex < newCapacity) {
        int writerIndex = this.writerIndex();
        if (writerIndex > newCapacity) {
          writerIndex = newCapacity;
          this.writerIndex(newCapacity);
        }
        System.arraycopy(oldArray, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
      } else {
        this.setIndex(newCapacity, newCapacity);
      }
      this.setArray(newArray);
      this.freeArray(oldArray);
    }
    return this;
  }

2. 扩容机制

在io.netty.buffer.AbstractByteBuf#ensureWritable0方法中,有这样一句代码:int newCapacity = this.alloc().calculateNewCapacity(this.writerIndex + minWritableBytes, this.maxCapacity);
这行代码是用来计算新的数组容量的,跟踪后是下面的方法:
io.netty.buffer.AbstractByteBufAllocator#calculateNewCapacity,重要的实现为:

 public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
    if (minNewCapacity < 0) {
      throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");
    } else if (minNewCapacity > maxCapacity) {
      throw new IllegalArgumentException(String.format("minNewCapacity: %d (expected: not greater than maxCapacity(%d)", minNewCapacity, maxCapacity));
    } else {
      int threshold = 4194304;
      if (minNewCapacity == 4194304) {
        return 4194304;
      } else {
        int newCapacity;
        if (minNewCapacity > 4194304) {
          newCapacity = minNewCapacity / 4194304 * 4194304;
          if (newCapacity > maxCapacity - 4194304) {
            newCapacity = maxCapacity;
          } else {
            newCapacity += 4194304;  // ②
          }

          return newCapacity;
        } else {
          for(newCapacity = 64; newCapacity < minNewCapacity; newCapacity <<= 1) {  // ①
          }

          return Math.min(newCapacity, maxCapacity);
        }
      }
    }
  }

这里的minNewCapacity=writerIndex+要写入的数组的长度
这行代码指明了如何扩容:

  1. ByteBuf的容量有一个阈值4194304(2 22);
  2. 当容量低于这个阈值的时候,扩容机制如下:
  • 初始扩容的newCapacity为64;
  • 如果minNewCapacity>64,那么就对newCapacity进行*2操作,直到大于minNewCapacity为止。
  1. 当高于这个阈值的时候,则每次递增这个阈值,防止指数递增造成的过多内存浪费

四、ByteBuf功能介绍

这里简单的对ByteBuf的相关操作进行介绍,但是具体的还是要参看api

  1. 顺序读操作,有很多针对不同数据类型的操作,如readByte,readShort等操作,会更改readerIndex
    在这里插入图片描述
  2. 顺序写操作,也有很多针对不同数据类型的写操作,如writeByte,writerInt等操作,会更改writerIndex
    在这里插入图片描述
  3. 回滚操作Mark和rest
    在对缓冲区进行读写的时候,由于某些原因,可能需要对之前的操作进行回滚(如读取多次),可以通过mark和rest对指针进行修改;
    ①. 在调用mark操作(markReaderIndex和markWriterIndex)的时候,会把指针记录下来,如:
  public ByteBuf markReaderIndex() {
    this.markedReaderIndex = this.readerIndex;
    return this;
  }

②. 在调用rest操作之后,相应的指针会进行重置,如:

  public ByteBuf resetReaderIndex() {
    this.readerIndex(this.markedReaderIndex);
    return this;
  }
 
  public ByteBuf readerIndex(int readerIndex) {
    if (readerIndex >= 0 && readerIndex <= this.writerIndex) {
      this.readerIndex = readerIndex;
      return this;
    } else {
      throw new IndexOutOfBoundsException(String.format("readerIndex: %d (expected: 0 <= readerIndex <= writerIndex(%d))", readerIndex, this.writerIndex));
    }
  }
  1. 跳过指定的字节数skipBytes(int length)方法;’
  2. 重用缓冲区:discardReadBytes()方法和discardSomeReadBytes()方法;
  3. 查找操作。有大量的用于查找的方法,如:forEachByte等等
  4. 拷贝或者截取出新的数组,如:copy(),slice()等;
  5. 转换标准的ByteBuffer:nioBuffer();
  6. 判断是否使用的是直接内存:isDirect()
  7. 判断是否是用byte数组实现的:hasArray()
  8. 如果是数组使用的,可以使用array()方法获取里面的字节

五、ByteBuf继承体系

(一)总体概览

在这里插入图片描述

  1. 内存分配的角度看,ByteBuf被分为两类:
  • 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收快,可以被JVM自动回收;缺点是在进行Socket的I/O操作的时候,需要进行一次复制,将堆内存对应的缓冲区数组复制到内核Channel中,性能有一定成都的下降。
  • 直接内存(DirectByteBuf)字节缓冲区:非堆内存,也就是直接内存,缺点是分配和回收相对较慢,但是操作Channel的时候,可直接操作,减少了内存复制,所以效率较高;
    经验表明:在I/O通讯线程的读写缓冲区使用DirectByteBuf,后端业务的编解码模块使用HeapByteBuf,这种组合可能是性能最优。
  1. 从内存回收角度,也可以被分为两类:
  • 基于对象池的ByteBuf
  • 普通的ByteBuf
    区别在于基于对象池的ByteBuf可以重用同一个ByteBuf对象,它自己维护了一个内存池,可以循环的利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC,但是维护和管理也更加复杂。

(二)AbstractByteBuf

1. 成员变量

  • int readerIndex: 读索引,读取操作的时候,会修改它的值;
  • int writerIndex:写索引,写操作的时候,会修改他的值;
  • private int markedReaderIndex:用来记录当前被读取到的索引位置,后续可以进行回滚;
  • private int markedWriterIndex:用来记录当前写入到的索引位置,后续可以进行回滚;
  • private int maxCapacity:最大容量
  • static final ResourceLeakDetector<ByteBuf> leakDetector :用于检查对象是否泄漏
    注意:AbstractByteBuf里面并没有定义ByteBuf的缓冲区实现,例如byte数组或者是DirectByteBuffer(java.nio.ByteBuffer),因为他也不知道子类是基于堆内存还是基于直接内存。

2. 成员方法

其实就和前面对ByteBuf功能介绍的一样,主要包括以下几种类型的方法,就不做一一介绍了:

(三)AbstractReferenceCountedByteBuf

1. 成员变量

private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt")
是一个AtomicIntegerFieldUpdater类型的变量,通过原子方式对成员变量进行更新等操作,以实现线程安全,消除锁。
private volatile int refCnt:用于跟踪对象的引用次数。

(四)UnpooledHeapByteBuf

基于堆内存进行内存分配的字节缓冲区,没有基于对象池实现,每次I/O操作都会创建一个,频繁的进行内存的申请和回收;

1. 成员变量

private final ByteBufAllocator alloc:用于内存分配
byte[] array:存放数据的缓冲区
private ByteBuffer tmpNioBuf:用来实现到java.nio.ByteBuffer的转换

(五)PooledByteBuf

内存对象池ByteBuf,可以减少ByteBuf申请空间和释放空间的性能消耗。

(六)PooledDirectByteBuf 和 UnpooledDirectByteBuf

一个是基于内存池的,一个不是基于内存池的,唯一的区别在于缓冲区的分配和销毁不同。

六、ByteBuf辅助类

(一)ByteBufHolder

我们经常遇到需要另外存储除有效的实际数据各种属性值。 HTTP 响应是一个很好的例子;与内容一起的字节的还有状态码, cookies,等。
Netty 提供 ByteBufHolder 处理这种常见的情况。 ByteBufHolder 还提供对于 Netty 的高级功能,如缓冲池,其中保存实际数据的 ByteBuf可以从池中借用,如果需要还可以自动释放。

  • data():获取所携带的数据
  • copy():拷贝数据

(二)ByteBufAllocator

字节缓冲区分配区,按照Netty的缓冲区实现不同,有两种不同的分配器:基于内存池的和普通的。
在这里插入图片描述
提供了很多用来分配ByteBuf的方法:
在这里插入图片描述

(三)CompositeByteBuf

可以将多个ByteBuf组装到一起,形成一个统一的视图。说白了,可以理解为一个把多个ByteBuf封装在一起的数组。

(四)ByteBufUtil

ByteBufUtil提供了一系列的方法用来操作ByteBuf,重要的方法如下:

  • encodeString(ByteBufAllocator alloc, CharBuffer src, Charset charset):对需要编码的字符串src按照指定的字符集charset进行编码,利用指定的ByteBufAllocator生成一个ByteBuf,如:
ByteBuf byteBuf = ByteBufUtil
        .encodeString(UnpooledByteBufAllocator.DEFAULT, CharBuffer.wrap("你好啊"), Charset.forName("GBK"));
  • String hexDump(ByteBuf buffer) : 可以将ByteBuf转换成16进制的字符串
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值