Netty 分享之 ByteBuf 再探

逅弈 转载请注明原创出处,谢谢!

我们已经知道ByteBuf是Netty中非常重要的一个组件,他就像物流公司的运输工具:卡车,火车,甚至是飞机。而物流公司靠什么盈利,就是靠运输货物,可想而知ByteBuf在Netty中是多么的重要。没有了ByteBuf,Netty就失去了灵魂,其他所有的都将变得毫无意义。

ByteBuf是由Byte和Buffer两个词组合成的一个词,但是因为JDK中已经有了一个ByteBuffer,并且使用非常复杂,API及其不友好,可谓是千夫所指。为了扭转ByteBuffer在大家心目中的形象,Netty重新设计了一个ByteBuffer,即 ByteBuf

从字面上我们可以知道 ByteBuf 是处理字节的,并且还有一种缓冲的能力。

ByteBuf在官方中是这样定义的:

A random and sequential accessible sequence of zero or more bytes (octets).
This interface provides an abstract view for one or more primitive byte
arrays ({@code byte[]}) and {@linkplain ByteBuffer NIO buffers}.

就是说 ByteBuf 是一个字节序列,可以随机或连续存取零到多个字节。他提供了一个统一的抽象,通过 ByteBuf 可以操作基础的字节数组和ByteBuffer缓冲区。

需要注意的是这里说的 interface 是不准确的,因为Trustin Lee在2013/7/8将ByteBuffer从接口改成了抽象类,具体的原因不得而知。

ByteBuf的结构

ByteBuf比JDK中原生的ByteBuffer好的原因是前者的设计比后者优秀,ByteBuf有读和写两个指针,而ByteBuffer只有一个指针,需要通过flip()方法在读和写之间进行模式切换,需要操作的越多往往犯错的概率就越大。ByteBuf将读和写进行的分离,使用者不用再关心现在是读还是写的模式,可以把更多的精力用在具体的业务上。

官方定义中指出,ByteBuf主要是通过两个指针进行数据的读和写,分别是 readerIndexwriterIndex ,并且整个ByteBuf被这两个指针最多分成三个部分,分别是可丢弃部分可读部分可写部分,可以用一张图直观的描述ByteBuf的结构,如下图所示:

image-byte-buf-structure.png

可能有人注意到了我说ByteBuf最多被分成三个部分,那是因为某些情况下可能只有一到两部分:

  • 刚初始化的时候
image-byte-buf-inited.png

刚初始化的时候,读写指针都是0,所有的内容都是可写部分,此时还没有可读部分和可丢弃部分。

  • 刚写完数据后
image-byte-buf-writed.png

刚写完一些数据后,读指针仍然是0,写指针向后移动了n,这里的n就是写入的字节数。

  • 读完一部分数据并丢弃之后
image-byte-buf-after-discard.png

写入完数据之后,紧接着读取一部分数据,然后立刻丢弃掉,此时ByteBuf的结构就会变成跟第二步中的一样。因为丢弃的动作会将读指针向左移动到0的位置,写指针向左移动的距离=原来读指针的值

ByteBuf的读写操作

写操作

ByteBuf中定义了两类方法可以往ByteBuf中写入内容:writeXX()setXX()

具体的setXX()类的方法可以用下面的一张表格来描述:

方法名 描述
setByte(int index, int value) 将指定位置上的内容修改为指定的byte的值<br />高24位上的内容将被丢弃
setBoolean(int index, boolean value) 将指定位置上的内容修改为指定的boolean的值
setBytes(int index,byte src) 将指定的字节内容<br />可以从byte[],ByteBuf,ByteBuffer,InputStream,Channel等中获取<br />转移到指定的位置
setChar*(int index, int value) 将指定位置上的内容修改为指定的character的UTF-16编码下2-byte的值<br />高16位上的内容将被丢弃
setShort*(int index, int value) 将指定位置上的内容修改为指定的integer的低16-bit的值<br />高16位上的内容将被丢弃
setMidium*(int index, int value) 将指定位置上的内容修改为指定的integer的中间24-bit的值<br />大多数重要的内容将被丢弃
setInt*(int index, int value) 将指定位置上的内容修改为指定的32-bit的integer的值
setFloat*(int index, float value) 将指定位置上的内容修改为指定的32-bit的float的值
setDouble*(int index, double value) 将指定位置上的内容修改为指定的64-bit的float的值
setLong*(int index, long value) 将指定位置上的内容修改为指定的64-bit的long的值
setZero(int index, int length) 将从指定位置index开始之后的length个长度的值设置为0x00

我们知道java中一个int占4个字节,即32bit,一个short占2个字节,一个int可以拆成2个short,所以就会存在当写入一个short时,参数用int来传值时,高16位的内容会被丢弃。这是因为一个int被拆成了两个short,而写入一个short到指定的位置时,那么另一个short就被丢弃了,且是高16位的这个short。

有的人注意到了上面好多方法后面都有,这是表示这些方法还有一种兄弟方法,如setInt对应的是setIntLE,这表示以小端字节序*的方式写入内容。简单来说一般网络传输采用大端字节序,另外我们人类写字节的顺序也是大端字节序,而计算机处理字节的顺序一般是小端字节序(但是也不绝对,计算机从低电平开始读取字节时效率更高),具体什么是大端字节序,什么是小端字节序不是本篇文章深入研究的范围,大家可以自行查阅有关资料。

PS:需要注意的是如果写入的位置index小于0,或者index加上写入内容的值超过capcity的话,会抛出 IndexOutOfBoundsException,所以就存在两个比较重要的方法:isWritable()isReadable(),他们将返回当前ByteBuf中是否还有足够的空间可以写和可以读

具体的writeXX()方法与上面的setXX()方法类似,不同的是writeXX()方法会更新写指针,即向ByteBuf中写入具体的内容后,writeIndex会向后移动与写入的内容字节数长度相同的距离。

读操作

跟写操作一样,ByteBuf的读操作也有两种方法,分别是getXX()和readXX()。

读操作包含的具体方法与写操作也是一一对应的,具体的可以把上面的那张表格中的set改为get,并且将第二个value参数移除即可,例如:getShort(int index)getInt(int index)等等。

与getXX()方法相关的另一类方法就是readXX()方法了,与get方法不同的是,read方法会更改读指针的值。

ByteBuf的种类

我们知道ByteBuf在4.x的版本中是一个抽象类,他有很多的抽象子类以及各种实现类。

画了一个简单的ByteBuf的各个实现类之间的关系,其中蓝色的类是被弃用的。

image-byte-buf-hierarchy.png

上图只是简单的列举的一些常用的ByteBuf类,如果你想知道ByteBuf所有的实现类,那么可以在IDEA中选

则ByteBuf类之后,然后在菜单 navigate 中点击 Type Hierarchy 或用快捷键:control+H,即可打开ByteBuf的类层次结构图,具体的层级结构如下图所示:

image-byte-buf-type-hierarchy.png

本篇文章只简单的让大家对于ByteBuf的种类有个大概的了解,具体的每一种ByteBuf的作用我将在后续的章节中进行介绍。

更多原创好文,请关注「逅弈逐码」

推荐阅读更多精彩内容