Netty專欄 ( 三)——— Netty的ByteBuf
@author 魯偉林
記錄《Netty 實戰》中各章節學習過程,寫下一些自己的思考和總結,幫助使用Netty框架的開發技術人員們,能夠有所得,避免踩坑。
本部落格目錄結構將嚴格按照書本《Netty 實戰》,省略與Netty無關的內容,可能出現跳小章節。
本部落格中涉及的完整程式碼:
GitHub地址: https://github.com/thinkingfioa/netty-learning/tree/master/netty-in-action。
本人部落格地址: https://blog.csdn.net/thinking_fioa
第5章 ByteBuf
Netty提供的ByteBuf與JDK的ByteBuffer相比,前者具有卓越的功能性和靈活性。
5.1 ByteBuf的API
ByteBuf提供讀訪問索引(readerIndex)和寫訪問索引(writerIndex)來控制位元組陣列。ByteBuf API具有以下優點:
- 允許使用者自定義緩衝區型別擴充套件
- 通過內建的複合緩衝區型別實現透明的零拷貝
- 容量可按需增長
- 讀寫這兩種模式之間不需要呼叫類似於JDK的ByteBuffer的flip()方法進行切換
- 讀和寫使用不同的索引
- 支援方法的鏈式呼叫
- 支援引用計數
- 支援池化
5.2 ByteBuf類 ----- Netty的資料容器
5.2.1 ByteBuf如何工作的
ByteBuf維護兩個不同的索引: 讀索引(readerIndex)和寫索引(writerIndex)
![](https://blog.csdn.net/thinking_fioa/article/details/docs/pics/5-1.png)
- ByteBuf維護了readerIndex和writerIndex索引
- 當readerIndex > writerIndex時,則丟擲IndexOutOfBoundsException
- ByteBuf容量 = writerIndex。
- ByteBuf可讀容量 = writerIndex - readerIndex
- readXXX()和writeXXX()方法將會推進其對應的索引。自動推進
- getXXX()和setXXX()方法將對writerIndex和readerIndex無影響
5.2.2 ByteBuf的使用模式
ByteBuf本質是: 一個由不同的索引分別控制讀訪問和寫訪問的位元組陣列。請記住這句話。ByteBuf共有三種模式: 堆緩衝區模式(Heap Buffer)、直接緩衝區模式(Direct Buffer)和複合緩衝區模式(Composite Buffer)
1. 堆緩衝區模式(Heap Buffer)
堆緩衝區模式又稱為:支撐陣列(backing array)。將資料存放在JVM的堆空間,通過將資料儲存在陣列中實現
- 堆緩衝的優點: 由於資料儲存在Jvm堆中可以快速建立和快速釋放,並且提供了陣列直接快速訪問的方法
- 堆緩衝的缺點: 每次資料與I/O進行傳輸時,都需要將資料拷貝到直接緩衝區
程式碼:
public static void heapBuffer() {
// 建立Java堆緩衝區
ByteBuf heapBuf = Unpooled.buffer();
if (heapBuf.hasArray()) { // 是陣列支撐
byte[] array = heapBuf.array();
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
int length = heapBuf.readableBytes();
handleArray(array, offset, length);
}
}
2. 直接緩衝區模式(Direct Buffer)
Direct Buffer屬於堆外分配的直接記憶體,不會佔用堆的容量。適用於套接字傳輸過程,避免了資料從內部緩衝區拷貝到直接緩衝區的過程,效能較好
- Direct Buffer的優點: 使用Socket傳遞資料時效能很好,避免了資料從Jvm堆記憶體拷貝到直接緩衝區的過程。提高了效能
- Direct Buffer的缺點: 相對於堆緩衝區而言,Direct Buffer分配記憶體空間和釋放更為昂貴
- 對於涉及大量I/O的資料讀寫,建議使用Direct Buffer。而對於用於後端的業務訊息編解碼模組建議使用Heap Buffer
程式碼:
public static void directBuffer() {
ByteBuf directBuf = Unpooled.directBuffer();
if (!directBuf.hasArray()) {
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(), array);
handleArray(array, 0, length);
}
}
3. 複合緩衝區模式(Composite Buffer)
Composite Buffer是Netty特有的緩衝區。本質上類似於提供一個或多個ByteBuf的組合檢視,可以根據需要新增和刪除不同型別的ByteBuf。
- 想要理解Composite Buffer,請記住:它是一個組合檢視。它提供一種訪問方式讓使用者自由的組合多個ByteBuf,避免了拷貝和分配新的緩衝區。
- Composite Buffer不支援訪問其支撐陣列。因此如果要訪問,需要先將內容拷貝到堆記憶體中,再進行訪問
- 下圖是將兩個ByteBuf:頭部+Body組合在一起,沒有進行任何複製過程。僅僅建立了一個檢視
程式碼:
public static void byteBufComposite() {
// 複合緩衝區,只是提供一個檢視
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct
ByteBuf bodyBuf = Unpooled.directBuffer(); // can be backing or direct
messageBuf.addComponents(headerBuf, bodyBuf);
messageBuf.removeComponent(0); // remove the header
for (ByteBuf buf : messageBuf) {
System.out.println(buf.toString());
}
}
5.3 位元組級操作
5.3.1 隨機訪問索引
ByteBuf的索引與普通的Java位元組陣列一樣。第一個位元組的索引是0,最後一個位元組索引總是capacity()-1。請記住下列兩條,非常有用:
- readXXX()和writeXXX()方法將會推進其對應的索引readerIndex和writerIndex。自動推進
- getXXX()和setXXX()方法用於訪問資料,對writerIndex和readerIndex無影響
程式碼:
public static void byteBufRelativeAccess() {
ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);// 不改變readerIndex值
System.out.println((char) b);
}
}
5.3.2 順序訪問索引
Netty的ByteBuf同時具有讀索引和寫索引,但JDK的ByteBuffer只有一個索引,所以JDK需要呼叫flip()方法在讀模式和寫模式之間切換。
- ByteBuf被讀索引和寫索引劃分成3個區域:可丟棄位元組區域,可讀位元組區域和可寫位元組區域
5.3.3 可丟棄位元組區域
可丟棄位元組區域是指:[0,readerIndex)之間的區域。可呼叫discardReadBytes()方法丟棄已經讀過的位元組。
1. discardReadBytes()效果 ----- 將可讀位元組區域(CONTENT)[readerIndex, writerIndex)往前移動readerIndex位,同時修改讀索引和寫索引。
2. discardReadBytes()方法會移動可讀位元組區域內容(CONTENT)。如果頻繁呼叫,會有多次資料複製開銷,對效能有一定的影響
5.3.4 可讀位元組區域
可讀位元組區域是指:[readerIndex, writerIndex)之間的區域。任何名稱以read和skip開頭的操作方法,都會改變readerIndex索引。
5.3.5 可寫位元組區域
可寫位元組區域是指:[writerIndex, capacity)之間的區域。任何名稱以write開頭的操作方法都將改變writerIndex的值。
5.3.6 索引管理
1. markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先備份當前的readerIndex,resetReaderIndex()則是將剛剛備份的readerIndex恢復回來。常用於dump ByteBuf的內容,又不想影響原來ByteBuf的readerIndex的值
2. readerIndex(int) ----- 設定readerIndex為固定的值
3. writerIndex(int) ----- 設定writerIndex為固定的值
4. clear() ----- 效果是: readerIndex=0, writerIndex(0)。不會清除記憶體
5. 呼叫clear()比呼叫discardReadBytes()輕量的多。僅僅重置readerIndex和writerIndex的值,不會拷貝任何記憶體,開銷較小。
5.3.7 查詢操作(indexOf)
查詢ByteBuf指定的值。類似於,String.indexOf("str")操作
1. 最簡單的方法 ----- indexOf()
2. 利用ByteProcessor作為引數來查詢某個指定的值。
程式碼:
public static void byteProcessor() {
ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
// 使用indexOf()方法來查詢
buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8);
// 使用ByteProcessor查詢給定的值
int index = buffer.forEachByte(ByteProcessor.FIND_CR);
}
5.3.8 派生緩衝區 ----- 檢視
派生緩衝區為ByteBuf提供了一個訪問的檢視。檢視僅僅提供一種訪問操作,不做任何拷貝操作。下列方法,都會呈現給使用者一個檢視,以供訪問:
1. duplicate()
2. slice()
3. slice(int, int)
4. Unpooled.unmodifiableBuffer(...)
5. Unpooled.wrappedBuffer(...)
6. order(ByteOrder)
7. readSlice(int)
理解
1. 上面的6中方法,都會返回一個新的ByteBuf例項,具有自己的讀索引和寫索引。但是,其內部儲存是與原物件是共享的。這就是檢視的概念
2. 請注意:如果你修改了這個新的ByteBuf例項的具體內容,那麼對應的源例項也會被修改,因為其內部儲存是共享的
3. 如果需要拷貝現有緩衝區的真實副本,請使用copy()或copy(int, int)方法。
4. 使用派生緩衝區,避免了複製記憶體的開銷,有效提高程式的效能
程式碼:
public static void byteBufSlice() {
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf sliced = buf.slice(0, 15);
System.out.println(sliced.toString(utf8));
buf.setByte(0, (byte)'J');
assert buf.getByte(0) == sliced.getByte(0); // return true
}
public static void byteBufCopy() {
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf copy = buf.copy(0, 15);
System.out.println(copy.toString(utf8));
buf.setByte(0, (byte)'J');
assert buf.getByte(0) != copy.getByte(0); // return true
}
5.3.9 讀/寫操作
如上文所提到的,有兩種類別的讀/寫操作:
1. get()和set()操作 ----- 從給定的索引開始,並且保持索引不變
2. read()和write()操作 ----- 從給定的索引開始,並且根據已經訪問過的位元組數對索引進行訪問
3. 下圖給出get()操作API,對於set()操作、read()操作和write操作可參考書籍或API
5.3.10 更多的操作
下面的兩個方法操作字面意思較難理解,給出解釋:
1. hasArray() ----- 如果ByteBuf由一個位元組陣列支撐,則返回true。通俗的講:ByteBuf是堆緩衝區模式,則代表其內部儲存是由位元組陣列支撐的。如果還沒理解,可參考5.2.2章節
2. array() ----- 如果ByteBuf是由一個位元組陣列支撐澤返回陣列,否則丟擲UnsupportedOperationException異常。也就是,ByteBuf是堆緩衝區模式
5.4 ByteBufHolder介面
ByteBufHolder為Netty的高階特性提供了支援,如緩衝區池化,可以從池中借用ByteBuf,並且在需要時自動釋放。
1. ByteBufHolder是ByteBuf的容器,可以通過子類實現ByteBufHolder介面,根據自身需要新增自己需要的資料欄位。可以用於自定義緩衝區型別擴充套件欄位。
2. Netty提供了一個預設的實現DefaultByteBufHolder。
程式碼
public class CustomByteBufHolder extends DefaultByteBufHolder{
private String protocolName;
public CustomByteBufHolder(String protocolName, ByteBuf data) {
super(data);
this.protocolName = protocolName;
}
@Override
public CustomByteBufHolder replace(ByteBuf data) {
return new CustomByteBufHolder(protocolName, data);
}
@Override
public CustomByteBufHolder retain() {
super.retain();
return this;
}
@Override
public CustomByteBufHolder touch() {
super.touch();
return this;
}
@Override
public CustomByteBufHolder touch(Object hint) {
super.touch(hint);
return this;
}
...
}
5.5 ByteBuf分配
建立和管理ByteBuf例項的多種方式:按需分配(ByteBufAllocator)、Unpooled緩衝區和ByteBufUtil類
5.5.1 按序分配: ByteBufAllocator介面
Netty通過介面ByteBufAllocator實現了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator:
1. ctx.channel().alloc().buffer() ----- 本質就是: ByteBufAllocator.DEFAULT
2. ByteBufAllocator.DEFAULT.buffer() ----- 返回一個基於堆或者直接記憶體儲存的Bytebuf。預設是堆記憶體
3. ByteBufAllocator.DEFAULT ----- 有兩種型別: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。對於Java程式,預設使用PooledByteBufAllocator(池化)。對於安卓,預設使用UnpooledByteBufAllocator(非池化)
4. 可以通過BootStrap中的Config為每個Channel提供獨立的ByteBufAllocator例項
解釋:
1. 上圖中的buffer()方法,返回一個基於堆或者直接記憶體儲存的Bytebuf ----- 預設是堆記憶體。原始碼: AbstractByteBufAllocator() { this(false); }
2. ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。預設是池化(PooledByteBufAllocator.DEFAULT)
5.5.2 Unpooled緩衝區 ----- 非池化
Unpooled提供靜態的輔助方法來建立未池化的ByteBuf。
注意:
1. 上圖的buffer()方法,返回一個未池化的基於堆記憶體儲存的ByteBuf
2. wrappedBuffer() ----- 建立一個檢視,返回一個包裝了給定資料的ByteBuf。非常實用
建立ByteBuf程式碼:
public void createByteBuf(ChannelHandlerContext ctx) {
// 1. 通過Channel建立ByteBuf
ByteBuf buf1 = ctx.channel().alloc().buffer();
// 2. 通過ByteBufAllocator.DEFAULT建立
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
// 3. 通過Unpooled建立
ByteBuf buf3 = Unpooled.buffer();
}
5.5.3 ByteBufUtil類
ByteBufUtil類提供了用於操作ByteBuf的靜態的輔助方法: hexdump()和equals
1. hexdump() ----- 以十六進位制的表示形式列印ByteBuf的內容。非常有價值
2. equals() ----- 判斷兩個ByteBuf例項的相等性
5.6 引用計數
Netty4.0版本中為ButeBuf和ButeBufHolder引入了引用計數技術。請區別引用計數和可達性分析演算法(jvm垃圾回收)
1. 誰負責釋放: 一般來說,是由最後訪問(引用計數)物件的那一方來負責將它釋放
2. buffer.release() ----- 引用計數減1
3. buffer.retain() ----- 引用計數加1
4. buffer.refCnt() ----- 返回當前物件引用計數值
5. buffer.touch() ----- 記錄當前物件的訪問位置,主要用於除錯。
6. 引用計數並非僅對於直接緩衝區(direct Buffer)。ByteBuf的三種模式: 堆緩衝區(heap Buffer)、直接緩衝區(dirrect Buffer)和複合緩衝區(Composite Buffer)都使用了引用計數,某些時候需要程式設計師手動維護引用數值
程式碼:
public static void releaseReferenceCountedObject(){
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// 引用計數加1
buffer.retain();
// 輸出引用計數
buffer.refCnt();
// 引用計數減1
buffer.release();
}
5.7 建議
1. 如果使用了Netty的ByteBuf,建議功能測試時,開啟記憶體檢測: -Dio.netty.leakDetectionLevel=paranoid
2. ByteBuf的三種模式: 堆緩衝區(heap Buffer)、直接緩衝區(dirrect Buffer)和複合緩衝區(Composite Buffer)都使用了引用計數,某些時候需要程式設計師手動維護引用數值。