Netty(2):核心部件:Buffer 缓冲器

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 
Netty(2):核心部件:Buffer 缓冲器_第1张图片
①字节,已读字节,可以被遗弃;
②字节,可读字节,即 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 heapBuf = ...;
if (heapBuf.hasArray()) {                
    byte[] array = heapBuf.array();            //获取缓冲区字节数组
    int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();     //计算可读第一字节的偏移量
    int length = heapBuf.readableBytes();    //获取缓冲区可读字节数
    handleArray(array, offset, length);      //使用缓冲区字节数组、可读第一字节偏移量、可读字节长度作为调用方法的参数
※ 访问非堆缓冲区 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 上,此时往往需要拷贝一个副本,如下:
 
ByteBuf directBuf = ...
if (!directBuf.hasArray()) {            
    int length = directBuf.readableBytes();      //获取直接缓冲区可读字节数
    byte[] array = new byte[length];             //拷贝字节缓冲区字节数组内容的副本
    directBuf.getBytes(directBuf.readerIndex(), array);       
    handleArray(array, 0, length);             //将缓存区字节数组传递给其他代码
}
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 messageBuf = ...;
ByteBuf headerBuf = ...; 
ByteBuf bodyBuf = ...;
messageBuf.addComponents(headerBuf, bodyBuf);  //向复合缓冲区视图中添加 header、body 缓冲区
......
for (int i = 0; i < messageBuf.numComponents(); i++) {          //遍历复合缓冲区中的所有 ByteBuf 实例
    System.out.println(messageBuf.component(i).toString());
}
messageBuf.removeComponent(0);     //复合缓冲区视图中移除 header 缓冲区内容

实际上,可以把 CompositeByteBuf 当作一个可迭代遍历的缓冲区容器,不过 CompositeByteBuf 不允许直接访问内部缓存区的数据,需要手动拷贝,类似于 Direct-Buffer,如下:
 
CompositeByteBuf compBuf = ...;
int length = compBuf.readableBytes();     //获取复合缓冲区的可读字节长度
byte[] array = new byte[length];          //创建一个新的数组,并拷贝的复合缓冲区的数组副本;
compBuf.getBytes(compBuf.readerIndex(), array);    
handleArray(array, 0, length);            //传递缓冲区数组副本给其他代码

※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 种方式:
 
//方式1:通过 Channel 创建
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); 
//方式2:通过 ChannelHandlerContext 创建
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc(); 

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 部分:
Netty(2):核心部件:Buffer 缓冲器_第2张图片

常用的 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 写入相应值;
示例如下:
 
// get() / set() 示例
ByteBuf buf = Unpooled.copiedBuffer("Hello World!", Charset.forName("UTF-8"));    //复制一个 String 的所有字节创建一个 ByteBuf
System.out.println((char)buf.getByte(0));                //打印第一个字符    
buf.setByte(0, (byte)'B');                            //更新索引 0 的数据;
// read() / write() 示例
System.out.println((char)buf.readByte());         //读取当前 readerIndex 的值;
buf.writeByte((byte)'?');                         //在当前的 writeIndex 写入值;

索引管理

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 中的每一个字节进行遍历;
 
//使用 forEachByte 进行边界检查
ByteBuf buffer = ...;
int indexNull = buffer.forEachByte(ByteBufProcessor.FIND_NUL);   //寻找第一个 NULL 字符
int indexCR = buffer.forEachByte(ByteBufProcessor.FIND_CR);     //寻找第一个 \n 换行符

获取缓冲区视图

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()来手动强制释放引用计数器;
 
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); 
ByteBuf buffer = allocator.directBuffer();
assert(buffer.refCnt() == 1);          //获取buffer的引用计数,此时断言通过;
boolean released = buffer.release();   //将buffer上的引用计数递减,当该计数为0时,返回 true;







你可能感兴趣的:(Netty,Netty,4.x,使用入门)