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

ByteBuf底层结构分析

针对I/O操作的读与写,分别维护对应的readerIndex与writerIndex两个指针,两者是互补影响的,读操作会使得readerIndex移动,写操作会使得writerIndex移动,整体的容量大小由capacity来表示,初始状态readerIndex与writerIndex都是0,而capacity是申请的容器大小。

+-------------------+------------------+------------------+
      | discardable bytes |  readable bytes  |  writable bytes  |
      |                   |     (CONTENT)    |                  |
      +-------------------+------------------+------------------+
      |                   |                  |                  |
      0      <=      readerIndex   <=   writerIndex    <=    capacity

该图能够反映互相之间的关系:

  • Readable bytes:这部分是存储的实际内容,可以用来被读取,被读取之后readerIndex将会相应地增加
// Iterates the readable bytes of a buffer.
{@link ByteBuf} buffer = ...;
while (buffer.isReadable()) {
  System.out.println(buffer.readByte());
}
  • Writable bytes:这部分是需要被填充的部分,任何与写相关的操作都会使writerIndex相应地增加
// Fills the writable bytes of a buffer with random integers.
ByteBuf buffer = ...;
while (buffer.maxWritableBytes() >= 4) {
   buffer.writeInt(random.nextInt());
}
  • Discardable bytes:这部分包含已经被读操作读取过的内容,能够通过调用discardReadBytes()方法使这部分被抛弃,也就会达到以下状态:
+------------------+--------------------------------------+
    |  readable bytes  |    writable bytes (got more space)   |
    +------------------+--------------------------------------+
    |                  |                                      |
readerIndex (0) <= writerIndex (decreased)        <=        capacity

这种抛弃操作可能会使Writable bytes这部分被移动,但是更推荐使用以下clear操作,仅仅将readerIndex与writerIndex进行处理

  • clear操作:将readerIndex和writerIndex都设置成0,并不会实际清楚ByteBuf中的内容,调用该方法之后将会达到以下状态:
+---------------------------------------------------------+
    |             writable bytes (got more space)             |
    +---------------------------------------------------------+
    |                                                         |
    0 = readerIndex = writerIndex            <=            capacity

Netty ByteBuf所提供的3种缓冲区类型

  • Heap buffer
    堆缓冲区是常用的一种类型,ByteBuf将数据存储到JVM的堆空间中,并且将实际的数据存放到byte array中来实现
  • 优点:由于数据是存储在JVM堆中,因此可以快速的创建与快速的释放,并且它提供了直接访问内部字节数组的方法
  • 缺点:每次读写数据时,都需要先将数据复制到直接缓冲区中再进行网路传输
  • Direct buffer
    直接缓冲区,在堆外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,因为它是由操作系统在本地内存进行的数据分配
  • 优点:在使用Socket进行数据传递时,性能非常好,因为数据直接位于操作系统的本地内存中,所以不需要从JVM将数据复制到直接缓冲区中
  • 缺点:因为Direct Buffer是直接在操作系统内存中的,所以内存空间的分配与释放要比堆空间更加复杂,而且速度要慢一些

直接缓冲区并不支持通过字节数组的方式来访问数据。对于后端业务的消息编解码来说,推荐使用HeapByteBuf;对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf

  • Composite Buffer
    复合缓冲区,可以拥有以上两种的缓冲区,通过一种视图来操作底层持有的多种类型Buffer

按需分配:ByteBufAllocator 接口

为了降低分配和释放内存的开销,Netty 通过 interface ByteBufAllocator 实现了 (ByteBuf 的)池化,它可以用来分配我们所描述过的任意类型的 ByteBuf 实例。可以通过 Channel(每个都可以有一个不同的 ByteBufAllocator 实例)或者绑定到 ChannelHandler 的 ChannelHandlerContext 获取一个到 ByteBufAllocator 的引用。Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。虽然Netty默认使用了PooledByteBufAllocator,但这可以很容易地通过ChannelConfig API或者在引导你的应用程序时指定一个不同的分配器来更改

Unpooled 缓冲区

可能某些情况下,你未能获取一个到 ByteBufAllocator 的引用。对于这种情况,Netty 提 供了一个简单的称为 Unpooled 的工具类,它提供了静态的辅助方法来创建未池化的 ByteBuf 实例。Unpooled 类还使得 ByteBuf 同样可用于那些并不需要 Netty 的其他组件的非网络项目, 使得其能得益于高性能的可扩展的缓冲区 API。

JDK的ByteBuffer与Netty的ByteBuf之间的差异对比

  • Netty的ByteBuf采用来读写索引分离的策略(readerIndex与writerIndex),一个初始化(尚未有任何数据)的ByteBuf所维护的readerIndex与writerIndex值都是0
  • 当读索引与写索引处于同一个位置时,如果我们继续读取,那么就会抛出IndexOutOfBoundsException
  • 对于ByteBuf的任何读写操作都会分别单独维护读索引与写索引。maxCapacity最大容量默认的限制就是Integer.MAX_VALUE

JDK的ByteBuffer的缺点

  • final byte[] hb;这是JDK的ByteBuffer对象中用于存储数据的对象申明。可以看到,其字节数组被申明为final的,也就是长度是固定不变的。一旦分配好后不恩能够动态扩容与收缩;而且当待存储的数据字节很大的就很可能会出现IndexOutOfBoundsExceptions。如果要预防这个异常,那就需要在存储之前完全确定好待存储的字节大小。如果ByteBuffer的空间不足,我们只有一种解决方案:创建一个全新的ByteBuffer对象,然后再将之前的ByteBuffer中的数据复制过去,这一切的操作都需要由开发者自己来手动完成
  • ByteBuffer只使用一个position指针来标识位置信息,在进行读写切换时就需要调用flip方法或是rewind方法,使用起来很不方便

Netty的ByteBuf的优点

  • 存储字节的数组是动态的,其最大值默认是Integer.MAX_VALUE。这里的动态性是体现在write方法中的,write方法在执行时会判断buffer容量,如果不足则自动扩容
  • ByteBuf的读写索引是完全分开的,使用起来就很方便