本文共 13914 字,大约阅读时间需要 46 分钟。
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 bytearrays ({@code byte[]}) and {@linkplain ByteBuffer NIO buffers}.
就是说 ByteBuf
是一个字节序列,可以随机或连续存取零到多个字节。他提供了一个统一的抽象,通过 ByteBuf
可以操作基础的字节数组和ByteBuffer缓冲区。
需要注意的是这里说的 interface
是不准确的,因为Trustin Lee在2013/7/8将ByteBuffer从接口改成了抽象类,具体的原因不得而知。
ByteBuf比JDK中原生的ByteBuffer好的原因是前者的设计比后者优秀,ByteBuf有读和写两个指针,而ByteBuffer只有一个指针,需要通过flip()方法在读和写之间进行模式切换,需要操作的越多往往犯错的概率就越大。ByteBuf将读和写进行了分离,使用者不用再关心现在是读还是写的模式,可以把更多的精力用在具体的业务上。
官方定义中指出,ByteBuf主要是通过两个指针进行数据的读和写,分别是 readerIndex
和 writerIndex
,并且整个ByteBuf被这两个指针最多分成三个部分,分别是可丢弃部分,可读部分和可写部分,可以用一张图直观的描述ByteBuf的结构,如下图所示:
可能有人注意到了我说ByteBuf最多被分成三个部分,那是因为某些情况下可能只有一到两部分:
刚初始化的时候,读写指针都是0,所有的内容都是可写部分,此时还没有可读部分和可丢弃部分。
刚写完一些数据后,读指针仍然是0,写指针向后移动了n
,这里的n
就是写入的字节数。
写入完数据之后,紧接着读取一部分数据,然后立刻丢弃掉,此时ByteBuf的结构就会变成跟第二步中的一样。因为丢弃的动作会将读指针向左移动到0的位置,写指针向左移动的距离=原来读指针的值
ByteBuf中定义了两类方法可以往ByteBuf中写入内容:writeXX()
和 setXX()
。
具体的setXX()类的方法可以用下面的一张表格来描述:
方法名 | 描述 |
---|---|
setByte(int index, int value) | 将指定位置上的内容修改为指定的byte的值高24位上的内容将被丢弃 |
setBoolean(int index, boolean value) | 将指定位置上的内容修改为指定的boolean的值 |
setBytes(int index,byte src) | 将指定的字节内容可以从byte[],ByteBuf,ByteBuffer,InputStream,Channel等中获取转移到指定的位置 |
setChar*(int index, int value) | 将指定位置上的内容修改为指定的character的UTF-16编码下2-byte的值高16位上的内容将被丢弃 |
setShort*(int index, int value) | 将指定位置上的内容修改为指定的integer的低16-bit的值高16位上的内容将被丢弃 |
setMidium*(int index, int value) | 将指定位置上的内容修改为指定的integer的中间24-bit的值大多数重要的内容将被丢弃 |
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在4.x的版本中是一个抽象类,他有很多的抽象子类以及各种实现类。
画了一个简单的ByteBuf的各个实现类之间的关系,其中蓝色的类是被弃用的。
上图只是简单的列举的一些常用的ByteBuf类,如果你想知道ByteBuf所有的实现类,那么可以在IDEA中选
则ByteBuf类之后,然后在菜单 navigate
中点击 Type Hierarchy
或用快捷键:control+H,即可打开ByteBuf的类层次结构图,具体的层级结构如下图所示:
本篇文章只简单的让大家对于ByteBuf的种类有个大概的了解,具体的每一种ByteBuf的作用我将在后续的章节中进行介绍。
有一点我们需要知道的是,ByteBuf的jar包,是可以单独使用的。比如某个项目中有一个场景,需要处理某个自定义的协议,那么我们在解析协议时,就可以将接收到的将字节内容写入一个ByteBuf,然后从ByteBuf中慢慢的将内容读取出来。下面让我们用一个例子简单的了解下ByteBuf的使用。
要想使用ByteBuf,首先肯定是要创建一个ByteBuf,更确切的说法就是要申请一块内存,后续可以在这块内存中执行写入数据读取数据等等一系列的操作。
那么如何创建一个ByteBuf呢?Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。
具体的层级关系如下图所示:有了Allocator之后,Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。
我们以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但最终都是以下这几种,只是参数不一样而已:
// 在堆上分配一个ByteBuf,并指定初始容量和最大容量public static ByteBuf buffer(int initialCapacity, int maxCapacity) { return ALLOC.heapBuffer(initialCapacity, maxCapacity);}// 在堆外分配一个ByteBuf,并指定初始容量和最大容量public static ByteBuf directBuffer(int initialCapacity, int maxCapacity) { return ALLOC.directBuffer(initialCapacity, maxCapacity);}// 使用包装的方式,将一个byte[]包装成一个ByteBuf后返回public static ByteBuf wrappedBuffer(byte[] array) { if (array.length == 0) { return EMPTY_BUFFER; } return new UnpooledHeapByteBuf(ALLOC, array, array.length);}// 返回一个组合ByteBuf,并指定组合的个数public static CompositeByteBuf compositeBuffer(int maxNumComponents){ return new CompositeByteBuf(ALLOC, false, maxNumComponents);}
其中包装方法除了上述这个方法之外,还有一些其他常用的包装方法,比如参数是一个ByteBuf的包装方法,比如参数是一个原生的ByteBuffer的包装方法,比如指定一个内存地址和大小的包装方法等等。
另外还有一些copy*开头的方法,实际是调用了buffer(int initialCapacity, int maxCapacity)或directBuffer(int initialCapacity, int maxCapacity)方法,然后将具体的内容write进生成的ByteBuf中返回。以上所有的这些方法都实际通过一个叫ALLOC的静态变量进行了调用,来实现具体的ByteBuf的创建,而这个ALLOC实际是一个ByteBufAllocator:private static final ByteBufAllocator ALLOC = UnpooledByteBufAllocator.DEFAULT;
ByteBufAllocator是一个专门负责ByteBuf分配的接口,对应的Unpooled实现类就是UnpooledByteBufAllocator。在UnpooledByteBufAllocator类中可以看到UnpooledByteBufAllocator.DEFAULT变量是一个final类型的静态变量
/** * Default instance which uses leak-detection for direct buffers. * 默认的UnpooledByteBufAllocator实例,并且会对堆外内存进行泄漏检测 */public static final UnpooledByteBufAllocator DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
ByteBuf和ByteBufAllocator之间是一种相辅相成的关系,ByteBufAllocator用来创建一个ByteBuf,而ByteBuf亦可以返回创建他的Allocator。ByteBuf和ByteBufAllocator之间是一种 抽象工厂模式
,具体可以用一张图描述如下:
下面我来用一个实际的例子来说明ByteBuf的使用,并通过观察在不同阶段ByteBuf的读写指针的值和ByteBuf的容量变化来更加深入的了解ByteBuf的设计,为了方便,我会用非池化的分配器来创建ByteBuf。
我构造了一个demo,来演示在ByteBuf中插入数据、读取数据、清空读写指针、数据清零、扩容等等方法,具体的代码如下:
private static void simpleUse(){ // 1.创建一个非池化的ByteBuf,大小为10个字节 ByteBuf buf = Unpooled.buffer(10); System.out.println("原始ByteBuf为====================>"+buf.toString()); System.out.println("1.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n"); // 2.写入一段内容 byte[] bytes = {1,2,3,4,5}; buf.writeBytes(bytes); System.out.println("写入的bytes为====================>"+Arrays.toString(bytes)); System.out.println("写入一段内容后ByteBuf为===========>"+buf.toString()); System.out.println("2.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n"); // 3.读取一段内容 byte b1 = buf.readByte(); byte b2 = buf.readByte(); System.out.println("读取的bytes为====================>"+Arrays.toString(new byte[]{b1,b2})); System.out.println("读取一段内容后ByteBuf为===========>"+buf.toString()); System.out.println("3.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n"); // 4.将读取的内容丢弃 buf.discardReadBytes(); System.out.println("将读取的内容丢弃后ByteBuf为========>"+buf.toString()); System.out.println("4.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n"); // 5.清空读写指针 buf.clear(); System.out.println("将读写指针清空后ByteBuf为==========>"+buf.toString()); System.out.println("5.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n"); // 6.再次写入一段内容,比第一段内容少 byte[] bytes2 = {1,2,3}; buf.writeBytes(bytes2); System.out.println("写入的bytes为====================>"+Arrays.toString(bytes2)); System.out.println("写入一段内容后ByteBuf为===========>"+buf.toString()); System.out.println("6.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n"); // 7.将ByteBuf清零 buf.setZero(0,buf.capacity()); System.out.println("将内容清零后ByteBuf为==============>"+buf.toString()); System.out.println("7.ByteBuf中的内容为================>"+Arrays.toString(buf.array())+"\n"); // 8.再次写入一段超过容量的内容 byte[] bytes3 = {1,2,3,4,5,6,7,8,9,10,11}; buf.writeBytes(bytes3); System.out.println("写入的bytes为====================>"+Arrays.toString(bytes3)); System.out.println("写入一段内容后ByteBuf为===========>"+buf.toString()); System.out.println("8.ByteBuf中的内容为===============>"+Arrays.toString(buf.array())+"\n");}
执行结果如下:
原始ByteBuf为====================>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)1.ByteBuf中的内容为===============>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]写入的bytes为====================>[1, 2, 3, 4, 5]写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 5, cap: 10)2.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]读取的bytes为====================>[1, 2]读取一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 2, widx: 5, cap: 10)3.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]将读取的内容丢弃后ByteBuf为========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)4.ByteBuf中的内容为===============>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]将读写指针清空后ByteBuf为==========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)5.ByteBuf中的内容为===============>[3, 4, 5, 4, 5, 0, 0, 0, 0, 0]写入的bytes为====================>[1, 2, 3]写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)6.ByteBuf中的内容为===============>[1, 2, 3, 4, 5, 0, 0, 0, 0, 0]将内容清零后ByteBuf为==============>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 3, cap: 10)7.ByteBuf中的内容为================>[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]写入的bytes为====================>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]写入一段内容后ByteBuf为===========>UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 14, cap: 64)8.ByteBuf中的内容为===============>[0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
下面让我们来仔细的研究下执行的过程,并分析下为什么会产生这样的执行结果。
刚初始化的ByteBuf对象,容量为10,读写指针都为0,且每个字节的值都为0,并且这些字节都是“可写”的,我们用红色来表示。
当写入一段内容后(这里写入的是5个字节),写指针向后移动了5个字节,写指针的值变成了5,而读指针没有发生变化还是0,但是读指针和写指针之间的字节现在变成了“可读”的状态了,我们用紫色来表示。
接着我们有读取了2个字节的内容,这时读指针向后移动了2个字节,读指针的值变成了2,写指针不变,此时0和读指针之间的内容变成了“可丢弃”的状态了,我们用粉色来表示。
紧接着,我们将刚刚读取完的2个字节丢弃掉,这时ByteBuf把读指针与写指针之间的内容(即 3
、4
、5
三个字节)移动到了0的位置,并且将读指针更新为0,写指针更新为原来写指针的值减去原来读指针的值。但是需要注意的是,第4和第5个字节的位置上,还保留的原本的内容,只是这两个字节由原来的“可读”变成了现在的“可写”。
然后,我们执行了一个 clear
方法,将读写指针同时都置为0了,此时所有的字节都变成“可写”了,但是需要注意的是,clear方法只是更改的读写指针的值,每个位置上原本的字节内容并没有发生改变。
然后再次写入一段内容后,读指针不变,写指针向后移动了具体的字节数,这里是向后移动了三个字节。且写入的这三个字节变成了“可读”状态。
清零(setZero
)和清空(clear
)的方法是两个概念完全不同的方法,“清零”是把指定位置上的字节的值设置为0,除此之外不改变任何的值,所以读写指针的值和字节的“可读写”状态与上次保持一致,而“清空”则只是将读写指针都置为0,并且所有字节都变成了“可写”状态。
最后我们往ByteBuf中写入超过ByteBuf容量的内容,这里是写入了11个字节,此时ByteBuf原本的容量不足以写入这些内容了,所以ByteBuf发生了扩容。其实只要写入的字节数超过可写字节数,就会发生扩容了。
那么扩容是怎么扩的呢,为什么容量从10扩容到64呢?我们从源码中找答案。
扩容肯定发生在写入字节的时候,让我们找到 writeBytes(byte[] bytes)
方法,具体如下:
@Overridepublic ByteBuf writeBytes(byte[] src) { writeBytes(src, 0, src.length); return this;}@Overridepublic ByteBuf writeBytes(byte[] src, int srcIndex, int length) { // 该方法检查是否有足够的可写空间,是否需要进行扩容 ensureWritable(length); setBytes(writerIndex, src, srcIndex, length); writerIndex += length; return this;}
在进入 ensureWritable(length)
方法内部查看,具体的代码如下:
@Overridepublic ByteBuf ensureWritable(int minWritableBytes) { if (minWritableBytes < 0) { throw new IllegalArgumentException(String.format( "minWritableBytes: %d (expected: >= 0)", minWritableBytes)); } ensureWritable0(minWritableBytes); return this;}final void ensureWritable0(int minWritableBytes) { // 检查该ByteBuf对象的引用计数是否为0,保证该对象在写入之前是可访问的 ensureAccessible(); if (minWritableBytes <= writableBytes()) { return; } if (minWritableBytes > maxCapacity - writerIndex) { throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } // Normalize the current capacity to the power of 2. // 计算新的容量,即为当前容量扩容至2的幂次方大小 int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity); // Adjust to the new capacity. // 设置扩容后的容量 capacity(newCapacity);}
从上面的代码中可以很清楚的看出来,计算新的容量的方法是调用的 ByteBufAllocator 的 calculateNewCapacity()
方法,继续跟进去该方法,这里的 ByteBufAllocator 的实现类是 AbstractByteBufAllocator
,具体的代码如下:
@Overridepublic int calculateNewCapacity(int minNewCapacity, int maxCapacity) { if (minNewCapacity < 0) { throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)"); } if (minNewCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "minNewCapacity: %d (expected: not greater than maxCapacity(%d)", minNewCapacity, maxCapacity)); } // 扩容的阈值,4兆字节大小 final int threshold = CALCULATE_THRESHOLD; // 4 MiB page if (minNewCapacity == threshold) { return threshold; } // If over threshold, do not double but just increase by threshold. // 如果要扩容后新的容量大于扩容的阈值,那么扩容的方式改为用新的容量加上阈值, // 否则将新容量改为双倍大小进行扩容 if (minNewCapacity > threshold) { int newCapacity = minNewCapacity / threshold * threshold; if (newCapacity > maxCapacity - threshold) { newCapacity = maxCapacity; } else { newCapacity += threshold; } return newCapacity; } // Not over threshold. Double up to 4 MiB, starting from 64. // 如果要扩容后新的容量小于4兆字节,则从64字节开始扩容,每次双倍扩容, // 直到小于指定的新容量位置 int newCapacity = 64; while (newCapacity < minNewCapacity) { newCapacity <<= 1; } return Math.min(newCapacity, maxCapacity);}
到这里就很清楚了,每次扩容时,有一个阈值t(4MB),计划扩容的大小为c,扩容后的值为n。
扩容的规则可以用下面的逻辑表示:
转载地址:http://ucvwx.baihongyu.com/