温馨提示:内容局限于本人的理解,如果有错误,请指正,谢谢!
学习目标:
(1)了解JDK 的ByteBuffer 的局限性
(2)了解ByteBuf的设计思想
(3)了解ByteBuf的API
前面章节,我们学习了NIO的缓冲区,当我们要进行数据传输的时候,往往需要用到缓冲区,常用的缓冲区就是JDK 的 ByteBuffer,也还有其它的缓冲区,针对不同的数据类型,都有不同的缓冲区。但NIO 编程很复杂,JDK 的ByteBuffer 也有其局限性,总结如下:
在初始缓冲区的时候,需要指定长度,长度一旦固定,将不能修改。不能缩容和扩容,如果存储对象,错误的预估了对象的大小,将导致索引越界异常。
JDK ByteBuffer的API使用起来不方便,比如要把一个缓冲区由写状态变成读状态,还需要通过flip方法,翻转缓冲区。开发人员必须小心的使用这些API,极容易出错。下面通过案例来看下flip方法:在缓冲区中,读写是通过position和limit来控制的,当写入的时候,初始的position=0,limit=可写的最大长度,当写入一个内容,
+---------------------------------------------------------+
| |
+---------------------------------------------------------+
| |
0=position limit ==capacity
当我写入一个字符’H’,就改变成下图了
+---------------------------------------------------------+
|H| |
+-+-------------------------------------------------------+
| |
0 < position limit ==capacity
此时position=1了。如果我要读取这个缓冲区中的内容,需要做翻转,否则读取到的就是position到limit直接的空数据。调用flip后
+---------------------------------------------------------+
|H| |
+-+-------------------------------------------------------+
| |
0=position limit=1 capacity
翻转的操作其实就是做了:limit = position;position=0 ,这样读取,position到limit的内容,才是我们想要的内容。
很多的api也不支持,如果要实现某些功能,还需要开发人员自己编程实现
注意这个是netty的类了,类名不一样。这个是对于JDK ByteBuffer 的局限性来设计的。那它是怎么缩容和扩容,和flip类似的操作的呢?带着疑问,继续往下看。
为了避免复杂的position和limit的关系,这里引入了writeIndex 和 readerIndex,看名字就知道,这是一个读的索引和写的索引。内部的结构是这样的。
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* | | (CONTENT) | |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
这里的区域简单介绍下:
discardable bytes:表示已经读取,可以丢弃的区域。
readable bytes:是还未读取,已存内容的区域。
writable bytes: 是可写入的内容的区域
当往ByteBuf 写入的时候,writeIndex增加,读取的时候会推动readerIndex增加,比如我读取一个调用writeInt(int value)方法,写入一个int 类型的值,writeIndex会在原理基础上增加int的字节长度,即writeIndex=writeIndex+4;如果调用readInt(),会在readerIndex的基础上减少int的字节长度,即readerIndex=readerIndex-4,其它的API也类似。
这是里面一个永恒的关系:
0 <= readerIndex <= writerIndex <= capacity
JDK的ByteBuffer 容量是固定的,如果存储的数据超过容量,会抛出异常,因此ByteBuf中引入了扩容机制,在写入的时候,进行容量判断,如果容纳不下,则按照一定比例进行扩容。
顺序读是read开头的方法,相当于JDK ByteBuffer 中的get操作。具体的API 如下图所示
比如
readByte就是从readerIndex开始读字节,读完readerIndex++,返回byte类型;
readInt就是从readerIndex开始读整型,读完readerIndex=readerIndex+4,返回整形。
顺序读是write开头的方法,相当于JDK ByteBuffer 中的put操作。具体的API 如下图所示
比如
writeBytes(ByteBuf src) 是从writeIndex开始写,把src中的数据都写入到当前缓冲区,写完writeIndex=writeIndex+readableBytes,如果src缓冲区的可读字节数大于当前缓冲区可写的字节数,将抛出IndexOutOfBoundsException异常。
writeZero(int length) 是将当前缓冲区填充为NUL(0x00),length是填充的长度,填充后writeIndex=writeIndex+length
随机读写的API都会判断index是否合法,如果不合法,直接抛出IndexOutOfBoundsException异常。
readerIndex 维护了读索引,往缓冲区读数据的时候,readerIndex会根据你读取的长度增加。
writeIndex 维护了写索引,往缓冲区写数据的时候,writeIndex会根据你写入的长度增加,引入这2个,大大降低了缓冲区的复杂度。
0 <= readerIndex <= writerIndex <= capacity
0到readerIndex 表示已经读取过的缓冲区
readerIndex到writerIndex表示未读取过的缓冲区
writerIndex到capacity表示还可以写入的缓冲区
discardable bytes 表示已经读取过,可以释放的区域。但是此方法是以时间换空间的操作。原因是调用discardable byte 会导致字节数组的内存复制。
例如:下图展示了写入了一部分数据的缓冲区,
调用前
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* | | (CONTENT) | |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
调用discardReadBytes方法后:
* +-------------------+-------------------------------------+
* | readable bytes | writable bytes |
* | (CONTENT) | |
* +-------------------+-------------------------------------+
* | | |
* readerIndex=0 <= writerIndex <= capacity
等于把discardable bytes区域的释放掉,然后把readable bytes移动到discardable bytes释放出来的区域,然后可写入的区域就变大了。所以discardable bytes要慎用。
clear方法是把readerIndex和writeIndex重置为0,并不会情况缓冲区内容,调用后全部区域变成可写状态。
* +---------------------------------------------------------+
* | writable bytes |
* | |
* +---------------------------------------------------------+
* | |
* 0=readerIndex=writerIndex capacity
mark方法其实就是备份readerIndex、writerIndex的值,reset就是还原到mark备份的值,例如有些操作需要回滚,就可以根据mark标记过的位置,调用reset回滚,在ByteBuf中,对应的有2个mark方法和2个reset方法,分别是:markReaderIndex和markWriterIndex、resetReaderIndex和resetWriterIndex。
netty 提供了nioBuffer相关方法转成NIO 的ByteBuffer
通过对ByteBuf的分析,我们知道其实ByteBuf是在ByteBuffer的局限性而设计的。简化了开发人员的学习成本,并且定义了顶层的api接口,后续将会对其一些实现类进行具体的分析。