Netty入门学习-ByteBuf
ByteBuf是Java NIO ByteBuffer的替代品,是网络数据基本单位字节的容器。
ByteBuf的API
Netty的数据处理API通过两个组件暴漏:抽象类ByteBuf和接口ByteBufHolder
ByteBuf优点:
- 他可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 容量可以按需增长
- 在读写两种模式之间切换不需要调用ByteBuffer的flip()方法
- 读写使用不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
ByteBuf——数据容器
工作原理
ByteBuf维护两个索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,readIndex会被递增已经被读取的字节数,同样的,当向ByteBuf中写入数据时,writeIndex也会被递增。
如果readIndex和writeIndex处于同样的位置,再次尝试读取数据将会触发IndexOutOfBoundsException
名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开头的操作则不会。
名称以set或者get开头的方法会有一个索引位置参数,将会在该索引位置上进行set或get操作
ByteBuf可以指定最大容量。如果写索引超过这个值会触发异常IllegalArgumentException。默认的最大值是Integer.MAX_VALUE
ByteBuf使用模式
堆缓冲区
将数据存储在JVM的对空间中,这种模式又被成为支撑数组。它能在没有使用池化的情况下提供快速的分配和释放。
当hasArray方法返回false时,尝试访问支撑数组将触发UnsupportedOperationException异常。
直接缓冲区
NIO引入的ByteBuffer类允许JVM实现通过本地调用来分配内存,这样可以避免在每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
直接缓冲区的主要特点是,分配和释放都比较昂贵。
复合缓冲区
它为多个ByteBuf提供了一个聚合视图。可以根据需要添加或者删除ByteBuf实例。Netty通过CompositeByteBuf(ByteBuf的子类)实现这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。
CompositeByteBuf中的ByteBuf实例可能同时包含直接内存分配和非直接内存分配。如果只有一个ByteBuf实例,那么CompositeByteBuf上的hasArray方法将返回该ByteBuf上的hasArray方法的值,否则将返回false。
CompositeByteBuf不支持访问支撑数组,因此访问CompositeByteBuf中的数据类似于访问直接缓冲区的模式。
Netty使用CompositeByteBuf来优化套接字I/O操作,尽可能消除由JDK的缓冲区实现所导致的性能以及内存使用率的惩罚,这种优化发生在Netty的核心代码中,不会被暴露出来。
字节级操作
随机访问索引
ByteBuf的索引是从0开始:第一个字节的索引就是0,最后一个字节的索引是capacity() - 1。
如果方法中有一个索引值参数,通过该方法访问数据既不会改变readIndex也不会改变writeIndex。
如果有需要,可以通过调用readIndex(index)或者writeIndex(index)手动移动两者。
顺序访问索引
ByteBuf内部分段示意图如下:
可丢弃字节
在上图中可丢弃字节指的就是已被读取过的字节,通过调用discardReadBytes()方法,可以丢弃它们并回收空间。可丢弃字节分段的初始大小为0,即readIndex,该值会随着read操作的执行而增加(get*操作不会移动readIndex)。
discardReadBytes()方法只是移动了可以读取的字节以及writeIndex,而没有对所有可写入的字节进行擦除写。
discardReadBytes()会导致内存复制,因为可读字节必须要移动到缓冲区开始的位置。
可读字节
可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认readINdex值为0。任何名称以read或者skip开头的操作都将检索或者跳过位于当前readIndex的数据,并将它增加已读字节数。
如果被调用的方法需要一个ByteBuf参数作为写入的目标,并且没有指定目标索引参数,那么该写入的目标的writeIndex也将被增加。
可写字节
可以字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writeIndex的默认值为0。任何以write开头的方法都将从当前的writeIndex开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是ByteBuf,并且没有指定源索引的值,则源缓冲区的readerIndex也会被增加相同的大小。
索引管理
JDK的InputStream定义了mark(int readLimit)和reset()方法,这些方法分别被用来将流中的当前位置标记为指定的值,以及将流重置到该位置。
在ByteBuf中,可以调用markReadIndex()、markWriteIndex()、resetReaderIndex()、resetWriterIndex()来标记和重置ByteBuf的readIndex和writeIndex,不过在ByteBuf中没有readLimit参数指定标记啥时候失效。
在ByteBuf中,也可以通过调用readerIndex(int)或者writeIndex(int)来将索引移动到指定位置。
在ByteBuf中,可以通过clear()方法将readerIndex和writeIndex都设置为0,但是不会清楚内存中的内容。调用clear()方法比调用discardReadBytes()更加轻量,因为clear只是重置索引,不会复制任何的内存。
查找操作
最简单的确定值的索引的方法是indexOf()。较复杂的查找可以通过一个ByteProcessor(Netty4.1版本以上,旧的版本采用ByteBufProcessor)参数达成。
派生缓冲区
派生缓冲区为ByteBuf提供了以专门的方式来呈现其内容的视图,这类视图的创建方式主要有以下几种:
- duplicate()
- slice()
- slice(int, int)
- Unpooled.unmodifiableBuffer(...)
- order(ByteOrder)
- readSlice(int)
以上方法都会返回一个新的ByteBuf实例,它具有自己的读索引、写索引和标记索引。它会和源实例共享内存,因此创建成本低廉,但是如果修改它的内容,也就意味着修改了对应的源实例。
如果需要一个现有缓冲区的真实副本,需要使用copy()或者copy(int,int)方法
读/写操作
读写操作主要分为两类:
- get()和set()操作,从给定的索引开始,并且保持索引不变
- read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
方法名称 | 描述 |
---|---|
getBoolean(int) | 返回给定索引处的Boolean值 |
getByte(int) | 返回给定索引处的字节 |
getUnsignedByte(int) | 将给定索引处的无符号字节值作为short返回 |
getMedium(int) | 返回给定索引处的24位的中等int值 |
getUnsignedMedium(int) | 返回给定索引处的无符号的24位的中等int值 |
getInt(int) | 返回给定索引处的int值 |
getUnsignedInt(int) | 将给定索引处的无符号int值作为long返回 |
getLong(int) | 返回给定索引处long值 |
getShort(int) | 返回给定索引处的short值 |
getUnsignedShort(int) | 将给定索引处的无符号short值作为int返回 |
getByte(int, ...) | 将该缓冲区中从给定索引开始的数据传送到指定的目的地 |
方法名称 | 描述 |
---|---|
setBoolean(int, boolean) | 设定给定索引处的Boolean值 |
setByte(int index, int value) | 设定给定索引处的字节值 |
setMedium(int index, int value) | 设定给定索引处的24位的中等int值 |
setInt(int index, int value) | 设定给定索引处的int值 |
setLong(int index, long value) | 设定给定索引处的long值 |
setShort(int index, int value) | 设定给定索引处的short值 |
方法名称 | 描述 |
---|---|
readBoolean() | 返回当前readIndex处的Boolean值,并将readIndex增加1 |
readByte() | 返回当前readIndex处的字节,并将readIndex增加1 |
readUnsignedByte() | 将当前readIndex处的无符号字节值作为short返回,并将readIndex增加1 |
readMedium() | 返回当前readIndex处的24位的中等int值,并将readIndex增加3 |
readUnsignedMedium() | 返回当前readIndex处的24位的无符号的中等int值,并将readIndex增加3 |
readInt() | 返回当前readIndex处的int值,并将readIndex增加4 |
readUnsignedMedium() | 返回当前readIndex处的24位的无符号的中等int值,并将readerIndex增加3 |
readInt() | 返回当前readIndex处的int值,并将readerIndex增加4 |
readUnsignedInt() | 将当前readerIndex处的无符号的int值作为long值返回,并将readIndex增加4 |
readLong() | 返回当前readIndex处的long值,并将readIndex增加8 |
readShort() | 返回当前readIndex处的short值,并将readIndex增加2 |
readUnsignedShort() | 将当前readIndex处的无符号short值作为int值返回,并将readIndex增加2 |
readBytes(ByteBuf byte[] destination, int dstIndex, [, int length]) | 将当前ByteBuf中从当前readIndex处开始的(如果设置了,length长度的字节)数据传送到一个目标ByteBuf或者byte[],从目标的dstIndex开始的位置。本地的readIndex将被增加已经传输的字节数。 |
方法 | 描述 |
---|---|
writeBoolean(boolean) | 在当前writeIndex处写入一个boolean值,并将writeIndex增加1 |
writByte(byte) | 在当前writeIndex处写入一个字节值,并将writeIndex增加1 |
writeMedium(int) | 在当前writeIndex处写入一个中等的int值,并将writeIndex增加3 |
writeInt(int) | 在当前writeIndex处写入一个int值,并将writeIndex增加4 |
writeLong(long) | 在当前writeIndex处写入一个long值,并将writeIndex增加8 |
writeShort(int) | 在当前writeIndex处写入一个short值,并将writeIndex增加2 |
writeBytes(source ByteBuf byte[] [,int srcIndex,int length]) | 从当前writeIndex开始,传输来自于指定源(ByteBuf或者byte[])的数据。如果提供了srcIndex和length,则从srcIndex开始读取,并且处理长度为length的字节。当前writeIndex将会被增加所写入的字节数。 |
方法 | 描述 |
---|---|
isReadable() | 如果至少有一个字节可供读取,则返回true |
isWritable() | 如果至少有一个字节可被写入,则返回true |
readableBytes() | 返回可被读取的字节数 |
writableBytes() | 返回可被写入的字节数 |
capacity() | 返回ByteBuf可容纳的字节数。在此之后,它会尝试再次扩展直到达到maxCapacity() |
maxCapacity() | 返回ByteBuf可以容纳的最大字节数 |
hasArray() | 如果ByteBuf由一个字节数组支撑,则返回true |
array() | 如果ByteBuf由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException异常 |
ByteBufHolder接口
ByteBufHolder为Netty的高级特性提供了支持,如缓冲区池化,其中可以从池中借用ByteBuf,并且在需要时自动释放。
名称 | 描述 |
---|---|
content() | 返回由这个ByteBufHolder所持有的ByteBuf |
copy() | 返回这个ByteBufHolder的一个深拷贝,包括一个其所包含的ByteBuf的非共享拷贝 |
duplicate() | 返回这个ByteBufHolder的一个浅拷贝,包括一个其所包含的ByteBuf的共享拷贝 |
ByteBuf分配
按需分配:ByteBufAllactor接口
为了降低分配和释放内存的开销,Netty通过ByteBufAllactor接口实现了(ByteBuf的)池化,它可以用来分配我们所描述过的任意类型的ByteBuf实例。
名称 | 描述 |
---|---|
buffer() buffer(int initialCapacity) buffer(int initialCapacity, int maxCapacity) | 返回一个基于堆或者直接内存存储的ByteBuf |
heapBuffer() heapBuffer(int initialCapacity) heapBuffer(int initialCapacity, int maxCapacity) | 返回一个基于堆内存存储的ByteBuf |
directBuffer() directBuffer(int initialCapacity) directBuffer(int initialCapacity, int maxCapacity) | 返回一个基于直接内存存储的ByteBuf |
compositeBuffer() compositeBuffer(int maxNumComponents) compositeDirectBuffer() compositeDirectBuffer(int maxNumComponents) compositeHeapBuffer() compositeHeapBuffer(int maxNumComponents) | 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf |
ioBuffer() | 返回一个用于套接字的I/O操作的ByteBuf |
Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和ByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度的减少内存碎片,后者的实现不池化ByteBuf实例,并且每次被调用时都会返回一个新的实例。
Netty4.1以后默认使用PooledByteBufAllocator
Unpooled缓冲区
Netty提供了一个简单的成为Unpooled的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf实例。
方法 | 描述 |
---|---|
buffer() buffer(int initialCapacity) buffer(int initialCapacity, int maxCapacity) | 返回一个未池化的基于堆内存存储的ByteBuf |
directBuffer() directBuffer(int initialCapacity) directBuffer(int initialCapacity, int maxCapacity) | 返回一个未池化的基于直接内存存储的ByteBUf |
wrappedBuffer() | 返回了一个包装了给定数据的ByteBuf |
copiedBuffer() | 返回了一个复制了给定数据的ByteBuf |
ByteBufUtil类
ByteBufUtil提供了用于操作ByteBuf的静态的辅助方法。
方法 | 描述 |
---|---|
hexdump() | 以十六进制的表示形式打印ByteBuf的内容 |
equals(ByteBuf, ByteBuf) | 用来判断两个ByteBuf实例的相等性 |
引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能技术。
Netty为ByteBuf和ByteBufHolder引入了引用计数技术,它们实现了ReferenceCounted接口。
试图访问一个已经被释放的引用计数的对象,将会导致一个IllegalReferenceCountException