Netty實戰五之ByteBuf

網絡數據的基本單位老是字節,Java NIO 提供了ByteBuffer做爲它的字節容器,可是其過於複雜且繁瑣。git

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

一、ByteBuf的API數組

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

如下是其優勢:cookie

-能夠被用戶自定義的緩衝區類型擴展網絡

-經過內置的複合緩衝區類型實現了透明的零拷貝數據結構

-容量能夠按需增加(相似JDK的StringBuilder)dom

-在讀和寫這兩個模式之間切換不須要調用ByteBuffer的flip()方法工具

-讀和寫使用了不一樣的索引佈局

-支持方法的鏈式調用

-支持引用計數

-支持池化

其餘類可用於管理ByteBuf實例的分配,以及執行各類針對於數據容器自己和它所持有的數據的操做。

二、ByteBuf如何工做

由於全部的網絡通訊都涉及字節序列的移動,因此高效易用的數據結構明顯是必不可少的。

ByteBuf維護了兩個不一樣的索引:一個用於讀取、一個用於寫入。當你從ByteBuf讀取時,它的readerIndex將會被遞增已經被讀取的字節數。一樣地,當你寫入BytBuf時,它的writerIndex也會被遞增。下圖展現了一個空ByteBuf的佈局結構和狀態。 輸入圖片說明

若是咱們打算讀取字節直到readerIndex達到和writeIndex一樣的值時會發生什麼,則將會到達「能夠讀取的」數據的末尾。就如同視圖讀取超出數組末尾的數據同樣,試圖讀取超出該點的數據將會觸發一個indexOutOfBoundsException。

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

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

三、ByteBuf的使用模式-堆緩衝區

一個由不一樣的索引分別控制讀訪問和寫訪問的字節數組。

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

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模式。咱們指望用於對象建立的內存分配永遠都來自於堆中,但這並非必須的——NIO在JDK1.4中引入的ByteBuffer類容許JVM實現經過本地調用來分配內存。這主要是爲了不在每次調用本地I/O操做以前(或者以後)將緩存區的內容複製到一箇中間緩衝區(或者從中間緩衝區把內容複製到緩衝區)。

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

直接緩衝區的主要缺點是,相對於基於堆的緩衝區,它們的分配和釋放都教委昂貴。若是你正在處理遺留代碼,你也可能會遇到另外一個缺點:由於數據不是在堆上,因此你不得不進行一次複製。以下代碼所示。顯然,這比使用支撐數組相比工做量更多。

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

五、ByteBuf的使用模式-複合緩衝區

它爲多個ByteBuf提供一個聚合視圖。在這裏你能夠根據須要添加或者刪除ByteBuf實例,這是一個JDK的ByteBuffer實現徹底缺失的特性。

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

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

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

由於咱們不想爲每一個消息都從新分配這兩個緩衝區,因此使用CompositeByteBuf是一個完美的選擇。它在消除了不必的複製的同時,暴露了通用的ByteBuf API。

輸入圖片說明

如下代碼展現瞭如何經過使用JDK的ByteBuffer來實現這一需求。建立一個包含兩個ByteBuffer的數組用來保存這些消息組件,同時建立了第三個ByteBuffer用來保存全部這些數據的副本。

//Use an array to hold 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 messageBuf = Unpooled.compositeBuffer();
        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 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的緩衝區實現所致使的性能以及內存使用率的懲罰。這種優化發生在Netty的核心代碼中,所以不會被暴露出來,可是你應該知道它帶來的影響。

六、字節級操做——隨機訪問索引

如同在普通的Java字節數組中同樣,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也不會改變weriterIndex。若是有須要,也能夠經過調用readerIndex(index)或者writerIndex(index)來手動移動這二者。

七、字節級操做——順序訪問索引

雖然ByteBuf同時具備讀索引和寫索引,可是JDK的ByteBuffer卻只有一個索引,這也就是爲何必須調用flip()方法來在讀模式和寫模式之間進行切換的緣由。下圖展現了ByteBuf是如何被它的兩個索引劃分紅3個區域的。 輸入圖片說明

八、字節級操做——可丟棄字節

在上圖中標記爲可丟棄字節的分段包含了已經被讀過的字節。經過調用discardReadBytes()方法,能夠丟棄它們並回收空間。這個分段的初始大小爲0,存儲在readerIndex中,會隨着read操做的執行而增長(get*操做不會移動readerIndex)。

下圖展現了上圖所展現的緩衝區上調用discardReadBytes()方法後的結果。能夠看到,可丟棄字節分段中的空間已經變爲可寫的了。注意,在調用discardReadBytes()以後,對可寫分段的內容並無任何的保證。(由於只是移動了能夠讀取的字節以及writerIndex,而沒有對全部可寫入的字節進行擦除寫。) 輸入圖片說明

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

九、字節級操做——可讀字節

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

若是被調用的方法須要一個ByteBuf參數做爲寫入的目標,而且沒有指定目標索引參數,那麼該目標緩衝區的writerIndex也將被增長,例如: readBytes(ByteBuf dest);

若是嘗試在緩衝區的可讀字節數已經耗盡時從中讀取數據,那麼將會引起一個IndexOutOfBoundsException。

下圖展現瞭如何讀取全部能夠讀的字節。

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

十、字節級操做——可寫字節

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

writeBytes(ByteBuf dest);

若是嘗試往目標寫入超過目標容量的數據,將會引起一個IndexOutOfBoundException。如下代碼是一個用隨機整數值填充緩存區,直到它空間不足爲止的例子。writeableBytes()方法在這裏被用來肯定該緩衝區中是否還有足夠的空間。

//Fills the writable bytes of a buffer with random integers ByteBuf buffer = ...; 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.注意,這並不會清除內存中的內容。 輸入圖片說明

和以前同樣,ByteBuf包含3個分段,下圖展現了在clear()方法被調用以後ByteBuf的狀態。 輸入圖片說明

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

十二、查找操做

在ByteBuf中有多種能夠用來肯定指定值的索引的方法,最簡單的是使用indexOf()方法。較複雜的查找能夠經過那些須要一個ByteBufProcessor做爲參數的方法達成。這個接口只定義了一個方法:

boolean process(byte value)

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

ByteBufProcessor針對一些常見的值定義了許多遍歷的方法。假設你的應用程序須要和所謂的包含有以NULL結尾的內容的FLash套接字集成。調用、 forEachByte(ByteBufProcessor.FIND_NUL) 將簡單高效地消費該Flash數據,由於在處理期間只會執行較少的邊界檢查。 如下代碼展現了一個查找回車符(\r)的例子

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

1三、派生緩衝區

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

·duplicate()

·slice()

·slice(int,int)

·Unpooled.unmodifiableBuffer(...)

·order(ByteOrder)

·readSlice(int)

每一個這些方法都將返回一個新的ByteBuf實例,它具備本身的讀索引、寫索引和標記索引。其內部存儲和JDK的ByteBuffer同樣也是共享的。這使得派生緩衝區的建立成本是很低廉的,可是這也意味着,若是你修改了它的內容,也同時修改了其對應的源實例,因此要當心。

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

如下代碼展現瞭如何使用slice(int,int)方法來操做ByteBuf的一個分段

Charset utf8 = Charset.forName("UTF-8"); //建立一個用於保存給定字符串的字節的ByteBuf 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); 

如下讓咱們看看,ByteBuf的分段的副本和切片有何區別

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

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

1四、讀/寫操做

get()和set()操做,從給定的索引開始,而且保持索引不變

read()和write()操做,從給定的索引開始,而且會根據已經訪問過的字節數對索引進行調整。 如下代碼說明了其用法,代表了他們不會改變讀索引和寫索引。

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

還有read()操做,其做用於當前的readerIndex或writeIndex。這些方法將用於從ByteBuf中讀取數據。如同它是一個流。

幾乎每一個read()方法都有對應的write()方法,用於將數據追加到ByteBuf中,如下代碼展現了read()和write()操做

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

1五、ByteBufHolder接口

咱們常常發現,除了實際的數據負載以外,咱們還須要存儲各類屬性值。HTTP響應即是一個很好的例子,除了表示爲字節的內容,還包括狀態碼、cookie等。

爲了處理這種常見的用例,Netty提供了ByteBufHolder。ByteBufHolder也爲Netty的高級特性提供了支持,如緩衝區池化,其中能夠從池中借用ByteBuf,而且在須要時自動釋放。

ByteBufHolder只有幾種用於訪問底層數據和引用計數的方法。

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

1六、按需分配:ByteBufAllocator接口

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

能夠經過Channel(每一個均可以有一個不一樣的ByteBufAllocator實例)或者綁定到ChannelHandler的ChannelHanlderContext獲取一個到ByteBufAllocator的引用。

io.netty.channel.Channel channel = ...;
        ByteBufAllocator allocator = channel.alloc();
        .....
        ChannelHandlerContext ctx = ...;
        ByteBufAllocator allocator1 = ctx.alloc();
        ....

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

雖然Netty默認使用了PooledByteBufAllocator,但這能夠很容易地經過ChannelConfig API或者在引導你的應用程序時指定一個不一樣的分配器來更改。

1七、Unpooled緩衝區

可能某些狀況下,你未能獲取一個到ByteBufAllocator調用,對於這種狀況,Netty提供了一個簡單的成爲Unpooled的工具類,它提供了靜態的輔助方法來建立未池化的ByteBuf實例。

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

1八、ByteBufUtil類

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

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

另外一個有用的方法是boolean equals(ByteBuf,ByteBuf),它被用來判斷兩個ByteBuf實例的相等性,若是你實現了本身的ByteBuf子類,你可能會發現ByteBufUtil的其它有用方法。

1九、引用計數

引用計數是一種經過在某個對象所持有的資源再也不被其它對象引用時釋放該對象所持有的資源來優化內存使用和性能的技術。

引用技術背後的想法並非特別的複雜,他主要設計跟蹤到某個特定對象的活動引用的數量。一個ReferenceCounted實現的實例將一般以活動的引用計數爲1做爲開始。只要引用計數大於0,就能保證對象不會被釋放。當活動引用的數量減小到0時,該實例就會被釋放。注意,雖然釋放的確切語義多是特定於實現的,可是至少已經釋放的對象應該不可再用了。

引用技術對於池化實現(PooledByteBufAllocator)來講是相當重要的,它下降了內存分配的開銷。

io.netty.channel.Channel channel = ...; //從Channel獲取ByteBufAllocator ByteBufAllocator allocator = channel.alloc(); ... //從ByteBufAllocator分配一個ByteBuf ByteBuf buffer = allocator.directBuffer(); //檢查引用技術是否爲預期的1 assert buffer.refCnt() == 1; 
ByteBuf buffer = ...; //減小到該對象的活動引用。當減小到0時,該對象被釋放,而且該方法返回true boolean released = buffer.release(); 

試圖訪問一個已經被釋放的引用計數對象,將會致使一個IllegalReferenceCountException。 注意,一個特定的(ReferenceCounted的實現)類,能夠用它本身的獨特方式來定義它的引用計數規則。例如,咱們能夠設想一個類,其release()方法的實現老是將引用計數設爲零,而不用關心它的當前值,從而一次性地使全部的活動都失效。

誰負責釋放 : 通常來講,是由最後訪問(引用計數)對象的那一方來負責將它釋放。

相關文章
相關標籤/搜索