zoukankan      html  css  js  c++  java
  • Netty之Unpooled_Bytebuf

    前言

    计算机存储基本单位是字节(byte),传输基本单位是bit(位),JAVA NIO提供了ByteBuffer等七种容器来提升传输时的效率,但是在使用时比较复杂,经常要进行读写切换,主要缺点如下:

    (1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的对象大于ByteBuffer的容量时,会发生索引越界异常;

    (2)ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;

    (3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

    为了弥补这些不足,Netty提供了自己的ByteBuffer实现—— ByteBuf

    ByteBuf介绍

    ByteBuf是Netty.Buffer中的类,主要特征如下:

    1. 读和写用不同的索引。
    2. 读和写可以随意的切换,不需要调用flip()方法。
    3. 容量能够被动态扩展,和StringBuilder一样。
    4. 用其内置的复合缓冲区可实现透明的零拷贝。
    5. 支持方法链。
    6. 支持引用计数。count == 0,release。
    7. 支持池。

    ByteBuf通过两个位置指针来协助缓冲区的读写操作,分别是读操作(使用readerIndex)、写操作(使用writerIndex)。

    readerIndex和writerIndex的取值一开始都是0,随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0~readerIndex的就被视为discard的,调discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法。readerIndex和writerIndex之间的数据是可读取的,等价于ByteBuffer position和limit之间的数据。writerIndex和capacity之间的空间是可写的,等价于ByteBuffer limit和capacity之间的可用空间。

    由于写操作不修改readerIndex指针,读操作不修改writerIndex指针,因此读写之间不再需要调整位置指针,这极大地简化了缓冲区的读写操作,避免了由于遗漏或者不熟悉flip()操作导致的功能异常。

    传统JAVA NIO ByteBuffer图示:

    ByteBuf读写示意图:

    ByteBuf零拷贝

    netty提供了CompositeByteBuf类实现零拷贝。大多数情况下,在进行网络数据传输时我们会将消息分为消息头head消息体body,甚至还会有其他部分,这里我们简单的分为两部分来进行探讨:

    以前的做法

    ByteBuffer header = ByteBuffer.allocate(1);
        header.put("a".getBytes()); 
        header.flip(); 
    ByteBuffer body = ByteBuffer.allocate(1);
        body.put("b".getBytes()); 
    	body.flip(); ByteBuffer message = ByteBuffer.allocate(header.remaining() + body.remaining()); 			message.put(header); 
        message.put(body);
        message.flip();
    while (message.hasRemaining()){
    		System.err.println((char)message.get());
            }
    

    这样为了得到完整的消息体相当于对内存进行了多余的两次拷贝,造成了很大的资源的浪费。

    netty提供的方法

    CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); 
    ByteBuf headerBuf = Unpooled.buffer(1); 
    	headerBuf.writeByte('a');
    ByteBuf bodyBuf = Unpooled.buffer(1);
    	bodyBuf.writeByte('b'); 
    	messageBuf.addComponents(headerBuf, bodyBuf); 
    for (ByteBuf buf : messageBuf) {
        System.out.println((char)buf.readByte()); 
        System.out.println(buf.toString());10        
    }
    

    这里通过CompositeByteBuf 对象将headerBuf 与bodyBuf组合到了一起,也得到了完整的消息体,但是并未进行内存上的拷贝。可以注意下我在上面代码段中进行的buf.toString()方法的调用,得出来的结果是:指向的还是原来分配的空间地址,也就证明了零拷贝的观点。

    ByteBuf引用计数

    retain和lock类似,release和unlock类似,内部维护一个计数器,计数器到0的时候就表示已经释放掉了。往一个已经被release掉的buffer中去写数据,会抛出IllegalReferenceCountException: refCnt: 0异常。

    ByteBuf buffer = Unpooled.buffer(1);
    int i = buffer.refCnt();         
    System.err.println("refCnt : " + i);    //refCnt : 1 
    buffer.retain(); 
    buffer.retain(); 
    buffer.retain(); 
    buffer.retain(); 
    i = buffer.refCnt();
    System.err.println("refCnt : " + i);      //refCnt : 5
    boolean release = buffer.release();
    i = buffer.refCnt();
    System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 4 ===== false
    release = buffer.release(4);
    i = buffer.refCnt();
    System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 0 ===== true
    

    引用计数器实现的原理并不复杂,仅仅只是涉及到一个指定对象的活动引用,对象被初始化后引用计数值为1。只要引用计数大于0,这个对象就不会被释放,当引用计数减到为0时,这个实例就会被释放,被释放的对象不应该再被使用。

    ByteBuf支持池

    Netty对ByteBuf的分配提供了池支持,具体的类是PooledByteBufAllocator。用这个分配器去分配ByteBuf可以提升性能以及减少内存碎片。Netty中默认用PooledByteBufAllocator当做ByteBuf的分配器。PooledByteBufAllocator对象可以从Channel中或者绑定了Channel的ChannelHandlerContext中去获取到。

    Channel channel = ...;
    ByteBufAllocator allocator = channel.alloc();
    ....
    ChannelHandlerContext ctx = ...;
    ByteBufAllocator allocator2 = ctx.alloc();
    

    ByteBuf API介绍

    创建ByteBuf

    // 创建一个heapBuffer,是在堆内分配的 
    ByteBuf heapBuf = Unpooled.buffer(5); 
    if (heapBuf.hasArray()) { 
        byte[] array = heapBuf.array(); 
        int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); 
        int length = heapBuf.readableBytes(); 
        handleArray(array, offset, length); 
    } 
    // 创建一个directBuffer,是分配的堆外内存
    ByteBuf directBuf = Unpooled.directBuffer();
    if (!directBuf.hasArray()) {
        int length = directBuf.readableBytes();
        byte[] array = new byte[length];
        directBuf.getBytes(directBuf.readerIndex(), array);
        handleArray(array, 0, length);
    }
    

    这两者的主要区别:

    a. 分配的堆外内存空间,在进行网络传输时就不用进行拷贝,直接被网卡使用。但是这些空间想要被jvm所使用,必须拷贝到堆内存中。
    b. 分配和释放堆外内存相比堆内存而言,代价是相当昂贵的。
    c. 使用这两者buffer中的数据的方式也略有不同,见上面的代码段。

    读写数据(readByte writeByte)

    ByteBuf heapBuf = Unpooled.buffer(5);
    heapBuf.writeByte(1);
    System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1
    heapBuf.readByte();
    System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1
    heapBuf.setByte(2, 2);
    System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1
    heapBuf.getByte(2);
    System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1
    

    进行readByte和writeByte方法的调用时会改变readIndex和writeIndex的值,而调用set和get方法时不会改变readIndex和writeIndex的值。上面的测试案例中打印的writeIndex和readIndex均为1,并未在调用set和get方法后被改变。

    discardReadBytes方法

    先看一张图:

    从上面的图中可以观察到,调用discardReadBytes方法后,readIndex置为0,writeIndex也往前移动了Discardable bytes长度的距离,扩大了可写区域。但是这种做法会严重影响效率,它进行了大量的拷贝工作。如果要进行数据的清除操作,建议使用clear方法。调用clear()方法将会将readIndex和writeIndex同时置为0,不会进行内存的拷贝工作,同时要注意,clear方法不会清除内存中的内容,只是改变了索引位置而已。

    Derived buffers

    这里介绍三个方法(浅拷贝):

    duplicate():直接拷贝整个buffer。
    slice():拷贝buffer中已经写了的数据。
    slice(index,length): 拷贝buffer中从index开始,长度为length的数据。
    readSlice(length): 从当前readIndex读取length长度的数据。

    上面这几个方法的虽然是拷贝,但是这几个方法并没有实际意义上去复制一个新的buffer出来,它和原buffer是共享数据的。所以说调用这些方法消耗是很低的,并没有开辟新的空间去存储,但是修改后会影响原buffer。这种方法也就是咱们俗称的浅拷贝
    要想进行深拷贝,这里可以调用copy()和copy(index,length)方法,使用方法和上面介绍的一致,但是会进行内存复制工作,效率很低。

    ByteBuf heapBuf = Unpooled.buffer(5); 
    heapBuf.writeByte(1); 
    heapBuf.writeByte(1); 
    heapBuf.writeByte(1); 
    heapBuf.writeByte(1); 
    // 直接拷贝整个buffer 
    ByteBuf duplicate = heapBuf.duplicate(); 
    duplicate.setByte(0, 2); 
    System.err.println("duplicate: " + duplicate.getByte(0) + "====heapBuf: " + heapBuf.getByte(0));//duplicate: 2====heapBuf: 2
    // 拷贝buffer中已经写了的数据
    ByteBuf slice = heapBuf.slice();
    System.err.println("slice capacity: " + slice.capacity());//slice capacity: 4
    slice.setByte(2, 5);
    ByteBuf slice1 = heapBuf.slice(0, 3);
    System.err.println("slice1 capacity: "+slice1.capacity());//slice1 capacity: 3
    System.err.println("duplicate: " + duplicate.getByte(2) + "====heapBuf: " + heapBuf.getByte(2));//duplicate: 5====heapBuf: 5复制代码
    
  • 相关阅读:
    Arch安装KDE5
    Gentoo解决Windows系统txt文本中文乱码问题
    用e2fsck修复受损的linux文件系统
    linux远程控制windows
    vim常用命令
    bash shell 常用快捷键
    Deepin Linux 安装JDK
    Linux清除磁盘上的RAID信息
    IDEA社区版运行并发布web项目
    CentOS 7 ibus 导入第三方词库
  • 原文地址:https://www.cnblogs.com/Courage129/p/14276926.html
Copyright © 2011-2022 走看看