ByteBuf : Netty的数据容器类

2018-08-21  本文已影响0人  OisCircle

哪里描述不正确望指正. 欢迎转载
大纲

知识点 概括
ByteBuf的数据模型 描述ByteBuf基本概念
来自不同地区的ByteBuf 介绍存储在不同内存区域的ByteBuf及其优缺点
byte级别的操作 ByteBuf基本的读写查操作
派生ByteBuf ByetBuf创建不同类型的副本
多种方式分配ByteBuf 介绍池化/非池化ByteBuf以及如何分配池化/非池化的ByteBuf
释放ByteBuf 基于引用计数, 释放资源
数据容器的选择
  • Java NIO使用ByteBuffer.class
  • Netty使用ByteBuf.class

Netty官方声称ByteBufByteBuffer更加方便使用


ByteBuf的数据模型
ByteBuf的数据模型.png
与NIOByteBuffer不同的是NettyByteBuf维护了两个索引

index的变化只与read*()write*()skip*()方法有关


来自不同地区的ByteBuf

将数据存储在 JVM 的堆空间中, 该模式被称为支撑数组(backing array), 最常用.

  • 优点
    能在没有使用池化的情况下提供快速的分配和释放(在堆上直接调用JAVA的API, 不用调用操作系统的接口)
  • 缺点
    传输时, 会先拷贝到直接缓冲区

声明一个支撑数组

ByteBuf buf = new UnpooledHeapByteBuf(ByteBufAllocator.DEFAULT, initalCapacity, maxCapacity);
//默认capacity是256
ByteBuf buf = Unpooled.buffer(capacity);
//建议池化方式创建, 下文将讲到如何分配ByteBuf

判断ByteBuf是否在JVM heap中, 并处理数组

ByteBuf heapBuf = Unpooled.copiedBuffer("Hello Netty", CharsetUtil.UTF_8);
if (heapBuf.hasArray()) { //检查支撑数组(true)
  byte[] array = heapBuf.array();
  handleArray(array);//处理
} 

如果ByteBuf::hasArray返回false时再次访问ByteBuf::array则抛出异常UnsupportedOperationException
调用ByteBuf::array之前总是要判断ByteBuf::hasArray

通过本地调用来为ByteBuf分配内存(非JVM heap)

  • 优点
    直接缓冲区对于网络数据传输是理想的选择, 传输时ByteBuf不用经过中间缓存区
  • 缺点
    分配和释放代价大(调用操作系统API), 下文中池化的缓冲区可以缓解这个缺点

声明一个直接缓冲区数组

ByteBuf buf = new UnpooledDirectByteBuf(ByteBufAllocator.DEFAULT, initialCapacity, maxCapacity);
//默认capacity是256
ByteBuf buf = Unpooled.directBuffer(capacity);
//建议池化方式创建, 下文将讲到如何分配ByteBuf

直接缓冲区的用法ByteBuf就如同其名字, 可以直接传输(无需经过中间缓存区)

聚合多个ByteBuf, Netty 通过一个 ByteBuf.class 子类——CompositeByteBuf.class——实现了这个模式,将多个ByteBuf聚合在一起提供统一操作

  • CompositeByteBuf中可能同时存在直接内存分配和非直接内存分配. 如果只有一个ByteBuf则调用CompositeByteBuf::hasArray时相当于调用ByteBuf::hasArray, 否则存在多个实例时, 调用CompositeByteBuf::hasArray时直接返回false
  • CompositeByteBuf.class内部维护了一个arrayList用来存放ByteBuf

使用场景

Http请求由header和(1或n个)content组成, 我们可以将headerByteBuf和contentByteBuf聚合为一个CompositeByteBuf

image.png

构建我们的Http请求主体

class HttpMessage{
  //聚合器
  CompositeByteBuf message;
  //使用不同内存模型的ByteBuf
  ByteBuf header;
  ByteBuf content;
  public HttpMessage(){
    message = Unpooled.compositeBuffer();
    header = Unpooled.directBuffer();
    content = Unpooled.buffer();
    //聚合
    message.addComponents(header, content);
    //也可以移除
    //message.removeComponent(0);
  }
}

访问我们的聚合器

//循环遍历每个component
message.forEach(byteBuf -> System.out.println(byteBuf.toString(CharsetUtil.UTF_8)));
//逐个获取component
ByteBuf header2 = message.component(0);
ByteBuf content2 = message.component(1);
//处理消息
handle(header2, content2);
//...

byte级别的操作

在高并发项目开发中, 如果能让我们灵活地操作字节(分配和释放), 系统性能和吞吐量将会有所提升

由于ByteBuf有两个索引, 所以一个ByteBuf的数据可被两个索引拆分为三个部分, 对应顺序访问中三个重要的概念

  • 可丢弃字节
  • 可读字节
  • 可写字节
    image.png

丢弃可丢弃字节, 通过调用ByteBuf::discardReadBytes方法, readerIndex会变为0, writerIndex会减少, 从而将可丢弃字节抛弃, 不过这会导致内存复制, 因为要把readerIndexwriterIndex之间的内容往左移动. 这里要注意的是writerIndexcapacity之间的数据不会移动也不会改变, 除非ByteBuf容量很紧凑, 否则应该少用该方法

read*()skip*()方法会增加当前readerIndex. 注意特例readBytes(ByteBuf dest)方法(将读取的byte写入dest)会增加当前ByteBufreaderIndex也会增加destwriterIndex, 但readBytes(ByteBuf dest, int dstIndex, int length )不会改变destwriterIndex, 因为指定了下标参数(具体请查看netty官方文档)

同上, writeBytes(ByteBuf dest)如果没有自定下标参数, 同样会增加dest的writerIndex

ByteBuf提供了一系列的字节级别读写, 举个简单的例子
readByte: 读取1个byte 然后 readerIndex+1
readInt: 读取4个byte 然后readerIndex+4
writeByte:写入1个byte 然后 writerIndex+1
writeInt:写入4个byte 然后 writerIndex+4
注意: 为了养成一个好的习惯, 读写时要随时注意ByteBuf::readableBytesByteBuf::writableBytes是否大于0, 否则抛出IndexOutOfBoundException

在操作ByteBuf的时候, 可能需要暂时存下当前索引的位置, 稍后返回该索引位置. 或者需要根据协议自定义跳转到指定索引位置

  1. 保存和重置索引: ByteBuf::markReaderIndexByteBuf::resetReaderIndex, 同理有ByteBuf::markWriterIndexByteBuf::resetWriterIndex
  2. 将索引移动到指定位置: ByteBuf::readerIndexByteBuf::writerIndex(都接收int参数)
  3. 清空ByteBuf: ByteBuf::clear(注意此方法不是将内容删除,而是将readerIndexwriterIndex重置为0)

简单查找: ByteBuf::indexOf, 接收一个byte参数, 返回第一个出现的index

高级查找: ByteBufProcessor已经在新版本中被弃用, 所以这里只关注ByteProcessor

ByteBufProcessor接口定义了多个常量并且内部有两个实现类IndexOfProcessorIndexNotOfProcessor
IndexOfProcessor用来查找第一个出现的字符下标, IndexNotOfProcessor用来查找第一个不一样的字符下标.
为了区分上面两个类的作用, 简单举个例子:
假设我们有ByteBuf内容: Netty

//找出'N'出现的第一个下标
int indexOf = buf.forEachByte(new ByteProcessor.IndexOfProcessor((byte)'N'));

上面语句的结果: indexOf = 0

//找出第一个不是'N'的下标
int indexNotOf = buf.forEachByte(new ByteProcessor.IndexNotOfProcessor(((byte) 'N')));

上面语句的结果: indexNotOf = 1
ByteBufProcessor还定义了一些常量, 下面简单地示范

//查找 LF ('\n')
buf.forEachByte(ByteProcessor.FIND_LF);

派生ByteBuf

以下方法用来获取ByteBuf的一个新实例, 这些实例拥有自己独立的索引(标记和读写指针), 但数据内容共享(同一个引用)

如果要生成一个数据独立的副本, 使用ByteBuf::copy

另外一种派生ByteBuf的方式是ByteBufHolder. 每个ByteBufHolder维护一个ByteBuf, 我们可以将ByteBufHolder看作一个池, 用户可以向ByteBufHolder借用ByteBuf, 并在没用的时候释放. 关于ByteBufHolder的实际用途, 本人目前尚未了解清楚, 希望了解的读者可以向我反馈


多种方式分配ByteBuf
  • 什么是池化/非池化的ByteBuf?
    Netty预先向操作系统申请一块内存, 用来存放池化的数据, 当我们创建一个池化的ByteBuf时, 一般不会创建新的ByteBuf, 而是复用了之前创建好的, 当我们的ByteBuf使用完, 释放完之后, ByteBuf不会被销毁, 而是被Netty放回了池中, 等待下一个请求, 继续分配这个ByteBuf, 这就是池化的ByteBuf
    而非池化的ByteBuf, 每次被请求时, 都会创建新的, 被释放时, 对象都会被销毁

  • 为什么要使用池化的ByteBuf?
    池化ByteBuf是为了避免创建/销毁ByteBuf造成的开销, 在许多Netty应用中, 一个请求所生成的ByteBuf在逻辑上是转瞬即逝的, 请求返回之后, 这些数据就成了GC回收的目标, 如果没有及时清理, 那么内存将会无限增加(Netty太快了).

下面介绍两种分配ByteBuf的途径, 通过获取到的分配器, 我们就可以根据需求分配池化/非池化的ByteBuf

  1. ByteBufAllocator
    一般从Channel::alloc或者ChannelHandlerContext::alloc中获取
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
...

ByteBufAllocator提供了一系列方法, 可以用来创建来自不同地区的ByteBuf, 基于堆内存, 基于直接内存, 聚合器, 还有阻塞型(I/O)的ByteBuf

ByteBufAllocator有两个实现类PooledByteBufAllocatorUnpooledByteBufAllocator
PooledByteBufAllocator池化了ByteBuf以用来减少内存碎片, 提高性能
UnpooledByteBufAllocator创建的ByteBuf都是一个新实例

  • Netty默认使用了池化的Allocator, 但我们可以在引导中自定使用池化还是非池化分配器
  1. Unpooled
    当无法在Channel或者ChannelHandlerContext获取ByteBufAllocator时, 推荐使用Unpooled(静态工具类)来创建未池化的ByteBuf

释放ByteBuf

Netty在第4版中为ByteBufByteBufHolder引入了引用计数技术, 它们都实现了ReferenceCounted接口
当一个对象所持有的资源被多个对象引用, 假设引用计数为n, 每个对象释放引用之后, 引用计数减1, 当引用计数等于0时, 说明对象已经没有用途, 对象持有的资源会被释放.

上一篇下一篇

猜你喜欢

热点阅读