天天看点

Netty学习之旅------第3篇---ByteBuf解读1. JDK ByteBuffer2. ByteBuf 的设计3. ByteBuf 功能介绍4 总结

Netty学习之旅------第3篇---ByteBuf解读

  • 1. JDK ByteBuffer
    • 1.1 长度固定
    • 1.2 操作复杂
    • 1.3 API 不丰富
  • 2. ByteBuf 的设计
    • 2.1 引入writeIndex 和 readerIndex
    • 2.2 扩容
  • 3. ByteBuf 功能介绍
    • 3.1 顺序读写
      • 3.1.2 顺序读
      • 3.1.2 顺序写
    • 3.2 随机读写
      • 3.2.1 随机读
      • 3.2.2 随机写
    • 3.3 readerIndex和writeIndex
    • 3.4 discardable bytes、readable bytes 和 writeable bytes
    • 3.5 clear mark 和 reset
      • 3.5.1 clear 方法
      • 3.5.2 mark 方法和reset 方法
    • 3.6 转成 JDK 的ByteBuffer
  • 4 总结

温馨提示:内容局限于本人的理解,如果有错误,请指正,谢谢!

学习目标:

(1)了解JDK 的ByteBuffer 的局限性

(2)了解ByteBuf的设计思想

(3)了解ByteBuf的API

1. JDK ByteBuffer

前面章节,我们学习了NIO的缓冲区,当我们要进行数据传输的时候,往往需要用到缓冲区,常用的缓冲区就是JDK 的 ByteBuffer,也还有其它的缓冲区,针对不同的数据类型,都有不同的缓冲区。但NIO 编程很复杂,JDK 的ByteBuffer 也有其局限性,总结如下:

1.1 长度固定

在初始缓冲区的时候,需要指定长度,长度一旦固定,将不能修改。不能缩容和扩容,如果存储对象,错误的预估了对象的大小,将导致索引越界异常。

1.2 操作复杂

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的内容,才是我们想要的内容。

1.3 API 不丰富

很多的api也不支持,如果要实现某些功能,还需要开发人员自己编程实现

2. ByteBuf 的设计

注意这个是netty的类了,类名不一样。这个是对于JDK ByteBuffer 的局限性来设计的。那它是怎么缩容和扩容,和flip类似的操作的呢?带着疑问,继续往下看。

2.1 引入writeIndex 和 readerIndex

为了避免复杂的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
           

2.2 扩容

JDK的ByteBuffer 容量是固定的,如果存储的数据超过容量,会抛出异常,因此ByteBuf中引入了扩容机制,在写入的时候,进行容量判断,如果容纳不下,则按照一定比例进行扩容。

3. ByteBuf 功能介绍

3.1 顺序读写

3.1.2 顺序读

顺序读是read开头的方法,相当于JDK ByteBuffer 中的get操作。具体的API 如下图所示

Netty学习之旅------第3篇---ByteBuf解读1. JDK ByteBuffer2. ByteBuf 的设计3. ByteBuf 功能介绍4 总结

比如

readByte就是从readerIndex开始读字节,读完readerIndex++,返回byte类型;

readInt就是从readerIndex开始读整型,读完readerIndex=readerIndex+4,返回整形。

3.1.2 顺序写

顺序读是write开头的方法,相当于JDK ByteBuffer 中的put操作。具体的API 如下图所示

Netty学习之旅------第3篇---ByteBuf解读1. JDK ByteBuffer2. ByteBuf 的设计3. ByteBuf 功能介绍4 总结

比如

writeBytes(ByteBuf src) 是从writeIndex开始写,把src中的数据都写入到当前缓冲区,写完writeIndex=writeIndex+readableBytes,如果src缓冲区的可读字节数大于当前缓冲区可写的字节数,将抛出IndexOutOfBoundsException异常。

writeZero(int length) 是将当前缓冲区填充为NUL(0x00),length是填充的长度,填充后writeIndex=writeIndex+length

3.2 随机读写

随机读写的API都会判断index是否合法,如果不合法,直接抛出IndexOutOfBoundsException异常。

3.2.1 随机读

以get开头的方法,都提供了根据index来读操作的API

Netty学习之旅------第3篇---ByteBuf解读1. JDK ByteBuffer2. ByteBuf 的设计3. ByteBuf 功能介绍4 总结

3.2.2 随机写

以set开头的方法,都提供了根据index来写操作的API

Netty学习之旅------第3篇---ByteBuf解读1. JDK ByteBuffer2. ByteBuf 的设计3. ByteBuf 功能介绍4 总结

3.3 readerIndex和writeIndex

readerIndex 维护了读索引,往缓冲区读数据的时候,readerIndex会根据你读取的长度增加。

writeIndex 维护了写索引,往缓冲区写数据的时候,writeIndex会根据你写入的长度增加,引入这2个,大大降低了缓冲区的复杂度。

0 <= readerIndex <= writerIndex <= capacity

0到readerIndex 表示已经读取过的缓冲区

readerIndex到writerIndex表示未读取过的缓冲区

writerIndex到capacity表示还可以写入的缓冲区

3.4 discardable bytes、readable bytes 和 writeable bytes

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要慎用。

3.5 clear mark 和 reset

3.5.1 clear 方法

clear方法是把readerIndex和writeIndex重置为0,并不会情况缓冲区内容,调用后全部区域变成可写状态。

*      +---------------------------------------------------------+
 *      |                    writable bytes                       |
 *      |                                                         |
 *      +---------------------------------------------------------+
 *      |                                                         |
 *      0=readerIndex=writerIndex                            capacity
           

3.5.2 mark 方法和reset 方法

mark方法其实就是备份readerIndex、writerIndex的值,reset就是还原到mark备份的值,例如有些操作需要回滚,就可以根据mark标记过的位置,调用reset回滚,在ByteBuf中,对应的有2个mark方法和2个reset方法,分别是:markReaderIndex和markWriterIndex、resetReaderIndex和resetWriterIndex。

3.6 转成 JDK 的ByteBuffer

netty 提供了nioBuffer相关方法转成NIO 的ByteBuffer

Netty学习之旅------第3篇---ByteBuf解读1. JDK ByteBuffer2. ByteBuf 的设计3. ByteBuf 功能介绍4 总结

4 总结

通过对ByteBuf的分析,我们知道其实ByteBuf是在ByteBuffer的局限性而设计的。简化了开发人员的学习成本,并且定义了顶层的api接口,后续将会对其一些实现类进行具体的分析。