Netty组件-ByteBuf

ByteBuf是netty对nio中ByteBuffer的升级和优化,是的数据流更加的方便操作和更叫的高效。

一、创建

package com.test.netty.c5;

import com.test.utils.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestByteBuf {
    public static void main(String[] args) {
        //可以动态扩容
        //ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer();
        System.out.println(byteBuf.getClass());
        ByteBufUtils.log(byteBuf);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 32; i++) {
            sb.append("a");
        }
        byteBuf.writeBytes(sb.toString().getBytes());
        ByteBufUtils.log(byteBuf);
    }
}

ByteBuf可以通过使用ByteBufAllocator工具类进行创建,默认使用的就是直接内存,选择使用堆内存进行创建,同时默认大小是256。当数据的大小大于ByteBuf的指定大小,就会进行自动扩容。但是在handler中使用ByteBuf的时候,尽量使用channelHandlerContext的alloc.buffer()方法进行创建。

二、直接内存和堆内存

同ByteBuffer一样,ByteBuf也可以使用直接内存或者是堆内存,一下是创建方法和使用内存情况:

  • ByteBufAllocator.DEFAULT.buffer(16); = 池化直接内存
  • ByteBufAllocator.DEFAULT.heapBuffer(16); = 池化堆内存
  • ByteBufAllocator.DEFAULT.directBuffer(16); = 池化直接内存

其实跟ByteBuffer的使用内存的优缺点一样:

  • 直接内存,分配效率低,但是使用效率高,不受GC回收的影响,需要注意手动释放(更好的配合池化)
  • 堆内存,分配效率高,但是使用效率相对低,受GC的影响,可以选择不手动释放

三、池化和非池化

其实池化的技术在开发中应用的非常广泛,线程池、数据库连接池等等,ByteBuf的池化技术也是一个意思,就是针对ByteBuf的重用,有点如下:

  • 不使用池化技术,每次都要重新分配存储,增加GC压力
  • 有了池化技术,可以重用ByteBuf,采用了jemalloc 类似的分配算法提升分配效率,并发高的时候,池化更加节约内存,减少内存溢出的可能性

是否开启池化技术,系统环境变量:-Dio.netty.allocator.type={unpooled|pooled}

注意:

  • 4.1之后,android平台不开启池化,其他平台默认开启
  • 4.1之前,池化不成熟,默认不开启

四、ByteBuf的组成

创建ByteBuf的时候,可以传递2个参数,第一个是初始容量,第二个是最大容量,最大容量默认是Integer.MAX_VALUE,当容量不够的时候,ByteBuf就会自动扩容,当扩容到最大容量的时候,就会抛出异常。

ByteBuf读写相对ByteBuffer有很大的提升,采用双指针的方式,一个读指针,一个写指针,结构如下:

扩容规则: 

  • 如果写入后的数据大小小于512字节,下一次扩容就是16的整数倍
  • 如果写入后的数据大小大于512字节,下一次扩容就是2的N次方

五、写入和读取方法

写入方法:

方法签名 含义 备注
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,所以可以使用链式调用
  • 注意大端写入和小端写入,网络编程中习惯用的是大段写入
  • 也可以使用相关set方法进行写入,但是不会改变写指针的位置

读出方法:

  • 以read开头的方法,读取后会改变读指针的位置,以get开头的方法正好相反
  • 如果期望重复读取,可以先使用 buffer.markReaderIndex() 进行标记,然后再使用 buffer.resetReaderIndex() 恢复标记位置

六、内存释放

因为ByteBuf可以使用直接内存,所以直接使用之后都需要进行手动的内存释放。Netty中提供了ReferenceCounted接口来进行内存的释放,并且每个ByteBuf都实现了改接口,释放算法:

  • ByteBuf初始对象的计数为1
  • 调用 release 方法计数-1,当计数为0的时候,内存释放
  • 调用 retain 方法计数+1,表示有地方在使用这个ByteBuf,保证不会因为其它地方调用 release方法而导致误回收 

因为pipelin的存在,数据是在整个handler链中进行流转的,所以ByteBuf在哪里释放就显得很重要,基本原则是哪个handler使用,就在哪个handler释放,虽然head和tail都有释放的功能,但是因为中间的handler可能对ByteBuff进行加工,传递到head和tail就已经不是ByteBuf对象了,所以还是要遵循基本原则:谁最后使用,谁负责release

  • 当handler使用了ByteBuf,并且不向下传递了,就调用release
  • 当到达最后一个handler了,不需要向下传递了,也需要调用release
  • 异常无法成功传递到下一个handler,也需要调用release
  • 出栈一般情况下,因为是最后转换成ByteBuf,就会由head进行释放

七、切片和合并

ByteBuf中有许多零拷贝的体现,切片和合并就是,不论切片还是合并,其实都是使用的原ByteBuf,但是对读写指针是独立的维护。所以在使用新生成的ByteBuf的时候,就要注意,如果原ByteBuf被内存释放了,那么新生成的ByteBuf也会无法使用,所以需要在使用的时候调用retain方法,让计数+1即可。

切片代码实例:

package com.test.netty.c5;


import com.test.utils.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestSlice {

    public static void main(String[] args) {
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
        byteBuf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i','j'});
        ByteBufUtils.log(byteBuf);
        //在切片过程中,没有发生数据的复制
        ByteBuf f1 = byteBuf.slice(0, 5);
        ByteBuf f2 = byteBuf.slice(5, 5);
        ByteBufUtils.log(f1);
        ByteBufUtils.log(f2);
        //切片后的ByteBuf是无法写入的
        //原有的ByteBuf释放内存后,切片后的也会受影响
        //上面两个原因都是因为切片后的ByteBuf是原始ByteBuf的映射

        f1.setByte(0, 'b');
        ByteBufUtils.log(f1);
        ByteBufUtils.log(byteBuf);
    }
}

 合并代码实例:

package com.test.netty.c5;

import com.test.utils.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;

public class TestCompositeByteBuf {

    public static void main(String[] args) {
        ByteBuf b1 = ByteBufAllocator.DEFAULT.buffer(10);
        b1.writeBytes(new byte[]{'a','b','c','d','e'});
        ByteBuf b2 = ByteBufAllocator.DEFAULT.buffer(10);
        b2.writeBytes(new byte[]{'f','g','h','i','j'});

        CompositeByteBuf byteBufs = ByteBufAllocator.DEFAULT.compositeBuffer();
        byteBufs.addComponents(true, b1, b2);
        ByteBufUtils.log(byteBufs);
    }

}

八、优势

  • 池化思想,提升使用效率
  • 读写指针,方便操作
  • 自动扩容
  • 方法链式调用,阅读和书写更加方便
  • 零拷贝思想体现多,如  slice、duplicate、CompositeByteBuf

猜你喜欢

转载自blog.csdn.net/liming0025/article/details/120073493