本章節主要學習一下netty當中的ByteBuf,ByteBuf是對字節數據的封裝。
接下來主要學習Bytebuf的使用以及其細節。
一、ByteBuf使用
1.1 創建ByteBuf
通常可以使用如下方式創建:
public static void main(String[] args) {
//創建一個容量爲10的ByteBuf
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
System.out.println(buf);
}
查看結果:
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 10)
由結果我們看到初始化了一個池化的直接內存buf,也就是說如果我們使用默認類型創建,會返回一個直接內存。
ridx表示read index,即讀取位置是0;
widx表示write index,即寫入位置是0;
cap表示容量,是10;
1.1.1 直接內存 和 堆內存
- 使用如下方式創建一個堆內存:
//創建一個堆內存buf
ByteBuf heapBuffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
關於堆內存是通過jvm進行回收的,所以我們不需要過多的干預。
- 使用如下方式創建一個直接內存
//創建一個直接內存buf
ByteBuf directBuffer = ByteBufAllocator.DEFAULT.directBuffer(10);
- 直接內存的特性:
- 直接內存創建和銷燬的代價昂貴,但讀寫性能高(少一次內存複製),適合配合池化功能一起用
- 直接內存對 GC 壓力小,因爲這部分內存不受 JVM 垃圾回收的管理,但也要注意及時主動釋放
關於Netty中直接內存的介紹,在後面涉及到再繼續介紹,此處簡單帶過。
1.1.2 池化 和 非池化
無論在任何使用池的地方,例如數據庫連接池,線程池等等。
池化最大的意義莫過於可以重用資源。
在Netty當中,引入池化機制,使得我們可以重用ByteBuf。
那麼池化對我們使用ByteBuf提供了哪些優點?
- 使我們不必在每次都得創建新的 ByteBuf 實例,這個操作對直接內存代價昂貴;即使是使用堆內存,也會增加 GC 壓力。
- 可以重用池中 ByteBuf 實例,並且採用了與 jemalloc(http://jemalloc.net/) 類似的內存分配算法提升分配效率。
- 高併發時,池化功能更節約內存,減少內存溢出的可能
1.1.3 ByteBuf的組成
其源碼如下:
public abstract class ByteBuf extends Object implements ReferenceCounted, Comparable<ByteBuf>
如上所示,是一個抽象類,繼承自Object,實現了ReferenceCounted,Comparable<ByteBuf>。
ReferenceCounted是用於ByteBuf的回收工作,從其名稱就能看出,是引用計數法,refCnt是引用數值;其提供了方法retain(),計數+1;release()方法,計數減1,當最終引用計數是0時,資源將被釋放。此處不多做介紹。
Comparable主要用作比較。
就像普通的原始字節數組一樣, ByteBuf使用從零開始的索引 。 這意味着第一個字節的索引始終爲0 ,最後一個字節的索引始終爲capacity - 1 。
ByteBuf提供了兩個指針變量來支持順序讀寫操作readerIndex用於讀操作和writerIndex用於寫操作。
下面通過圖的形式展示緩衝區的變化過程:
如上所示的緩衝區劃分方式,相比於之前學習的NIO中的ByteBuffer有一定的優勢:
- 在ByteBuffer中,讀寫通過position這一個指針,需要通過flip()去切換;而在ByteBuf中,讀寫指針區分了,使用更加方便。
- ByteBuf引入了自動擴容的能力。
1.2 寫入ByteBuf
關於ByteBuf的寫入,提供了很多的方法,我直接將圖粘在下方:
1.2.1 寫入示例
public static void main(String[] args) {
//申請長度是10的buffer
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
//寫入5個長度的字節數組
byte[] bytes = new byte[]{1, 2, 3, 4, 5};
byteBuf.writeBytes(bytes);
System.out.println(byteBuf);
//再次寫入5個
byteBuf.writeBytes(bytes);
System.out.println(byteBuf);
}
結果:
PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 10, cap: 10)
比較簡單,關於其他方式,不演示了。
1.2.2 大端寫入和小端寫入
在前面的圖中有一些方法是帶有LE的,可以成爲小端寫入,比如
writeIntLE(int value)
其中的LE是 Little Endian 的縮寫,可以理解爲小字節序、低字節序。
與之相對應的就是 Big Endian,大字節序或高字節序。高字節序沒有特別標註,因爲在網絡編程中通常使用的都是高字節序。如下面的代碼就是高字節序:
writeInt(int value)
舉例:寫入兩個數字888和888L
其二進制表示就是:
| 0000 0000 | 0000 0000 | 0000 0011 | 0111 1000 |
| 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0011 | 0111 1000 |
將其轉換成字節數組就是
[0,0,3,120]
[0,0,0,0,0,0,3,120]
有如下測試代碼:
public static void main(String[] args) {
int a = 888;
//大端寫入
ByteBuf byteBuf1 = ByteBufAllocator.DEFAULT.buffer();
byteBuf1.writeInt(a);
byte[] bytes1 = new byte[4];
byteBuf1.readBytes(bytes1);
System.out.println(Arrays.toString(bytes1));
//小端寫入
ByteBuf byteBuf2 = ByteBufAllocator.DEFAULT.buffer();
byteBuf2.writeIntLE(a);
byte[] bytes2 = new byte[4];
byteBuf2.readBytes(bytes2);
System.out.println(Arrays.toString(bytes2));
long b = 888L;
//大端寫入
ByteBuf byteBuf3 = ByteBufAllocator.DEFAULT.buffer();
byteBuf3.writeLong(b);
byte[] bytes3 = new byte[8];
byteBuf3.readBytes(bytes3);
System.out.println(Arrays.toString(bytes3));
//小端寫入
ByteBuf byteBuf4 = ByteBufAllocator.DEFAULT.buffer();
byteBuf4.writeLongLE(b);
byte[] bytes4 = new byte[8];
byteBuf4.readBytes(bytes4);
System.out.println(Arrays.toString(bytes4));
}
結果:
[0, 0, 3, 120]
[120, 3, 0, 0]
[0, 0, 0, 0, 0, 0, 3, 120]
[120, 3, 0, 0, 0, 0, 0, 0]
結論:int佔4個字節,long佔8個字節,大端寫入是從左向右寫;小端寫入從右向左寫。
網絡傳輸通常使用大端傳輸(Big Endian)
1.2.3 set方式的寫入
還有一些列以set命名的寫入方式,以下面爲例:
public abstract ByteBuf setBytes(int index, byte[] src)
這一類方法要求我們指定index,從此index開始寫入數據, 此方法不會修改此緩衝區的readerIndex或writerIndex 。
示例代碼如下:
public static void main(String[] args) {
//申請長度是10的buffer
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
//寫入5個長度的字節數組
byte[] bytes = new byte[]{1, 2, 3, 4, 5};
//設置從index是5的位置開始寫入
byteBuf.setBytes(5, bytes);
System.out.println(byteBuf);
//手動設置index寫入位置到10
byteBuf.writerIndex(10);
System.out.println(byteBuf);
//長度10的字節數組進行讀取
byte[] readBytes = new byte[10];
byteBuf.readBytes(readBytes);
System.out.println(Arrays.toString(readBytes));
}
結果:
PooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
PooledUnsafeHeapByteBuf(ridx: 0, widx: 10, cap: 10)
[0, 0, 0, 0, 0, 1, 2, 3, 4, 5]
1.2.4 擴容
此處我們繼續使用在1.2.1小節中的代碼,繼續增加5個長度:
public static void main(String[] args) {
//申請長度是10的buffer
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
//寫入5個長度的字節數組
byte[] bytes = new byte[]{1, 2, 3, 4, 5};
byteBuf.writeBytes(bytes);
System.out.println(byteBuf);
//再次寫入5個
byteBuf.writeBytes(bytes);
System.out.println(byteBuf);
//再次寫入5個
byteBuf.writeBytes(bytes);
System.out.println(byteBuf);
}
結果:
PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 10, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 15, cap: 16)
如上所示發現最後一條的容量變成了16。可是我們初始給的只有10,此處就是自動進行了擴容操作。
AbstractByteBuf中的ensureWritable0方法就是擴容方法,其擴容代碼如下所示:
final void ensureWritable0(int minWritableBytes) {
//獲取當前寫入下標
final int writerIndex = writerIndex();
//預計寫入後的下標
final int targetCapacity = writerIndex + minWritableBytes;
//如果小於初始容量,不擴容
if (targetCapacity <= capacity()) {
ensureAccessible();
return;
}
//超過最大容量,跑出異常
if (checkBounds && targetCapacity > maxCapacity) {
ensureAccessible();
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}
// 將目標容量歸一化爲2的冪。
//此處用初始化ByteBuf時指定的默認大小,會根據初始賦值計算出2的冪
//會用該默認大小 減去 寫入位置的下標,得到能寫入的大小的最大值
final int fastWritable = maxFastWritableBytes();
//如果能寫入的最大值 大於 需要寫入的容量,則將能寫入的值 加上 當前寫入下標做爲新的容量
//否則使用calculateNewCapacity去分配
int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
: alloc().calculateNewCapacity(targetCapacity, maxCapacity);
// Adjust to the new capacity.
capacity(newCapacity);
}
maxFastWritableBytes內部會獲取一個maxLength,這個值是在allocate緩衝區是計算出來的2的冪,且小於512的,用它減去當前寫入index的位置。
public int maxFastWritableBytes() {
return Math.min(maxLength, maxCapacity()) - writerIndex;
}
下面看下初始化時如何計算出的這個默認值:
int normalizeCapacity(int reqCapacity) {
checkPositiveOrZero(reqCapacity, "reqCapacity");
// 如果此處大於chunkSize(16777217),則將這個值作爲分配的值
if (reqCapacity >= chunkSize) {
return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
}
// >= 512
if (!isTiny(reqCapacity)) {
// 翻倍(乘以2)
int normalizedCapacity = reqCapacity;
normalizedCapacity --;
normalizedCapacity |= normalizedCapacity >>> 1;
normalizedCapacity |= normalizedCapacity >>> 2;
normalizedCapacity |= normalizedCapacity >>> 4;
normalizedCapacity |= normalizedCapacity >>> 8;
normalizedCapacity |= normalizedCapacity >>> 16;
normalizedCapacity ++;
if (normalizedCapacity < 0) {
normalizedCapacity >>>= 1;
}
assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;
return normalizedCapacity;
}
if (directMemoryCacheAlignment > 0) {
return alignCapacity(reqCapacity);
}
// Quantum-spaced
if ((reqCapacity & 15) == 0) {
return reqCapacity;
}
return (reqCapacity & ~15) + 16;
}
在上面的代碼中,有一個默認值chunkSize,如果分配的值大於chunkSize(16777217),則將這個值作爲分配的值,而在後面擴容時ensureWritable0中的判斷:
int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable : this.alloc().calculateNewCapacity(targetCapacity, this.maxCapacity);
此時會走重新計算容量的方法:
his.alloc().calculateNewCapacity(targetCapacity, this.maxCapacity);
具體代碼如下:
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
checkPositiveOrZero(minNewCapacity, "minNewCapacity");
//如果需要的容量大於最大容量,則拋出異常
if (minNewCapacity > maxCapacity) {
throw new IllegalArgumentException(String.format(
"minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
minNewCapacity, maxCapacity));
}
//定義一頁4M的閾值
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
//如果需要的容量等於頁閾值,就返回該閾值
if (minNewCapacity == threshold) {
return threshold;
}
// 如果需要容量大於 頁閾值
if (minNewCapacity > threshold) {
//計算最小需要幾個頁閾值的大小,並賦予一個新的容量值
int newCapacity = minNewCapacity / threshold * threshold;
//如果新容量,比 最大容量 - 一個分頁還要大,即剩餘不到一個頁,則賦予最大容量
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
//在賦予的新容量基礎上 在加一個頁容量
newCapacity += threshold;
}
return newCapacity;
}
// 如果沒有超過頁閾值,則從64開始,最大增加到4
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
//左移一位,即*2
newCapacity <<= 1;
}
// 取新賦予容量和最大值中的最小值
return Math.min(newCapacity, maxCapacity);
}
關於擴容的問題就看到這裏了。
1.3 讀取ByteBuf
1.3.1 代碼演示
關於讀取方法都是和寫入相對應的,這裏不列舉了,直接上示例代碼
public static void main(String[] args) {
//申請長度是10的buffer
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
//寫入5個長度的字節數組
byte[] bytes = new byte[]{1, 2, 3, 4, 5};
byteBuf.writeBytes(bytes);
System.out.println(byteBuf);
//直接讀取下一個字節
System.out.println(byteBuf.readByte());
System.out.println(byteBuf);
System.out.println(byteBuf.readByte());
System.out.println(byteBuf);
//讀取自己數組
byteBuf.readBytes(new byte[3]);
System.out.println(byteBuf);
}
結果:
PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 10)
1
PooledUnsafeDirectByteBuf(ridx: 1, widx: 5, cap: 10)
2
PooledUnsafeDirectByteBuf(ridx: 2, widx: 5, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 5, widx: 5, cap: 10)
1.3.2 重複讀取
1)使用get開頭的方法,不會修改index。
2)使用mark:
public static void main(String[] args) {
//申請長度是10的buffer
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
//寫入5個長度的字節數組
byte[] bytes = new byte[]{1, 2, 3, 4, 5};
byteBuf.writeBytes(bytes);
//設置一個mark
byteBuf.markReaderIndex();
//讀取自己數組
System.out.println(byteBuf.readByte());
System.out.println(byteBuf);
//重置到mark
byteBuf.resetReaderIndex();
//讀取自己數組
System.out.println(byteBuf.readByte());
System.out.println(byteBuf);
}
結果:
1
PooledUnsafeDirectByteBuf(ridx: 1, widx: 5, cap: 10)
1
PooledUnsafeDirectByteBuf(ridx: 1, widx: 5, cap: 10)
限於篇幅原因,先寫到這,後面繼續更新ByteBuf的文章。有學到幫忙點個贊啦~~