Java NIO 提供了 ByteBuffer 做爲它的字節容器, 可是這個類使用起來過於複雜, 並且也有些繁瑣.java
Netty 的 ByteBuffer 的代替品是 ByteBuf.數組
ByteBuf 的 API 緩存
Netty 的數據處理 API 經過兩個組件暴露cookie
public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> public interface ByteBufHolder extends ReferenceCounted
下面是 ByteBuf API 的優勢:網絡
ByteBuf 維護了兩個不一樣的索引: 一個用於讀取, 一個用於寫入. 當你從 ByteBuf 讀取時, 它的 readerIndex 將會被遞增到讀取的字節數. 一樣的, 當你寫入 ByteBuf 時, 它的 writerIndex 也會被遞增.app
要了解這些索引兩兩之間的關係, 請考慮一下, 若是打算讀取字節直到 readerIndex 達到和 writerIndex 一樣的值時會發生什麼.socket
在那時, 你將會到達 "能夠讀取的" 數據的末尾. 就如同試圖讀取超出數組末尾的數據同樣, 會觸發一個 IndexOutOfBoundsException
.函數
名稱以 read 或 write 開頭的 ByteBuf 方法, 將會推動其對應的索引, 而名稱以 set 或 get 開頭的操做則不會. 後面的這些方法將在做爲一個參數傳入的一個相對索引上執行操做.工具
能夠指定 ByteBuf 的最大容量. 默認的限制是 Interger.MAX_VALUE.
最經常使用的 ByteBuf 模式是將數據存儲在 JVM 的堆空間中. 這種模式被稱爲 支撐數組 , 它能在沒有使用池化的狀況下提供快速的分配和釋放. 代碼以下:性能
public static void main(String args[]) { ByteBuf heapBuf = Unpooled.copiedBuffer("heap space", CharsetUtil.UTF_8); if (heapBuf.hasArray()) { // 檢查 ByteBuf 是否有一個支持數組 byte[] array = heapBuf.array(); int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();// 計算第一個字節的偏移量 int length = heapBuf.readableBytes();// 計算可讀字節數 System.out.println(Arrays.toString(array)); System.out.println(offset); System.out.println(length); } else { System.out.println("No Heap Array"); } }
當hasArray()
返回false
時嘗試訪問支持數組會拋出UnsupportedOperationException
. 這個用法與JDK的ByteBuffer
相似.
直接緩衝區是另一種 ByteBuf
模式.
直接緩衝區的內容並非駐留在 Java 的堆上, 利用 java.nio.ByteBuffer
是藉助於 JVM 調用操做系統的底層通訊函數, 直接操做直接緩衝區能夠減小中間緩衝區的複製操做, 進而提供程序性能, 其目的是:
-XX:MaxDirectMemorySize=xxM
大小限制下, 使用 Heap 以外的內存, GC對此 」無能爲力」 , 也就意味着規避了在高負載下頻繁的 GC 過程對應用線程的中斷影響.可是直接緩衝區的缺點是在內存空間的分配和釋放上比堆緩衝區更復雜, 另一個缺點是若是要將數據傳遞給遺留代碼處理, 由於數據不是在堆上, 你可能不得不做出一個副本, 以下:
public static void main(String args[]) { ByteBuf directBuf = Unpooled.directBuffer(100); directBuf.writeBytes("direct buffer".getBytes()); if (!directBuf.hasArray()) { // 檢查 ByteBuf 是否由數組支撐. 若是不是, 則這是一個直接緩衝區 int length = directBuf.readableBytes(); // 獲取可讀字節數 byte[] array = new byte[length]; // 分配一個新的數組來保存具備該長度的字節數據 directBuf.getBytes(directBuf.readerIndex(), array); // 將字節複製到該數組 System.out.println(Arrays.toString(array)); System.out.println(length); } }
複合緩衝區爲多個 ByteBuf
提供一個聚合視圖, 能夠根據須要添加或者刪除 ByteBuf 實例.
Netty 提供了 ByteBuf
的子類 CompositeByteBuf
類來處理複合緩衝區, CompositeByteBuf
只是一個視圖.
CompositeByteBuf
中的 ByteBuf
實例可能同時包含直接內存分配和非直接內存分配.
若是其中只有一個實例, 那麼對 CompositeByteBuf
上的 hasArray()
方法的調用將返回該組件上的 hasArray()方法的值; 不然它將返回 false. 由於有可能包含直接緩衝區或堆緩衝區.
例如, 一條消息由 header
和 body
兩部分組成, 將 header
和 body
組裝成一條消息發送出去, 可能 body
相同, 只是 header
不一樣, 使用 CompositeByteBuf
就不用每次都從新分配一個新的緩衝區.
下圖顯示CompositeByteBuf
組成 header
和 body
:
下面代碼顯示了使用 JDK 的 ByteBuffer
的一個實現. 兩個 ByteBuffer
的數組建立保存消息的組件,第三個建立用於保存全部數據的副本.
// 使用數組保存消息的各個部分 ByteBuffer[] message = { header, body }; // 使用副原本合併這兩個部分 ByteBuffer message2 = ByteBuffer.allocate( header.remaining() + body.remaining()); message2.put(header); message2.put(body); message2.flip();
這種作法顯然是低效的; 分配和複製操做不是最優的方法, 操縱數組使代碼顯得很笨拙.
下面看使用 CompositeByteBuf
的改進版本.
/** * Netty 經過一個 ByteBuf 子類——CompositeByteBuf——實現了組合模式,它提供了一 個將多個緩衝區表示爲單個合併緩衝區的虛擬表示 */ public class CompositeBuf { public static void main(String args[]){ CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); ByteBuf headerBuf = Unpooled.copiedBuffer("head", CharsetUtil.UTF_8); // can be backing or direct ByteBuf bodyBuf = Unpooled.copiedBuffer("body", CharsetUtil.UTF_8); // can be backing or direct messageBuf.addComponents(headerBuf, bodyBuf); System.out.println("Remove Head Before------------------"); printCompositeBuffer(messageBuf); for (ByteBuf buf : messageBuf) { System.out.println(buf.toString(CharsetUtil.UTF_8)); } messageBuf.removeComponent(0); // remove the header System.out.println("Remove Head After------------------"); printCompositeBuffer(messageBuf); for (ByteBuf buf : messageBuf) { System.out.println(buf.toString(CharsetUtil.UTF_8)); } } public static void printCompositeBuffer(CompositeByteBuf compBuf){ int length = compBuf.readableBytes(); byte[] array = new byte[length]; compBuf.getBytes(compBuf.readerIndex(), array); System.out.println (Arrays.toString(array)); System.out.println (length); } }
Netty 嘗試使用 CompositeByteBuf 優化 socket I/O 操做, 消除原生 JDK 中可能存在的的性能低和內存消耗問題.
雖然這是在 Netty 的核心代碼中進行的優化, 而且是不對外暴露的, 可是做爲開發者仍是應該意識到其影響.
從空間初始化方式上來分, ByteBuf 分爲: 緩存方式分配和非緩存方式分配.
爲了下降分配和釋放內存的開銷, Netty 經過 interface ByteBufAllocator
實現了 ByteBuf
的池化, 它能夠用來分配咱們所描述過的任意類型的 ByteBuf
實例 (基於堆緩衝區的, 基於直接緩衝區的, 基於複合緩衝區的).
// 返回一個基於堆或者直接內存存儲的 ByteBuf ByteBuf buffer(); ByteBuf buffer(int initialCapacity); ByteBuf buffer(int initialCapacity, int maxCapacity); // 返回一個基於堆內存的 ByteBuf ByteBuf heapBuffer(); ByteBuf heapBuffer(int initialCapacity); ByteBuf heapBuffer(int initialCapacity, int maxCapacity); // 返回一個基於直接內存的 ByteBuf ByteBuf directBuffer(); ByteBuf directBuffer(int initialCapacity); ByteBuf directBuffer(int initialCapacity, int maxCapacity); // 經過設置一個最大值, 基於堆內存或直接內存, 返回一個混合緩衝區. CompositeByteBuf compositeBuffer(int maxNumComponents); CompositeByteBuf compositeHeapBuffer(); CompositeByteBuf compositeHeapBuffer(int maxNumComponents); CompositeByteBuf compositeDirectBuffer(); CompositeByteBuf compositeDirectBuffer(int maxNumComponents); // 返回一個用於套接字 I/O 操做的 ByteBuf ioBuffer()
ioBuffer() 重點
默認的, 當所運行的環境具備sun.misc.Unsafe
支持時, 返回基於直接內存的 ByteBuf, 不然返回一個基於堆內存的 ByteBuf; 當指定使用PreferHeapByteBufAllocator
時, 則只會返回基於堆內存的 ByteBuf.
能夠經過 Channle
(每一個均可以有一個不一樣的 ByteBufAllocator
實例) 或者 ChannleHandlerContext
獲取到 ByteBufAllocator
.
Netty 提供了兩種 ByteBufAllocator
的實現:
Channel channel = ...; ByteBufAllocator byteBufAllocator = channel.alloc(); ChannelHandlerContext channelHandlerContext = ...; ByteBufAllocator bufAllocator = channelHandlerContext.alloc();
前者池化了ByteBuf
的實例以提升性能並最大限度地減小內存碎片(PooledByteBufAllocator
默認).
後者不池化ByteBuf
實例, 而且在每次它被調用時都會返回一個新的實例(UnpooledByteBufAllocator
).
可能某些狀況下, 你未能獲取一個到 ByteBufAllocator
的引用. 對於這種狀況, Netty 提供了一個簡單的稱爲 Unpooled
(封裝了 UnpooledByteBufAllocator
) 的工具類, 它提供了靜態的輔助方法來建立未池化的 ByteBuf 實例. 提供的方法以下:
buffer
: 返回一個未池化的基於堆內存存儲的 ByteBuf
.directBuffer
: 返回一個未池化的基於直接內存存儲的 ByteBuf
wrappedBuffer
: 返回一個包裝了給定數據的 ByteBuf
copiedBuffer
: 返回一個複製了給定數據的 ByteBuf
ByteBuf directBuf = Unpooled.directBuffer(100); directBuf.writeBytes("direct buffer".getBytes()); System.out.println(directBuf.indexOf(0,directBuf.readableBytes(), (byte) 'u'));
Unpooled
類還使得ByteBuf
一樣可用於那些並不須要 Netty 的其餘組件的非網絡項目, 使得其能得益於高性能的可擴展的緩衝區 API.
如同在普通的 Java 字節數組中同樣, ByteBuf
的索引是從零開始的: 第一個字節的索引是0, 最後一個字節的索引老是 capacity() - 1
, 能夠經過 index
進行訪問:
public static void main(String args[]) { ByteBuf directBuf = Unpooled.directBuffer(100); directBuf.writeBytes("direct buffer".getBytes()); System.out.println(directBuf.getByte(5)); }
這種方式並不會改變readerIndex
和weiterIndex
. 若是有須要, 也能夠經過調用readerIndex(index)
或者writerIndex(index)
來手動移動這二者.
雖然 ByteBuf
同時具備讀索引和寫索引, 可是 JDK 的 ByteBuffer
卻只有一個索引, 這也就是爲何必須調用 flip()
方法來在讀模式和寫模式之間進行切換的緣由. ByteBuf
被兩個索引劃分紅 3 個區域.
可丟棄字節的分段包含了已經被讀過的字節. 經過調用 discardReadBytes()
方法, 能夠丟棄它們並回收空間. 這個分段的初始大小爲 0, 存儲在 readerIndex
中, 會隨着 read
操做的執行而增長.
下圖展現的緩衝區上調用 discardReadBytes()
方法後的結果. 能夠看到, 可丟棄字節分段中的空間已經變爲可寫的了.
注意, 在調用 discardReadBytes()
以後, 對可寫分段的內容並無任何的保證.
頻繁地調用 discardReadBytes()
方法能夠確保可寫分段的最大化, 可是這將極有可能會致使內存複製, 由於可讀字節 (圖中標記爲 CONTENT 的部分) 必須被移動到緩衝區的開始位置. 咱們建議只在有真正須要的時候才這樣作, 例如, 當內存很是寶貴的時候.
ByteBuf 的可讀字節分段存儲了實際數據. 新分配的、包裝的或者複製的緩衝區默認 readerIndex
值爲 0. 任何名稱以 read
或者 skip
開頭的操做都將檢索或者跳過位於當前 readerIndex
的數據, 而且將它增長已讀字節數.
若是嘗試在緩衝區的可讀字節數已經耗盡時從中讀取數據, 那麼將會引起一個 IndexOutOfBoundsException
.
可寫字節分段是指一個擁有未定義內容的、寫入就緒的內存區域. 新分配的緩衝區的 writerIndex
的默認值爲 0. 任何名稱以 write
開頭的操做都將從當前的 writerIndex
處開始寫數據, 並將它增長已經寫入的字節數.
若是寫操做的目標也是 ByteBuf, 而且沒有指定源索引的值, 則源緩衝區的 readerIndex
也一樣會被增長相同的大小.
若是嘗試往目標寫入超過目標容量的數據, 將會引起一個 IndexOutOfBoundException
.
ByteBuf directBuf = Unpooled.directBuffer(100); directBuf.writeBytes("direct buffer".getBytes()); System.out.println("可寫字節容量:"+directBuf.writableBytes()); System.out.println("初始化可讀字節:"+directBuf.readableBytes()); System.out.println("初始化可丟棄字節:"+directBuf.readerIndex()+"\n"); directBuf.readBytes(2); System.out.println("讀取兩個字節"+"\n"); System.out.println("讀取後可寫字節容量:"+directBuf.writableBytes()); System.out.println("讀取後可讀字節:"+directBuf.readableBytes()); System.out.println("讀取後可丟棄字節:"+directBuf.readerIndex()+"\n"); directBuf.discardReadBytes(); System.out.println("執行discardReadBytes後可寫字節容量:"+directBuf.writableBytes()); System.out.println("執行discardReadBytes後可讀字節:"+directBuf.readableBytes()); System.out.println("執行discardReadBytes後可丟棄字節:"+directBuf.readerIndex());
輸出爲:
可寫字節容量:87 初始化可讀字節:13 初始化可丟棄字節:0 讀取兩個字節 讀取後可寫字節容量:87 讀取後可讀字節:11 讀取後可丟棄字節:2 執行discardReadBytes後可寫字節容量:89 執行discardReadBytes後可讀字節:11 執行discardReadBytes後可丟棄字節:0
索引管理的相關操做以下:
markReaderIndex()
、 markWriterIndex()
、 resetWriterIndex()
和 resetReaderIndex()
來標記和重置 ByteBuf
的 readerIndex
和 writerIndex
.readerIndex(int)
或者 writerIndex(int)
來將索引移動到指定位置. 試圖將任何一個索引設置到一個無效的位置都將致使一個 IndexOutOfBoundsException
.clear()
方法來將 readerIndex
和 writerIndex
都設置爲 0. 調用 clear()
比調用 discardReadBytes()
輕量得多, 由於它將只是重置索引而不會複製任何的內存.示例以下:
ByteBuf directBuf = Unpooled.directBuffer(100); directBuf.writeBytes("direct buffer".getBytes()); System.out.println("初始化可讀字節:"+directBuf.readableBytes()); directBuf.markReaderIndex(); System.out.println("執行markReaderIndex"+"\n");//標記讀索引 directBuf.readBytes(2); System.out.println("讀取兩個字節"+"\n"); System.out.println("讀取後可讀字節:"+directBuf.readableBytes()); directBuf.resetReaderIndex();//恢復讀索引 System.out.println("執行resetReaderIndex後可讀字節:"+directBuf.readableBytes()); directBuf.clear(); System.out.println("執行clear後可讀字節:"+directBuf.readableBytes()); directBuf.readBytes(2);//可讀字節變爲0,此時再讀取會拋出IndexOutOfBoundsException
輸出以下:
初始化可讀字節:13 執行markReaderIndex 讀取兩個字節 讀取後可讀字節:11 執行resetReaderIndex後可讀字節:13 執行clear後可讀字節:0 Exception in thread "main" java.lang.IndexOutOfBoundsException: readerIndex(0) + length(2) exceeds writerIndex(0): UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(ridx: 0, widx: 0, cap: 100) at io.netty.buffer.AbstractByteBuf.checkReadableBytes0(AbstractByteBuf.java:1403) at io.netty.buffer.AbstractByteBuf.checkReadableBytes(AbstractByteBuf.java:1390) at io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:843) at com.eric.bytebuf.OperationSample.indexManage(OperationSample.java:32) at com.eric.bytebuf.OperationSample.main(OperationSample.java:16)
派生緩衝區爲 ByteBuf 提供了以專門的方式來呈現其內容的視圖。這類視圖是經過如下方法被建立的:
duplicate() slice() slice(int, int) Unpooled.unmodifiableBuffer(…) order(ByteOrder) readSlice(int)
上述這些方法都將返回一個新的 ByteBuf 實例, 它具備本身的讀索引、寫索引和標記 索引.
可是其內部存儲和原始對象是共享的. 該種方式建立成本很低廉, 可是這也意味着, 若是你修改了它的內容, 也同時修改了其對應的源實例, 因此要當心.
若是須要一個現有緩衝區的真實副本, 請使用 copy()
或者 copy(int, int)
方法. 不一樣於派生緩衝區, 由這個調用所返回的 ByteBuf 擁有獨立的數據副本.
Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); ByteBuf slice = buf.slice(0, 15); System.out.println(slice.toString(utf8)); buf.setByte(0,'J'); assert buf.getByte(0) == slice.getByte(0);
正如前文所提到過的, 有兩種類別的讀/寫操做:
get()
和 set()
操做, 從給定的索引開始,而且保持索引不變;read()
和 write()
操做, 從給定的索引開始,而且會根據已經訪問過的字節數對索引進行調整.咱們常常發現, 除了實際的數據負載以外, 咱們還須要存儲各類屬性值. HTTP 響應即是一個很好的例子, 除了表示爲字節的內容, 還包括狀態碼、cookie 等.
爲了處理這種常見的用例, Netty 提供了 ByteBufHolder
. ByteBufHolder
也爲 Netty 的高級特性提供了支持, 如緩衝區池化, 其中能夠從池中借用 ByteBuf, 而且在須要時自動釋放.
ByteBufHolder
只有幾種用於訪問底層數據和引用計數的方法:
content()
: 返回由這個 ByteBufHolder
所持有的 ByteBuf.copy()
: 返回這個 ByteBufHolder
的一個深拷貝, 包括一個其所包含的 ByteBuf 的非共享拷貝.duplicate()
: 返回這個 ByteBufHolder
的一個淺拷貝, 包括一個其所包含的 ByteBuf 的共享拷貝.系統默認自帶了一系列的 ByteBufHolder
, 以 MemoryFileUpload
爲例, 該類經過封裝將 filename
, contentType
, contentTransferEncoding
屬性與對應的file進行關聯.