ByteBuf : Netty的数据容器类

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

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

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

  • 读和写使用了不同的索引(读写模式切换不需要调用ByteBuffer::filp方法)
  • 支持方法的链式调用
  • 支持引用计数(释放ByteBuf资源)
  • 支持池化(避免冗余)
  • 可以被用户自定义的缓冲区类型扩展
  • 通过内置的复合缓冲区类型实现了透明的零拷贝
  • 容量可以按需增长

ByteBuf的数据模型

ByteBuf的数据模型.png

与NIOByteBuffer不同的是NettyByteBuf维护了两个索引

  • readerIndex
    调用read*()方法时, index+
    调用get*()方法时, index不变
  • writerIndex
    调用write*()方法时, index+
    调用set*()方法时, index不变
    调用skip*()方法时, index+

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::getByte(int)读取任意位置的字节
    ByteBuf::setByte(int)写入任意位置的字节
  • 顺序读写

由于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

  • 管理索引(readerIndex,writerIndex)

在操作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::duplicate返回所有内容
  • Bytebuf::slice返回readerIndexwriterIndex之间的内容
  • Bytebuf::slice(int, int)
  • ByteBuf::order
  • Unpooled::unmodifiableBuffer

如果要生成一个数据独立的副本, 使用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时, 说明对象已经没有用途, 对象持有的资源会被释放.

  • 释放引用
    realease()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容