Netty编程(四)—— ByteBuf

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

创建

ByteBuf 可以通过ByteBufAllocator选择allocator并调用对应的 buffer( )方法来创建的,默认使用直接内存作为ByteBuf,容量为256个字节,可以指定初始容量的大小。

public class ByteBufStudy {
    public static void main(String[] args) {
        // 创建ByteBuf
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
        ByteBufUtil.log(buffer);

        // 向buffer中写入数据
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < 20; i++) {
            sb.append("a");
        }
        buffer.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8));

        // 查看写入结果
        ByteBufUtil.log(buffer);
    }
}
复制代码

结果可视化:

在这里插入图片描述

  • 当ByteBuf的容量无法容纳所有数据时,ByteBuf会进行扩容操作

  • 如果在handler中创建ByteBuf,建议使用 ChannelHandlerContext ctx.alloc().buffer() 来创建

直接内存与堆内存

通过下面这种方法创建的ByteBuf,使用的是基于直接内存的ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
复制代码

此外,还可以使用下面的代码来创建池化基于堆的 ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
复制代码

也可以使用下面的代码来创建池化基于直接内存的 ByteBuf

ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
复制代码
  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
  • 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放

池化与非池化

类似于线程池的思想,预先把昂贵的资源创建好,省去创建的时间和步骤,用完后需要归还。池化的最大意义在于可以重用 ByteBuf,他的优点有:

  • 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
  • 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能
public class ByteBufStudy {
    public static void main(String[] args) {
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16);
        System.out.println(buffer.getClass());

        buffer = ByteBufAllocator.DEFAULT.heapBuffer(16);
        System.out.println(buffer.getClass());

        buffer = ByteBufAllocator.DEFAULT.directBuffer(16);
        System.out.println(buffer.getClass());
    }
}  

复制代码
// 使用池化的直接内存
class io.netty.buffer.PooledUnsafeDirectByteBuf
    
// 使用池化的堆内存    
class io.netty.buffer.PooledUnsafeHeapByteBuf
    
// 使用池化的直接内存    
class io.netty.buffer.PooledUnsafeDirectByteBuf
复制代码

组成

ByteBuf主要有以下几个组成部分

  • 最大容量与当前容量

    • 在构造ByteBuf时,可传入两个参数,分别代表初始容量和最大容量,若未传入第二个参数(最大容量),最大容量默认为Integer.MAX_VALUE
    • 当ByteBuf容量无法容纳所有数据时,会进行扩容操作,若超出最大容量,会抛出java.lang.IndexOutOfBoundsException异常
  • 对于读写操作与ByteBuffer只用position进行控制不同的是, ByteBuf分别由读指针和写指针两个指针控制

    • 进行读写操作时,无需进行模式的切换

      • 读指针前的部分被称为废弃部分,是已经读过的内容
      • 读指针与写指针之间的空间称为可读部分
      • 写指针与当前容量之间的空间称为可写部分

在这里插入图片描述

写入

常用方法如下

方法签名 含义 备注
writeBoolean(boolean value) 写入 boolean 值 用一字节 01|00 代表 true|false
writeByte(int value) 写入 byte 值
writeShort(int value) 写入 short 值
writeInt(int value) 写入 int 值 Big Endian(大端写入),即 0x250,写入后 00 00 02 50
writeIntLE(int value) 写入 int 值 Little Endian(小端写入),即 0x250,写入后 50 02 00 00
writeLong(long value) 写入 long 值
writeChar(int value) 写入 char 值
writeFloat(float value) 写入 float 值
writeDouble(double value) 写入 double 值
writeBytes(ByteBuf src) 写入 netty 的 ByteBuf
writeBytes(byte[] src) 写入 byte[]
writeBytes(ByteBuffer src) 写入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset) 写入字符串 CharSequence为字符串类的父类,第二个参数为对应的字符集

注意

  • 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用来写入不同的数据
  • 网络传输中,默认习惯是 Big Endian,使用 writeInt(int value)
  • CharSequence 是String、StringBuffer、StringBuilder的父类

下面这个例子分别写入了字节类型,int类型以及long类型,值得注意的是,写long类型会进行扩容:

public class test {

    public static void main(String[] args) {
        // 创建ByteBuf
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20);

        // 向buffer中写入byte类型,1字节大小
        buffer.writeBytes(new byte[]{1, 2, 3, 4});
		// 向buffer中写入int类型,4字节大小
        buffer.writeInt(5);
		// 向buffer中小端写入int类型,4字节大小
        buffer.writeIntLE(6);
		// 向buffer中写入long类型,8字节大小
        buffer.writeLong(7);
        log(buffer);
    }

    private static void log(ByteBuf buffer) {
        //ByteBuf 可视化
        int length = buffer.readableBytes();
        int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
        StringBuilder buf = new StringBuilder(rows * 80 * 2)
                .append("read index:").append(buffer.readerIndex())
                .append(" write index:").append(buffer.writerIndex())
                .append(" capacity:").append(buffer.capacity())
                .append(NEWLINE);
        appendPrettyHexDump(buf, buffer);
        System.out.println(buf.toString());
    }
}

复制代码

在这里插入图片描述

扩容

当ByteBuf中的容量无法容纳写入的数据时,会进行扩容操作,借用上面说的例子:

buffer.writeLong(7);
log(buffer);

// 扩容前
read index:0 write index:12 capacity:16
...

// 扩容后
read index:0 write index:20 capacity:20
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 06 00 00 00 00 00 00 00 |................|
|00000010| 00 00 00 07                                     |....            |
+--------+-------------------------------------------------+----------------+
复制代码

扩容规则

  • 如何写入后数据大小未超过 512 字节,则选择下一个 16 的整数倍进行扩容,例如写入后大小为 12 字节,则扩容后 capacity 是 16 字节

  • 如果写入后数据大小超过 512 字节,则选择下一个 2 n 2^n ,例如写入后大小为 513 字节,则扩容后 capacity 是 210=1024 字节( 2 9 2^9 =512 已经不够了)

  • 扩容不能超过 maxCapacity,否则会抛出java.lang.IndexOutOfBoundsException异常

读取

读取主要是通过一系列read方法进行读取,读取时会根据读取数据的字节数移动读指针,此外Netty支持重复读取,如果需要重复读取,需要调用buffer.markReaderIndex()对读指针进行标记,并通过buffer.resetReaderIndex()将读指针恢复到mark标记的位置。具体可以看下面的代码:

public static void main(String[] args) {
        // 创建ByteBuf
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20);

        // 向buffer中写入数据
        buffer.writeBytes(new byte[]{1, 2, 3, 4});
        buffer.writeInt(5);
        ByteBufferUtil.log(buffer);

        // 读取4个字节
        System.out.println(buffer.readByte());
        System.out.println(buffer.readByte());
        System.out.println(buffer.readByte());
        System.out.println(buffer.readByte());
        ByteBufferUtil.log(buffer);

        // 通过mark与reset实现重复读取
        buffer.markReaderIndex();
        System.out.println(buffer.readInt());
        ByteBufferUtil.log(buffer);

        // 恢复到mark标记处
        buffer.resetReaderIndex();
        ByteBufferUtil.log(buffer);
    }
复制代码

在这里插入图片描述

释放

Netty 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口

  • 每个 ByteBuf 对象的初始计数为 1
  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收
  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用

释放规则

因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在每个 ChannelHandler 中都去调用 release ,就失去了传递性(如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递),所以基本规则是,谁是最后使用者,谁负责 release

  • 入站 ByteBuf 处理原则

    • 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
    • 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
    • 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
    • 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
    • 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
  • 出站 ByteBuf 处理原则

    • 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
  • 异常处理原则

    • 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true :

      while (!buffer.release()) {}
      复制代码

当ByteBuf被传到了pipeline的head与tail时,ByteBuf会被其中的方法彻底释放。

切片Slice

之前在NIO网络编程(十)—— 零拷贝技术 - 掘金 (juejin.cn)中介绍了一下NIO中的零拷贝问题,这里介绍一下Netty中使用Slice方法实现零拷贝的。

ByteBuf切片是零拷贝的体现之一,对原始 ByteBuf 进行切片成多个 ByteBuf,切片后的 ByteBuf 并没有发生内存复制,还是使用原始 ByteBuf 的内存,切片后的 ByteBuf 维护独立的 read,write 指针。

  • 得到分片后的buffer后,要调用其retain方法,使其内部的引用计数加一。避免原ByteBuf释放,导致切片buffer无法使用

  • 修改原ByteBuf中的值,也会影响切片后得到的ByteBuf

  • 切片不能扩容,即不能向切片写入内容

  • 一般使用slice,就需要retain,使用完后使用 release

img

public static void main(String[] args) {
        // 创建ByteBuf
        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(16, 20);

        // 向buffer中写入数据
        buffer.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});

        // 将buffer分成两部分,参数:start,length,在切片过程中没有发生数据复制,同一块内存
        ByteBuf slice1 = buffer.slice(0, 5);
        ByteBuf slice2 = buffer.slice(5, 5);

        // 需要让分片的buffer引用计数加一
        // 避免原Buffer释放导致分片buffer无法使用
        slice1.retain();
        slice2.retain();

        ByteBufferUtil.log(slice1);
        ByteBufferUtil.log(slice2);

        // 更改原始buffer中的值
        System.out.println("===========修改原buffer中的值===========");
        buffer.setByte(0,5);

        System.out.println("===========打印slice1===========");
        ByteBufferUtil.log(slice1);
    }
复制代码

上方的代码的执行结果:

在这里插入图片描述

猜你喜欢

转载自juejin.im/post/7034094974014914567