Netty中的ByteBuf
Netty的数据容器
在网络通信中,涉及到数据传输的情况都以字节为为单位进行。Netty为了能够提供更方便以及高效的数据传输,设计了一套有别于NIO ByteBuffer的字节缓冲区对象,在Netty中这个新的对象被叫做ByteBuf。
ByteBuf的工作原理
ByteBuf维护了两个下标索引,一个是读取索引readerIndex,一个是写入索引writerIndex。当写入一个数据的时候,writerIndex向后移动一个位置,当读取一个数据的时候readerIndex向后移动一个位置,但是当使用set或者get方法的时候这两个索引并不会向后移动。此外,如果读取的数据超过了ByteBuf的最后一个数据,会出现IndexOutOfBoundsException,如果写入的数据超过了ByteBuf的最大长度,也会抛出异常,ByteBuf默认的最大长度是Integer.MAX_VALUE。
ByteBuf的使用模式
ByteBuf的使用模式可以理解为三种创建ByteBuf的方式,分别是堆缓冲区、直接缓冲区和复合缓冲区。
堆缓冲区
这种方式是将ByteBuf创建JVM堆空间中,核心是一个byte数组,它能在没有池化的情况下提供快速的分配和释放。如果需要访问堆缓冲区,需要先判断当前ByteBuf中是否有支撑数组,通过hasArray()方法可以判断。如果没有支撑数组直接访问支撑数组,则会出现异常。针对堆缓冲区的优缺点如下:
- 优点
- 分配和释放速度快,且可以被 JVM 自动垃圾回收。
- 由于数据在 JVM 堆上,可以直接通过 array() 方法访问 byte[],数据读取和操作便捷。
- 缺点
- 在执行 I/O 操作时(例如写入 SocketChannel),需要额外做一次内存复制,即将堆内存数据复制到内核空间缓冲区。
通过以上优缺点不难发现堆缓冲区的适用于分配快,适用于处理逻辑比较复杂、小数据量或内存管理频繁的场景。
直接缓冲区
直接缓冲区和堆缓冲区则不同,游离于JVM之外。在Java中对象基本上都是在堆中分配内存空间,但是如果ByteBuf使用的是直接缓冲区,则可以在堆外内存中分配内存空间。作为在堆外分配内存,直接缓冲区的优缺点同样很明显,其优缺点如下所示:
- 优点
- 高性能 I/O:在网络传输(Socket)时,直接缓冲区避免了数据从 JVM 堆内存到直接内存的拷贝,直接将数据从内核空间发送出去,效率高。
- 避免了 GC 压力:不占用堆空间,减轻了垃圾回收负担。
- 缺点
- 内存的分配和释放比堆缓冲区更昂贵(需要调用系统调用)。
- 不适合存储较短生命周期的数据。
通过以上的优缺点不难发现,直接缓冲区的适用场景相较于堆缓冲区有着更高的要求,并且只适合长连接、高并发场景,特别是数据在网络传输中频繁读写的场景。
复合缓冲区
复合缓冲区可以为多个ByteBuf提供一个聚合视图,Netty通过CompositeByteBuf实现复合缓冲区,这只是一个虚拟的视图,并不实际复制底层数据。但是该模式可以无需显示复制数据,实现零拷贝。由于复合缓冲区的这一特点,其适用于处理拆包/粘包消息,这种优化发生在Netty的核心代码中,并没有显示的展示出来。
字节级操作
ByteBuf是Netty中保存数据的载体,想要更好地学习ByteBuf需要了解对ByteBuf中数据的操作方法。
随机访问索引
ByteBuf可以像普通的数组一样根据下标,随机访问数据。索引从0开始到capacity() - 1结束。想要获取ByteBuf的byte数组可以使用getBytes()方法。
顺序访问索引
顺序访问索引则是通过read开头的方法实现,调用一次readerIndex就会向后移动相应的位置。
可丢弃字节
对于已经读取过的数据,即可丢弃字节。在ByteBuf中提供了discardReadBytes()方法,用于将以读取的数据从ByteBuf中移除,其核心是修改readerIndex和writerIndex的位置,将readerIndex置为0,将writerIndex置为(writerIndex - readerIndex),并将为读取的字节前移。
可读字节
readerIndex和writerIndex闭合区间的数据都是可读字节。任何以read或skip开头的方法都会使readerIndex的值增加。如果调用的方法需要一个ByteBuf作为目标Buf,则该目标Buf的writerIndex会增加,如:targetBuf.writeBytes(srcBuf);。
可写字节
可写字节只的是大于等于writerIndex,小于ByteBuf长度之间的数据。任何以write开头的方法都会将当前writerIndex后移。如果写入的数据超过了目标容量的数据,会抛出异常。
索引管理
在NIO里为ByteBuffer定义了mark和reset方法,分别用来标记当前索引位置和重置索引到标记的位置。在ByteBuf中也提供了对readerIndex和writerIndex索引标记和重置的方法,分别是markReaderIndex()、markWriterIndex()、resetWriterIndex()和resetReaderIndex(),也可以通过调用readerIndex(int)或者writerIndex(int)来将索引移动到指定位置。 如果将索引设置到一个无效的位置,则会抛出异常。此外,如果调用clear()方法,则会将readerIndex和writerIndex都重置为0。
查找操作
为了方便查找特定的字符,在ByteBuf中定义了forEachByte(ByteBufProcessor processor)方法。这个方法的核心是通过ByteBufProcessor参数实现查询还是不查询某个字节。如果要查找某一个字节,则构建一个IndexOfProcessor对象;如果想要查询不是某一个字节,则构建一个IndexNotOfProcessor对象。需要注意的一点,查找操作是从readerIndex开始,到writerIndex结束,也就是说查询,查的是未读取的数据。
读/写操作
get/set方法都是对特定索引位置的数据进行读取或修改,使用get/set方法不会修改readerIndex和writerIndex。read/write方法可以从一个ByteBuf/byte[]中读取/写入数据。其中readBytes(ByteBuf dst, int dstIndex, int length)将当前ByteBuf中从当前readerIndex处开始的数据传送到一个目标源,从目标的dstIndex开始的位置。本地的readerIndex将被增加已经传输的字节数;writeBytes(ByteBuf src, int srcIndex, int length)从当前 writerIndex开始,传输来自于指定源的数据。如果提供了srcIndex和length,则从srcIndex开始读取,并且处理长度为length的字节。当前writerIndex将会被增加所写入的字节数。
ByteBuf分配
ByteBuf的创建可以通过多种方式实现,下文详细介绍多种实现方式。
ByteBufAllocator接口
Netty通过ByteBufAllocator接口实现了ByteBuf的池化,它将用来分配上文提到的三种模式的ByteBuf。通过buffer()方法可以创建一个基于堆或直接内存的ByteBuf;通过headBuffer()方法可以创建一个基于堆的ByteBuf;通过directBuffer()方法可以创建一个基于直接内存的ByteBuf;通过comosite开头的方法,可以创建一个通过添加最大到指定数目的基于堆或直接内存存储的缓冲区来扩展的返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf。可以通过Channel或者绑定到ChannelHandler的ChannelHandlerContext获取一个ByteBufAllocator的引用。
Netty中提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大程度地减少内存碎片。后者的实现不池化ByteBuf实例,每次调用都会返回一个新的实例。Netty默认使用的是PooledByteBufAllocator,可以通过ChannelConfig修改ByteBuf的实例化实现方式
Unpooled缓冲区
Netty提供了一个创建非池化ByteBuf的工具类,Unpooled中包含一些静态方法可以直接创建非池化的ByteBuf。
ByteBufUtil类
ByteBufUtil提供了用于操作ByteBuf的静态的辅助方法,其中hexdump()方法可以返回ByteBuf保存的数据的十六进制形式,equals()方法可以比较两个ByteBuf实例是否相同。
引用计数
Netty中的引用计数和JVM中判断对象是否存活所使用的引用计数法类型,都是通过判断当前对象是否存在被引用的情况来决定是否需要释放当前对象。ByteBuf通过实现ReferenceCounted接口实现引用计数的功能,当一个ByteBuf被创建的时候其引用计数为1,当调用release()方法后将其引用计数减1,为0时表示当前ByteBuf实例就会被释放。如果访问一个被释放的ByteBuf实例,则会抛出异常。引用计数对于池化实现至关重要,降低了内存分配的开销。
以上是对Netty中ByteBuf的介绍,是本人学习Netty过程中一些记录,可能会存在一些不正确的地方,仅供参考。

浙公网安备 33010602011771号