netty bytebuf初探

ByteBuffer是Java NIO库中提供的基本类. 对于NIO编程是必不可少的类. 但是, 如果使用ByteBuffer不得不面对以下缺点:

  • 空间定长, ByteBuffer通过静态方法ByteBuffer.allocate()ByteBuffer.allocateDirect()创建, Buffer的尺寸是调用这两个方法的时候就确定了的.
  • ByteBuffer只有一个指针. 如果用一个ByteBuffer存储数据, 再传给另一个方法读取, 那么你需要将ByteBuffer的指针调整到要读的数据的起始位置再读. 在这一点上通常容易引起混乱. 加大了编码的复杂度.

Netty的初衷是为Java NIO编程而提供的便捷的解决方案. 但它自然还是要在基础API上进行构建, 这自然还是要使用到ByteBuffer. 在如何使用ByteBuffer这一点上还是有些不错的设计和技巧的.

ByteBuffer示例

1
2
3
4
ByteBuffer allocate = ByteBuffer.allcate(4);
allocate.putInt(4);
allcate.flip();
allocate.getInt();

这里可以看到ByteBuffer的单指针的问题, 如果忘记进行flip, 那后面读取整数就会引发越界了. 在边读边写的情况下, 来回调整指针又容易引发一些奇怪的问题.

##ByteBuf API
ByteBuf针对以上一些问题和Netty自身需求, 对ByteBuffer进行了许多改进.

  1. 支持基本数据类型
    同ByteBuffer一样, 实现了基本数据类型的读取, 同时还能支持了读取unsigned数据类型.
  2. 读写操作
    read/write系列方法
    ByteBuffer采用双指针的方式, 简化了读取. 同readIndex指向读取的起点, writeIndex指向写操作的起点. readIndex不会超过writeIndex. readIndex之前的部分会被变成discard的, 可以用discardReadBytes方法释放这部分空间. 类似于ByteBuffercompact操作. 这样, ByteBuf的读写操作就不会互相影响了. 极大地简化了使用.
  3. 扩展
    通常ByteBuffer的动态扩展是在写入前检查剩余空间, 如果不够, 则重新申请空间, 并复制原内容到新的Buffer, 再进行写入.
    ByteBufwrite方法提供了容量检查和扩展, 无需担心溢出问题.
    :扩展操作的具体实现是AbstractByteBuf.ensureWritable0(int).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // AbstractByteBuf

    public ByteBuf (byte[] src, int srcIndex, int length) {
    ensureAccessible();
    ensureWritable(length);
    setBytes(writerIndex, src, srcIndex, length);
    writerIndex += length;
    return this;
    }
  4. discard操作
    ByteBufzhdiscard空间应该尽量利用, 但是不应该经常使用discardReadBytes()方法, 因为discardReadBytes会发生内存复制.

  5. clear操作
    不会清空数据, 只是将操作索引进行初始化.
  6. mark&reset
    • markReaderIndex: 记录当前读位置
    • resetReadIndex: 回到记录的读位置
    • markWriteIndex: 记录当前的写位置
    • resteWriteIndex: 回到记录的写位置
  7. 搜索
    • indexOf(Int, Int, Byte)
    • bytesBefore(Byte)
    • bytesBefore(Int, Byte)
      bytesBefore(Int, Int ,Byte)
  8. 遍历
    通过forEach系列函数对ByteBuf内容进行遍历.
    ByteBufProcessor定义了一些常用的操作
    • _ByteBufProcessor.FIND_NUL_ (0x00)
    • _ByteBufProcessor.FIND_CR_ (‘r’)
    • _ByteBufProcessor.FIND_LF_ (‘n’)
    • _ByteBufProcessor.FIND_CRLF_ (‘rn’)
    • _ByteBufProcessor.FIND_WHITESPACE_ (‘r’)
  9. 复制
    共享内存:

    • duplicate() 复制对象, 不复制索引
    • slice() 当前可用数据部分 (ReadIndex, WriteIndex)
    • slice(Int, Int) 指定的内存段

      不共享内存

    • copy() 复制数据
    • copy(Int, Index) 复制指定区域数据
  10. 转换为原生Buffer
    nioBuffer系列方法.
  11. 指定位置读写
    get/set系列方法, 同样会对读写合法性进行检验. 由继承类实现.

ByteBuf功能分析

功能接口分类

  1. 堆分配
    直接分配到Java虚拟机的堆.
  2. 直接内存
    非堆内存, 借助Java NIO API分配. 分配回收速度慢一些, 但在NIO操作中可以减少一次读写的复制操作, 速度比堆内存快.

Tips: 建议I/O通信使用DirectByteBuf, 后端业务消息编解码视同HeapByteBuf.《Netty权威指南》

可重用能力分类:

  1. 池化的ByteBuf // Pooled
  2. 引用计数的ByteBuf. // Unpooled

    AbstractByteBuf

    继承并实现了ByteBuf的方法, 但在其实现中定义了更多操作细化的’_’前缀的abstract方法.
    主要实现了在读写上的一些通用操作:

内存空间

内存分配

非池化ByteBuf都采用了Allocator分配器对内存进行分配.

1
private final ByteBufAllocator alloc;

对于扩展行为的操作则由Allocator来决定.
申请的内存位置, 可以分为直接内存和堆内存两类.

堆内存

Heap ByteBuf: 通过在堆内申请字节数组获得内存空间

直接内存

Direct ByteBuf: 通过Java NIO库中的DirectByteBuffer申请直接内存空间.

内存操作

Java操作(Heap)

直接通过Java语法层面进行数据操作

Unsafe操作(Heap/Direct)

通过Unsafe提供的API进行底层化的数据操作.

  1. Heap操作Unsafe直接提供了API.
  2. Direct操作需要获取到DirectByteBuffer的内存地址才能进行操作.

Unsafe操作实现更底层化, 相较Java语法层面实现效率更高.

Direct操作(Direct)

通过DirectByteBuffer的API对数据进行操作

内存扩展

对于UnpooledByteBuf, 都是通过以下步骤:

  1. 计算扩展后容量
  2. 申请相应内存
  3. 复制数据到新内存
  4. 重设ByteBuf的内存区
    这里和dicard内存区一样发生了内存复制, 会对性能有所损耗. 所以, 扩容也是应该尽量避免的.