Netty组件 之 ByteBuf

Netty组件 之 ByteBuf

是对字节数据的封装

1)创建

ByteBuf buffer = ByteBufAllocator.Default.buffer(10)
log(buffer)

上面代码创建了一个默认的ByteBuf(池化基于直接内存的ByteBuf),初始容量是10

输出

read index:0 write index:0 capacity:10

其中log方法参考如下:

    private static void log(ByteBuf buffer){
        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());
    }

2)直接内存 vs 堆内存

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

ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);

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

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

3)池化vs非池化

池化的最大意义在于可以重用ByteBuf,优点有

  • 没有池化,则每次都得创建新的ByteBuf实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加GC压力。
  • 有了池化,则可以重用池中ByteBuf实例,并且采用了与Jemalloc类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能

池化功能是否开启,可以通过下面的系统环境变量来设置

-Dio.netty.allocator.type={unpooled|pooled}
  • 4.1 以后,非Android平台默认启用池化实现,Android启用非池化实现
  • 4.1 之前,池化功能还不成熟,默认是非池化实现

4)组成

ByteBuf 由四部分组成

Netty组件 之 ByteBuf

最开始读写指针都在0 位置

5)写入

先写入4个字节

buffer.writeBytes(new byte[]{1,2,3,4});
log(buffer)

结果是

read index:0 write index:4 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04                                     |....            |
+--------+-------------------------------------------------+----------------+

再写入一个int整数,也是4个字节

buffer.writeInt(4);
log(buffer);

结果是

read index:0 write index:8 capacity:10
         +-------------------------------------------------+
         |  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                         |........        |
+--------+-------------------------------------------------+----------------+

6)扩容

再写入一个int整数时,容量不够了(初始容量是10),这时会引发扩容

buffer.writeInt(6);
log(buffer)

扩容规则是:

  • 如何写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为12,则扩容后capacity是16
  • 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为513,则扩容后capacity是 210=1024(29=512 已经不够了)
  • 扩容不能超过 max capacity 会报错

结果是

read index:0 write index:12 capacity:64
         +-------------------------------------------------+
         |  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 00 00 00 06             |............    |
+--------+-------------------------------------------------+----------------+

7)读取

例如读了4次,每次一个字节

        System.out.println(buffer.readByte());
        System.out.println(buffer.readByte());
        System.out.println(buffer.readByte());
        System.out.println(buffer.readByte());
        log(buffer);

读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分

1
2
3
4
read index:4 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06                         |........        |
+--------+-------------------------------------------------+----------------+

如果需要重复读取int整数5,怎么办?

可以在read前先做个标记 mark

buffer.markReaderIndex();
System.out.println(buffer.readInt());
log(buffer);

结果

5
read index:8 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 06                                     |....            |
+--------+-------------------------------------------------+----------------+

这时要重复读取的话,重置到标记位置reset

buffer.resetReaderIndex();
log(buffer)

结果

read index:4 write index:12 capacity:64
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06                         |........        |
+--------+-------------------------------------------------+----------------+

8)retain & release

由于Netty 中有堆外内存的ByteBuf 实现,堆外内存最好是手动来释放,而不是等GC垃圾回收。

  • UnpooledHeapByteBuf 使用的是JVM内存,只需等GC回收内存即可。
  • UnpooledDirectByteBuf 使用的就是直接内存,需要特殊的方法来回收内存。
  • PooledByteBuf 和 它的子类使用了池化机制,需要更复杂的规则来回收。

回收内存的源码实现,请关注下面方法的不同实现

protected abstract void deallocate()

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

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

9)slice

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

Netty组件 之 ByteBuf

例,原始ByteBuf 进行slice操作

        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
        buf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i','j'});

        ByteBuf slice1 = buf.slice(0, 5);
        ByteBuf slice2 = buf.slice(5, 5);

        log(slice1);
        log(slice2);

结果

read index:0 write index:5 capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65                                  |abcde           |
+--------+-------------------------------------------------+----------------+

read index:0 write index:5 capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 66 67 68 69 6a                                  |fghij           |
+--------+-------------------------------------------------+----------------+

10)duplicate

【零拷贝】的体现之一,就好比截取了原始ByteBuf所有内容,并且没有max capacity的限制,也是与原始ByteBuf使用同一块底层内存,只是读写指针是独立的。

Netty组件 之 ByteBuf

11)copy

会将底层内存数据进行深拷贝,因此无论读写,都与原始ByteBuf无关。

12)composite

将多个小的 ByteBuf 组合成一个大的ByteBuf

        ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer();
        buf1.writeBytes(new byte[]{1,2,3,4,5});

        ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
        buf2.writeBytes(new byte[]{6,7,8,9,10});

        //效率低  需要将buf1 buf2的 数据复制到buffer中
//        ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
//        buffer.writeBytes(buf1).writeBytes(buf2);
//        log(buffer);

        //效率高  避免了内存的复制
        CompositeByteBuf buffer = ByteBufAllocator.DEFAULT.compositeBuffer();
        buffer.addComponents(true,buf1,buf2);
        log(buffer);

结果

read index:0 write index:10 capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
+--------+-------------------------------------------------+----------------+

上一篇:netty系列之:使用UDP协议


下一篇:从零开始实现简单 RPC 框架 6:网络通信之 Netty