Netty學習5——ByteBuf

ByteBuf

    網絡數據的基本單位總是字節。Java NIO提供了ByteBuffer作爲它的字節容器,但是這個類使用起來過於複雜,而且也有些繁瑣。

    Netty的ByteBuffer替代品是ByteBuf,一個強大的實現,既解決了JDK API的侷限性,又爲網絡應用程序的開發者提供了更好地API。

1.ByteBuf的API

    Netty的數據處理API通過兩個組件暴露——abstract class ByteBuf 和 interface ByteBufHolder。

    ByteBuf API的優點:

(1)它可以被用戶自定義的緩衝區類型擴展;

(2)通過內置的符合緩衝區類型實現了透明的零拷貝;

(3)容量可以按需增長(類似於JDK的StringBuilder);

(4)在讀和寫這兩種模式之間切換不需要調用ByteBuffer的flip()方法;

(5)讀和寫使用了不同的索引;

(6)支持方法的鏈式調用;

(7)支持引用計數;

(8)支持池化。

    其他類可用於管理ByteBuf實例的分配,以及執行各種針對於數據容器本身和它所持有的數據的操作。

2.ByteBuf類——Netty的數據容器

    因爲所有的網絡通信都設計字節序列的移動,所以高效易用的數據結構必不可少,Netty的ByteBuf實現滿足並超越了這些需求。

2.1它是如何工作的

    ByteBuf維護了兩個不同的索引:一個用於讀取,一個用於寫入。當你從ByteBuf讀取時,它的readIndex將會被遞增已經被讀取的字節數。同樣地,當你寫入ByteBuf時,它的writeIndex也會被遞增。

    如果打算讀取字節直到readIndex達到和writeIndex同樣的值時會發生什麼?在那時,你將會到達“可以讀取的”數據的末尾。就如同試圖讀取超出數組末尾的數據一樣,試圖讀取超出該點的數據將會觸發一個IndexOutOfBoundsException。

    名稱以read或者write開頭的ByteBuf方法,將會推進其對應的索引,而名稱以set或者get 開頭的操作則不會。後面的這些方法將在作爲一個參數傳入的一個相對索引上執行操作。

    可以指定ByteBuf的最大容量。試圖移動寫索引(即writerIndex)超過這個值將會觸發一個異常。(默認的限制是Integer.MAX_VALUE。)

2.2ByteBuf的使用模式

    在使用Netty時,你將遇到幾種常見的圍繞ByteBuf而構建的使用模式。

(1)堆緩衝區

    最常用的ByteBuf模式是將數據存儲在JVM的堆空間中。這種模式被稱爲支撐數組(backing array),它能在沒有使用池化的情況下提供快速的分配和釋放。這種方式,如下代碼所示,非常適合於有遺留的數據需要處理的情況。

ByteBuf heapBuf = ...;
//檢查ByteBuf是否有一個支撐數組
if(heapBuf.hasArray()){
    //如果有,則獲取對該數組的引用
    byte[] array = heapBuf.array();
    //計算第一個字節的偏移量
    int offset = heapBuf.arrayOffset() + heapBuf.readIndex();
    //獲得可讀字節數
    int length = heapBuf.readableBytes();
    //使用數組、偏移量和長度作爲參數調用你的方法
    handleArray(array, offset, length);
}
注意:當hasArray()方法返回false時,嘗試訪問支撐數組將觸發一個UnsupportedOperationException。這個模式類似於JDK的ByteBuffer的用法。

(2)直接緩衝區

    直接緩衝區是另外一種ByteBuf模式。我們期望用於對象創建的內存分配永遠都來自於堆中,但這並不是必須的——NIO在JDK1.4中引入的ByteBuffer類允許JVM實現通過本地調用來分配內存。這主要爲了避免在每次調用本地I/O操作之前(或者之後)將緩衝區的內容複製到一箇中間緩衝區(或者從中間緩衝區把內容複製到緩衝區)。

    ByteBuffer的Javadoc明確指出:“直接緩衝區的內容將駐留在常規的會被垃圾回收的堆之外。”這也就解釋了爲何直接緩衝區對於網絡數據傳輸是理想的選擇。如果你的數據包含在一個堆上分配的緩衝區中,那麼事實上,在通過套接字發送它之前,JVM將會在內部把你的緩衝區複製到一個直接緩衝區中。

    直接緩衝區的主要缺點就是,相對於基於堆的緩衝區,它們的分配和釋放都較爲昂貴。如果你正在處理遺留代碼,你也可能會遇到第二個缺點:因爲數據不是在堆上,所以你不得不進行一次複製,如以下代碼所示:

ByteBuf directBuf = ...;
//檢查ByteBuf是否由數組支撐。如果不是,則這是一個直接緩衝區
if (!directBuf.hasArray()) {
    //獲取可讀字節數
    int length = directBuf.readableBytes();
    //分配一個新的數組來保存具有該長度的字節數據
    byte[] array = new byte[length];
    //將字節複製到該數組        
    directBuf.getBytes(directBuf.readBuf.readerIndex(), array);
    //使用數組、偏移量和長度作爲參數調用你的方法
    handleArray(array, 0, length);
}
2.3複合緩衝區

    第三種也是最後一種模式使用的是複合緩衝區,它爲多個ByteBuf提供一個聚合視圖。在這裏你可以根據需要添加或者刪除ByteBuf實例,這是一個JDK的ByteBuffer實現完全缺失的特性。

    Netty通過一個ByteBuf子類——CompositeByteBuf——實現了這個模式,它提供了一個將多個緩衝區表示爲單個合併緩衝區的虛擬表示。

警告 CompositeByteBuf中的ByteBuf實例可能同時包含直接內存分配和非直接內存分配。如果其中只有一個實例,那麼對CompositionByteBuf上的hasArray()方法的調用將返回該組件上的hasArray()方法的值;否則它將返回false。


使用ByteBuffer的複合緩衝區模式

//Use an array to hold the message parts
ByteBuffer[] message = new ByteBuffer[] {header, body};
//Create a new ByteBuffer and use copy to merge the header and body
ByteBuffer message2 = ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();

分配和複製操作,以及伴隨着對數組管理的需要,使得以上版本的實現效率低下而且笨拙。

以下是使用CompositeByteBuf的版本

CompositionByteBuf messageBuf = Unpooled.compositionBuffer();
ByteBuf = headerBuf = ...;//can be backing or direct 
ByteBuf bodyBuf = ...;//can be backing or direct
//將ByteBuf實例追加到CompositeByteBuf
messageBuf.addComponents(headerBuf, bodyBuf);
......
//刪除位於索引位置爲0(第一個組件)的ByteBuf
messageBuf.removeComponent(0);//remove the header
//循環遍歷所有的ByteBuf實例
for(ByteBuf buf: messageBuf){
    System.out.println(buf.toString());
}

CompositeByteBuf可能不支持訪問其支撐數據,因此訪問CompositeByteBuf中的數組類似於(訪問)直接緩衝區的模式,如下代碼所示:

訪問CompositeByteBuf中的數據

CompositeByteBuf compBuf = Unpooled.compositeBuffer();
//獲得可讀字節數
int length = comBuf.readableBytes();
//分配一個具有可讀字節數長度的新數組
byte[] array = new byte[length];
//將字節讀到該數組中
compBuf.getBytes(compBuf.readerIndex(), array);
//使用偏移量和長度作爲參數使用該數組
handleArray(array, 0, array.length);

    需要注意的是,Netty使用了CompositeByteBuf來優化套接字的I/O操作,儘可能地消除了由JDK緩衝區實現所導致的性能以及內存使用率的懲罰。這種優化發生在Netty的核心代碼中,因此不會被暴露出來。

    CompositeByteBuf API 除了從ByteBuf繼承的方法,CompositeByteBuf提供了大量的附加功能。

3.字節級操作

    ByteBuf提供了許多超出基本讀、寫操作的方法用於修改它的數據。

3.1隨機訪問索引

    ByteBuf的索引是從零開始的:第一個字節的索引是0,最後一個字節的索引總是capacity()-1。以下代碼表明,對存儲機制的封裝使得遍歷ByteBuf的內容非常簡單。

訪問數據:

ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i++) {
    byte b = buffer.getByte(i);
    System.out.println((char)b);
}

    需要注意的是,使用那些需要一個索引值參數的方法(的其中)之一來訪問數據既不會改變readerIndex也不會改變writeIndex。如果有需要,也可以通過調用readerIndex(index)或者writerIndex(index)來手動移動這兩者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章