netty基础04_数据缓冲区

网络数据以字节byte传输;
通常会使用缓冲区来作为字节的容器;例如:byte数组、nio使用的ByteBuffer;
netty也提供了类似的数据缓冲api;
 
Netty 的数据处理 API :
    abstract class ByteBuf 
    interface ByteBufHolder
 
ByteBuf的优点:
    它可以被用户自定义的缓冲区类型扩展;
    通过内置的复合缓冲区类型实现了透明的零拷贝;
    容量可以按需增长(类似于 JDK 的 StringBuilder);
    在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
    读和写使用了不同的索引;
    支持方法的链式调用;
    支持引用计数;
    支持池化。
 
1.ByteBuf
ByteBuf的特点:
    1】ByteBuf工作机制:ByteBuf维护了两个不同的索引,一个用于读取(readerIndex),一个用于写入(writerIndex);
        readerIndex和writerIndex的初始值都是0,当从ByteBuf中读取数据时,它的readerIndex将会被递增(它不会超过writerIndex),当向ByteBuf写入数据时,它的writerIndex会递增。
    2】名称以readXXX或者writeXXX开头的ByteBuf方法,会推进对应的索引,而以setXXX或getXXX开头的操作不会。
    3】在读取之后,缓冲区0~readerIndex的就被视为可丢弃字节,调用discardReadBytes方法,可以释放这部分空间;
    4】readerIndex和writerIndex之间的数据是可读取的,writerIndex和最大容量之间的空间是可写的;
 
ByteBuf类似于一个字节数组,只不过有更加强大的功能;
可以通过读索引和写索引来控制对缓冲区数据的访问;
 
例如:最大容量为16个字节的空ByteBuf
 
可以给ByteBuf指定最大容量;
    指定最大容量后,超出该容量的写入操作将报错;
    最大容量默认为 Integer.MAX_VALUE;
 
ByteBuf中有两个索引:读索引 readerIndex、写索引 writerIndex
当写入数据到ByteBuf后,writerIndex的值增加写入的字节数;
当从ByteBuf中读取数据后,readerIndex的值增加读取的字节数;
readerIndex的值不能超过writerIndex,否则会报数组越界错误;
比如:缓冲区中写入了5个字节时,不能读超过5个字节的数据,因为此时从第6个字节开始都不是正确的;
 
2. ByteBuf 的使用模式
ByteBuf的模式: HEAP BUFFER(堆缓冲区)、 DIRECT BUFFER(直接缓冲区)、 COMPOSITE BUFFER(复合缓冲区)
 
1)HEAP BUFFER(堆缓冲区)
最常用的模式;
模式是将数据存储在 JVM 的堆空间中;
这种模式下的ByteBuf会有一个支持数组;
 
实例代码:
ByteBuf heapBuf = ...;
if (heapBuf.hasArray()) { //1、检查 ByteBuf 是否有支持数组
    byte[] array = heapBuf.array(); //2、如果有的话,得到引用数组
    int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); //3、计算第一字节的偏移量
    int length = heapBuf.readableBytes();//4、获取可读的字节数
    handleArray(array, offset, length); //5、使用数组,偏移量和长度作为调用方法的参数
}
注意: 访问非堆缓冲区 ByteBuf 的数组会导致UnsupportedOperationException,
可以使用ByteBuf.hasArray()来检查是否支持访问数组。
 
2)DIRECT BUFFER(直接缓冲区)
数据保存在调用本地方法申请的内存中;
JDK1.4 中被引入 NIO 的ByteBuffer 类允许 JVM 通过本地方法调用分配内存;
 
优点:
     1】通过免去中间交换的内存拷贝, 提升IO处理速度; 
         如果数据存放在堆中分配的缓冲区,在通过 socket 发送数据之前,JVM 需要将先数据复制到直接缓冲区。
     2】可以避免垃圾回收对速度影响
         DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力“;
         也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响;   
 
缺点:
    1】内存的空间分配和释放复杂;
    2】不适合处理数据 
处理数据时,需要在堆上拷贝一个副本 ;:
ByteBuf directBuf = ...
if (!directBuf.hasArray()) { //1、检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。
    int length = directBuf.readableBytes();//2、获取可读的字节数
    byte[] array = new byte[length]; //3、分配一个新的数组来保存字节
    directBuf.getBytes(directBuf.readerIndex(), array); //4、字节复制到数组
    handleArray(array, 0, length); //5、将数组,偏移量和长度作为参数调用某些处理方法
}
使用直接缓冲区,但需要处理数据,将数据拷贝了一份保存在堆中的byte数组中;
比直接使用堆缓冲区多了一步操作;
 
3)COMPOSITE BUFFER(复合缓冲区)
多个ByteBuf的组合视图;
CompositeByteBuf 中的 ByteBuf 实例可能同时包含直接内存分配和非直接内存分配;
可以动态添加、删除其中的ByteBuf;
Netty 提供了 ByteBuf 的子类 CompositeByteBuf 类来处理复合缓冲区;
 
复合缓冲区中对ByteBuf的增删查:
CompositeByteBuf messageBuf = ...;
ByteBuf headerBuf = ...; // 可以支持或直接
ByteBuf bodyBuf = ...; // 可以支持或直接
messageBuf.addComponents(headerBuf, bodyBuf);//在复合缓冲区中添加ByteBuf
// ....
messageBuf.removeComponent(0); // 移除headerBuf 
for (int i = 0; i < messageBuf.numComponents(); i++) { //3遍历复合缓冲区中的ByteBuf
    System.out.println(messageBuf.component(i).toString());
}
 
复合缓冲区访问数据:
CompositeByteBuf compBuf = ...;
int length = compBuf.readableBytes(); //1、得到的可读的字节数
byte[] array = new byte[length]; //2、分配一个新的数组,数组长度为可读字节长度
compBuf.getBytes(compBuf.readerIndex(), array); //3、读取字节到数组
handleArray(array, 0, length); //4、使用数组,把偏移量和长度作为参数
 
3.字节操作
ByteBuf除了基本的读写操作外,还可以通过字节操作来对其中的数据做修改;
 
1)随机访问索引
ByteBuf的随机访问索引从0开始:第一个字节的索引是0,最后一个字节的索引总是 capacity() - 1
ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i++) {
    byte b = buffer.getByte(i);
    System.out.println((char) b);
}
 
使用索引访问数据时不会推进读写索引;
    例如:getByte(i)并不会将读索引推进到索引i处;
如果想推进读写索引,可以使用 ByteBuf 的 readerIndex(index) 或 writerIndex(index);
 
2)可丢弃字节
ByteBuf中的数据可分为三部分:
    1】字节,可以被丢弃,因为它们已经被读
    2】还没有被读的字节是:“readable bytes(可读字节) ”
    3】空间可加入多个字节的是:“writeable bytes(写字节) ”
对于可丢弃字节的部分,可以调用 discardReadBytes() 来回收空间;
回收可丢弃字节后的结果:
    读索引置0;
    写索引减小;
    可丢弃字节消失;
    可读字节容量不变;
    可写字节容量变大;
清除已读区域使用了内存复制,会影响性能,一般在内存宝贵时才使用;
 
3)可读字节
ByteBuf的读索引 readerIndex 的默认值为 0;
readxxx、skipxxx方法会根据处理的字节数增加读索引readerIndex的值;
 
如果读操作是一个指定 ByteBuf 参数作为写入的对象时, 目标缓冲区的 writerIndex 也会增加;
例如:
src.readBytes(ByteBuf dest);
从缓冲区src中读取数据,并写入dest;
此时src的读索引会增加,dest的写索引也会增加相同的值;
如果试图从缓冲器读取已经用尽的可读的字节,则抛出IndexOutOfBoundsException。
 
遍历可读字节
ByteBuf buffer= ...;
while (buffer.isReadable()) {
    System.out.println(buffer.readByte());
}
 
4)可写字节
一个新分配的缓冲区的 writerIndex 的默认值是 0 。
writexxx方法当前的 writerIndex 写入数据时,writerIndex递增字节写入的数量。
 
如果写操作的目标也是 ByteBuf ,且未指定源索引,则源缓冲区的 readerIndex 将增加相同的量。
例如:
src.writeBytes(ByteBuf dest);
从缓冲区dest中读取数据写入缓冲区src中;
src的写索引增加,dest的读索引也增加相同的值;
 
如果试图写入的字节数超出可写字节的容量,则抛出 IndexOutOfBoundException
 
往可写字节中写入随机数:
        ByteBuf heapBuf = Unpooled.buffer(10);
        while (heapBuf.writableBytes() > 4) {
            heapBuf.writeInt(new Random().nextInt());
        }
 
5)索引管理
1】mark和reset
java的InputStream中有方法mark和reset,用来重新定位;
作用:
    调用mark方法会记下当前调用mark方法的时刻,InputStream被读到的位置。
    调用reset方法就会回到该位置。
例如:
 
String content = "BoyceZhang!";  
 InputStream inputStream = new ByteArrayInputStream(content.getBytes());  
   
 // 判断该输入流是否支持mark操作  
 if (!inputStream.markSupported()) {  
     System.out.println("mark/reset not supported!");  
 }  
 int ch;    
 boolean marked = false;    
 while ((ch = inputStream.read()) != -1) {  
       
     //读取一个字符输出一个字符    
     System.out.print((char)ch);    
     //读到 'e'的时候标记一下  
      if (((char)ch == 'e')& !marked) {    
         inputStream.mark(content.length());  //先不要理会mark的参数  
          marked = true;    
      }    
                   
      //读到'!'的时候重新回到标记位置开始读  
       if ((char)ch == '!' && marked) {    
           inputStream.reset();    
           marked = false;  
       }    
 }  
   
 //程序最终输出:BoyceZhang!Zhang!   
ByteBuf 读索引readerIndex 和 写索引writerIndex 通过调用markReaderIndex(), markWriterIndex(), resetReaderIndex() 和 resetWriterIndex()来设置和重新定位。
 
2】readerIndex(int) 或 writerIndex(int) 将读、写指针移动到指定的位置
3】clear()方法可以将读索引和写索引归0,该方法只改变指针位置,不涉及内存复制;
 
6)获取索引
ByteBuf中有多种可以用来确定指定值的索引的方法;
最简单的是使用indexOf()方法;
 
复制的查找可以使用ByteProcessor;
例如:查找ByteBuf中的回车符索引
ByteBuf buffer = ...;
int index = buffer.forEachByte(ByteProcessor.FIND_CR);
 
7)缓冲区视图
ByteBuf提供了一些方法来创建其内容的视图;
视图就是一个新的ByteBuf实例,有自己的读、写、标记索引;
视图的内存和源ByteBuf共享,意味着 如果你修改了它的内容,也同时修改了其对应的源实例;
如果要获得真实的ByteBuf副本,需要使用 copy()或者 copy(int, int)方法。
 
创建视图的方法:
    duplicate();
    slice();
    slice(int, int);
    Unpooled.unmodifiableBuffer(…);
    order(ByteOrder);
    readSlice(int)。
 
使用视图:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf sliced = buf.slice(0, 15);    //创建视图
System.out.println(sliced.toString(utf8));    //输出Netty in Action
 
buf.setByte(0, (byte)'J');    //修改源ByteBuf第一个字节:N->J
System.out.println(sliced.toString(utf8));    //视图ByteBuf引用的数据也会改变,输出Jetty in Action
 
使用copy复制:
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf copy = buf.copy(0, 15);    
System.out.println(copy.toString(utf8));    //输出Netty in Action
buf.setByte(0, (byte) 'J');
System.out.println(copy.toString(utf8));    //复制的ByteBuf是独立的实例,修改源ByteBuf不会对其造成影响,依然输出Netty in Action
 
8)ByteBuf的api
1】get、set操作
get、set操作不会改变ByteBuf的读写索引;
大多数get操作都有其对应的get操作;
 
get操作
 
set操作
 
2】读写操作
read操作,用于从 ByteBuf 中读取数据;
write方法,用于将数据追加到 ByteBuf 中; 几乎每个 read()方法都有对应的 write()方法;
读写操作会修改读写索引的值;
 
read
 
write
 
3】其它操作
 
4. ByteBufHolder
相当于一个ByteBuf的容器;
用来保存ByteBuf,以及一些额外需要的数据;
例如: HTTP 响应中与内容一起的字节的还有状态码, cookies,等。
ByteBufHolder常用方法:
 
5. ByteBuf 实例管理
ByteBuf的实例创建方式主要有: 分配器ByteBufAllocator、非池化缓存 Unpooled;
工具类ByteBufUtil可以用来操作ByteBuf实例;
 
1)ByteBufAllocator
Netty 通过 interface ByteBufAllocator 实现了ByteBuf 的池化;
 
常用的方法:
 
可以通过Channel或者ChannelHandlerContext的alloc()方法获取ByteBufAllocator的引用:
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); //1、从 channel 获得
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc(); //2、从 ChannelHandlerContext 获得
 
Netty提供了两种ByteBufAllocator的实现: PooledByteBufAllocator和UnpooledByteBufAllocator。
1】PooledByteBufAllocator
    netty默认使用的ByteBufAllocator实现为PooledByteBufAllocator;
    PooledByteBufAllocator用ByteBuf实例池来减小内存的使用;
    也就是说使用池化的ByteBuf不需要每次新建一个ByteBuf实例;
    如果不想用PooledByteBufAllocator,可以通过ChannelConfig或通过bootstrap引导设置一个不同的实现来改变;
2】UnpooledByteBufAllocator
     实现非池化ByteBuf实例, 在每次它被调用时都会返回一个新的实例
 
2) Unpooled
可能某些情况下,无法获取到ByteBufAllocator 的引用;
对于这种情况netty提供了Unpooled;
Unpooled是一个用来创建非池化ByteBuf实例的工具类;
 
常用方法:
 
3) ByteBufUtil
ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法;
常用方法:
     hexDump()    -》 返回指定 ByteBuf 中可读字节的十六进制字符串,可以用于调试程序时打印 ByteBuf 的内容;十六进制比二进制更方便操作;
     boolean equals(ByteBuf, ByteBuf)    -》 用来比较 ByteBuf 实例是否相等;
 
6.引用计数器
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。
引用计数原理:
     跟踪到某个特定对象的活动引用的数量;
     一个 ReferenceCounted 实现类的实例将通常以活动的引用计数为 1 作为开始;
     只要引用计数大于 0, 就能保证对象不会被释放;
     当活动引用的数量减少到 0 时,该实例就会被释放;
 
ByteBuf 和 ByteBufHolder实现了 ReferenceCounted接口;
常用方法:
     refCnt()    -》返回计数器值
     release()    -》用来减少计数器值,释放实例对象; 当减少到 0时该对象被释放, 并且该方法返回 true
如果尝试访问已经释放的对象,将会抛出 IllegalReferenceCountException 异常;
 
获取引用计数器值:
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ByteBuf buffer = allocator.directBuffer();
System.out.println("当前ByteBuf引用计数值为:"+ buffer.refCnt());

 

 
 
 
posted @ 2020-05-29 16:41  L丶银甲闪闪  阅读(533)  评论(0编辑  收藏  举报