ByteBuf
ByteBuf API的优点:
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长(类似于JDK的StringBuilder)
- 在读和写两种模式之间切换不需要调用ByteBuffer的flip()方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
ByteBuf维护了两个不同的索引,名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开头的操作则不会。
如果读取字节时readerIndex读索引达到了和writerIndex同样的值时会发生什么,在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOf-BoundsException。
可以指定ByteBuf的最大容量,试图移动写索引超过这个值将会触发一个异常,默认最大容量为Integer.MAX_VALUE
使用模式
-
堆缓冲区
最常见的ByteBuf模式是将数据存储在JVM的堆空间中,这种模式被称为支撑数组,它能在没有使用池化的情况下提供快速的分配和释放。可以由hasArray()来判断检查ByteBuf是否数据支撑,如果不是,则这是一个直接缓冲区。
-
直接缓冲区
直接缓冲区的主要缺点是,相对于堆缓冲区,它们的分配和释放都较为昂贵。
-
复合缓冲区
复合缓冲区CompositeByteBuf,它为多个ByteBuf提供一个聚合视图。比如HTTP协议,分为消息头和消息体,这两部分可能由应用程序的不同模块产生,各有各的ByteBuf,将会在消息被发送的时候组装为一个ByteBuf,此时可以将这两个ByteBuf聚合为一个CompositeByteBuf,然后使用统一和通用的ByteBuf API来操作。
分配
如何在我们的程序中获得ByteBuf的实例,并使用它呢?Netty提供了两种方式。
-
ByteBufAllocator
Netty通过ByteBufAllocator接口分配给我们所描述过得任意类型的ByteBuf实例。
方法名 方法描述 buffer() 返回一个基于堆或者直接内存存储的ByteBuf heapBuffer() 返回一个基于堆内存存储的ByteBuf directBuffer() 返回一个基于直接内存存储的ByteBuf compositeBuffer() 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展CompositeByteBuf ioBuffer() 返回一个用于套接字的I/O操作的ByteBuf,当所运行的环境具有sun.misc.Unsafe支持时,返回基于直接内存存储的ByteBuf,否则返回基于堆内存存储的ByteBuf;当指定使用,PreferHeapByteBufAllocator时,则会返回基于堆内存存储的ByteBuf。 可以通过Channel或者绑定到ChannelHandler的ctx来获取一个ByteBufAllocator的引用。
Netty提供了两种ByteAllocator的实现:PoolByteAllocator和UnpoolByteAllocator。前者池化了ByteBuf的实例以提高性能并最大限度的减少内存碎片。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。
Netty4.1默认使用了PooledByteBufAllocator池化技术。
-
Unpooled缓冲区
在某些情况下,可能无法获取到ByteBufAllocator的引用,Netty提供了一个简单的称为Unpooled的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf实例。
方法名称 | 方法描述 |
---|---|
buffer | 返回一个未池化的基于堆内存存储的ByteBuf |
directBuffer | 返回一个未池化的基于直接内存存储的ByteBuf |
wrappedBuffer | 返回一个包装了给定数据的ByteBuf |
copiedBuffer | 返回一个赋值了给定数据的ByteBuf |
Unpooled 类还使得ByteBuf 同样可用于那些并不需要Netty 的其他组件的非网络项目,使得其能得益于高性能的可扩展的缓冲区API。
随机访问索引/顺序访问索引/读写操作
如同在普通的Java字节数组中一样,ByteBuf的索引是从零开始的,第一个字节的索引是0,最后一个字节的索引总是capacity()-1。使用那些需要一个索引值参数(随机访问,也即是数组下标)的方法之一来访问数据既不会改变readerIndex也不会改变writerIndex。如果有需要,也可以通过调用readerIndex(index)或者writerindex(index)来手动移动这两者。顺序访问通过索引访问
有两种类型的读写操作:
get/set操作,从给定的索引开始,并且保持索引不变。
read/write操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
除了读写操作外,还提供了一些其它操作:
isReadable() 如果至少有一个字节可供读取,则返回true
isWritable() 如果至少有一个字节可被写入,则返回true
readableBytes() 返回可被读取的字节数
writableBytes() 返回可被写入的字节数
capacity() 返回ByteBuf 可容纳的字节数。超过此值时,它会尝试再次扩展直到达到maxCapacity()
maxCapacity() 返回ByteBuf 可以容纳的最大字节数
hasArray() 如果ByteBuf 由一个字节数组支撑,则返回true
array() 如果 ByteBuf 由一个字节数组支撑则返回该数组,否则它将抛出一个UnsupportedOperationException 异常
可丢弃字节/可读字节/可写字节
可丢弃字节的分段包含了已经被读过的字节,通过调用discardreadBytes()方法,可以丢弃它们并回收空间。可丢弃字节的初始大小为0,存储在readerIndex中,会随着read操作的执行而增加(get操作不会移动readerIndex)。
缓冲区上调用discardReadBytes( ) 方法后,可丢弃字节分段中的空间已经变为可写的了,虽然频繁的调用discardReadBytes( )方法可以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节需要移动到缓冲区的开始位置。所以非必要的情况下不建议这样做。
ByteBuf的可读字节分段存储了实际数据,新分配的缓冲区默认readerIndex值为0
可写字节分段是指一个未使用的内存区域,新分配的缓冲区默认writerIndex的默认值为0。任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并向后移动已经写入的字节数。
索引管理
调用markReaderIndex()、markWriterIndex()、resetWriterIndex()和resetReaderIndex()来标记和重置ByteBuf 的readerIndex 和writerIndex。也可以通过调用readerIndex(int)或者writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException。可以通过调用clear()方法来将readerIndex 和writerIndex 都设置为0。注意,这并不会清除内存中的内容。
查找操作
在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()方法。较复杂的查找可以通过调用forEachByte()。
引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第4 版中为ByteBuf引入了引用计数技术, interface ReferenceCounted。
工具类
ByteBufUtil 提供了用于操作ByteBuf 的静态的辅助方法。因为这个API 是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。
这些静态方法中最有价值的可能就是hexdump()方法,它以十六进制的表示形式打印ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录ByteBuf 的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。
另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个ByteBuf实例的相等性。
资源释放
当某个 ChannelInboundHandler 的实现重写 channelRead()方法时,它要负责显式地释放与池化的 ByteBuf 实例相关的内存。Netty 为此提供了一个实用方法ReferenceCountUtil.release()
Netty 将使用 WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。
1、对于入站请求,Netty 的 EventLoo 在处理 Channel 的读操作时进行分配 ByteBuf,对于这类 ByteBuf,需要我们自行进行释放,有三种方式,或者使用SimpleChannelInboundHandler,或者在重写 channelRead()方法使用ReferenceCountUtil.release()或者使用 ctx.fireChannelRead 继续向后传递;
2、对于出站请求,不管 ByteBuf 是否由我们的业务创建的,当调用了 write 或者writeAndFlush 方法后,Netty 会自动替我们释放,不需要我们业务代码自行释放。
至此,Netty基础部分已经讲完了,接下来就会深入到源码底层去了解每一种组件的实现原理和作用,源码部分看起来可能会比较痛苦,不过这也是必须要跨过去的坎,希望大家能够坚持的看下去,机会总是给有准备的人!也希望大家有什么疑问或文章有错误在评论区指出,一定会第一时间回复!