Netty之ByteBuf

本文內容主要參考<<Netty In Action>>,偏筆記向.java

網絡編程中,字節緩衝區是一個比較基本的組件.Java NIO提供了ByteBuffer,可是使用過的都知道ByteBuffer對於讀寫數據操做仍是有些麻煩的,切換讀寫狀態須要flip().Netty框架對字節緩衝區進行了封裝,名稱是ByteBuf,相較於ByteBuffer更靈活.編程

1.ByteBuf特色概覽

  • 用戶能夠自定義緩衝區類型對其擴展
  • 經過內置的符合緩衝區類型實現了透明的零拷貝
  • 容量能夠按需增加(相似StringBuilder)
  • 切換讀寫模式不用調用flip()方法
  • 讀寫使用各自的索引
  • 支持方法的鏈式調用
  • 支持引用計數
  • 支持池化

2.ByteBuf類介紹

2.1工做模式

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

2.2使用模式

2.2.1堆緩衝區

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

2.2.2直接緩衝區

直接緩衝區存儲於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);
        }
2.2.3符合緩衝區(CompositeByteBuf)

聚合緩衝區是個很是好用的東西,是多個ByteBuf的聚合視圖,能夠添加或刪除ByteBuf實例.

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

多個ByteBuf組成一個完整的消息是很常見的,好比headerbody組成的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合併成一個.

3.字節級操做

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);
}

3.1readerIndex和writerIndex

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());
}

3.2索引管理

JDK 的InputStream 定義了 mark(int readlimit)reset()方法,這些方法分別被用來將流中的當前位置標記爲指定的值,以及將流重置到該位置。
一樣,能夠經過調用 markReaderIndex()markWriterIndex()resetWriterIndex()resetReaderIndex()來標記和重置 ByteBufreaderIndexwriterIndex。這些和InputStream上的調用相似,只是沒有readlimit 參數來指定標記何時失效。

若是將索引設置到一個無效位置會拋出IndexOutOfBoundsException.

能夠經過clear()歸零索引,歸零索引不會清除數據.

3.3查找

ByteBuf中不少方法能夠肯定的索引,如indexOf().

複雜查找能夠經過那些須要一個ByteBufProcessor做爲參數的方法完成.這個接口應該可使用lambda表達式(可是我如今使用的Netty4.1.12已經廢棄了該接口,應該使用ByteProcessor).

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

3.4派生緩衝區

派生緩衝區就是,基於原緩衝區一頓操做生成新緩衝區.好比複製,切分等等.

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,留在文末展現吧.

4.ByteBufHolder接口

咱們常常發現, 除了實際的數據負載以外, 咱們還須要存儲各類屬性值。 HTTP 響應即是一個很好的例子, 除了表示爲字節的內容,還包括狀態碼、 cookie 等。
爲了處理這種常見的用例, Netty 提供了 ByteBufHolder。 ByteBufHolder 也爲 Netty 的高級特性提供了支持,如緩衝區池化,其中能夠從池中借用 ByteBuf, 而且在須要時自動釋放。ByteBufHolder 只有幾種用於訪問底層數據和引用計數的方法。

5.ByteBuf的分配

咱們能夠經過ByteBufAllocator來分配一個ByteBuf實例.ByteBufAllocator接口實現了ByteBuf的池化.

能夠經過 Channel(每一個均可以有一個不一樣的 ByteBufAllocator實例)或者綁定到ChannelHandlerChannelHandlerContext獲取一個到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 的其餘有用方法。

6.引用計數

引用計數是一種經過在某個對象所持有的資源再也不被其餘對象引用時釋放該對象所持有的資源來優化內存使用和性能的技術。 它們都實現了 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

7.API

ByteBuf

ByteBufAllocator

Unpooled

相關文章
相關標籤/搜索