述
上文中,我们服务端与客户端之间的通信的数据都是通过ByteBuf来传输的,本文来详细的看一下这个ByteBuf
ByteBuf数据结构
ByteBuf数据结构如下图:
从图中来看,ByteBuf是一个字节容器,容器里面的数据一共有三个部分
- 第一部分是已经丢弃的字节,这部分数据是无效的
- 第二部分是可读字节,这部分数据是ByteBuf的主体数据,从ByteBuf里面读取的数据都来自这一部分
- 最后一部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段,虚线表示的是该 ByteBuf 最多还能扩容多少容量
这三部分内容是被两个指针给划分出来的,从左到右依次是读指针(readerIndex)、写指针(writerIndex),然后还有容量capacity表示ByteBuf底层内存的总容量.
数据读写
读数据
从 ByteBuf 中每读取一个字节, readerIndex 自增1,ByteBuf 里面总共有 writerIndex-readerIndex 个字节可读, 由此可以推论出当 readerIndex 与 writerIndex 相等的时, ByteBuf 不可读
写数据
写数据是从 writerIndex 指向的部分开始写,每写一个字节,writerIndex 自增1,直到增到 capacity,这个时候,表示 ByteBuf 已经不可写了
最大容量
ByteBuf 里面其实还有一个参数 maxCapacity,当向 ByteBuf 写数据的时候,如果容量不足,那么这个时候可以进行扩容,直到 capacity 扩容到 maxCapacity,超过 maxCapacity 就会报错
相关API
容量API
1 | readerIndex() 与 readerIndex(int) |
前者表示返回当前的读指针 readerIndex, 后者表示设置读指针
1 | writeIndex() 与 writeIndex(int) |
前者表示返回当前的写指针 writerIndex, 后者表示设置写指针
1 | markReaderIndex() 与 resetReaderIndex() |
前者表示把当前的读指针保存起来,后者表示把当前的读指针恢复到之前保存的值
下面来看两个代码片段:1
2
3int readerIndex = buffer.readerIndex();
// .. 其他操作
buffer.readerIndex(readerIndex);
1 | buffer.markReaderIndex(); |
这两个代码的作用其实是一样的,都是先记录一下读指针当前的位置,然后做完其他操作之后在恢复到记录下来的位置,推荐使用下面的这种方法,不需要额外创建一个变量,常见使用场景为解析自定义协议的数据包.
1 | markWriterIndex() 与 resetWriterIndex() |
这个是针对写指针的,作用和上面的一样
读写API
关于 ByteBuf 的读写都可以看作从指针开始的地方开始读写数据
1 | writeBytes(byte[] src) 与 buffer.readBytes(byte[] dst) |
writeBytes()
表示把字节数组 src 里面的数据全部写到 ByteBuf
readBytes()
指的是把 ByteBuf 里面的数据全部读取到 dst
这里 dst 字节数组的大小通常等于 readableBytes() ,而 src 字节数组大小的长度通常小于等于 writableBytes()
1 | writeByte(byte b) 与 buffer.readByte() |
riteByte() 表示往 ByteBuf 中写一个字节
buffer.readByte() 表示从 ByteBuf 中读取一个字节
类似的还有 writeBoolean()
、writeChar()
、writeShort()
、writeInt()
、writeLong()
、writeFloat()
、writeDouble()
与 readBoolean()
、readChar()
、readShort()
、readInt()
、readLong()
、readFloat()
、readDouble()
与读写 API 类似的 API 还有 getBytes()
、getByte()
与 setBytes()
、setByte()
系列,唯一的区别就是 get/set 不会改变读写指针,而 read/write 会改变读写指针,这点在解析数据的时候千万要注意
1 | release() 与 retain() |
Netty使用堆外内存,堆外内存是不被JVM直接管理的,申请到的内存无法被垃圾回收器直接回收需要手动回收,即申请到的内存必须手工释放,否则会造成内存泄漏.
ByteBuf通过引用计数方式管理,如果ByteBuf没有地方被引用到,需要回收底层内存.
默认情况下,当创建完ByteBuf时,其引用为1,然后每次调用retain()
方法,引用加1,调用release()
方法原理是将引用计数减1,减完发现引用计数为0时,回收ByteBuf底层分配内存.
1 | slice()、duplicate()、copy() |
slice()
方法会从原始 ByteBuf 中截取一段,这段数据是从 readerIndex 到 writeIndex(上图中的绿色部分,也就是原始ByteBuf的可读部分),同时,返回的新的 ByteBuf 的最大容量 maxCapacity 为原始 ByteBuf 的 readableBytes()
简单来说,就是把原始的ByteBuf的可读部分单独截取出来成一个新的ByteBuf,然后最大长度就是原始ByteBuf的可读长度
duplicate()
方法会把整个 ByteBuf 都截取出来,包括所有的数据,指针信息
slice()与duplicate()对比:
- 相同点: 底层内存以及引用计数与原始的ByteBuf共享,即经过slice()或者duplicate()返回的ByteBuf调用write系列方法都会影响到原始的ByteBuf,但是它们都维持着与原始ByteBuf相同的内存引用计数和不同的读写指针
- 不同点: slice()只截取从readerIndex到writerIndex之间的数据,返回的ByteBuf最大容量被限制到原始ByteBuf的readableBytes(),duplicate()是把整个ByteBuf都与原始的ByteBuf共享
还有一个是copy()方法,slice() 方法与 duplicate() 方法不会拷贝数据,它们只是通过改变读写指针来改变读写的行为,而最后一个方法 copy() 会直接从原始的 ByteBuf 中拷贝所有的信息,包括读写指针以及底层对应的数据,因此,往 copy() 返回的 ByteBuf 中写数据不会影响到原始的 ByteBuf
slice()方法与duplicate()方法不会改变原始ByteBuf的引用计数,所以原始的ByteBuf调用release()
之后发现引用计数为零的时候开始释放内存,调用这两个方法返回的ByteBuf也会被释放,此时如果再对它们进行读写会报错.因此需要调用一次retain()
方法增加引用,表示它们对应的底层的内存多一次引用,引用计数为2,在释放内存的时候需要调用两次 release()
方法将引用计数降到零才会释放内存
这三个方法均维护着自己的读写指针,与原始的 ByteBuf 的读写指针无关,相互之间不受影响
1 | retainedSlice() 与 retainedDuplicate() |
这两个方法就是在截取片段的同时增加其内存的引用计数,这两个API等价于以下两端代码:1
slice().retain(); 与 duplicate().retain();
多个 ByteBuf 可以引用同一段内存,通过引用计数来控制内存的释放,遵循谁 retain() 谁 release() 的原则
使用到 slice 和 duplicate 方法的时候,千万要理清内存共享,引用计数共享,读写指针不共享几个概念
最后,一个ByteBuf的例子,放在了gitHub上,传送门