0%

Netty的字节容器---ByteBuf

网络数据的基本单位总是字节。Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty 的ByteBuffer 替代品是ByteBuf,一个强大的实现,既解决了JDK API 的局限性,又为网络应用程序的开发者提供了更好的API。ByteBuf本质的原理就是引用了一段内存,这段内存可以是堆内也可以是堆外的,然后用引用计数来控制这段内存是否需要被释放,使用读写指针来控制对 ByteBuf 的读写,可以理解为是外观模式的一种使用。

基本结构

image-20210903174536572

  • ByteBuf 是一个字节容器,容器里面的的数据分为三个部分,第一个部分是已经丢弃的字节,这部分数据是无效的;第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;最后一部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段。

  • 以上三段内容是被两个指针给划分出来的,从左到右,依次是读指针(readerIndex)、写指针(writerIndex),然后还有一个变量 capacity,表示 ByteBuf 底层内存的总容量。

  • ByteBuf 中每读取一个字节,readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读, 由此可以推论出当 readerIndexwriterIndex 相等的时候,ByteBuf 不可读。

  • 写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了

ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,如果容量不足,那么这个时候可以进行扩容,直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错

API

容量

名 称 描 述
capacity() 表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节)
maxCapacity() 表示 ByteBuf 底层最大能够占用多少字节的内存,当向 ByteBuf 中写数据的时候,如果发现容量不足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常
readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于 writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false
writableBytes() ByteBuf 当前可写的字节数,它的值等于 capacity-writerIndex,如果两者相等,则表示不可写,isWritable() 返回 false,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果发现往 ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于 maxCapacity-writerIndex

索引管理

名 称 描 述
readerIndex() 返回当前的读指针
writeIndex() 返回当前的写指针
markReaderIndex() 把当前的读指针保存起来
resetReaderIndex() 把当前的读指针恢复到之前保存的值

读写操作

有两种类别的读/写操作:

  • get()set()操作,从给定的索引开始,并且保持索引不变;
  • read()write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。

基于读写指针和容量、最大可扩容容量,衍生出一系列的读写方法。其中比较重要的有:

名 称 描 述
readByte() 返回当前readerIndex 处的字节,并将readerIndex 增加1(从 ByteBuf 中读取一个字节)
writeByte(int) 在当前writerIndex 处写入一个字节值,并将writerIndex 增加1(往 ByteBuf 中写一个字节)
readBytes(destination byte[]) ByteBuf 里面的数据全部读取到 destination ByteBuf->destination
writeBytes(source byte[]) 把字节数组 source 里面的数据写到 ByteBufsource->ByteBuf

类似的 API 还有 writeBoolean()writeChar()writeShort()writeInt()writeLong()writeFloat()writeDouble()readBoolean()readChar()readShort()readInt()readLong()readFloat()readDouble() ,这里就不一一赘述了。

内存管理

由于 Netty 可以使用堆外内存,而堆外内存是不被 jvm 直接管理的,也就是说申请到的内存无法被垃圾回收器直接回收,所以需要我们手动回收,否则会造成内存泄漏。Netty 的 ByteBuf 是通过引用计数的方式管理的,如果一个 ByteBuf 没有地方被引用到,需要回收底层内存。默认情况下,当创建完一个 ByteBuf,它的引用为1,然后每次调用 retain() 方法, 它的引用就+1, release() 方法原理是将引用计数-1,减完之后如果发现引用计数为0,则直接回收 ByteBuf 底层的内存。

池化

为了降低分配和释放内存的开销,Netty 通过 ByteBufAllocator 实现了ByteBuf 的池化,它可以用来分配我们所描述过的任意类型的ByteBuf 实例(直接内存、堆内存)。

1
2
3
4
5
//从Channel 获取一个到 ByteBufAllocator 的引用
ByteBufAllocator allocator = channel.alloc();

//从ChannelHandlerContext 获取一个到ByteBufAllocator 的引用
ByteBufAllocator allocator2 = ctx.alloc();

一般使用buffer(int initialCapacity, int maxCapacity);返回一个基于堆或者直接内存存储的ByteBuf

复制

slice()duplicate()copy()三者的返回值都是一个新的 ByteBuf 对象:

  • slice() 方法从原始 ByteBuf 中截取一段,这段数据是从 readerIndexwriteIndex,同时,返回的新的 ByteBuf 的最大容量 maxCapacity 为原始 ByteBufreadableBytes()

  • duplicate() 方法把整个 ByteBuf 都截取出来,包括所有的数据,指针信息

    slice() 方法与 duplicate() 方法不会拷贝数据,它们只是通过改变读写指针来改变读写的行为。底层内存以及引用计数与原始的 ByteBuf 共享,也就是说返回的 ByteBuf 调用 write 系列方法都会影响到原始的 ByteBuf,但是它们都维持着与原始 ByteBuf 不同的读写指针。

  • copy() 会直接从原始的 ByteBuf 中拷贝所有的信息,包括读写指针以及底层对应的数据,因此, copy() 返回的 ByteBuf 中写数据不会影响到原始的 ByteBuf

  • retainedSlice()retainedDuplicate()它们的作用是在截取内存片段的同时,增加内存的引用计数。

多个 ByteBuf 可以引用同一段内存,而Netty会通过引用计数来控制内存的释放,应当遵循谁 retain()release() 的原则。我们建议,在一个函数体里面,只要增加了引用计数(包括 ByteBuf 的创建和手动调用 retain() 方法),就必须调用 release() 方法,否则往往会出现内存泄露的问题。