bytebuf池_图文分析ByteBuf是什么
ByteBuf是什么
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 byte
arrays ({@code byte[]}) and {@linkplain ByteBuffer NIO buffers}.
就是說 ByteBuf 是一個字節序列,可以隨機或連續存取零到多個字節。他提供了一個統一的抽象,通過 ByteBuf 可以操作基礎的字節數組和ByteBuffer緩沖區。
需要注意的是這里說的 interface 是不準確的,因為Trustin Lee在2013/7/8將ByteBuffer從接口改成了抽象類,具體的原因不得而知。
ByteBuf的結構
ByteBuf比JDK中原生的ByteBuffer好的原因是前者的設計比后者優秀,ByteBuf有讀和寫兩個指針,而ByteBuffer只有一個指針,需要通過flip()方法在讀和寫之間進行模式切換,需要操作的越多往往犯錯的概率就越大。ByteBuf將讀和寫進行了分離,使用者不用再關心現在是讀還是寫的模式,可以把更多的精力用在具體的業務上。
官方定義中指出,ByteBuf主要是通過兩個指針進行數據的讀和寫,分別是 readerIndex 和 writerIndex ,并且整個ByteBuf被這兩個指針最多分成三個部分,分別是可丟棄部分,可讀部分和可寫部分,可以用一張圖直觀的描述ByteBuf的結構,如下圖所示:
可能有人注意到了我說ByteBuf最多被分成三個部分,那是因為某些情況下可能只有一到兩部分:
剛初始化的時候
剛初始化的時候,讀寫指針都是0,所有的內容都是可寫部分,此時還沒有可讀部分和可丟棄部分。
剛寫完數據后
剛寫完一些數據后,讀指針仍然是0,寫指針向后移動了n,這里的n就是寫入的字節數。
讀完一部分數據并丟棄之后
寫入完數據之后,緊接著讀取一部分數據,然后立刻丟棄掉,此時ByteBuf的結構就會變成跟第二步中的一樣。因為丟棄的動作會將讀指針向左移動到0的位置,寫指針向左移動的距離=原來讀指針的值
ByteBuf的讀寫操作
寫操作
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的種類
我們知道ByteBuf在4.x的版本中是一個抽象類,他有很多的抽象子類以及各種實現類。
畫了一個簡單的ByteBuf的各個實現類之間的關系,其中藍色的類是被棄用的。
上圖只是簡單的列舉的一些常用的ByteBuf類,如果你想知道ByteBuf所有的實現類,那么可以在IDEA中選
則ByteBuf類之后,然后在菜單 navigate 中點擊 Type Hierarchy 或用快捷鍵:control+H,即可打開ByteBuf的類層次結構圖,具體的層級結構如下圖所示:
本篇文章只簡單的讓大家對于ByteBuf的種類有個大概的了解,具體的每一種ByteBuf的作用我將在后續的章節中進行介紹。
ByteBuf的使用
有一點我們需要知道的是,ByteBuf的jar包,是可以單獨使用的。比如某個項目中有一個場景,需要處理某個自定義的協議,那么我們在解析協議時,就可以將接收到的將字節內容寫入一個ByteBuf,然后從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]
執行過程分析
下面讓我們來仔細的研究下執行的過程,并分析下為什么會產生這樣的執行結果。
1.初始化一個大小為10的ByteBuf
剛初始化的ByteBuf對象,容量為10,讀寫指針都為0,且每個字節的值都為0,并且這些字節都是“可寫”的,我們用紅色來表示。
2.寫入一段內容
當寫入一段內容后(這里寫入的是5個字節),寫指針向后移動了5個字節,寫指針的值變成了5,而讀指針沒有發生變化還是0,但是讀指針和寫指針之間的字節現在變成了“可讀”的狀態了,我們用紫色來表示。
3.讀取一段內容
接著我們有讀取了2個字節的內容,這時讀指針向后移動了2個字節,讀指針的值變成了2,寫指針不變,此時0和讀指針之間的內容變成了“可丟棄”的狀態了,我們用粉色來表示。
4.將讀取的內容丟棄
緊接著,我們將剛剛讀取完的2個字節丟棄掉,這時ByteBuf把讀指針與寫指針之間的內容(即 3、4、5 三個字節)移動到了0的位置,并且將讀指針更新為0,寫指針更新為原來寫指針的值減去原來讀指針的值。但是需要注意的是,第4和第5個字節的位置上,還保留的原本的內容,只是這兩個字節由原來的“可讀”變成了現在的“可寫”。
5.將讀寫指針清空
然后,我們執行了一個 clear 方法,將讀寫指針同時都置為0了,此時所有的字節都變成“可寫”了,但是需要注意的是,clear方法只是更改的讀寫指針的值,每個位置上原本的字節內容并沒有發生改變。
6.再次寫入一段較少的內容
然后再次寫入一段內容后,讀指針不變,寫指針向后移動了具體的字節數,這里是向后移動了三個字節。且寫入的這三個字節變成了“可讀”狀態。
7.將ByteBuf中的內容清零
清零(setZero)和清空(clear)的方法是兩個概念完全不同的方法,“清零”是把指定位置上的字節的值設置為0,除此之外不改變任何的值,所以讀寫指針的值和字節的“可讀寫”狀態與上次保持一致,而“清空”則只是將讀寫指針都置為0,并且所有字節都變成了“可寫”狀態。
8.寫入一段超過容量的內容
最后我們往ByteBuf中寫入超過ByteBuf容量的內容,這里是寫入了11個字節,此時ByteBuf原本的容量不足以寫入這些內容了,所以ByteBuf發生了擴容。其實只要寫入的字節數超過可寫字節數,就會發生擴容了。
擴容分析
那么擴容是怎么擴的呢,為什么容量從10擴容到64呢?我們從源碼中找答案。
擴容肯定發生在寫入字節的時候,讓我們找到 writeBytes(byte[] bytes) 方法,具體如下:
@Override
public ByteBuf writeBytes(byte[] src) {
writeBytes(src, 0, src.length);
return this;
}
@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
// 該方法檢查是否有足夠的可寫空間,是否需要進行擴容
ensureWritable(length);
setBytes(writerIndex, src, srcIndex, length);
writerIndex += length;
return this;
}
在進入 ensureWritable(length) 方法內部查看,具體的代碼如下:
@Override
public 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 ,具體的代碼如下:
@Override
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));
}
// 擴容的閾值,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。
擴容的規則可以用下面的邏輯表示:
如果c
如果c>t,則n=c/t*t+t
得出的結論
ByteBuf有讀和寫兩個指針,用來標記“可讀”、“可寫”、“可丟棄”的字節
調用write*方法寫入數據后,寫指針將會向后移動
調用read*方法讀取數據后,讀指針將會向后移動
寫入數據或讀取數據時會檢查是否有足夠多的空間可以寫入和是否有數據可以讀取
寫入數據之前,會進行容量檢查,當剩余可寫的容量小于需要寫入的容量時,需要執行擴容操作
擴容時有一個4MB的閾值,需要擴容的容量小于閾值或大于閾值所對應的擴容邏輯不同
clear等修改讀寫指針的方法,只會更改讀寫指針的值,并不會影響ByteBuf中已有的內容
setZero等修改字節值的方法,只會修改對應字節的值,不會影響讀寫指針的值以及字節的可讀寫狀態
總結
以上是生活随笔為你收集整理的bytebuf池_图文分析ByteBuf是什么的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python数据类型总结_Python
- 下一篇: 笔记本电脑如何保养_电脑保养只是吹一吹?