netty中用于进行信息承载和交流的类叫做ByteBuf,从名字可以看出这是Byte的缓存区,是对字节数据的封装
粗略地可以从2个维度进行区分:内存分布
和内存回收
- 按照内存分布维度:堆内存字节缓冲区、直接内存字节缓冲区
- 按照内存回收维度:基于对象池,普通缓冲区
创建简单使用
1. public class test1 { 2. public static void main(String[] args) { 3. ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10); 4. log(buffer); 5. } 6. 7. private static void log(ByteBuf buffer) { 8. int length = buffer.readableBytes(); 9. int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4; 10. StringBuilder buf = new StringBuilder(rows * 80 * 2) 11. .append("read index:").append(buffer.readerIndex()) 12. .append(" write index:").append(buffer.writerIndex()) 13. .append(" capacity:").append(buffer.capacity()) 14. .append(NEWLINE); 15. appendPrettyHexDump(buf, buffer); 16. System.out.println(buf.toString()); 17. } 18. }
上面代码创建了一个默认的 ByteBuf(池化基于直接内存的 ByteBuf),初始容量是 10
输出:
read index:0 write index:0 capacity:10
直接内存vs堆内存
可以使用下面的代码来创建池化基于堆的 ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
也可以使用下面的代码来创建池化基于直接内存的 ByteBuf(默认的)
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
- 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
- 直接内存对 GC 压力小,因为这部分内存不受 JVM 垃圾回收的管理,但也要注意及时主动释放
池化vs非池化
池化的最大意义在于可以重用 ByteBuf,优点有
- 没有池化,则每次都得创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力
- 有了池化,则可以重用池中 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
- 4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
- 4.1 之前,池化功能还不成熟,默认是非池化实现
组成
ByteBuf 由四部分组成
最开始读写指针都在 0 位置
- capacity:它表示ByteBuf的容量大小,即ByteBuf最多能够容纳多少字节数据。
- readerIndex和writerIndex:它们表示ByteBuf中可读和可写的字节索引位置。
- maxCapacity:它表示ByteBuf的最大容量大小,即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) | 写入字符串 |
注意
- 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用
- 网络传输,默认习惯是 Big Endian
先写入 4 个字节:
1. ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10); 2. buffer.writeBytes(new byte[]{1, 2, 3, 4}); 3. log(buffer);
1. read index:0 write index:4 capacity:10 2. +-------------------------------------------------+ 3. | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 4. +--------+-------------------------------------------------+----------------+ 5. |00000000| 01 02 03 04 |.... | 6. +--------+-------------------------------------------------+----------------+
再写进一个int整数,也就是四个字节
buffer.writeInt(5);
1. read index:0 write index:8 capacity:10 2. +-------------------------------------------------+ 3. | 0 1 2 3 4 5 6 7 8 9 a b c d e f | 4. +--------+-------------------------------------------------+----------------+ 5. |00000000| 01 02 03 04 00 00 00 05 |........ | 6. +--------+-------------------------------------------------+----------------+
还有一类方法是 set 开头的一系列方法,也可以写入数据,但不会改变写指针位置。
buffer.setByte(4,1);
扩容
再写进一个整数时,容量就不够了(初始容量为10),这个时候就会引发扩容
1. ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(10); 2. buffer.writeBytes(new byte[]{1, 2, 3, 4}); 3. buffer.writeInt(5); 4. log(buffer); 5. buffer.writeInt(6); 6. log(buffer);
具体的扩容规则:
- 如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16
- 如果写入后数据大小超过 512,则选择下一个 2^n,例如写入后大小为 513,则扩容后 capacity 是 2^10=1024(2^9=512 已经不够了)
- 扩容不能超过 max capacity 会报错