一、ByteBuffer
当我们进行数据传输的时候,往往用到数据缓冲区,常用的缓冲区是JDK NIO提供的java.nio.Buffer。
在NIO中,7种基本数据类型(boolean除外)都有自己的缓冲区实现。对于NIO编程而言,主要使用的是ByteBuffer。
然而,ByteBuffer有自己的一些缺陷:
-
长度固定。一旦分配完成,容量不能进行扩展和收缩;
-
只有一个标识位置的指针position,读写的时候需要手动调用flip()和rewind()等方法;
-
API功能有限。一些高级和使用的特性都不支持;
为了弥补这些缺陷,Netty提供了自己的缓冲区ByteBuffer类ByteBuf。
二、ByteBuffer工作原理
JDK ByteBuffer由于只有一个位置指针用来处理读写操作,所以每次读写之后都需要额外调用flip()方法和clear()等方法用来重置指针,如以下使用场景:
String str = "Hello,World";
ByteBuffer buffer = ByteBuffer.allocate(50);
buffer.put(str.getBytes());
buffer.flip();
byte[] newArray = new byte[buffer.remaining()];
buffer.get(newArray);
System.out.println(new String(newArray));
这里如果没有调用flip()方法,将得不到正确的结果,原因如下:
- 在调用flip方法之前,数组效果如下:
读取到的是position到limit之间的数据,那么将什么也读取不到 - 当我们调用了flip()之后,数据的标识会发生变化。
flip()方法:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
所以调用完了之后,数组情况如下:
这时候再读取position和limit之间的内容,就能正确读取了。
三、ByteBuf工作原理
首先需要说明的是,ByteBuf是通过以下两种方式进行实现的:
- 参考JDK ByteBuffer的实现,增加额外的功能,解决原ByteBuffer的缺点;
- 聚合JDK ByteBuffer,通过Facade模式对其进行包装。
所以具体的ByteBuf的实现,也有多种。如直接基于byte数组的,也有继承自java.nio.ByteBuffer的
(一)ByteBuf的改进
针对ByteBuffer单个指针维护的复杂问题,ByteBuf进行了如下优化:
- ByteBuf通过两个指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex;
- 开始的时候,readerIndex和writerIndex的值都是0;
- 随着数据的写入writerIndex会增加,读取数据会使readerIndex增加,但是它不会超过writerIndex。
- 在读取之后,0~readerIndex之间的数据会被视为discard的,调用discardReadBytes会释放空间,他的作用类似于ByteBuffer的compact方法。
- readerIndex和writerIndex之间的数据是可以读取的,等价于ByteBuffer的position和limit之间的数据;
- weiterIndex和capacity之间的空间是可写的,等价于ByteBuffer的limit和capacity之间的可用空间;
- 由于读操作不会改变writerIndex,写操作不会改变readerIndex, 因此读写操作之间不需要再调整指针,大大的简化了读写操作,避免了漏调flip()方法引起的问题;
- writableBytes()方法会返回可以写入的空间大小,readableBytes()方法
(二)ByteBuf的操作指针变化
- 初始分配的ByteBuf
2. 写入N个字节后的ByteBuf
3.读取M(M<N)个字节后的ByteBuf
4.调用discardReadBytes之后
当调用了clear()方法之后,会再次回到初始状态。
(三)ByteBuf的动态扩展
1. 扩容过程分析
ByteBuffer一已经分配,大小就固定了,不能动态的扩展和收缩,ByteBuf却可以,那么他是如何实现的呢?
跟踪AbstractByteBuf类的writeBytes方法(writeByte、writeDouble等等原理类似)如下:
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
this.ensureWritable(length); //确保能够写入
this.setBytes(this.writerIndex, src, srcIndex, length);
this.writerIndex += length;
return this;
}
public ByteBuf writeBytes(byte[] src) {
this.writeBytes((byte[])src, 0, src.length);
return this;
}
这里通过ensureWritable方法来保证字节数组能够写入,内容如下:
public ByteBuf ensureWritable(int minWritableBytes) {
if (minWritableBytes < 0) {
throw new IllegalArgumentException(String.format("minWritableBytes: %d (expected: >= 0)", minWritableBytes));
} else {
this.ensureWritable0(minWritableBytes); //确保容量ok
return this;
}
}
再次通过ensureWritable0来确保数组容量是够的
final void ensureWritable0(int minWritableBytes) {
this.ensureAccessible();
if (minWritableBytes > this.writableBytes()) {
if (minWritableBytes > this.maxCapacity - this.writerIndex) {
throw new IndexOutOfBoundsException(String.format("writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", this.writerIndex, minWritableBytes, this.maxCapacity, this));
} else {
int newCapacity = this.alloc().calculateNewCapacity(this.writerIndex + minWritableBytes, this.maxCapacity); //计算出新的数组容量
this.capacity(newCapacity); //进行扩容
}
}
}
调用了capacity方法进行了扩容
public ByteBuf capacity(int newCapacity) {
this.checkNewCapacity(newCapacity);
int oldCapacity = this.array.length;
byte[] oldArray = this.array;
byte[] newArray;
if (newCapacity > oldCapacity) { //扩容
newArray = this.allocateArray(newCapacity); //根据新的容量创建出一个空的数组
System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); //把旧数组元素拷贝到新数组
this.setArray(newArray); //设置存储数据的数组为新创建的数组
this.freeArray(oldArray); //释放旧的数组
} else if (newCapacity < oldCapacity) { // 收缩
newArray = this.allocateArray(newCapacity);
int readerIndex = this.readerIndex();
if (readerIndex < newCapacity) {
int writerIndex = this.writerIndex();
if (writerIndex > newCapacity) {
writerIndex = newCapacity;
this.writerIndex(newCapacity);
}
System.arraycopy(oldArray, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
} else {
this.setIndex(newCapacity, newCapacity);
}
this.setArray(newArray);
this.freeArray(oldArray);
}
return this;
}
2. 扩容机制
在io.netty.buffer.AbstractByteBuf#ensureWritable0方法中,有这样一句代码:int newCapacity = this.alloc().calculateNewCapacity(this.writerIndex + minWritableBytes, this.maxCapacity);
这行代码是用来计算新的数组容量的,跟踪后是下面的方法:
io.netty.buffer.AbstractByteBufAllocator#calculateNewCapacity,重要的实现为:
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
if (minNewCapacity < 0) {
throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");
} else if (minNewCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format("minNewCapacity: %d (expected: not greater than maxCapacity(%d)", minNewCapacity, maxCapacity));
} else {
int threshold = 4194304;
if (minNewCapacity == 4194304) {
return 4194304;
} else {
int newCapacity;
if (minNewCapacity > 4194304) {
newCapacity = minNewCapacity / 4194304 * 4194304;
if (newCapacity > maxCapacity - 4194304) {
newCapacity = maxCapacity;
} else {
newCapacity += 4194304; // ②
}
return newCapacity;
} else {
for(newCapacity = 64; newCapacity < minNewCapacity; newCapacity <<= 1) { // ①
}
return Math.min(newCapacity, maxCapacity);
}
}
}
}
这里的minNewCapacity=writerIndex+要写入的数组的长度
这行代码指明了如何扩容:
- ByteBuf的容量有一个阈值4194304(2 22);
- 当容量低于这个阈值的时候,扩容机制如下:
- 初始扩容的newCapacity为64;
- 如果minNewCapacity>64,那么就对newCapacity进行*2操作,直到大于minNewCapacity为止。
- 当高于这个阈值的时候,则每次递增这个阈值,防止指数递增造成的过多内存浪费
四、ByteBuf功能介绍
这里简单的对ByteBuf的相关操作进行介绍,但是具体的还是要参看api
- 顺序读操作,有很多针对不同数据类型的操作,如readByte,readShort等操作,会更改readerIndex
- 顺序写操作,也有很多针对不同数据类型的写操作,如writeByte,writerInt等操作,会更改writerIndex
- 回滚操作Mark和rest
在对缓冲区进行读写的时候,由于某些原因,可能需要对之前的操作进行回滚(如读取多次),可以通过mark和rest对指针进行修改;
①. 在调用mark操作(markReaderIndex和markWriterIndex)的时候,会把指针记录下来,如:
public ByteBuf markReaderIndex() {
this.markedReaderIndex = this.readerIndex;
return this;
}
②. 在调用rest操作之后,相应的指针会进行重置,如:
public ByteBuf resetReaderIndex() {
this.readerIndex(this.markedReaderIndex);
return this;
}
public ByteBuf readerIndex(int readerIndex) {
if (readerIndex >= 0 && readerIndex <= this.writerIndex) {
this.readerIndex = readerIndex;
return this;
} else {
throw new IndexOutOfBoundsException(String.format("readerIndex: %d (expected: 0 <= readerIndex <= writerIndex(%d))", readerIndex, this.writerIndex));
}
}
- 跳过指定的字节数skipBytes(int length)方法;’
- 重用缓冲区:discardReadBytes()方法和discardSomeReadBytes()方法;
- 查找操作。有大量的用于查找的方法,如:forEachByte等等
- 拷贝或者截取出新的数组,如:copy(),slice()等;
- 转换标准的ByteBuffer:nioBuffer();
- 判断是否使用的是直接内存:isDirect()
- 判断是否是用byte数组实现的:hasArray()
- 如果是数组使用的,可以使用array()方法获取里面的字节
五、ByteBuf继承体系
(一)总体概览
- 内存分配的角度看,ByteBuf被分为两类:
- 堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收快,可以被JVM自动回收;缺点是在进行Socket的I/O操作的时候,需要进行一次复制,将堆内存对应的缓冲区数组复制到内核Channel中,性能有一定成都的下降。
- 直接内存(DirectByteBuf)字节缓冲区:非堆内存,也就是直接内存,缺点是分配和回收相对较慢,但是操作Channel的时候,可直接操作,减少了内存复制,所以效率较高;
经验表明:在I/O通讯线程的读写缓冲区使用DirectByteBuf,后端业务的编解码模块使用HeapByteBuf,这种组合可能是性能最优。
- 从内存回收角度,也可以被分为两类:
- 基于对象池的ByteBuf
- 普通的ByteBuf
区别在于基于对象池的ByteBuf可以重用同一个ByteBuf对象,它自己维护了一个内存池,可以循环的利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC,但是维护和管理也更加复杂。
(二)AbstractByteBuf
1. 成员变量
int readerIndex
: 读索引,读取操作的时候,会修改它的值;int writerIndex
:写索引,写操作的时候,会修改他的值;private int markedReaderIndex
:用来记录当前被读取到的索引位置,后续可以进行回滚;private int markedWriterIndex
:用来记录当前写入到的索引位置,后续可以进行回滚;private int maxCapacity
:最大容量static final ResourceLeakDetector<ByteBuf> leakDetector
:用于检查对象是否泄漏
注意:AbstractByteBuf里面并没有定义ByteBuf的缓冲区实现,例如byte数组或者是DirectByteBuffer(java.nio.ByteBuffer),因为他也不知道子类是基于堆内存还是基于直接内存。
2. 成员方法
其实就和前面对ByteBuf功能介绍的一样,主要包括以下几种类型的方法,就不做一一介绍了:
(三)AbstractReferenceCountedByteBuf
1. 成员变量
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt")
:
是一个AtomicIntegerFieldUpdater类型的变量,通过原子方式对成员变量进行更新等操作,以实现线程安全,消除锁。
private volatile int refCnt
:用于跟踪对象的引用次数。
(四)UnpooledHeapByteBuf
基于堆内存进行内存分配的字节缓冲区,没有基于对象池实现,每次I/O操作都会创建一个,频繁的进行内存的申请和回收;
1. 成员变量
private final ByteBufAllocator alloc
:用于内存分配
byte[] array
:存放数据的缓冲区
private ByteBuffer tmpNioBuf
:用来实现到java.nio.ByteBuffer的转换
(五)PooledByteBuf
内存对象池ByteBuf,可以减少ByteBuf申请空间和释放空间的性能消耗。
(六)PooledDirectByteBuf 和 UnpooledDirectByteBuf
一个是基于内存池的,一个不是基于内存池的,唯一的区别在于缓冲区的分配和销毁不同。
六、ByteBuf辅助类
(一)ByteBufHolder
我们经常遇到需要另外存储除有效的实际数据各种属性值。 HTTP 响应是一个很好的例子;与内容一起的字节的还有状态码, cookies,等。
Netty 提供 ByteBufHolder 处理这种常见的情况。 ByteBufHolder 还提供对于 Netty 的高级功能,如缓冲池,其中保存实际数据的 ByteBuf可以从池中借用,如果需要还可以自动释放。
- data():获取所携带的数据
- copy():拷贝数据
(二)ByteBufAllocator
字节缓冲区分配区,按照Netty的缓冲区实现不同,有两种不同的分配器:基于内存池的和普通的。
提供了很多用来分配ByteBuf的方法:
(三)CompositeByteBuf
可以将多个ByteBuf组装到一起,形成一个统一的视图。说白了,可以理解为一个把多个ByteBuf封装在一起的数组。
(四)ByteBufUtil
ByteBufUtil提供了一系列的方法用来操作ByteBuf,重要的方法如下:
encodeString(ByteBufAllocator alloc, CharBuffer src, Charset charset)
:对需要编码的字符串src按照指定的字符集charset进行编码,利用指定的ByteBufAllocator生成一个ByteBuf,如:
ByteBuf byteBuf = ByteBufUtil
.encodeString(UnpooledByteBufAllocator.DEFAULT, CharBuffer.wrap("你好啊"), Charset.forName("GBK"));
String hexDump(ByteBuf buffer)
: 可以将ByteBuf转换成16进制的字符串