本文內容主要參考<<Netty In Action>>,偏筆記向.java
網絡編程中,字節緩衝區是一個比較基本的組件.Java NIO提供了ByteBuffer
,可是使用過的都知道ByteBuffer
對於讀寫數據操做仍是有些麻煩的,切換讀寫狀態須要flip()
.Netty框架對字節緩衝區進行了封裝,名稱是ByteBuf
,相較於ByteBuffer
更靈活.編程
StringBuilder
)flip()
方法ByteBuf
維護了兩個指針,一個用於讀取(readerIndex
),一個用於寫入(writerIndex
).api
使用ByteBuf的API中的read*
方法讀取數據時,readerIndex
會根據讀取字節數向後移動,可是get*
方法不會移動readerIndex
;使用write*
數據時,writerIndex
會根據字節數移動,可是set*
方法不會移動writerIndex
.(read*
表示read
開頭的方法,其他意義相同)數組
讀取數據時,若是readerIndex
超過了writerIndex
會觸發IndexOutOfBoundsException
.緩存
能夠指定ByteBuf
容量最大值,capacity(int)
或ensureWritable(int)
,當超出容量時會拋出異常.cookie
將ByteBuf
存入JVM的堆空間.可以在沒有池化的狀況下提供快速的分配和釋放.網絡
除此以外,ByteBuf的堆緩衝區還提供了一個後備數組(backing array).後備數組和ByteBuf中的數據是對應的,若是修改了backing array
中的數據,ByteBuf
中的數據是同步的.框架
public static void main(String[] args) { ByteBuf heapBuf = Unpooled.buffer(1024); if(heapBuf.hasArray()){ heapBuf.writeBytes("Hello,heapBuf".getBytes()); System.out.println("數組第一個字節在緩衝區中的偏移量:"+heapBuf.arrayOffset()); System.out.println("緩衝區中的readerIndex:"+heapBuf.readerIndex()); System.out.println("writerIndex:"+heapBuf.writerIndex()); System.out.println("緩衝區中的可讀字節數:"+heapBuf.readableBytes());//等於writerIndex-readerIndex byte[] array = heapBuf.array(); for(int i = 0;i < heapBuf.readableBytes();i++){ System.out.print((char) array[i]); if(i==5){ array[i] = (int)'.'; } } //不會修改readerIndex位置 System.out.println("\n讀取數據後的readerIndex:"+heapBuf.readerIndex()); //讀取緩衝區的數據,查看是否將逗號改爲了句號 while (heapBuf.isReadable()){ System.out.print((char) heapBuf.readByte()); } }
輸出:dom
數組第一個字節在緩衝區中的偏移量:0 緩衝區中的readerIndex:0 writerIndex:13 緩衝區中的可讀字節數:13 Hello,heapBuf 讀取數據後的readerIndex:0 Hello.heapBuf
若是
hasArray()
返回false
,嘗試訪問backing array會報錯socket
直接緩衝區存儲於JVM堆外的內存空間.這樣作有一個好處,當你想把JVM中的數據寫給socket,須要將數據複製到直接緩衝區(JVM堆外內存)再交給socket.若是使用直接緩衝區,將減小複製這一過程.
可是直接緩衝區也是有不足的,與JVM堆的緩衝區相比,他們的分配和釋放是比較昂貴的.並且還有一個缺點,面對遺留代碼的時候,可能不肯定ByteBuf使用的是直接緩衝區仍是堆緩衝區,你可能須要進行一次額外的複製.如代碼示例.
與自帶後備數組的堆緩衝區來說,這要多作一些工做.因此,若是肯定容器中的數據會被做爲數組來訪問,你可能更願意使用堆內存.
//實際上你不知道從哪得到的引用,這多是一個直接緩衝區的ByteBuf //忽略Unpooled.buffer方法,當作不知道從哪得到的directBuf ByteBuf directBuf = Unpooled.buffer(1024); //若是想要從數組中訪問數據,須要將直接緩衝區中的數據手動複製到數組中 if (!directBuf.hasArray()) { int length = directBuf.readableBytes(); byte[] array = new byte[length]; directBuf.getBytes(directBuf.readerIndex(), array); handleArray(array, 0, length); }
聚合緩衝區是個很是好用的東西,是多個ByteBuf的聚合視圖,能夠添加或刪除ByteBuf實例.
CompositeByteBuf中的ByteBuf實例可能同事包含直接內存分配和非直接內存分配.若是其中只有一個實例,那麼調用CompositeByteBuf中的
hasArray()
方法將返回該組件上的hasArray()
方法的值,不然返回false
多個ByteBuf組成一個完整的消息是很常見的,好比header
和body
組成的HTTP協議傳輸的消息.消息中的body
有時候可能能重用,咱們不想每次都建立重複的body
,咱們能夠經過CompositeByteBuf來複用body
.
對比一下JDK中的ByteBuffer
實現複合緩衝區和Netty中的CompositeByteBuf
.
//JDK版本實現複合緩衝區 public static void byteBufferComposite(ByteBuffer header, ByteBuffer body) { //使用一個數組來保存消息的各個部分 ByteBuffer[] message = new ByteBuffer[]{ header, body }; // 建立一個新的ByteBuffer來複制合併header和body ByteBuffer message2 = ByteBuffer.allocate(header.remaining() + body.remaining()); message2.put(header); message2.put(body); message2.flip(); } //Netty中的CompositeByteBuf public static void byteBufComposite() { CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); ByteBuf headerBuf = Unpooled.buffer(1024); // 多是直接緩存也多是堆緩存中的 ByteBuf bodyBuf = Unpooled.buffer(1024); // 多是直接緩存也多是堆緩存中的 messageBuf.addComponents(headerBuf, bodyBuf); //... messageBuf.removeComponent(0); // remove the header for (ByteBuf buf : messageBuf) { System.out.println(buf.toString()); } }
CompositeByteBuf
不支持訪問其後備數組,因此訪問CompositeByteBuf
中的數據相似於訪問直接緩衝區
CompositeByteBuf compBuf = Unpooled.compositeBuffer(); int length = compBuf.readableBytes(); byte[] array = new byte[length]; //將CompositeByteBuf中的數據複製到數組中 compBuf.getBytes(compBuf.readerIndex(), array); //處理一下數組中的數據 handleArray(array, 0, array.length);
Netty使用CompositeByteBuf
來優化socket的IO操做,避免了JDK緩衝區實現所致使的性能和內存使用率的缺陷.內存使用率的缺陷是指對可複用對象大量的複製,Netty對其在內部作了優化,雖然沒有暴露出來,可是應該知道CompositeByteBuf的優點和JDK自帶工具的弊端.
JDK的NIO包中提供了Scatter/Gather I/O技術,字面意思是打散和聚合,能夠理解爲把單個ByteBuffer切分紅多個或者把多個ByteBuffer合併成一個.
ByteBuf的索引從0開始,最後一個索引是capacity()-1
.
遍歷演示
ByteBuf buffer = Unpooled.buffer(1024); for (int i = 0; i < buffer.capacity(); i++) { byte b = buffer.getByte(i);//這種方法不會移動readerIndex指針 System.out.println((char) b); }
JDK中的ByteBuffer
只有一個索引,須要經過flip()
來切換讀寫操做,Netty中的ByteBuf
既有讀索引,也有寫索引,經過兩個索引把ByteBuf劃分了三部分.
能夠調用discardReadBytes()
方法可丟棄可丟棄字節並回收空間.
調用discardReadBytes()
方法以後
使用read*
或skip*
方法都會增長readerIndex
.
移動readerIndex
讀取可讀數據的方式
ByteBuf buffer = ...; while (buffer.isReadable()) { System.out.println(buffer.readByte()); }
write*
方法寫入ByteBuf
時會增長writerIndex,若是超過容量會拋出IndexOutOfBoundException
.
writeableBytes()
能夠返回可寫字節數.
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
參數來指定標記何時失效。
若是將索引設置到一個無效位置會拋出IndexOutOfBoundsException
.
能夠經過clear()
歸零索引,歸零索引不會清除數據.
ByteBuf中不少方法能夠肯定值的索引,如indexOf()
.
複雜查找能夠經過那些須要一個ByteBufProcessor
做爲參數的方法完成.這個接口應該可使用lambda
表達式(可是我如今使用的Netty4.1.12已經廢棄了該接口,應該使用ByteProcessor
).
ByteBuf buffer = ...; int index = buffer.forEachByte(ByteProcessor.FIND_CR);
派生緩衝區就是,基於原緩衝區一頓操做生成新緩衝區.好比複製,切分等等.
duplicate()
;slice()
; slice(int, int)
;Unpooled.unmodifiableBuffer(…)
;order(ByteOrder)
; readSlice(int)
.
每一個這些方法都將返回一個新的 ByteBuf 實例,它具備本身的讀索引、寫索引和標記
索引。 其內部存儲和 JDK 的 ByteBuffer 同樣也是共享的。這使得派生緩衝區的建立成本
是很低廉的,可是這也意味着,若是你修改了它的內容,也同時修改了其對應的源實例,所
以要當心
//複製 public static void byteBufCopy() { 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); } //切片 public static void byteBufSlice() { Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); ByteBuf sliced = buf.slice(0, 15); System.out.println(sliced.toString(utf8)); buf.setByte(0, (byte)'J'); assert buf.getByte(0) == sliced.getByte(0); }
還有一些讀寫操做的API,留在文末展現吧.
咱們常常發現, 除了實際的數據負載以外, 咱們還須要存儲各類屬性值。 HTTP 響應即是一個很好的例子, 除了表示爲字節的內容,還包括狀態碼、 cookie 等。
爲了處理這種常見的用例, Netty 提供了 ByteBufHolder。 ByteBufHolder 也爲 Netty 的高級特性提供了支持,如緩衝區池化,其中能夠從池中借用 ByteBuf, 而且在須要時自動釋放。ByteBufHolder 只有幾種用於訪問底層數據和引用計數的方法。
咱們能夠經過ByteBufAllocator
來分配一個ByteBuf
實例.ByteBufAllocator
接口實現了ByteBuf的池化.
能夠經過 Channel
(每一個均可以有一個不一樣的 ByteBufAllocator
實例)或者綁定到ChannelHandler
的 ChannelHandlerContext
獲取一個到ByteBufAllocator
的引用。
//從Channel獲取一個ByteBufAllocator的引用 Channel channel = ...; ByteBufAllocator allocator = channel.alloc(); .... //從ChannelHandlerContext獲取ByteBufAllocator 的引用 ChannelHandlerContext ctx = ...; ByteBufAllocator allocator2 = ctx.alloc();
Netty提供了兩種ByteBufAllocator的實現: PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的實例以提升性能並最大限度地減小內存碎片。 後者的實現不 池化ByteBuf實例, 而且在每次它被調用時都會返回一個新的實例。
默認使用的是PooledByteBufAllocator
,能夠經過ChannelConfig
修改.
Unpooled緩衝區
可能有時候拿不到ByteBufAllocator
引用的話,可使用Unpooled工具類來建立未持化ByteBuf
實例.
ByteBufUtil類
ByteBufUtil 提供了用於操做 ByteBuf 的靜態的輔助方法。由於這個 API 是通用的, 而且和池化無關,因此這些方法已然在分配類的外部實現。
這些靜態方法中最有價值的可能就是 hexdump()方法, 它以十六進制的表示形式打印ByteBuf 的內容。這在各類狀況下都頗有用,例如, 出於調試的目的記錄 ByteBuf 的內容。十六進制的表示一般會提供一個比字節值的直接表示形式更加有用的日誌條目,此外,十六進制的版本還能夠很容易地轉換回實際的字節表示。
另外一個有用的方法是 boolean equals(ByteBuf, ByteBuf), 它被用來判斷兩個 ByteBuf實例的相等性。若是你實現本身的 ByteBuf 子類,你可能會發現 ByteBufUtil 的其餘有用方法。
引用計數是一種經過在某個對象所持有的資源再也不被其餘對象引用時釋放該對象所持有的資源來優化內存使用和性能的技術。 它們都實現了 interface ReferenceCounted。 引用計數背後的想法並非特別的複雜;它主要涉及跟蹤到某個特定對象的活動引用的數量。一個 ReferenceCounted 實現的實例將一般以活動的引用計數爲 1 做爲開始。只要引用計數大於 0, 就能保證對象不會被釋放。當活動引用的數量減小到 0 時,該實例就會被釋放。注意,雖然釋放的確切語義多是特定於實現的,可是至少已經釋放的對象應該不可再用了。
//從Channel獲取ByteBufAllocator Channel channel = ...; ByteBufAllocator allocator = channel.alloc(); .... //從ByteBufAllocator分配一個ByteBuf ByteBuf buffer = allocator.directBuffer(); assert buffer.refCnt() == 1;//引用計數是否爲1