當咱們進行數據傳輸的時候,每每須要使用到緩衝區,經常使用的緩衝區就是JDK NIO類庫提供的java.nio.Buffer。java
實際上,7種基礎類型(Boolean除外)都有本身的緩衝區實現,對於NIO編程而言,咱們主要使用的是ByteBuffer。從功能角度而言,ByteBuffer徹底能夠知足NIO編程的須要,可是因爲NIO編程的複雜性,ByteBuffer也有其侷限性,它的主要缺點以下。編程
(1)ByteBuffer長度固定,一旦分配完成,它的容量不能動態擴展和收縮,當須要編碼的POJO對象大於ByteBuffer的容量時,會發生索引越界異常;後端
(2)ByteBuffer只有一個標識位置的指針position,讀寫的時候須要手工調用flip()和rewind()等,使用者必須當心謹慎地處理這些API,不然很容易致使程序處理失敗;api
(3)ByteBuffer的API功能有限,一些高級和實用的特性它不支持,須要使用者本身編程實現。數組
爲了彌補這些不足,Netty提供了本身的ByteBuffer實現——ByteBuf。緩存
網絡數據的基本單位老是字節。Java NIO 提供了ByteBuffer 做爲它的字節容器,可是這個類使用起來過於複雜,並且也有些繁瑣。cookie
Netty 的ByteBuffer 替代品是ByteBuf,一個強大的實現,既解決了JDK API 的侷限性,又爲網絡應用程序的開發者提供了更好的API。在本章中咱們將會說明和JDK 的ByteBuffer 相比,ByteBuf 的卓越功能性和靈活性。這
也將有助於更好地理解Netty 數據處理的通常方式。網絡
Netty 的數據處理API 經過兩個組件暴露——abstract class ByteBuf 和interface ByteBufHolder。
下面是一些ByteBuf API 的優勢:數據結構
其餘類可用於管理ByteBuf 實例的分配,以及執行各類針對於數據容器自己和它所持有的數據的操做。咱們將在仔細研究ByteBuf 和ByteBufHolder 時探討這些特性。dom
一般狀況下,當咱們對ByteBuffer進行put操做的時候,若是緩衝區剩餘可寫空間不夠,就會發生BufferOverflowException異常。爲了不發生這個問題,一般在進行put操做的時候會對剩餘可用空間進行校驗,若是剩餘空間不足,須要從新建立一個新的ByteBuffer,並將以前的ByteBuffer複製到新建立的ByteBuffer中,最後釋放老的ByteBuffer,代碼示例以下。
public ByteBuffer put(ByteBuffer src) { if (src instanceof HeapByteBuffer) { if (src == this) throw new IllegalArgumentException(); HeapByteBuffer sb = (HeapByteBuffer)src; int n = sb.remaining(); if (n > remaining()) throw new BufferOverflowException(); System.arraycopy(sb.hb, sb.ix(sb.position()), hb, ix(position()), n); sb.position(sb.position() + n); position(position() + n); } else if (src.isDirect()) { int n = src.remaining(); if (n > remaining()) throw new BufferOverflowException(); src.get(hb, ix(position()), n); position(position() + n); } else { super.put(src); } return this; }
由於全部的網絡通訊都涉及字節序列的移動,因此高效易用的數據結構明顯是必不可少的。Netty 的ByteBuf 實現知足並超越了這些需求。讓咱們首先來看看它是如何經過使用不一樣的索引來簡化對它所包含的數據的訪問的吧。
ByteBuf 維護了兩個不一樣的索引:一個用於讀取,一個用於寫入。當你從ByteBuf 讀取時,它的readerIndex 將會被遞增已經被讀取的字節數。一樣地,當你寫入ByteBuf 時,它的writerIndex 也會被遞增。圖5-1 展現了一個空ByteBuf 的佈局結構和狀態(一個讀索引和寫索引都設置爲0 的16 字節ByteBuf)。
能夠看到,正常狀況下,一個ByteBuf被兩個索引分紅三部分。
readerIndex 達到和writerIndex 位於同一位置,表示咱們到達"能夠讀取的"數據的末尾。就如同試圖讀取超出數組末尾的數據同樣,試圖讀取超出該點的數據將會觸發一個IndexOutOfBoundsException。
名稱以read 或者write 開頭的ByteBuf 方法,將會推動其對應的索引,而名稱以set 或者get 開頭的操做則不會。後面的這些方法將在做爲一個參數傳入的一個相對索引上執行操做。
和ByteBuffer 同樣,ByteBuf也是一個緩存區類,它有三種緩存區類型:
最經常使用的ByteBuf 模式是將數據存儲在JVM 的堆空間中,能夠被jvm自動回收。這種模式被稱爲支撐數組(backing array),它能在沒有使用池化的狀況下提供快速的分配和釋放。這種方式,如代碼清單5-1 所示,很是適合於有遺留的數據須要處理的狀況。
ByteBuf heapBuf = ...; //檢查ByteBuf 是否有一個支撐數組 if (heapBuf.hasArray()) { //若是有,則獲取對該數組的引用 byte[] array = heapBuf.array(); //計算第一個字節的偏移量。 int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); //得到可讀字節數 int length = heapBuf.readableBytes(); //使用數組、偏移量和長度做爲參數調用你的方法 handleArray(array, offset, length); }
當hasArray()方法返回false 時,嘗試訪問支撐數組將觸發一個UnsupportedOperationException。這個模式相似於JDK 的ByteBuffer 的用法。
堆緩存區的缺點在於若是進行Socket的I/O讀寫,須要額外進行一次內存複製,將堆內存對應的緩衝區複製到內核的channel中,性能會有必定的降低。
咱們先來了解一下什麼是直接緩存區:
咱們知道java的ByteBuffer類型就有直接和非直接緩存區這兩種類型。
他們的區別以下:
咱們直接看代碼:
ByteBuf directBuf = ...; //檢查ByteBuf 是否由數組支撐。若是不是,則這是一個直接緩衝區 if (!directBuf.hasArray()) { //獲取可讀字節數 int length = directBuf.readableBytes(); //分配一個新的數組來保存具備該長度的字節數據 byte[] array = new byte[length]; //將字節複製到該數組 directBuf.getBytes(directBuf.readerIndex(), array); //使用數組、偏移量和長度做爲參數調用你的方法 handleArray(array, 0, length); }
第三種也是最後一種模式使用的是複合緩衝區,它爲多個ByteBuf 提供一個聚合視圖。在這裏你能夠根據須要添加或者刪除ByteBuf 實例,這是一個JDK 的ByteBuffer 沒有的特性。
Netty 經過一個ByteBuf 子類——CompositeByteBuf——實現了這個模式,它提供了一個將多個緩衝區表示爲單個合併緩衝區的虛擬表示。
爲了舉例說明,讓咱們考慮一下一個由兩部分——頭部和主體——組成的將經過HTTP 協議傳輸的消息。這兩部分由應用程序的不一樣模塊產生,將會在消息被髮送的時候組裝。該應用程序能夠選擇爲多個消息重用相同的消息主體。當這種狀況發生時,對於每一個消息都將會建立一個新的頭部。
由於咱們不想爲每一個消息都從新分配這兩個緩衝區,因此使用CompositeByteBuf 是一個完美的選擇。它在消除了不必的複製的同時,暴露了通用的ByteBuf API。圖5-2 展現了生成的消息佈局。
代碼清單5-3 展現瞭如何經過使用JDK 的ByteBuffer 來實現這一需求。建立了一個包含兩個ByteBuffer 的數組用來保存這些消息組件,同時建立了第三個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); message 2.flip();
分配和複製操做,以及伴隨着對數組管理的須要,使得這個版本的實現效率低下並且笨拙。
代碼清單5-4 展現了一個使用了CompositeByteBuf 的版本。
CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); //將ByteBuf 實例追加到CompositeByteBuf ByteBuf headerBuf = ...; // can be backing or direct ByteBuf bodyBuf = ...; // can be backing or direct messageBuf.addComponents(headerBuf, bodyBuf); ..... //刪除位於索引位置爲 0(第一個組件)的ByteBuf messageBuf.removeComponent(0); // remove the header //循環遍歷全部的ByteBuf 實例 for (ByteBuf buf : messageBuf) { System.out.println(buf.toString()); }
CompositeByteBuf 可能不支持訪問其支撐數組,所以訪問CompositeByteBuf 中的數據相似於(訪問)直接緩衝區的模式,如代碼清單5-5 所示。
CompositeByteBuf compBuf = Unpooled.compositeBuffer(); //得到可讀字節數 int length = compBuf.readableBytes(); //分配一個具備可讀字節數長度的新數組 byte[] array = new byte[length]; //將字節讀到該數組中 compBuf.getBytes(compBuf.readerIndex(), array); //使用偏移量和長度做爲參數使用該數組 handleArray(array, 0, array.length);
經驗代表:ByteBuf的最佳實踐是在I/O通訊線程的讀寫緩衝區使用DirectByteBuf,後端業務消息的編解碼模塊使用HeapByteBuf
ByteBuf 提供了許多超出基本讀、寫操做的方法用於修改它的數據。在接下來的章節中,咱們將會討論這些中最重要的部分。
如同在普通的Java 字節數組中同樣,ByteBuf 的索引是從零開始的:第一個字節的索引是0,最後一個字節的索引老是capacity() - 1。代碼清單5-6 代表,對存儲機制的封裝使得遍歷ByteBuf 的內容很是簡單。
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)來手動移動這二者。
在ByteBuf中有多種能夠用來肯定指定值的索引的方法。最簡單的是使用indexOf()方法。較複雜的查找能夠經過那些須要一個ByteBufProcessor做爲參數的方法達成。這個接口只定義了一個方法:
boolean process(byte value)
它將檢查輸入值是不是正在查找的值。
ByteBufProcessor針對一些常見的值定義了許多便利的枚舉。假設你的應用程序須要和所謂的包含有以NULL結尾的內容的Flash套接字,能夠調用:
forEach Byte(ByteBufProcessor.FIND_NUL)
如代碼清單展現了一個查找回車符(r)的索引的例子。:
ByteBuf buffer = ...; int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
正如咱們所提到過的,有兩種類別的讀/寫操做:
表5-1 列舉了最經常使用的get()方法。完整列表請參考對應的API 文檔。
這裏面getBytes方法咱們須要強調一下,好比buf.getBytes(buf.readerIndex(), array);表示將從buf實例的readerIndex爲起點的數據傳入指定的目的地(一個數組中)。
如今,讓咱們研究一下read()操做,其做用於當前的readerIndex 或writerIndex。這些方法將用於從ByteBuf 中讀取數據,如同它是一個流。表5-3 展現了最經常使用的方法。
幾乎每一個read()方法都有對應的write()方法,用於將數據追加到ByteBuf 中。注意,表5-4 中所列出的這些方法的參數是須要寫入的值,而不是索引值
正如咱們以前看過的這張圖:
在上圖中標記爲可丟棄字節的分段包含了已經被讀過的字節。經過調用discardReadBytes()方法,能夠丟棄它們並回收空間。這個分段的初始大小爲0,存儲在readerIndex 中,會隨着read 操做的執行而增長(get*操做不會移動readerIndex)。
上圖展現了下圖中所展現的緩衝區上調用discardReadBytes()方法後的結果。能夠看到,可丟棄字節分段中的空間已經變爲可寫的了。注意,在調用discardReadBytes()以後,對可寫分段的內容並無任何的保證。
雖然你可能會傾向於頻繁地調用discardReadBytes()方法以確保可寫分段的最大化,可是請注意,這將極有可能會致使內存複製,由於可讀字節(圖中標記爲CONTENT 的部分)必須被移動到緩衝區的開始位置。咱們建議只在有真正須要的時候才這樣作,例如,當內存很是寶貴的時候。
ByteBuf 的可讀字節分段存儲了實際數據。新分配的、包裝的或者複製的緩衝區的默認的readerIndex 值爲0。任何名稱以read 或者skip 開頭的操做都將檢索或者跳過位於當前readerIndex 的數據,而且將它增長已讀字節數。
如下代碼清單展現瞭如何讀取全部能夠讀的字節。
ByteBuf buffer = ...; while (buffer.isReadable()) { System.out.println(buffer.readByte()); }
可寫字節分段是指一個擁有未定義內容的、寫入就緒的內存區域。新分配的緩衝區的writerIndex 的默認值爲0。任何名稱以write 開頭的操做都將從當前的writerIndex 處開始寫數據,並將它增長已經寫入的字節數。若是嘗試往目標寫入超過目標容量的數據,將會引起一個IndexOutOfBoundException。
如下代碼清單是一個用隨機整數值填充緩衝區,直到它空間不足爲止的例子。writeableBytes()方法在這裏被用來肯定該緩衝區中是否還有足夠的空間。
// Fills the writable bytes of a buffer with random integers. ByteBuf buffer = ...; //由於一個int爲四個字節 while (buffer.writableBytes() >= 4) { buffer.writeInt(random.nextInt()); }
JDK 的InputStream 定義了mark(int readlimit)和reset()方法,這些方法分別被用來將流中的當前位置標記爲指定的值,以及將流重置到該位置。
一樣,能夠經過調用markReaderIndex()、markWriterIndex()、resetWriterIndex()和resetReaderIndex()來標記和重置ByteBuf 的readerIndex 和writerIndex。這些和InputStream 上的調用相似,只是沒有readlimit 參數來指定標記何時失效。
也能夠經過調用readerIndex(int)或者writerIndex(int)來將索引移動到指定位置。試圖將任何一個索引設置到一個無效的位置都將致使一個IndexOutOfBoundsException。能夠經過調用clear()方法來將readerIndex 和writerIndex 都設置爲0。注意,這並不會清除內存中的內容。
調用clear()比調用discardReadBytes()輕量得多,由於它將只是重置索引而不會複製任何的內存。
派生緩衝區爲ByteBuf 提供了以專門的方式來呈現該ByteBuf內容的視圖。這類視圖能夠經過如下方法被建立的:
每一個這些方法都將返回一個新的ByteBuf 實例,它具備本身的讀索引、寫索引和標記索引。其內部存儲和JDK 的ByteBuffer同樣也是共享的。這使得派生緩衝區的建立成本是很低廉的,可是這也意味着,若是你修改了它的內容,也同時修改了其對應的源實例,因此要當心。
Charset utf8 = Charset.forName("UTF-8"); //建立一個ByteBuf "Netty in Action" ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //建立該ByteBuf 從索引0 開始到索引15結束的一個新切片 ByteBuf sliced = buf.slice(0, 15); System.out.println(sliced.toString(utf8)); //更新索引0 處的字節 buf.setByte(0, (byte)'J'); //將會成功,由於數據是共享的,對其中一個所作的更改對另一個也是可見的 assert buf.getByte(0) == sliced.getByte(0);
若是須要一個現有緩衝區的真實副本,請使用copy()或者copy(int, int)方法。不一樣於派生緩衝區,由這個調用所返回的ByteBuf 擁有獨立的數據副本。
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);
若是咱們不修改原始ByteBuf 的切片或者副本,這兩種場景是相同的。只要有可能,咱們儘可能使用slice()方法來避免複製內存的開銷。
咱們常常發現,除了實際的數據負載以外,咱們還須要存儲各類屬性值。HTTP 響應即是一個很好的例子,除了表示爲字節的內容,還包括狀態碼、cookie 等。
爲了處理這種常見的用例,Netty 提供了ByteBufHolder,咱們能夠看看他的默認實現:
能夠看出,它主要就是封裝了一個ByteBuf對象,以及對這個對象的一些操做api。如今假如咱們要構造一個HTTP響應的對象,那麼就能夠在繼承ByteBufHolder的基礎上在拓展其餘的好比狀態碼、cookie等字段,達到本身的目的。
它經常使用的api以下:
在這一節中,咱們將描述管理ByteBuf 實例的不一樣方式。
爲了下降分配和釋放內存的開銷,Netty 經過interface ByteBufAllocator 實現了(ByteBuf 的)池化,它能夠用來分配咱們所描述過的任意類型的ByteBuf 實例。
關於ioBuffer,默認地,當所運行的環境具備sun.misc.Unsafe 支持時,返回基於直接內存存儲的ByteBuf,不然返回基於堆內存存儲的ByteBuf;當指定使用PreferHeapByteBufAllocator 時,則只會返回基於堆內存存儲的ByteBuf。
咱們能夠經過Channel(每一個均可以有一個不一樣的ByteBufAllocator 實例)或者綁定到ChannelHandler 的ChannelHandlerContext 獲取一個到ByteBufAllocator 的引用。代碼清單5-14 說明了這兩種方法。
獲取一個到ByteBufAllocator 的引用
//從Channel 獲取一個到ByteBufAllocator 的引用 Channel channel = ...; ByteBufAllocator allocator = channel.alloc(); .... //從ChannelHandlerContext 獲取一個到ByteBufAllocator 的引用 ChannelHandlerContext ctx = ...; ByteBufAllocator allocator2 = ctx.alloc(); ...
Netty提供了兩種ByteBufAllocator的實現:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的實例以提升性能並最大限度地減小內存碎片。此實現使用了一種稱爲jemalloc的已被大量現代操做系統所採用的高效方法來分配內存。後者的實現不池化ByteBuf實例,而且在每次它被調用時都會返回一個新的實例。
Netty默認使用了PooledByteBufAllocator
可能某些狀況下,你未能獲取一個到ByteBufAllocator 的引用。對於這種狀況,Netty 提供了一個簡單的稱爲Unpooled 的工具類,它提供了靜態的輔助方法來建立未池化的ByteBuf實例。表5-8 列舉了這些中最重要的方法。
Unpooled 類還使得ByteBuf 一樣可用於那些並不須要Netty 的其餘組件的非網絡項目,使得其能得益於高性能的可擴展的緩衝區API。
ByteBufUtil 提供了用於操做ByteBuf 的靜態的輔助方法。由於這個API 是通用的,而且和池化無關,因此這些方法已然在分配類的外部實現。
這些靜態方法中最有價值的可能就是hexdump()方法,它以十六進制的表示形式打印ByteBuf 的內容。這在各類狀況下都頗有用,例如,出於調試的目的記錄ByteBuf 的內容。十六進制的表示一般會提供一個比字節值的直接表示形式更加有用的日誌條目,此外,十六進制的版本還能夠很容易地轉換回實際的字節表示。
另外一個有用的方法是boolean equals(ByteBuf, ByteBuf),它被用來判斷兩個ByteBuf實例的相等性。若是你實現本身的ByteBuf 子類,你可能會發現ByteBufUtil 的其餘有用方法。
引用計數是一種經過在某個對象所持有的資源再也不被其餘對象引用時釋放該對象所持有的資源來優化內存使用和性能的技術。Netty 在第4 版中爲ByteBuf 和ByteBufHolder 引入了引用計數技術,它們都實現了interface ReferenceCounted。
引用計數背後的想法並非特別的複雜;它主要涉及跟蹤到某個特定對象的活動引用的數量。一個ReferenceCounted 實現的實例將一般以活動的引用計數爲1 做爲開始。只要引用計數大於0,就能保證對象不會被釋放。當活動引用的數量減小到0 時,該實例就會被釋放。注意,雖然釋放的確切語義多是特定於實現的,可是至少已經釋放的對象應該不可再用了。
引用計數對於池化實現(如PooledByteBufAllocator)來講是相當重要的,它下降了內存分配的開銷。代碼清單5-15 展現了相關的示例。
//從Channel 獲取ByteBufAllocator Channel channel = ...; ByteBufAllocator allocator = channel.alloc(); .... //從ByteBufAllocator分配一個ByteBuf ByteBuf buffer = allocator.directBuffer(); //檢查引用計數是否爲預期的1 assert buffer.refCnt() == 1; ... //減小到該對象的活動引用。當減小到0 時,該對象被釋放,而且該方法返回true ByteBuf buffer = ...; boolean released = buffer.release();
試圖訪問一個已經被釋放的引用計數的對象,將會致使一個IllegalReferenceCountException。
注意,一個特定的(ReferenceCounted 的實現)類,能夠用它本身的獨特方式來定義它的引用計數規則。例如,咱們能夠設想一個類,其release()方法的實現老是將引用計數設爲零,而不用關心它的當前值,從而一次性地使全部的活動引用都失效。
誰負責釋放release呢 通常來講,是由最後訪問(引用計數)對象的那一方來負責將它釋放。在第6 章中,咱們將會解釋這個概念和ChannelHandler 以及ChannelPipeline 的相關性。