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