ByteBuf--Netty的数据容器

网友投稿 702 2022-05-30

ByteBuf--Netty的数据容器

网络传输的基本单位是字节,在Java NIO中,JDK提供了Buffer接口,以及其相关的实现作为NIO操作 数据的容器,如ByteBuffer等等。 而Netty为了解决Buffer原生接口的复杂操作提供了ByteBuf, ByteBuf是一个很好的经过优化过的数据容器,我们可以将字节数据添加到ByteBuf中或从ByteBuf中获取数据, 相比于原生Buffer,ByteBuf更加灵活和易用。

Netty的数据处理主要通过两个API提供:

abstract class ByteBuf

interface ByteBufHolder

使用ByteBuf API能够给我们带来良好的编码体验,如

正常情况下,ByteBuf比ByteBuffer的性能更好;

实现了ReferenceCounted引用计数接口,优化了内存的使用;

容量可以动态增长,如StringBuilder之于String;

在读和写这两种模式切换时,无需像ByteBuffer一样调用flip方法,更易于操作;

...

ByteBuf还有很多好处,上面列举的只是一部分,其他优点就需要各位同学慢慢了解了。

ByteBuf实现

ByteBuf维护了两个不同的索引:一个是用于读取的readerIndex , 一个是用于写入的writerIndex。 当我们写入字节到ByteBuf后,writerIndex增加,开始读取字节后,readerIndex开始增加。读取字节直到 readerIndex和writerIndex到达同一位置(已经读取到末尾了),ByteBuf就变为不可读。 这就好比当我们访问数组时,超出了它的范围时,程序会抛出IndexOutOfBoundException。

当我们调用ByteBuf以read或write开头的方法时,将会增加这个ByteBuf的读索引或写索引,而诸如set或get 的方法则不会改变索引。

我们可以指定ByteBuf的最大容量,如果对ByteBuf的写入操作导致writerIndex超出了最大容量,那么程序将会 抛出一个异常,ByteBuf的最大容量是Integer.MAX_VALUE。

ByteBuf大致的结构和状态:

ByteBuf使用模式

ByteBuf有多种使用模式,我们可以根据需求构建不同使用模式的ByteBuf。

堆缓冲区(HeapByteBuf): 最常用的ByteBuf模式是将数据存储在JVM的堆空间中,实际上是通过数组存储数据, 所以这种模式被称为支撑数组(Backing Array)。它可以在没有使用池化的情况下快速分配和释放, 适合用于有遗留数据需要处理的情况。

直接缓冲区(DirectByteBuf): 在Java中,我们创建的对象大部分都是存储在堆区之中的,但这不是绝对的,在NIO的API中, 允许Buffer分配直接内存,即操作系统的内存,这样做的好处非常明显: 前面在传输章节介绍过的零拷贝技术的 特点之一就是规避了多次IO拷贝,而现在数据直接就在直接内存中,而不是在JVM应用进程中,这不仅减少了拷贝次数, 是否还意味着减少了用户态与内核态的上下文切换呢? 直接缓冲区的缺点也比较明显: 直接内存的分配和释放都较为昂贵,而且因为直接 缓冲区的数据不是在堆区的,所以我们在某些时候可能需要将直接缓冲区的数据先拷贝一个副本到堆区, 再对这个副本进行操作。 与支撑数组相比,直接缓冲区的工作可能更多,所以如果事先知道数据会作为 一个数组来被访问,那么我们应该使用堆内存。

复合缓冲区(CompositeByteBuf): CompositeByteBuf为多个ByteBuf提供了一个聚合视图, 我们可以根据需要,向CompositeByteBuf中添加或删除ByteBuf实例,所以CompositeByteBuf中可能 同时包含直接缓冲区模式和堆缓冲区模式的ByteBuf。对于CompositeByteBuf的hasArray方法, 如果CompositeByteBuf中只有一个ByteBuf实例,那么CompositeByteBuf的hasArray方法 将直接返回这唯一一个ByteBuf的hasArray方法的结果,否则返回false。 除此之外,CompositeByteBuf还提供了许多附加的功能,可以查看Netty的文档学习。

ByteBuf--Netty的数据容器

字节级别的操作

除了普通的读写操作,ByteBuf还提供了修改数据的方法。

如数组的索引访问一样,ByteBuf的索引访问也是从零开始的,第一个字节的索引是0,最后一个字节的索引总是 capacity - 1:

注意:使用getByte方式访问,既不会改变readerIndex,也不会改变writerIndex。

JDK的ByteBuffer只有一个索引position,所以当ByteBuffer在读和写模式之间切换时,需要使用flip方法。 而ByteBuf同时具有读索引和写索引,则无需切换模式,在ByteBuf内部,其索引满足:

0 <= readerIndex <= writerIndex <= capacity

这样的规律,当使用readerIndex读取字节,或使用writerIndex写入字节时,ByteBuf内部的分段大致如下:

上图介绍了在ByteBuf内部大致有3个分段,接下来我们就详细的介绍下这三个分段。

上图中,当readerIndex读取一部分字节后,之前读过的字节就属于已读字节,可以被丢弃了,通过调用 ByteBuf的discardReadBytes方法我们可以丢弃这个分段,丢弃这个分段实际上是删除这个分段的已读字节, 然后回收这部分空间:

ByteBuf的可读字节分段存储了尚未读取的字节,我们可以使用readBytes等方法来读取这部分数据,如果我们读取 的范围超过了可读字节分段,那么ByteBuf会抛出IndexOutOfBoundsException异常,所以在读取数据之前,我们 需要使用isReadable方法判断是否仍然有可读字节分段。

可写字节分段即没有被写入数据的区域,我们可以使用writeBytes等方法向可写字节分段写入数据,如果我们写入 的字节超过了ByteBuf的容量,那么ByteBuf也会抛出IndexOutOfBoundsException异常。

我们可以通过markReaderIndex,markWriterIndex方法来标记当前readerIndex和writerIndex的位置, 然后使用resetReaderIndex,resetWriterIndex方法来将readerIndex和writerIndex重置为之前标记过的 位置。

我们还可以使用clear方法来将readerIndex和writerIndex重置为0,但是clear方法并不会清空ByteBuf的 内容,下面clear方法的实现:

由于调用clear后,数据并没有被清空,但整个ByteBuf仍然是可写的,这比discardReadBytes轻量的多, DiscardReadBytes还要回收已读字节空间。

在ByteBuf中,有多种可以确定值的索引的方法,最简单的方法是使用ByteBuf的indexOf方法。 较为复杂的查找可以通过ByteBuf的forEachByte方法,forEachByte方法所需的参数是ByteProcessor, 但我们无需去实现ByteProcessor,因为ByteProcessor已经为我们定义好了两个易用的实现。

API Java

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【云小课】应用平台第23课 短信发送失败别慌张,小课给你来帮忙
下一篇:HBase 读写过程中的线程池模型原理实现
相关文章