Netty 核心部件:Buffer 缓冲器
JDK NIO 提供了 ByteBuffer 作为字节数据容器,但是实际使用过程会比较繁琐,特别是读写切换;Netty 提供了自己的缓冲区实现 BytedBuf ,用于简化字节缓冲区的使用;
JDK ByteBuffer 和 Netty ByteBuf 一个很大的区别是:JDK ByteBuffer 有 4 个索引(mark、position、limit、capcity),其中使用 position、limit 这2个索引控制读写操作,而 Netty 使用 readerIndex、writerIndex 这2个索引代替,Netty BytedBuf 中各个索引的关系如下:
- 索引关系:0 <= readerIndex <= writerIndex <= capacity;
- 初始值:0 = readerIndex = writerIndex
①字节,已读字节,可以被遗弃;
②字节,可读字节,即 readable bytes;
③字节,可写入字节,即 writeable bytes;
由于使用 readerIndex、writerIndex 索引控制读写,因此 Netty ByteBuf 在读写切换时并不需要像 JDK ByteBuffer 那样需要频繁地调用 filp() 方法;
ByteBuf 的工作模式
在 Netty 4.X,字节数据容器 ByteBuf 拥有 3 种使用模式:
heap-buffer、direct-buffer、composite-buffer;
Heap-Buffer(堆缓冲区)
这是最常用的 ByteBuf 工作模式,Heap-Buffer 将字节数据储存在 JVM 堆空间(JVM-Heap),堆空间可以快速分配,当不使用时可以快速释放,同时还提供了直接访问缓冲区直接数组的方法(
ByteBuf.array() );
这种工作模式类似于 JDK ByteBuffer 的工作模式;
※ 访问非堆缓冲区 ByteBuf 的数组会导致
UnsupportedOperationException,可以使用
ByteBuf.hasArray() 来检查缓冲区是否支持访问数组;
Direct-Buffer(直接缓冲区)
Direct-Buffer 是基于 JDK 1.4 引入 NIO 的 ByteBuffer,允许 JVM 通过本地方法调用分配内存,缓冲区数据不直接储存在 JVM Heap 上,带来的优点如下:
- 免去中间的内存拷贝,提升 IO 处理速度(Direct-Buffer 中的内容可以驻留在垃圾回收扫描的 );
- Direct-Buffer 在 -XX:MaxDirectMemorySize=xxM 大小限制下,使用 Heap 之外的内存,JVM GC 无法直接操作,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响;
Direct-Buffer 对于通过 socket 实现传输数据的应用来说,是一种十分理想的数据储存方式。如果数据是储存在 Heap 中分配的缓冲区,那么实际上在通过 socket 发送数据之前,JVM 需要先将数据复制到直接缓冲区,这样需要多一次缓冲区数据内容的拷贝;
但是 Direct-Buffer 也带来了以下的缺点:
- Direct-buffer 在内存空间的分配、释放上比 Heap-Buffer 更加复杂;
- 如果要将 Direct-Buffer 容器数据传递给其他代码处理,因为数据不是在 Heap 上,此时往往需要拷贝一个副本,如下:
※
DirectBuffer.hasArray() 永远为 false,如上,因为字节缓存区的数组不储存在 JVM Heap 上;
Composite-Buffer(复合缓冲区)
Composite-Buffer 复合缓冲区是 Netty 自己实现的缓冲区工作模式,
JDK 的 ByteBuffer 没有这样的功能;
可以创建多个不同的 ByteBuf,然后提供一个这些 ByteBuf 组合的视图,即复合缓冲区,复合缓冲区就像一个列表,允许动态的添加和删除其中的 ByteBuf;
Netty 提供了 ByteBuf 的子类 CompositeByteBuf 类来处理复合缓冲区,CompositeByteBuf 只是一个视图;
Composite-Buffer 一个典型的使用是储存 http message ,一般一条 http 消息 会包含 header 和 body,如果需要发送的若干消息 body 相同,只是 header 不同,使用 CompositeBuffer 就不需要每次都为一条新的消息分配一个新的缓冲区,如果使用 JDK NIO 来实现这个过程,会见 body 和 header 合并为一个缓冲区,在数组的复制和操纵上很不方便;
实际上,可以把 CompositeByteBuf 当作一个可迭代遍历的缓冲区容器,不过 CompositeByteBuf 不允许直接访问内部缓存区的数据,需要手动拷贝,类似于 Direct-Buffer,如下:
※CompositeByteBuf.hasArray() 总是返回 false,因为它可能既包含堆缓冲区,也包含直接缓冲区;
ByteBuf 的内存分配方式
ByteBuf 实例主要有以下 3 种内存分配方式,即创建实例的方式:
ByteBufAllocator
Netty 中的池类 ByteBufAllocator 可以用于分配缓存资源,由于使用了一个资源池来对 ByteBuf 进行管理,能极大地
减少分配和释放内存的开销;
常用的相关 API 如下:
buffer([int initalCapacity [ ,int maxCapacity]]) |
返回一个 direct-buffer 或 heap-buffer ,取决于具体实现,可以指定初始容量、最大容量; |
heapBuffer() |
返回一个 heap-buffer,同样可以如上指定初始容量、最大容量; |
directBuffer() |
返回一个 direct-buffer; |
compositeBuffer([int maxNumComposite]) |
返回一个 composite-buffer,可以指定最大组件数量; |
heapCompositeBuffer() |
返回一个 composite-buffer,其中的组件都为 heap-buffer; |
directCompositeBuffer |
返回一个 composite-buffer,其中的组件都为 direct-buffer; |
ioBuffer() |
返回一个适合 socket I/O 传输的 direct-buffer; |
创建 ByteBufAllocator 有以下 2 种方式:
Unpooled
Netty 提供了工具类 Unpooled,,它提供了静态辅助方法来创建非池化的 ByteBuf 实例,用于快速创建 ByteBuf 实例:
常用的相关 API 如下:
buffer([int initalCapacity [ ,int maxCapacity]]) |
返回一个 direct-buffer 或 heap-buffer ,取决于具体实现,可以指定初始容量、最大容量; |
directBuffer() |
返回一个 direct-buffer,同样可以如上指定初始容量、最大容量; |
wrappedBuffer(byte[] array) |
包裹指定的数组,创建一个新的 ByteBuf; |
compositeBuffer() |
返回一个 cpmposite-buffer,可以指定最大组件数量; |
copiedBuffer(byte[] array / ByteBuf buffer) |
复制制定的数组或 bytebuf,创建一个新的 ByteBuf; |
ByteBufUtil
ByteBufUtil 提供了一系列辅助方法用于操纵 ByteBuf,用于子串提取,ByteBuf 比较,ByteBuf 2进制与8进制、16进制转换;
如常用的
equals(ByteBuf, ByteBuf) 用于
比较两个 ByteBuf 是否一致;
详见: http://netty.io/4.0/api/index.html
ByteBuf 的字节级别操作
读写操作
读/写操作主要由2类:
- get()/set() :从给定的索引开始,保持不变
- read()/write() :从给定的索引开始,与字节访问的数量来适用,递增当前的写索引或读索引;
get / set 方法不会影响原来的 readerIndex、writerIndex;
当每次调用 read 方法时,readerIndex 会做出相应的移动,同时 writerIndex 如果小于 readerIndex 时,会更新到和 readerIndex 同样的位置;
当每次调用 wirteIndex 方法时,writerIndex 会做出相应的移动,readerIndex 不会被影响;
readerIndex 、writerIndex 索引将整个 ByteBuf 划分为以下 3 部分:
常用的 API 如下:
getByte(int index),getBytes(int index, byte[] dest) getInt(int), getFloat(int), getChar(int) ..... |
获取指定下标索引的字节,字节子串,int 值,float 值,char 值等等; |
setByte(int index, int value),setBytes(int index, byte[] src) setInt(int,int), setFloat(int,float), setChar(int,int) ..... |
设置指定下标的值; |
readByte(),readBytes(byte[] dst) readInt(),readFloat(), readChar() |
读取当前 readerIndex 的字节,字节组,int 值,float 值,char 值等等; |
writeByte(int value), writeBytes(byte[] src / InputStream in, int length) writeChar(int), writeFloat(float) .... |
在当前 writerIndex 写入相应值; |
示例如下:
索引管理
Netty 提供了以下 API 来对 readerIndex、 writerIndex 索引进行控制;
int readerIndex() / int writerIndex() |
获取当前的 readerIndex、writerIndex; |
readerIndex(int index) / writerIndex(int index) |
设置当前的 readerIndex、writerIndex; |
markReaderIndex() / markWriterIndex() |
在当前的 readerIndex、writerIndex 设置 mark 索引; |
resetReaderIndex() / resetWriterIndex() |
将 readerIndex、writerIndex 重设到之前的 mark 位置; |
clear() |
将 readerIndex、writerIndex 都设置为 0,但不会清除内存中的内容; |
查询操作
如果要确定指定值在缓冲器中的索引,除了可以使用
indexOf() 方法之外,还可以通过
forEachByte() 来对 ByteBuf 中的每一个字节进行遍历;
获取缓冲区视图
ByteBuf 视图用于展示 ButeBuf 内容,即衍生缓冲区,Netty 提供了一系列用户获取 ByteBuf 视图的方法,这些方法都返回一个新的 ByteBuf 实例,包括独立的 readerIndex、writerIndex,但是共享内部储存数据,这使得使用衍生缓冲区的创建、修改内容代价更低;
这些方法 API 包括:
duplicate() |
获取整个 ByteBuf 的视图; |
slice() |
获取 ByteBuf 可读部分的视图(readerIndex - writerIndex) |
order(endianness) |
获取 ByteBuf 相应字节顺序的视图; |
其他 API
boolean isReadable() |
返回当前 ByteBuf 是否有可读字节; |
boolean isWritable() |
返回当前 ByteBuf 是否有可写字节; |
int readableBytes() |
返回可读字节数量(writerIndex - readerIndex); |
int writablesBytes() |
返回可写字节数量(capacity - writerIndex); |
int capacity() |
返回当前 ByteBuf 的容量; |
int maxCapacity() |
返回最大容量; |
boolean hasArray() |
检查当前 ByteBuf 是否支持数组获取(只有 heap-buffer 可以); |
byte[] array() |
获取当前 ByteBuf 的数组,常常和以上方法配合使用; |
ByteBufAllocator alloc() |
获取当前 ByteBuf 所归属的 ByteBufAllocator(如果该 ByteBuf 是由缓存池分配的话); |
引用计数器
Netty 4.x 中为 ByteBuf 和 ByteBufHolder 引入了引用计数器,它们都是实现了 ReferenceCounted 接口;
引用计数器够在特定的对象上跟踪引用的数目,实现了ReferenceCounted 的类的实例会通常开始于一个活动的引用计数器为 1,而如果对象活动的引用计数器大于0,就会被保证不被释放,当数量引用减少到0,将释放该实例;
这种技术就是诸如 PooledByteBufAllocator 这种减少内存分配开销的核心实现;
可以通过 refCnt() 来获取引用计数器的引用计数,
release()来手动强制释放引用计数器;