Netty学习之ByteBuf

Netty学习之ByteBuf

前言

在网络中传输的基本单元是字节byte,虽然在NIO中提供了一个ByteBuffer作为字节的容器,不过由于ByteBuffer比较难使用,所以Netty自己实现了一个,ByteBuf,并且提供了比较灵活的操作方式以及操作工具,本节我们将详细学习这一部分的知识。

ByteBuf

在Netty中,数据通过ByteBuf以及ByteBufHolder来进行操作,ByteBuf具有众多优秀的特性

  • 易于扩展
  • 通过内置的composite buffer类型可以实现zero-copy
  • 根据需要扩展容量,类似于JDK中的(StringBuilder)
  • 自动在读写模式进行切换,相比ByteBuffer需要调用filp()更加方便
  • 采用读写两个指针
  • 支持链式调用
  • 支持引用计数
  • 支持池化技术

工作原理

ByteBuf可以理解为一个字节数组,并且维护两个不同的指针:读指针以及写指针,当从ByteBuf中读取数据(read开头的函数)的时候,读指针增加,当写入数据(write开头的函数)的时候,写指针增加,当读写指针相同时,表示已经没有数据可以读取,继续读取会抛出IndexOutOfBoundException,set以及get开头的函数不会影响指针,默认的最大容量是Integer.MAX_VALUE,当写入超过容量时,会触发异常。

ByteBuf类型

基于堆的ByteBuf,将数据存储在JVM的堆内存中,并且其内部是一个字节数组,但没有进行缓存的时候,可以快速地申请以及回收内存,比较适合处理常规数据。

基于直接内存的ByteBuf,ByteBuf的内存空间是通过直接内存申请的(本地方法调用分配的内存),好处在于可以避免在发生I/O调用的时候,将数据从堆空间拷贝到直接内存中(节省一次拷贝),比较适合于进行网络数据传输,由于数据没有在堆中,所以操作的时候需要先拷贝到堆空间中(需要手动操作),缺点是空间的申请以及回收比较消耗资源,而且不受gc的管理。

上面两个可以通过hasArray()进行区分,基于堆的返回true,基于直接内存的返回false

组合ByteBuf(CompositeByteBuf),提供了多个ByteBuf的聚合视图,可以往其中添加或者删除ByteBuf实例,可能包含上面两种类型的ByteBuf,所以使用的时候需要注意。

获取ByteBuf

通过ByteBufAllocator

Netty通过ByteBufAllocator接口,来提供获取ByteBuf的操作

buffer();
buffer(initCapacity);
buffer(initCapacity, maxCapacity);

heapBuffer();
heapBuffer(initCapacity);
heapBuffer(initCapacity, maxCapacity);

directBuffer();
directBuffer(..);
directBuffer(.., ..);

compositeBuffer();
compositeBuffer(..);
compositeDirectBuffer();
compositeDirectBuffer(..);
compositeHeapBuffer();
compositeHeapBuffer(..);

ioBuffer(); // for i/o in socket

可以从Channel或者ChannelHanderContext中获取ByteBuffAllocator实例

public void testAllocator() {
    NioServerSocketChannel channel = new NioServerSocketChannel();
    ByteBufAllocator alloc = channel.alloc();
    ByteBuf byteBuf = alloc.heapBuffer();
    System.out.println(byteBuf.hasArray());

    ChannelHandlerContext ctx = ...;
    ByteBufAllocator allocator = ctx.alloc();
}

同时,Netty提供了两种ByteBufAllocator的默认实现:PooledByteBufAllocator以及UnpooledByteBufAllocator

// true表示调用buffer()时,使用直接内存,false表示堆内存
ByteBufAllocator allocator = new UnpooledByteBufAllocator(true);
ByteBufAllocator allocator = new PooledByteBufAllocator(true);

true跟false的区别仅在于buffer(),如果是调用heapBuffer(),那还是堆内存,跟true/false无关

通过Unpooled

在有一些情况,我们没有办法获取ByteBuffAllocator,则可以通过Unpooled工具来创建未缓存的ByteBuf实例

buffer(); // 基于堆的ByteBuff
buffer(..); // 同上

directBuffer(); // 同上

wrappedBuffer(); // 包装给定内容
copiedBuffer(); // 拷贝给定内容

ByteBuf操作

随机访问

ByteBuf支持类似于数组的访问形式,并且其下标从0开始,最后一个Byte为capacity() - 1

ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes());
for (int i = 0; i < buffer.capacity(); i++) {
    System.out.print((char)buffer.getByte(i));
}

连续访问

ByteBuf由三个部分组成,如下图所示

+----------+------------+----------------+
|          |            |                |
| 已经读取  |  可以读取   |   可以写入     |
|          |            |                |
+----------+------------+----------------+
0------readerIndex---writeIndex-----capacity

其中已经读取的数据不可能再被读取(指的是当读指针已经移动后,读指针之前的数据),读指针与写指针之间的数据则可以被继续读取,写指针与容量之间的空间可以继续写入。

当调用ByteBuf#discardReadByytes()后,后面的数据会移动到前面,使得读指针归为0,注意该操作会比较消耗资源,一般只在内存资源比较紧张的时候才进行该操作。

移动指针

在ByteBuf中,存在读写指针,所以可以根据需要移动指针,当调用read开头的函数时,读指针会移动,write开头的函数时,write指针会移动,同时,可以调用readIndex(int)将读指针设置到指定位置,超过可读位置会抛出异常,writeIndex(int)同理。

可以调用clear()将读写指针均设置为0,该操作并没有清空内容,也即数据依旧可以读取出来,该操作比discardReadBytes()消耗更低,因为只是重置了指针。

搜索操作

用于查找某个字节的下标,可以使用indexOf(),也可以使用复杂的操作ByteBufProcessor#process(byte value),同时ByteBufProcessor定义了一系列公用操作,如ByteBufProcessor.FIND_CRByteBufProcessor.FIND_LF

衍生操作

衍生操作提供了一些可以从当前ByteBuf中获取ByteBuf的操作,其底层公用一个ByteBuff,但是具有自己的读写指针

public void testDerived() {
    ByteBuf buffer = Unpooled.copiedBuffer("hello world".getBytes());
    // 影子拷贝,底层其实是同一个
    ByteBuf duplicate = buffer.duplicate();
    buffer.setByte(0, 'a');
    // a
    System.out.println((char)duplicate.getByte(0));

    // 影子切片,可以带参数
    ByteBuf slice = buffer.slice();
    buffer.setByte(0, 'a');
    // a
    System.out.println((char) slice.getByte(0));

    ByteBuf byteBuf = buffer.readSlice(buffer.readableBytes());
    for (int i = 0; i < byteBuf.capacity(); i++) {
        System.out.print((char) byteBuf.getByte(i));
    }
}

如果是要拷贝数据,则应该使用copy()或者copy(int, int)操作

ByteBufUtils

在Netty中,同时还提供了ByteBufUtils工具类来操作ByteBuf,如hexDump()可以用于打印ByteBuf的内容,更多关于ByteBufUtils,可以参考API即可。

引用计数

Netty为ByteBuf以及ByteBufHolder引入了引用计数,两者均实现了ReferenceCounted接口,可以用于提高性能。

当引用计数的值大于0的时候,对应的资源不会被释放,当计数值等于0时,资源会被释放掉。

通常来说,最后一个使用资源的对象需要释放掉该资源,即调用其release()方法

总结

本小节主要详细学习了Netty中的数据容器,ByteBuf,在Netty中,所有的数据都是存放在ByteBuf中,所以,对Netty中数据的操作,其实就是对ByteBuf的操作,ByteBuf有三种不同的类型,基于堆的,基于直接内存的,组合类型的,在使用的时候需要根据情况选择合适的容器,同时,为了提高性能,Netty中引入了引用计数的概念,所以,当资源不需要使用的时候,需要显示释放掉对应的资源。

你可能感兴趣的:(Netty学习之ByteBuf)