概述
Netty 中的 ByteBuf 和 NIO 中的 ByteBuffer 的區別。
- 1、Netty 中的 ByteBuf 支持動態的擴容和縮容。而 NIO 中的 ByteBuffer不支持。
- 2、Netty 中的 ByteBuf 使用了 readIndex 和 writeIndex 來進行讀寫操作,而 NIO 中的 ByteBuffer 讀寫時,還需要 filp()、rewind() 等操作進行讀寫切換才能讀寫。
- 3、Netty 中的 ByteBuf 使用了 markedReaderIndex 和 markedWriterIndex 來進行標記,讀寫時標記互不影響。而 NIO 中的 ByteBuffer 則只有一個 mark。讀寫切換時 mark都重置爲 -1。
- 4、Netty 還支持 ByteBuf 的池化技術。
比如:
PooledHeapByteBuf
PooledUnsafeHeapByteBuf
PooledDirectByteBuf
PooledUnsafeDirectByteBuf
下面我們簡單分析下 Netty 中的 ByteBuf。
writeByte() 方法
Netty 的 ByteBuf 支持動態擴容。在 put 操作的時候會對剩餘可用空間進行校驗。如果剩餘空間不足,則會自動擴容 。NIO 中的 ByteBuffer 不支持動態擴容。
Netty 中的 ByteBuf 還簡化 NIO 中的 ByteBuffer 讀寫問題。
NIO 中 ByteBuffer,使用 postion、limit、capacity、mark 來維護讀寫操作、每次讀寫還需要 flip 等操作,而 Netty 中的 ByteBuf 簡化了讀寫操作,用 readerIndex 和 writeIndex 來簡化讀寫操作、讀寫不需要來回切換。
public ByteBuf writeByte(int value) {
// 確保 buffer 可寫,並且處理擴容、縮容
ensureWritable0(1);
// 寫入 value,並增加 writerIndex
_setByte(writerIndex++, value);
return this;
}
final void ensureWritable0(int minWritableBytes) {
// 1、判斷是否可訪問
ensureAccessible();
// 2、如果寫入字節小於可寫字節數,則跳出檢查
if (minWritableBytes <= writableBytes()) {
return;
}
if (checkBounds) {
// 3、如果寫入字節大於 Buffer 剩餘可寫入量,則拋出異常
if (minWritableBytes > maxCapacity - writerIndex) {
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}
}
// 4、計算 Buffer 的容量
int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);
// 5、調整 Buffer 的容量大小 (擴大或減小 capacity)
capacity(newCapacity);
}
1、判斷是否可訪問
2、如果寫入字節小於可寫字節數,則跳出檢查
3、如果寫入字節大於 Buffer 剩餘可寫入量,則拋出異常
4、計算 Buffer 的容量
5、調整 Buffer 的容量大小 (擴大或減小 capacity)
計算 Buffer 擴容大小
/**
* minNewCapacity 需要的支持的最小容量(寫索引+當前寫入字節)
* maxCapacity 最大容量
*/
public 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));
}
// 容量計算閾值,如果 Buffer 容量小於4M,則每次都擴容2倍,大於4M,則每次擴容4M
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
// 如果需要寫入的容量大小等於 threshold,則直接返回 threshold值。
if (minNewCapacity == threshold) {
return threshold;
}
// 如果 minNewCapacity 超過 threshold, 就不再每次擴容2倍
if (minNewCapacity > threshold) {
int newCapacity = minNewCapacity / threshold * threshold;
// 這塊邏輯大概意思:需要擴容的最小容量+4M,如果超過 maxCapacity,則把設置爲 maxCapacity
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
}
// 否則,每次擴容4M
else {
newCapacity += threshold;
}
return newCapacity;
}
// 不超過 threshold 閾值. 則每次擴容兩倍,如果小於64的,則直接返回64。
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
return Math.min(newCapacity, maxCapacity);
}
這裏的主要邏輯爲:
1、threshold 設置爲 4M,當需要擴容時判斷 minNewCapacity 是否等於 threshold ,如果等於則使用 threshold 作爲緩衝區大小。
2、如果minNewCapacity 容量大於 threshold 容量,就不能採用每次擴容2倍的方式進行擴容,而是採用每次擴容 4M 的方式。擴容時還需要判斷是否達到最大值,如果達到最大值,則使用最大值。
3、如果 minNewCapacity 容量小於 threshold 容量,就成倍的進行擴容,最小容量爲64。
調整 Buffer 的容量大小
@Override
public ByteBuf capacity(int newCapacity) {
// 檢查 newCapacity 是否在 0 - maxCapacity 之間
checkNewCapacity(newCapacity);
int oldCapacity = array.length;
byte[] oldArray = array;
// 如果 newCapacity 大於 oldCapacity 則擴容
if (newCapacity > oldCapacity) {
// 創建一個長度爲 newCapacity 的 byte[] 數組
byte[] newArray = allocateArray(newCapacity);
// 把數據 copy 到新的 byte[] 數組中
System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
// newArray 替換原來的 array
setArray(newArray);
// UnpooledHeapByteBuf.freeArray() 無操作,空方法。
freeArray(oldArray);
}
// 如果 newCapacity 小於 oldCapacity 則縮減容量
else if (newCapacity < oldCapacity) {
// 創建一個長度爲 newCapacity 的 byte[] 數組
byte[] newArray = allocateArray(newCapacity);
int readerIndex = readerIndex();
// 如果讀索引小於 Buffer 大小
if (readerIndex < newCapacity) {
int writerIndex = writerIndex();
// 寫索引大於 newCapacity, 則把寫索引設置爲 newCapacity
if (writerIndex > newCapacity) {
writerIndex(writerIndex = newCapacity);
}
// 把數據 copy 到新的 byte[] 數組中
System.arraycopy(oldArray, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
}
// 如果讀索引大於 newCapacity,則說明無可用的讀寫。則把讀寫索引設置爲 newCapacity
else {
setIndex(newCapacity, newCapacity);
}
// newArray 替換原來的 array
setArray(newArray);
freeArray(oldArray);
}
return this;
}
discardReadBytes
discardReadBytes 就相當於 NIO ByteBuffer 中的 compact() 方法,壓縮緩衝區,從而可以在緩衝區中寫更多的數據。
調用 discardReadBytes() 方法時需要注意的是,每次 discard 操作,都會發生字節數組的內存複製,所以頻繁的調用會導致性能下降,因此在調用之前確認下是否需要每次調用。
- discardReadBytes() 之前
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
- discardReadBytes() 之後
+------------------+--------------------------------------+
| readable bytes | writable bytes (got more space) |
+------------------+--------------------------------------+
| | |
readerIndex (0) <= writerIndex (decreased) <= capacity
通過discard前後可用看出,discard後將會有更多的空間可用寫入數據。
/**
* 丟棄掉已經讀過的數據,相當於 NIO ByteBuffer.compact() 方法。
*/
@Override
public ByteBuf discardReadBytes() {
// 判斷是否可訪問
ensureAccessible();
// 如果 readerIndex = 0,說明沒有已經讀取過的數據,不需要 discard
if (readerIndex == 0) {
return this;
}
// 如果 readerIndex != writerIndex,則需要調整讀寫索引,並移動數據
if (readerIndex != writerIndex) {
// 複製readerIndex和writerIndex 之間的數據,前移到0的座標位置。
setBytes(0, this, readerIndex, writerIndex - readerIndex);
// 修改寫索引的值
writerIndex -= readerIndex;
// 調整 marker 值
adjustMarkers(readerIndex);
// 讀索引設置爲0
readerIndex = 0;
}
// 如果讀寫索引相等,說明沒有要讀取的數據,則直接把 讀寫索引置位0即可。相當於清空索引。
else {
// 調整 marker 值
adjustMarkers(readerIndex);
// 讀寫索引直接設置爲0,即可。
writerIndex = readerIndex = 0;
}
return this;
}
調整 讀標記 和 寫標記
Netty 的 ByteBuf 使用了 markedReaderIndex 和 markedWriterIndex 來分別代表讀寫的標記。讀寫標記會不影響。
protected final void adjustMarkers(int decrement) {
int markedReaderIndex = this.markedReaderIndex;
// 如果讀標記小於discard的大小,讀標記設置爲0。
if (markedReaderIndex <= decrement) {
this.markedReaderIndex = 0;
int markedWriterIndex = this.markedWriterIndex;
// 如果寫標記小於 decrement,則把寫標記置位0,否則爲 寫標記減去 decrement
if (markedWriterIndex <= decrement) {
this.markedWriterIndex = 0;
} else {
this.markedWriterIndex = markedWriterIndex - decrement;
}
} else {
this.markedReaderIndex = markedReaderIndex - decrement;
markedWriterIndex -= decrement;
}
}
clear() 方法
public ByteBuf clear() {
// 設置讀寫索引都爲0
readerIndex = writerIndex = 0;
return this;
}
只需要把 readerIndex 和 writerIndex 都設置爲0即可,而不需要清楚 Buffer 裏面的數據。因爲再寫的時候會覆蓋掉。
- clear() 之前
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
- clear() 之後
+---------------------------------------------------------+
| writable bytes (got more space) |
+---------------------------------------------------------+
| |
0 = readerIndex = writerIndex <= capacity