写点什么

netty 系列之:netty 中的 ByteBuf 详解

发布于: 刚刚

简介 netty 中用于进行信息承载和交流的类叫做 ByteBuf,从名字可以看出这是 Byte 的缓存区,那么 ByteBuf 都有哪些特性呢?一起来看看。


ByteBuf 详解 netty 提供了一个 io.netty.buffer 的包,该包里面定义了各种类型的 ByteBuf 和其衍生的类型。


netty Buffer 的基础是 ByteBuf 类,这是一个抽象类,其他的 Buffer 类基本上都是由该类衍生而得的,这个类也定义了 netty 整体 Buffer 的基调。


先来看下 ByteBuf 的定义:


public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {ByteBuf 实现了两个接口,分别是 ReferenceCounted 和 Comparable。Comparable 是 JDK 自带的接口,表示该类之间是可以进行比较的。而 ReferenceCounted 表示的是对象的引用统计。当一个 ReferenceCounted 被实例化之后,其引用 count=1,每次调用 retain() 方法,就会增加 count,调用 release() 方法又会减少 count。当 count 减为 0 之后,对象将会被释放,如果试图访问被释放过后的对象,则会报访问异常。


如果一个对象实现了 ReferenceCounted,并且这个对象里面包含的其他对象也实现了 ReferenceCounted,那么当容器对象的 count=0 的时候,其内部的其他对象也会被调用 release()方法进行释放。


综上,ByteBuf 是一个可以比较的,可以计算引用次数的对象。他提供了序列或者随机的 byte 访问机制。


注意的是,虽然 JDK 中有自带的 ByteBuffer 类,但是 netty 中的 ByteBuf 算是对 Byte Buffer 的重新实现。他们没有关联关系。


创建一个 BuffByteBuf 是一个抽象类,并不能直接用来实例化,虽然可以使用 ByteBuf 的子类进行实例化操作,但是 netty 并不推荐。netty 推荐使用 io.netty.buffer.Unpooled 来进行 Buff 的创建工作。Unpooled 是一个工具类,可以为 ByteBuf 分配空间、拷贝或者封装操作。


下面是创建几个不同 ByteBuf 的例子:


import static io.netty.buffer.Unpooled.*;


ByteBuf heapBuffer = buffer(128);ByteBuf directBuffer = directBuffer(256);ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]);ByteBuf copiedBuffer = copiedBuffer(ByteBuffer.allocate(128));


上面我们看到了 4 种不同的 buff 构建方式,普通的 buff、directBuffer、wrappedBuffer 和 copiedBuffer。


普通的 buff 是固定大小的堆 buff,而 directBuffer 是固定大小的 direct buff。direct buff 使用的是堆外内存,省去了数据到内核的拷贝,因此效率比普通的 buff 要高。


wrappedBuffer 是对现有的 byte arrays 或者 byte buffers 的封装,可以看做是一个视图,当底层的数据发生变化的时候,Wrapped buffer 中的数据也会发生变化。


Copied buffer 是对现有的 byte arrays、byte buffers 或者 string 的深拷贝,所以它和 wrappedBuffer 是不同的,Copied buffer 和原数据之间并不共享数据。


随机访问 Buff 熟悉集合的朋友应该都知道,要想随机访问某个集合,一定是通过 index 来访问的,ByteBuf 也一样,可以通过 capacity 或得其容量,然后通过 getByte 方法随机访问其中的 byte,如下所示:


    //随机访问    ByteBuf buffer = heapBuffer;    for (int i = 0; i < buffer.capacity(); i ++) {        byte b = buffer.getByte(i);        System.out.println((char) b);    }
复制代码


序列读写读写要比访问复杂一点,ByteBuf 提供了两个 index 用来定位读和写的位置,分别是 readerIndex 和 writerIndex ,两个 index 分别控制读和写的位置。


下图显示的一个 buffer 被分成了三部分,分别是可废弃的 bytes、可读的 bytes 和可写的 bytes。


+-------------------+------------------+------------------+| discardable bytes |  readable bytes  |  writable bytes  ||                   |     (CONTENT)    |                  |+-------------------+------------------+------------------+|                   |                  |                  |0      <=      readerIndex   <=   writerIndex    <=    capacity
复制代码


上图还表明了 readerIndex、writerIndex 和 capacity 的大小关系。


其中 readable bytes 是真正的内容,可以通过调用 read* 或者 skip* 的方法来进行访问或者跳过,调用这些方法的时候,readerIndex 会同步增加,如果超出了 readable bytes 的范围,则会抛出 IndexOutOfBoundsException。默认情况下 readerIndex=0。


下面是一个遍历 readable bytes 的例子:


    //遍历readable bytes    while (directBuffer.isReadable()) {        System.out.println(directBuffer.readByte());    }
复制代码


首先通过判断是否是 readable 来决定是否调用 readByte 方法。


Writable bytes 是一个未确定的区域,等待被填充。可以通过调用 write*方法对其操作,同时 writerIndex 会同步更新,同样的,如果空间不够的话,也会抛出 IndexOutOfBoundsException。默认情况下 新分配的 writerIndex =0 ,而 wrapped 或者 copied buffer 的 writerIndex=buf 的 capacity。


下面是一个使用 writable Byte 的例子:


    //写入writable bytes    while (wrappedBuffer.maxWritableBytes() >= 4) {        wrappedBuffer.writeInt(new Random().nextInt());    }
复制代码


Discardable bytes 是已经被读取过的 bytes,初始情况下它的值=0,每当 readerIndex 右移的时候,Discardable bytes 的空间就会增加。如果想要完全删除或重置 Discardable bytes,则可以调用 discardReadBytes()方法,该方法会将 Discardable bytes 空间删除,将多余的空间放到 writable bytes 中,如下所示:


调用 discardReadBytes() 之前:


+-------------------+------------------+------------------+| discardable bytes |  readable bytes  |  writable bytes  |+-------------------+------------------+------------------+|                   |                  |                  |0      <=      readerIndex   <=   writerIndex    <=    capacity
复制代码


调用 discardReadBytes()之后:


+------------------+--------------------------------------+|  readable bytes  |    writable bytes (got more space)   |+------------------+--------------------------------------+|                  |                                      |
复制代码


readerIndex (0) <= writerIndex (decreased) <= capacity


注意,虽然 writable bytes 变多了,但是其内容是不可控的,并不能保证里面的内容是空的或者不变。


调用 clear()方法会将 readerIndex 和 writerIndex 清零,注意 clear 方法只会设置 readerIndex 和 writerIndex 的值,并不会清空 content,看下面的示意图:


调用 clear()之前:


+-------------------+------------------+------------------+| discardable bytes |  readable bytes  |  writable bytes  |+-------------------+------------------+------------------+|                   |                  |                  |0      <=      readerIndex   <=   writerIndex    <=    capacity
复制代码


调用 clear()之后:


+---------------------------------------------------------+|             writable bytes (got more space)             |+---------------------------------------------------------+|                                                         |0 = readerIndex = writerIndex            <=            capacity
复制代码


搜索 ByteBuf 提供了单个 byte 的搜索功能,如 indexOf(int, int, byte) 和 bytesBefore(int, int, byte)两个方法。


如果是要对 ByteBuf 遍历进行搜索处理的话,可以使用 forEachByte(int, int, ByteProcessor),这个方法接收一个 ByteProcessor 用于进行复杂的处理。


其他衍生 buffer 方法 ByteBuf 还提供了很多方法用来创建衍生的 buffer,如下所示:


duplicate()slice()slice(int, int)readSlice(int)retainedDuplicate()retainedSlice()retainedSlice(int, int)readRetainedSlice(int)


要注意的是,这些 buf 是建立在现有 buf 基础上的衍生品,他们的底层内容是一样的,只有 readerIndex, writerIndex 和做标记的 index 不一样。所以他们和原 buf 是有共享数据的。如果你希望的是新建一个全新的 buffer,那么可以使用 copy()方法或者前面提到的 Unpooled.copiedBuffer。


在前面小节中,我们讲到 ByteBuf 是一个 ReferenceCounted,这个特征在衍生 buf 中就用到了。我们知道调用 retain() 方法的时候,引用 count 会增加,但是对于 duplicate(), slice(), slice(int, int) 和 readSlice(int) 这些方法来说,虽然他们也是引用,但是没有调用 retain()方法,这样原始数据会在任意一个 Buf 调用 release()方法之后被回收。


如果不想有上面的副作用,那么可以将方法替换成 retainedDuplicate(), retainedSlice(), retainedSlice(int, int) 和 readRetainedSlice(int) ,这些方法会调用 retain()方法以增加一个引用。


和现有 JDK 类型的转换之前提到了 ByteBuf 是对 ByteBuffer 的重写,他们是不同的实现。虽然这两个不同,但是不妨碍将 ByteBuf 转换 ByteBuffer。


当然,最简单的转换是把 ByteBuf 转换成 byte 数组 byte[]。要想转换成 byte 数组,可以先调用 hasArray() 进行判断,然后再调用 array()方法进行转换。


同样的 ByteBuf 还可以转换成为 ByteBuffer ,可以先调用 nioBufferCount()判断能够转换成为 ByteBuffers 的个数,再调用 nioBuffer() 进行转换。


返回的 ByteBuffer 是对现有 buf 的共享或者复制,对返回之后 buffer 的 position 和 limit 修改不会影响到原 buf。


最后,使用 toString(Charset) 方法可以将 ByteBuf 转换成为 String。


总结 ByteBuf 是 netty 的底层基础,是传输数据的承载对象,深入理解 ByteBuf 就可以搞懂 netty 的设计思想,非常不错。


本文的例子可以参考:learn-netty4


本文已收录于 http://www.flydean.com/02-netty-bytebuf/


最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!


欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 刚刚阅读数: 2
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:netty中的ByteBuf详解