讀古今文學網 > Netty實戰 > 第5章 ByteBuf >

第5章 ByteBuf

本章主要內容

  • ByteBuf——Netty的數據容器
  • API的詳細信息
  • 用例
  • 內存分配

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

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

在本章中我們將會說明和JDK的ByteBuffer相比,ByteBuf的卓越功能性和靈活性。這也將有助於更好地理解Netty數據處理的一般方式,並為將在第6章中針對ChannelPipelineChannelHandler的討論做好準備。

5.1 ByteBuf的API

Netty的數據處理API通過兩個組件暴露——abstract class ByteBufinterface ByteBufHolder

下面是一些ByteBuf API的優點:

  • 它可以被用戶自定義的緩衝區類型擴展;
  • 通過內置的復合緩衝區類型實現了透明的零拷貝;
  • 容量可以按需增長(類似於JDK的StringBuilder);
  • 在讀和寫這兩種模式之間切換不需要調用ByteBufferflip方法;
  • 讀和寫使用了不同的索引;
  • 支持方法的鏈式調用;
  • 支持引用計數;
  • 支持池化。

其他類可用於管理ByteBuf實例的分配,以及執行各種針對於數據容器本身和它所持有的數據的操作。我們將在仔細研究ByteBufByteBufHolder時探討這些特性。

5.2 ByteBuf類——Netty的數據容器

因為所有的網絡通信都涉及字節序列的移動,所以高效易用的數據結構明顯是必不可少的。Netty的ByteBuf實現滿足並超越了這些需求。讓我們首先來看看它是如何通過使用不同的索引來簡化對它所包含的數據的訪問的吧。

5.2.1 它是如何工作的

ByteBuf維護了兩個不同的索引:一個用於讀取,一個用於寫入。當你從ByteBuf讀取時,它的readerIndex將會被遞增已經被讀取的字節數。同樣地,當你寫入ByteBuf時,它的writerIndex也會被遞增。圖5-1展示了一個空ByteBuf的佈局結構和狀態。

圖5-1 一個讀索引和寫索引都設置為0的16字節ByteBuf

要瞭解這些索引兩兩之間的關係,請考慮一下,如果打算讀取字節直到readerIndex達到和writerIndex同樣的值時會發生什麼。在那時,你將會到達「可以讀取的」數據的末尾。就如同試圖讀取超出數組末尾的數據一樣,試圖讀取超出該點的數據將會觸發一個IndexOutOf-BoundsException

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

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

5.2.2 ByteBuf的使用模式

在使用Netty時,你將遇到幾種常見的圍繞ByteBuf而構建的使用模式。在研究它們時,我們心裡想著圖5-1會有所裨益—— 一個由不同的索引分別控制讀訪問和寫訪問的字節數組。

1.堆緩衝區

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

代碼清單5-1 支撐數組

ByteBuf heapBuf = ...;
if (heapBuf.hasArray) {  ← --  檢查ByteBuf 是否有一個支撐數組
  byte array = heapBuf.array;  ← --  如果有,則獲取對該數組的引用 
  int offset = heapBuf.arrayOffset + heapBuf.readerIndex;  ← --  計算第一個字節的偏移量。
  int length = heapBuf.readableBytes;  ← --  獲得可讀字節數
  handleArray(array, offset, length);  ← --  使用數組、偏移量和長度作為參數調用你的方法
}  

注意 當hasArray方法返回false時,嘗試訪問支撐數組將觸發一個UnsupportedOperationException。這個模式類似於JDK的ByteBuffer的用法。

2.直接緩衝區

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

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

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

顯然,與使用支撐數組相比,這涉及的工作更多。因此,如果事先知道容器中的數據將會被作為數組來訪問,你可能更願意使用堆內存。

代碼清單5-2 訪問直接緩衝區的數據

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

3.復合緩衝區

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

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

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

為了舉例說明,讓我們考慮一下一個由兩部分——頭部和主體——組成的將通過HTTP協議傳輸的消息。這兩部分由應用程序的不同模塊產生,將會在消息被發送的時候組裝。該應用程序可以選擇為多個消息重用相同的消息主體。當這種情況發生時,對於每個消息都將會創建一個新的頭部。

因為我們不想為每個消息都重新分配這兩個緩衝區,所以使用CompositeByteBuf是一個完美的選擇。它在消除了沒必要的複製的同時,暴露了通用的ByteBuf API。圖5-2展示了生成的消息佈局。

圖5-2 持有一個頭部和主體的CompositeByteBuf

代碼清單5-3展示了如何通過使用JDK的ByteBuffer來實現這一需求。創建了一個包含兩個ByteBuffer的數組用來保存這些消息組件,同時創建了第三個ByteBuffer用來保存所有這些數據的副本。

代碼清單5-3 使用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;  

分配和複製操作,以及伴隨著對數組管理的需要,使得這個版本的實現效率低下而且笨拙。代碼清單5-4展示了一個使用了CompositeByteBuf的版本。

代碼清單5-4 使用CompositeByteBuf的復合緩衝區模式

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

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

代碼清單5-5 訪問CompositeByteBuf中的數據

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

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

CompositeByteBuf API 除了從ByteBuf繼承的方法,CompositeByteBuf提供了大量的附加功能。請參考Netty的Javadoc以獲得該API的完整列表。

5.3 字節級操作

ByteBuf提供了許多超出基本讀、寫操作的方法用於修改它的數據。在接下來的章節中,我們將會討論這些中最重要的部分。

5.3.1 隨機訪問索引

如同在普通的Java字節數組中一樣,ByteBuf的索引是從零開始的:第一個字節的索引是0,最後一個字節的索引總是capacity - 1。代碼清單5-6表明,對存儲機制的封裝使得遍歷ByteBuf的內容非常簡單。

代碼清單5-6 訪問數據

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

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

5.3.2 順序訪問索引

雖然ByteBuf同時具有讀索引和寫索引,但是JDK的ByteBuffer卻只有一個索引,這也就是為什麼必須調用flip方法來在讀模式和寫模式之間進行切換的原因。圖5-3展示了ByteBuf是如何被它的兩個索引劃分成3個區域的。

圖5-3 ByteBuf的內部分段

5.3.3 可丟棄字節

在圖5-3中標記為可丟棄字節的分段包含了已經被讀過的字節。通過調用discardRead-Bytes方法,可以丟棄它們並回收空間。這個分段的初始大小為0,存儲在readerIndex中,會隨著read操作的執行而增加(get*操作不會移動readerIndex)。

圖5-4展示了圖5-3中所展示的緩衝區上調用discardReadBytes方法後的結果。可以看到,可丟棄字節分段中的空間已經變為可寫的了。注意,在調用discardReadBytes之後,對可寫分段的內容並沒有任何的保證[4]。

圖5-4 丟棄已讀字節之後的ByteBuf

雖然你可能會傾向於頻繁地調用discardReadBytes方法以確保可寫分段的最大化,但是請注意,這將極有可能會導致內存複製,因為可讀字節(圖中標記為CONTENT的部分)必須被移動到緩衝區的開始位置。我們建議只在有真正需要的時候才這樣做,例如,當內存非常寶貴的時候。

5.3.4 可讀字節

ByteBuf的可讀字節分段存儲了實際數據。新分配的、包裝的或者複製的緩衝區的默認的readerIndex值為0。任何名稱以read或者skip開頭的操作都將檢索或者跳過位於當前readerIndex的數據,並且將它增加已讀字節數。

如果被調用的方法需要一個ByteBuf參數作為寫入的目標,並且沒有指定目標索引參數,那麼該目標緩衝區的writerIndex也將被增加,例如:

readBytes(ByteBuf dest);  

如果嘗試在緩衝區的可讀字節數已經耗盡時從中讀取數據,那麼將會引發一個IndexOutOf-BoundsException

代碼清單5-7展示了如何讀取所有可以讀的字節。

代碼清單5-7 讀取所有數據

ByteBuf buffer = ...;
while (buffer.isReadable) {
  System.out.println(buffer.readByte);
}  

5.3.5 可寫字節

可寫字節分段是指一個擁有未定義內容的、寫入就緒的內存區域。新分配的緩衝區的writerIndex的默認值為0。任何名稱以write開頭的操作都將從當前的writerIndex處開始寫數據,並將它增加已經寫入的字節數。如果寫操作的目標也是ByteBuf,並且沒有指定源索引的值,則源緩衝區的readerIndex也同樣會被增加相同的大小。這個調用如下所示:

writeBytes(ByteBuf dest);  

如果嘗試往目標寫入超過目標容量的數據,將會引發一個IndexOutOfBoundException[5]

代碼清單5-8是一個用隨機整數值填充緩衝區,直到它空間不足為止的例子。writeableBytes方法在這裡被用來確定該緩衝區中是否還有足夠的空間。

代碼清單5-8 寫數據

// Fills the writable bytes of a buffer with random integers.
ByteBuf buffer = ...;
while (buffer.writableBytes >= 4) {
  buffer.writeInt(random.nextInt);
}  

5.3.6 索引管理

JDK的InputStream定義了mark(int readlimit)reset方法,這些方法分別被用來將流中的當前位置標記為指定的值,以及將流重置到該位置。

同樣,可以通過調用markReaderIndexmarkWriterIndexresetWriterIndexresetReaderIndex來標記和重置ByteBufreaderIndexwriterIndex。這些和InputStream上的調用類似,只是沒有readlimit參數來指定標記什麼時候失效。

也可以通過調用readerIndex(int)或者writerIndex(int)來將索引移動到指定位置。試圖將任何一個索引設置到一個無效的位置都將導致一個IndexOutOfBoundsException

可以通過調用clear方法來將readerIndexwriterIndex都設置為0。注意,這並不會清除內存中的內容。圖5-5(重複上面的圖5-3)展示了它是如何工作的。

圖5-5 clear方法被調用之前

和之前一樣,ByteBuf包含3個分段。圖5-6展示了在clear方法被調用之後ByteBuf的狀態。

圖5-6 在clear方法被調用之後

調用clear比調用discardReadBytes輕量得多,因為它將只是重置索引而不會複製任何的內存。

5.3.7 查找操作

在ByteBuf中有多種可以用來確定指定值的索引的方法。最簡單的是使用indexOf方法。較複雜的查找可以通過那些需要一個ByteBufProcessor[6]作為參數的方法達成。這個接口只定義了一個方法:

boolean process(byte value)  

它將檢查輸入值是否是正在查找的值。

ByteBufProcessor針對一些常見的值定義了許多便利的方法。假設你的應用程序需要和所謂的包含有以NULL結尾的內容的Flash套接字[7]集成。調用

forEachByte(ByteBufProcessor.FIND_NUL)  

將簡單高效地消費該Flash數據,因為在處理期間只會執行較少的邊界檢查。

代碼清單5-9展示了一個查找回車符(\r)的例子。

代碼清單5-9 使用ByteBufProcessor來尋找\r

ByteBuf buffer = ...;
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);  

5.3.8 派生緩衝區

派生緩衝區為ByteBuf提供了以專門的方式來呈現其內容的視圖。這類視圖是通過以下方法被創建的:

  • duplicate;
  • slice;
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。

每個這些方法都將返回一個新的ByteBuf實例,它具有自己的讀索引、寫索引和標記索引。其內部存儲和JDK的ByteBuffer一樣也是共享的。這使得派生緩衝區的創建成本是很低廉的,但是這也意味著,如果你修改了它的內容,也同時修改了其對應的源實例,所以要小心。

ByteBuf複製 如果需要一個現有緩衝區的真實副本,請使用copy或者copy(int, int)方法。不同於派生緩衝區,由這個調用所返回的ByteBuf擁有獨立的數據副本。

代碼清單5-10展示了如何使用slice(int,int)方法來操作ByteBuf的一個分段。

代碼清單5-10 對ByteBuf進行切片

Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);  ← --  創建一個用於保存給定字符串的字節的ByteBuf
ByteBuf sliced = buf.slice(0, 15);  ← --  創建該ByteBuf 從索引0 開始到索引15結束的一個新切片
System.out.println(sliced.toString(utf8));   ← --  將打印「Netty in Action」
buf.setByte(0, (byte)'J');   ← --  更新索引0 處的字節
assert buf.getByte(0) == sliced.getByte(0);  ← --  將會成功,因為數據是共享的,對其中一個所做的更改對另外一個也是可見的  

現在,讓我們看看ByteBuf的分段的副本和切片有何區別,如代碼清單5-11所示。

代碼清單5-11 複製一個ByteBuf

Charset utf8 = Charset.forName("UTF-8");
 ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);  ← --  創建ByteBuf 以保存所提供的字符串的字節
 ByteBuf copy = buf.copy(0, 15);  ← --  創建該ByteBuf 從索引0 開始到索引15結束的分段的副本
System.out.println(copy.toString(utf8));  ← --   將打印「Netty in Action」
buf.setByte(0, (byte) 'J');  ← --  更新索引0 處的字節 
assert buf.getByte(0) != copy.getByte(0);  ← --  將會成功,因為數據不是共享的  

除了修改原始ByteBuf的切片或者副本的效果以外,這兩種場景是相同的。只要有可能,使用slice方法來避免複製內存的開銷。

5.3.9 讀/寫操作

正如我們所提到過的,有兩種類別的讀/寫操作:

  • getset操作,從給定的索引開始,並且保持索引不變;
  • readwrite操作,從給定的索引開始,並且會根據已經訪問過的字節數對索引進行調整。

表5-1列舉了最常用的get方法。完整列表請參考對應的API文檔。

表5-1 get操作

名  稱

描  述

getBoolean(int)

返回給定索引處的Boolean

getByte(int)

返回給定索引處的字節

getUnsignedByte(int)

將給定索引處的無符號字節值作為short返回

getMedium(int)

返回給定索引處的24位的中等int

getUnsignedMedium(int)

返回給定索引處的無符號的24位的中等int

getInt(int)

返回給定索引處的int

getUnsignedInt(int)

將給定索引處的無符號int值作為long返回

getLong(int)

返回給定索引處的long

getShort(int)

返回給定索引處的short

getUnsignedShort(int)

將給定索引處的無符號short值作為int返回

getBytes(int, ...)

將該緩衝區中從給定索引開始的數據傳送到指定的目的地

大多數的這些操作都有一個對應的set方法。這些方法在表5-2中列出。

表5-2 set操作

名  稱

描  述

setBoolean(int, boolean)

設定給定索引處的Boolean

setByte(int index, int value)

設定給定索引處的字節值

setMedium(int index, int value)

設定給定索引處的24位的中等int

setInt(int index, int value)

設定給定索引處的int

setLong(int index, long value)

設定給定索引處的long

setShort(int index, int value)

設定給定索引處的short

代碼清單5-12說明了getset方法的用法,表明了它們不會改變讀索引和寫索引。

代碼清單5-12 getset方法的用法

Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);   ← --  創建一個新的ByteBuf以保存給定字符串的字節
System.out.println((char)buf.getByte(0));  ← --  打印第一個字符'N'
int readerIndex = buf.readerIndex;   ← --  存儲當前的readerIndex 和writerIndex
int writerIndex = buf.writerIndex;
buf.setByte(0, (byte)'B');   ← --  將索引0 處的字節更新為字符'B'
System.out.println((char)buf.getByte(0));   ← --  打印第一個字符,現在是'B' 
assert readerIndex == buf.readerIndex;  ← --  將會成功,因為這些操作並不會修改相應的索引 
assert writerIndex == buf.writerIndex;  

現在,讓我們研究一下read操作,其作用於當前的readerIndexwriterIndex。這些方法將用於從ByteBuf中讀取數據,如同它是一個流。表5-3展示了最常用的方法。

表5-3 read操作

名  稱

描  述

readBoolean

返回當前readerIndex處的Boolean,並將readerIndex增加1

readByte

返回當前readerIndex處的字節,並將readerIndex增加1

readUnsignedByte

將當前readerIndex處的無符號字節值作為short返回,並將readerIndex增加1

readMedium

返回當前readerIndex處的24位的中等int值,並將readerIndex增加3

readUnsignedMedium

返回當前readerIndex處的24位的無符號的中等int值,並將readerIndex增加3

readInt

返回當前readerIndexint值,並將readerIndex增加4

readUnsignedInt

將當前readerIndex處的無符號的int值作為long值返回,並將readerIndex增加4

readLong

返回當前readerIndex處的long值,並將readerIndex增加8

readShort

返回當前readerIndex處的short值,並將readerIndex增加2

readUnsignedShort

將當前readerIndex處的無符號short值作為int值返回,並將readerIndex增加2

readBytes(ByteBuf | byte destination, int dstIndex [,int length])

將當前ByteBuf中從當前readerIndex處開始的(如果設置了,length長度的字節)數據傳送到一個目標ByteBuf或者byte,從目標的dstIndex開始的位置。本地的readerIndex將被增加已經傳輸的字節數

幾乎每個read方法都有對應的write方法,用於將數據追加到ByteBuf中。注意,表5-4中所列出的這些方法的參數是需要寫入的值,而不是索引值。

表5-4 寫操作

名  稱

描  述

writeBoolean(boolean)

在當前writerIndex處寫入一個Boolean,並將writerIndex增加1

writeByte(int)

在當前writerIndex處寫入一個字節值,並將writerIndex增加1

writeMedium(int)

在當前writerIndex處寫入一個中等的int值,並將writerIndex增加3

writeInt(int)

在當前writerIndex處寫入一個int值,並將writerIndex增加4

writeLong(long)

在當前writerIndex處寫入一個long值,並將writerIndex增加8

writeShort(int)

在當前writerIndex處寫入一個short值,並將writerIndex增加2

writeBytes(source ByteBuf |byte [,int srcIndex ,int length])

從當前writerIndex開始,傳輸來自於指定源(ByteBuf或者byte)的數據。如果提供了srcIndexlength,則從srcIndex開始讀取,並且處理長度為length的字節。當前writerIndex將會被增加所寫入的字節數

代碼清單5-13展示了這些方法的用法。

代碼清單5-13 ByteBuf上的readwrite操作

Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);   ← --  創建一個新的ByteBuf 以保存給定字符串的字節
System.out.println((char)buf.readByte);  ← --  打印第一個字符'N'
int readerIndex = buf.readerIndex;  ← --   存儲當前的readerIndex
int writerIndex = buf.writerIndex;  ← --   存儲當前的writerIndex
buf.writeByte((byte)'?');  ← --  將字符'?'追加到緩衝區  
assert readerIndex == buf.readerIndex;
assert writerIndex != buf.writerIndex;  ← --  將會成功,因為writeByte方法移動了writerIndex  

5.3.10 更多的操作

表5-5 列舉了由ByteBuf提供的其他有用操作。

表5-5 其他有用的操作

名  稱

描  述

isReadable

如果至少有一個字節可供讀取,則返回true

isWritable

如果至少有一個字節可被寫入,則返回true

readableBytes

返回可被讀取的字節數

writableBytes

返回可被寫入的字節數

capacity

返回ByteBuf可容納的字節數。在此之後,它會嘗試再次擴展直 到達到maxCapacity

maxCapacity

返回ByteBuf可以容納的最大字節數

hasArray

如果ByteBuf由一個字節數組支撐,則返回true

array

如果 ByteBuf由一個字節數組支撐則返回該數組;否則,它將拋出一個UnsupportedOperationException異常

5.4 ByteBufHolder接口

我們經常發現,除了實際的數據負載之外,我們還需要存儲各種屬性值。HTTP響應便是一個很好的例子,除了表示為字節的內容,還包括狀態碼、cookie等。

為了處理這種常見的用例,Netty提供了ByteBufHolderByteBufHolder也為Netty的高級特性提供了支持,如緩衝區池化,其中可以從池中借用ByteBuf,並且在需要時自動釋放。

ByteBufHolder只有幾種用於訪問底層數據和引用計數的方法。表5-6列出了它們(這裡不包括它繼承自ReferenceCounted的那些方法)。

表5-6 ByteBufHolder的操作

名  稱

描  述

content

返回由這個ByteBufHolder所持有的ByteBuf

copy

返回這個ByteBufHolder的一個深拷貝,包括一個其所包含的ByteBuf的非共享拷貝

duplicate

返回這個ByteBufHolder的一個淺拷貝,包括一個其所包含的ByteBuf的共享拷貝

如果想要實現一個將其有效負載存儲在ByteBuf中的消息對象,那麼ByteBufHolder將是個不錯的選擇。

5.5 ByteBuf分配

在這一節中,我們將描述管理ByteBuf實例的不同方式。

5.5.1 按需分配:ByteBufAllocator接口

為了降低分配和釋放內存的開銷,Netty通過interface ByteBufAllocator實現了(ByteBuf的)池化,它可以用來分配我們所描述過的任意類型的ByteBuf實例。使用池化是特定於應用程序的決定,其並不會以任何方式改變ByteBuf API(的語義)。

表5-7 列出了ByteBufAllocator提供的一些操作。

表5-7 ByteBufAllocator的方法

名  稱

描  述

buffer
buffer(int initialCapacity);
buffer(int initialCapacity, int maxCapacity);

返回一個基於堆或者直接內存存儲的ByteBuf

heapBuffer
heapBuffer(int initialCapacity)
heapBuffer(int initialCapacity, int maxCapacity)

返回一個基於堆內存存儲的ByteBuf

directBuffer
directBuffer(int initialCapacity)
directBuffer(int initialCapacity, int maxCapacity)

返回一個基於直接內存存儲的ByteBuf

compositeBuffer
compositeBuffer(int maxNumComponents)
compositeDirectBuffer
compositeDirectBuffer(int maxNumComponents);
compositeHeapBuffer
compositeHeapBuffer(int maxNumComponents);

返回一個可以通過添加最大到指定數目的基於堆的或者直接內存存儲的緩衝區來擴展的CompositeByteBuf

ioBuffer [8]

返回一個用於套接字的I/O操作的ByteBuf

可以通過Channel(每個都可以有一個不同的ByteBufAllocator實例)或者綁定到ChannelHandlerChannelHandlerContext獲取一個到ByteBufAllocator的引用。代碼清單5-14說明了這兩種方法。

代碼清單5-14 獲取一個到ByteBufAllocator的引用

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc;  ← --  從Channel 獲取一個到ByteBufAllocator 的引用
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc;  ← --  從ChannelHandlerContext 獲取一個到ByteBufAllocator 的引用
...  

Netty提供了兩種ByteBufAllocator的實現:PooledByteBufAllocatorUnpooled-ByteBufAllocator。前者池化了ByteBuf的實例以提高性能並最大限度地減少內存碎片。此實現使用了一種稱為jemalloc[9]的已被大量現代操作系統所採用的高效方法來分配內存。後者的實現不池化ByteBuf實例,並且在每次它被調用時都會返回一個新的實例。

雖然Netty默認[10]使用了PooledByteBufAllocator,但這可以很容易地通過Channel-Config API或者在引導你的應用程序時指定一個不同的分配器來更改。更多的細節可在第8章中找到。

5.5.2 Unpooled緩衝區

可能某些情況下,你未能獲取一個到ByteBufAllocator的引用。對於這種情況,Netty提供了一個簡單的稱為Unpooled的工具類,它提供了靜態的輔助方法來創建未池化的 ByteBuf實例。表5-8列舉了這些中最重要的方法。

表5-8 Unpooled的方法

名  稱

描  述

buffer
buffer(int initialCapacity)
buffer(int initialCapacity, int maxCapacity)

返回一個未池化的基於堆內存存儲的ByteBuf

directBuffer
directBuffer(int initialCapacity)
directBuffer(int initialCapacity, int maxCapacity)

返回一個未池化的基於直接內存存儲的ByteBuf

wrappedBuffer

返回一個包裝了給定數據的ByteBuf

copiedBuffer

返回一個複製了給定數據的ByteBuf

Unpooled類還使得ByteBuf同樣可用於那些並不需要Netty的其他組件的非網絡項目,使得其能得益於高性能的可擴展的緩衝區API。

5.5.3 ByteBufUtil類

ByteBufUtil提供了用於操作ByteBuf的靜態的輔助方法。因為這個API是通用的,並且和池化無關,所以這些方法已然在分配類的外部實現。

這些靜態方法中最有價值的可能就是hexdump方法,它以十六進制的表示形式打印ByteBuf的內容。這在各種情況下都很有用,例如,出於調試的目的記錄ByteBuf的內容。十六進制的表示通常會提供一個比字節值的直接表示形式更加有用的日誌條目,此外,十六進制的版本還可以很容易地轉換回實際的字節表示。

另一個有用的方法是boolean equals(ByteBuf, ByteBuf),它被用來判斷兩個ByteBuf實例的相等性。如果你實現自己的ByteBuf子類,你可能會發現ByteBufUtil的其他有用方法。

5.6 引用計數

引用計數是一種通過在某個對象所持有的資源不再被其他對像引用時釋放該對像所持有的資源來優化內存使用和性能的技術。Netty在第4版中為ByteBufByteBufHolder引入了引用計數技術,它們都實現了interface ReferenceCounted

引用計數背後的想法並不是特別的複雜;它主要涉及跟蹤到某個特定對象的活動引用的數量。一個ReferenceCounted實現的實例將通常以活動的引用計數為1作為開始。只要引用計數大於0,就能保證對像不會被釋放。當活動引用的數量減少到0時,該實例就會被釋放。注意,雖然釋放的確切語義可能是特定於實現的,但是至少已經釋放的對象應該不可再用了。

引用計數對於池化實現(如PooledByteBufAllocator)來說是至關重要的,它降低了內存分配的開銷。代碼清單5-15和代碼清單5-16展示了相關的示例。

代碼清單5-15 引用計數

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc;  ← --  從Channel 獲取ByteBufAllocator
....
ByteBuf buffer = allocator.directBuffer;  ← --  從ByteBufAllocator分配一個ByteBuf    
assert buffer.refCnt == 1; ← --  檢查引用計數是否為預期的1
...  

代碼清單5-16 釋放引用計數的對象

ByteBuf buffer = ...;
boolean released = buffer.release;  ← --  減少到該對象的活動引用。當減少到0 時,該對像被釋放,並且該方法返回true
...  

試圖訪問一個已經被釋放的引用計數的對象,將會導致一個IllegalReferenceCount- Exception

注意,一個特定的(ReferenceCounted的實現)類,可以用它自己的獨特方式來定義它的引用計數規則。例如,我們可以設想一個類,其release方法的實現總是將引用計數設為零,而不用關心它的當前值,從而一次性地使所有的活動引用都失效。

誰負責釋放 一般來說,是由最後訪問(引用計數)對象的那一方來負責將它釋放。在第6章中,我們將會解釋這個概念和ChannelHandler以及ChannelPipeline的相關性。

5.7 小結

本章專門探討了Netty的基於ByteBuf的數據容器。我們首先解釋了ByteBuf相對於JDK所提供的實現的優勢。我們還強調了該API的其他可用變體,並且指出了它們各自最佳適用的特定用例。

我們討論過的要點有:

  • 使用不同的讀索引和寫索引來控制數據訪問;
  • 使用內存的不同方式——基於字節數組和直接緩衝區;
  • 通過CompositeByteBuf生成多個ByteBuf的聚合視圖;
  • 數據訪問方法——搜索、切片以及複製;
  • 讀、寫、獲取和設置API;
  • ByteBufAllocator池化和引用計數。

在下一章中,我們將專注於ChannelHandler,它為你的數據處理邏輯提供了載體。因為ChannelHandler大量地使用了ByteBuf,你將開始看到Netty的整體架構的各個重要部分最終走到了一起。


[1] 也就是說用戶直接或者間接使capacity(int)或者ensureWritable(int)方法來增加超過該最大容量時拋出異常。——譯者注

[2] Java平台,標準版第8版API規範,java.nio,class ByteBuffer:http://docs.oracle. com/javase/8/docs/api/ java/nio/ByteBuffer.html。

[3] 這尤其適用於JDK所使用的一種稱為分散/收集I/O(Scatter/Gather I/O)的技術,定義為「一種輸入和輸出的方法,其中,單個系統調用從單個數據流寫到一組緩衝區中,或者,從單個數據源讀到一組緩衝區中」。《Linux System Programming》,作者Robert Love(O』Reilly, 2007)。

[4] 因為只是移動了可以讀取的字節以及writerIndex,而沒有對所有可寫入的字節進行擦除寫。——譯者注

[5] 在往ByteBuf中寫入數據時,其將首先確保目標ByteBuf具有足夠的可寫入空間來容納當前要寫入的數據,如果沒有,則將檢查當前的寫索引以及最大容量是否可以在擴展後容納該數據,可以則會分配並調整容量,否則就會拋出該異常。——譯者注

[6] 在Netty 4.1.x中,該類已經廢棄,請使用io.netty.util.ByteProcessor。——譯者注

[7] 有關Flash套接字的討論可參考Flash ActionScript 3.0 Developer』s Guide中Networking and Communication部分裡的Sockets頁面:http://help.adobe.com/en_US/as3/dev/WSb2ba3b1aad8a27b0-181c51321220efd9d1c-8000.html。

[8] 默認地,當所運行的環境具有sun.misc.Unsafe支持時,返回基於直接內存存儲的ByteBuf,否則返回基於堆內存存儲的ByteBuf;當指定使用PreferHeapByteBufAllocator時,則只會返回基於堆內存存儲的ByteBuf。——譯者注

[9] Jason Evans的「A Scalable Concurrent malloc(3) Implementation for FreeBSD」(2006):http://people.freebsd. org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf。

[10] 這裡指Netty4.1.x,Netty4.0.x默認使用的是UnpooledByteBufAllocator。——譯者注