9.4 ByteBuf源碼分析
9.4.1 類圖
ByteBuf的子類實現非常多,其中關鍵的實現類如下:
可以使用兩種方式對ByteBuf進行分類:按底層實現方式和按是否使用對象池。
-
按底層實現
- HeapByteBuf
HeapByteBuf的底層實現為JAVA堆內的字節數組。堆緩沖區與普通堆對象類似,位于JVM堆內存區,可由GC回收,其申請和釋放效率較高。常規JAVA程序使用建議使用該緩沖區。 - DirectByteBuf
DirectByteBuf的底層實現為操作系統內核空間的字節數組。直接緩沖區的字節數組位于JVM堆外的NATIVE堆,由操作系統管理申請和釋放,而DirectByteBuf的引用由JVM管理。直接緩沖區由操作系統管理,一方面,申請和釋放效率都低于堆緩沖區,另一方面,卻可以大大提高IO效率。由于進行IO操作時,常規下用戶空間的數據(JAVA即堆緩沖區)需要拷貝到內核空間(直接緩沖區),然后內核空間寫到網絡SOCKET或者文件中。如果在用戶空間取得直接緩沖區,可直接向內核空間寫數據,減少了一次拷貝,可大大提高IO效率,這也是常說的零拷貝。 - CompositeByteBuf
CompositeByteBuf,顧名思義,有以上兩種方式組合實現。這也是一種零拷貝技術,想象將兩個緩沖區合并為一個的場景,一般情況下,需要將后一個緩沖區的數據拷貝到前一個緩沖區;而使用組合緩沖區則可以直接保存兩個緩沖區,因為其內部實現組合兩個緩沖區并保證用戶如同操作一個普通緩沖區一樣操作該組合緩沖區,從而減少拷貝操作。
- HeapByteBuf
-
按是否使用對象池
- UnpooledByteBuf
UnpooledByteBuf為不使用對象池的緩沖區,不需要創建大量緩沖區對象時建議使用該類緩沖區。 - PooledByteBuf
PooledByteBuf為對象池緩沖區,當對象釋放后會歸還給對象池,所以可循環使用。當需要大量且頻繁創建緩沖區時,建議使用該類緩沖區。Netty4.1默認使用對象池緩沖區,4.0默認使用非對象池緩沖區。
- UnpooledByteBuf
9.4.2 關鍵類分析
9.4.2.1 ByteBuf
ByteBuf
被定義為抽象類,但其中并未實現任何方法,故可看做一個接口,該接口擴展了ReferenceCounted
實現引用計數。該類最重要的方法如下:
int getInt(int index);
ByteBuf setInt(int index, int value);
int readInt();
ByteBuf writeInt(int value);
這些方法從緩沖區取得或設置一個4字節整數,區別在于getInt()
和setInt()
并不會改變索引,readInt()
和writeInt()
分別會將讀索引和寫索引增加4,因為int占4個字節。該類方法有大量同類,可操作布爾數Boolean
,字節Byte
,字符Char
,2字節短整數Short
,3字節整數Medium
,4字節整數Int
,8字節長整數Long
,4字節單精度浮點數Float
,8字節雙精度浮點數Double
以及字節數組ByteArray
。
該類的一些方法遵循這樣一個準則:空參數的方法類似常規getter方法,帶參數的方法類似常規setter方法。比如capacity()
表示緩沖區當前容量,capacity(int newCapacity)
表示設置新的緩沖區容量。在此列出這些方法的帶參數形式:
ByteBuf capacity(int newCapacity); // 設置緩沖區容量
ByteBuf order(ByteOrder endianness); // 設置緩沖區字節序
ByteBuf readerIndex(int readerIndex); // 設置緩沖區讀索引
ByteBuf writerIndex(int writerIndex); // 設置緩沖區寫索引
上述后兩個方法,可操作讀寫索引,除此之外,還有以下方法可以操作讀寫索引:
ByteBuf setIndex(int readerIndex, int writerIndex); // 設置讀寫索引
ByteBuf markReaderIndex(); // 標記讀索引,寫索引可類比
ByteBuf resetReaderIndex(); // 重置為標記的讀索引
ByteBuf skipBytes(int length); // 略過指定字節(增加讀索引)
ByteBuf clear(); // 讀寫索引都置0
緩沖區可寫可讀性判斷,以可讀性為例:
int readableBytes(); // 可讀的字節數
boolean isReadable(); // 是否可讀
boolean isReadable(int size); // 指定的字節數是否可讀
堆緩沖區和直接緩沖區的判斷,有以下方法:
boolean hasArray(); // 判斷底層實現是否為字節數組
byte[] array(); // 返回底層實現的字節數組
int arrayOffset(); // 底層字節數組的首字節位置
boolean isDirect(); // 判斷底層實現是否為直接ByteBuffer
boolean hasMemoryAddress(); // 底層直接ByteBuffer是否有內存地址
long memoryAddress(); // 直接ByteBuffer的首字節內存地址
這一組方法主要用于區分緩沖區的底層實現,前3個方法用于對底層實現為字節數組的堆緩沖區進行操作,后三個方法用于底層為NIO Direct ByteBuffer的緩沖區進行操作。
使用緩沖區時,經常需要在緩沖區查找某個特定字節或對某個字節進行操作,為此ByteBuf提供了一系列方法:
// 首個特定字節的絕對位置
int indexOf(int fromIndex, int toIndex, byte value);
// 首個特定字節的相對位置,相對讀索引
int bytesBefore(byte value);
int bytesBefore(int length, byte value);
int bytesBefore(int index, int length, byte value);
// processor返回false時的首個位置
int forEachByte(ByteBufProcessor processor);
int forEachByte(int index, int length, ByteBufProcessor processor);
int forEachByteDesc(ByteBufProcessor processor);
int forEachByteDesc(int index, int length, ByteBufProcessor processor);
接口ByteBufProcessor
的process(byte)
方法定義的不符合常規,返回true時表示希望繼續處理下一個字節,返回false時表示希望停止處理返回當前位置。由此,當我們希望查找到換行符時,代碼如下:
public boolean process(byte value) throws Exception {
return value != '\r' && value != '\n';
}
這正是Netty默認實現的一個ByteBufProcessor FIND_CRLF,可見當我們期望查找換行符時,process的代碼卻要表達非換行符,此處設計不太合理,不人性化。作為折中,ByteBufProcessor
中提供了大量默認的處理器,可滿足大量場景的需求。如果確有必要實現自己的處理器,一定注意實現方法需要反義。
復制或截取緩沖區也是一個頻繁操作,ByteBuf提供了以下方法:
ByteBuf copy();
ByteBuf copy(int index, int length);
ByteBuf slice();
ByteBuf slice(int index, int length);
ByteBuf duplicate();
其中copy()
方法生成的ByteBuf完全獨立于原ByteBuf,而slice()
和duplicate()
方法生成的ByteBuf與原ByteBuf共享相同的底層實現,只是各自維護獨立的索引和標記,使用這兩個方法時,特別需要注意結合使用場景確定是否調用retain()
增加引用計數。
有關JAVA NIO中的ByteBuffer的方法:
int nioBufferCount();
ByteBuffer nioBuffer();
ByteBuffer nioBuffer(int index, int length);
ByteBuffer internalNioBuffer(int index, int length);
ByteBuffer[] nioBuffers();
ByteBuffer[] nioBuffers(int index, int length);
這些方法在進行IO的寫操作時會大量使用,一般情況下,用戶很少調用這些方法。
最后需要注意的是toString()
方法:
String toString();
String toString(Charset charset);
String toString(int index, int length, Charset charset);
不帶參數的toString()
方法是JAVA中Object的標準重載方法,返回ByteBuf的JAVA描述;帶參數的方法則返回使用指定編碼集編碼的緩沖區字節數據的字符形式。
不再列出ByteBuf
繼承自ReferenceCounted
接口的方法,可見ByteBuf
是一個含有過多的抽象方法的抽象類(接口)。此處,Netty使用了聚合的設計方法,將ByteBuf
的子類可能使用的方法都集中到了基類。再加上使用工廠模式生成ByteBuf
,給用戶程序員帶來了極大便利:完全不用接觸具體的子類,只需要使用頂層接口進行操作。
了解了ByteBuf
的所有方法,那么我們逐個擊破,繼續分析AbstractByteBuf
。
9.4.2.2 AbstractByteBuf
抽象基類AbstractByteBuf中定義了ByteBuf的通用操作,比如讀寫索引以及標記索引的維護、容量擴增以及廢棄字節丟棄等等。首先看其中的私有變量:
int readerIndex; // 讀索引
int writerIndex; // 寫索引
private int markedReaderIndex; // 標記讀索引
private int markedWriterIndex; // 標記寫索引
private int maxCapacity; // 最大容量
與變量相關的setter和getter方法不再分析,分析第一個關鍵方法:計算容量擴增的方法calculateNewCapacity(minNewCapacity)
,其中參數表示擴增所需的最小容量:
private int calculateNewCapacity(int minNewCapacity) {
final int maxCapacity = this.maxCapacity;
final int threshold = 1048576 * 4; // 4MB的閾值
if (minNewCapacity == threshold) {
return threshold;
}
// 所需的最小容量超過閾值4MB,每次增加4MB
if (minNewCapacity > threshold) {
int newCapacity = (minNewCapacity / threshold) * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity; // 超過最大容量不再擴增
} else {
newCapacity += threshold; // 增加4MB
}
return newCapacity;
}
// 此時所需的最小容量小于閾值4MB,容量翻倍
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1; // 使用移位運算表示*2
}
return Math.min(newCapacity, maxCapacity);
}
可見ByteBuf的最小容量為64B,當所需的擴容量在64B和4MB之間時,翻倍擴容;超過4MB之后,則每次擴容增加4MB,且最終容量(小于maxCapacity時)為4MB的最小整數倍。容量擴增的具體實現與ByteBuf的底層實現緊密相關,最終實現的容量擴增方法capacity(newCapacity)
由底層實現。
接著分析丟棄已讀字節方法discardReadBytes()
:
public ByteBuf discardReadBytes() {
if (readerIndex == 0) {
return this;
}
if (readerIndex != writerIndex) {
// 將readerIndex之后的數據移動到從0開始
setBytes(0, this, readerIndex, writerIndex - readerIndex);
writerIndex -= readerIndex; // 寫索引減少readerIndex
adjustMarkers(readerIndex); // 標記索引對應調整
readerIndex = 0; // 讀索引置0
} else {
// 讀寫索引相同時等同于clear操作
adjustMarkers(readerIndex);
writerIndex = readerIndex = 0;
}
return this;
}
只需注意其中的setBytes()
,從一個源數據ByteBuf中復制數據到ByteBuf中,在本例中數據源ByteBuf就是它本身,所以是將readerIndex之后的數據移動到索引0開始,也就是丟棄readerIndex之前的數據。adjustMarkers()
重新調節標記索引,方法實現簡單,不再進行細節分析。需要注意的是:讀寫索引不同時,頻繁調用discardReadBytes()
將導致數據的頻繁前移,使性能損失。由此,提供了另一個方法discardSomeReadBytes()
,當讀索引超過容量的一半時,才會進行數據前移,核心實現如下:
if (readerIndex >= capacity() >>> 1) {
setBytes(0, this, readerIndex, writerIndex - readerIndex);
writerIndex -= readerIndex;
adjustMarkers(readerIndex);
readerIndex = 0;
}
當然,如果并不想丟棄字節,只期望讀索引前移,可使用方法skipBytes()
:
public ByteBuf skipBytes(int length) {
checkReadableBytes(length);
readerIndex += length;
return this;
}
接下來以getInt()
和readInt()
為例,分析常用的數據獲取方法。
public int getInt(int index) {
checkIndex(index, 4); // 索引正確性檢查
return _getInt(index);
}
protected abstract int _getInt(int index);
該方法對索引進行正確性檢查,然后將實際操作交給子類負責具體實現的_getInt()
方法。
public int readInt() {
checkReadableBytes0(4); // 檢查索引
int v = _getInt(readerIndex);
readerIndex += 4; // 讀索引增加
return v;
}
可見readInt
與getInt
的最大區別在于是否自動維護讀索引,readInt
將增加讀索引,getInt
則不會對索引產生任何影響。
數據設置方法setInt()
和writeInt()
的實現可對應類比,代碼如下:
public ByteBuf setInt(int index, int value) {
checkIndex(index, 4);
_setInt(index, value);
return this;
}
protected abstract void _setInt(int index, int value);
public ByteBuf writeInt(int value) {
ensureAccessible();
ensureWritable0(4);
_setInt(writerIndex, value);
writerIndex += 4;
return this;
}
此外,在AbstractByteBuf
中實現了ByteBuf
中很多和索引相關的無參方法,比如copy()
:
public ByteBuf copy() {
return copy(readerIndex, readableBytes());
}
具體實現只對無參方法設定默認索引,然后委托給有參方法由子類實現。
最后再分析一下檢索字節的方法,比如indexOf()
、bytesBefore()
、forEachByte()
等,其中的實現最后都委托給forEachByte()
,核心代碼如下:
private int forEachByteAsc0(int start, int end,
ByteBufProcessor processor) throws Exception {
for (; start < end; ++start) {
if (!processor.process(_getByte(start))) {
return start;
}
}
return -1;
}
原理也很簡單,從頭開始使用_getByte()
取出每個字節進行比較。if語句中的邏輯非!符號取反,這個設計并不好,容易讓人產生誤解。比如,indexOf()
查找一個字節的ByteBufProcessor
實現如下:
private static class IndexOfProcessor implements ByteBufProcessor {
private final byte byteToFind;
public IndexOfProcessor(byte byteToFind) {
this.byteToFind = byteToFind;
}
@Override
public boolean process(byte value) {
// 期望找到某個字節,但此處需要使用!=
return value != byteToFind;
}
}
此外的其他方法不再分析,接著分析與引用計數相關的AbstractReferenceCountedByteBuf
。
9.4.2.3 AbstractReferenceCountedByteBuf
從名字可以推斷,該抽象類實現引用計數相關的功能。引用計數的功能簡單理解就是:當需要使用一個對象時,計數加1;不再使用時,計數減1。如何實現計數功能呢?考慮到引用計數的多線程使用情形,一般情況下,我們會選擇簡單的AtomicInteger
作為計數,使用時加1,釋放時減1。這樣的實現是沒有問題的,但Netty選擇了另一種內存效率更高的實現方式:volatile
+ FieldUpdater
。
首先看使用的成員變量:
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
private volatile int refCnt = 1; // 實際的引用計數值,創建時為1
增加引用計數的方法:
private ByteBuf retain0(int increment) {
for (;;) {
int refCnt = this.refCnt;
final int nextCnt = refCnt + increment;
if (nextCnt <= increment) {
throw new IllegalReferenceCountException(refCnt, increment);
}
if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
break;
}
}
return this;
}
public ByteBuf retain() {
return retain0(1);
}
實現較為簡單,只需注意compareAndSet(obj, expect, update)
。這是一個原子操作,當前字段的實際值如果與expect相同,則會將字段值更新為update;否則,更新失敗返回false。所以,代碼中使用for(;;)
循環直到該字段的新值被更新。
減少引用計數的方法也類似:
private boolean release0(int decrement) {
for (;;) {
int refCnt = this.refCnt;
if (refCnt < decrement) {
throw new IllegalReferenceCountException(refCnt, -decrement);
}
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
if (refCnt == decrement) {
deallocate();
return true;
}
return false;
}
}
}
public boolean release() {
return release0(1);
}
至此,ByteBuf
的抽象層代碼分析完畢。稍作休整,進入正題:ByteBuf的具體實現子類。