1. 程式人生 > >Netty專欄 ( 三)——— Netty的ByteBuf

Netty專欄 ( 三)——— Netty的ByteBuf

@author 魯偉林
記錄《Netty 實戰》中各章節學習過程,寫下一些自己的思考和總結,幫助使用Netty框架的開發技術人員們,能夠有所得,避免踩坑。
本部落格目錄結構將嚴格按照書本《Netty 實戰》,省略與Netty無關的內容,可能出現跳小章節。
本部落格中涉及的完整程式碼:
GitHub地址: https://github.com/thinkingfioa/netty-learning/tree/master/netty-in-action。
本人部落格地址: https://blog.csdn.net/thinking_fioa

第5章 ByteBuf

Netty提供的ByteBuf與JDK的ByteBuffer相比,前者具有卓越的功能性和靈活性。

5.1 ByteBuf的API

ByteBuf提供讀訪問索引(readerIndex)和寫訪問索引(writerIndex)來控制位元組陣列。ByteBuf API具有以下優點:

  1. 允許使用者自定義緩衝區型別擴充套件
  2. 通過內建的複合緩衝區型別實現透明的零拷貝
  3. 容量可按需增長
  4. 讀寫這兩種模式之間不需要呼叫類似於JDK的ByteBuffer的flip()方法進行切換
  5. 讀和寫使用不同的索引
  6. 支援方法的鏈式呼叫
  7. 支援引用計數
  8. 支援池化

5.2 ByteBuf類 ----- Netty的資料容器

5.2.1 ByteBuf如何工作的

ByteBuf維護兩個不同的索引: 讀索引(readerIndex)和寫索引(writerIndex)

。如下圖:

  1. ByteBuf維護了readerIndex和writerIndex索引
  2. 當readerIndex > writerIndex時,則丟擲IndexOutOfBoundsException
  3. ByteBuf容量 = writerIndex。
  4. ByteBuf可讀容量 = writerIndex - readerIndex
  5. readXXX()和writeXXX()方法將會推進其對應的索引。自動推進
  6. getXXX()和setXXX()方法將對writerIndex和readerIndex無影響

5.2.2 ByteBuf的使用模式

ByteBuf本質是: 一個由不同的索引分別控制讀訪問和寫訪問的位元組陣列。請記住這句話。ByteBuf共有三種模式: 堆緩衝區模式(Heap Buffer)、直接緩衝區模式(Direct Buffer)和複合緩衝區模式(Composite Buffer)

1. 堆緩衝區模式(Heap Buffer)

堆緩衝區模式又稱為:支撐陣列(backing array)。將資料存放在JVM的堆空間,通過將資料儲存在陣列中實現

  • 堆緩衝的優點: 由於資料儲存在Jvm堆中可以快速建立和快速釋放,並且提供了陣列直接快速訪問的方法
  • 堆緩衝的缺點: 每次資料與I/O進行傳輸時,都需要將資料拷貝到直接緩衝區

程式碼:

public static void heapBuffer() {
    // 建立Java堆緩衝區
    ByteBuf heapBuf = Unpooled.buffer(); 
    if (heapBuf.hasArray()) { // 是陣列支撐
        byte[] array = heapBuf.array();
        int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
        int length = heapBuf.readableBytes();
        handleArray(array, offset, length);
    }
}

2. 直接緩衝區模式(Direct Buffer)

Direct Buffer屬於堆外分配的直接記憶體,不會佔用堆的容量。適用於套接字傳輸過程,避免了資料從內部緩衝區拷貝到直接緩衝區的過程,效能較好

  • Direct Buffer的優點: 使用Socket傳遞資料時效能很好,避免了資料從Jvm堆記憶體拷貝到直接緩衝區的過程。提高了效能
  • Direct Buffer的缺點: 相對於堆緩衝區而言,Direct Buffer分配記憶體空間和釋放更為昂貴
  • 對於涉及大量I/O的資料讀寫,建議使用Direct Buffer。而對於用於後端的業務訊息編解碼模組建議使用Heap Buffer

程式碼:

public static void directBuffer() {
    ByteBuf directBuf = Unpooled.directBuffer();
    if (!directBuf.hasArray()) {
        int length = directBuf.readableBytes();
        byte[] array = new byte[length];
        directBuf.getBytes(directBuf.readerIndex(), array);
        handleArray(array, 0, length);
    }
}

3. 複合緩衝區模式(Composite Buffer)

Composite Buffer是Netty特有的緩衝區。本質上類似於提供一個或多個ByteBuf的組合檢視,可以根據需要新增和刪除不同型別的ByteBuf。

  • 想要理解Composite Buffer,請記住:它是一個組合檢視。它提供一種訪問方式讓使用者自由的組合多個ByteBuf,避免了拷貝和分配新的緩衝區。
  • Composite Buffer不支援訪問其支撐陣列。因此如果要訪問,需要先將內容拷貝到堆記憶體中,再進行訪問
  • 下圖是將兩個ByteBuf:頭部+Body組合在一起,沒有進行任何複製過程。僅僅建立了一個檢視

程式碼:

public static void byteBufComposite() {
    // 複合緩衝區,只是提供一個檢視
    CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
    ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct
    ByteBuf bodyBuf = Unpooled.directBuffer();   // can be backing or direct
    messageBuf.addComponents(headerBuf, bodyBuf);
    messageBuf.removeComponent(0); // remove the header
    for (ByteBuf buf : messageBuf) {
        System.out.println(buf.toString());
    }
}

5.3 位元組級操作

5.3.1 隨機訪問索引

ByteBuf的索引與普通的Java位元組陣列一樣。第一個位元組的索引是0,最後一個位元組索引總是capacity()-1。請記住下列兩條,非常有用:

  • readXXX()和writeXXX()方法將會推進其對應的索引readerIndex和writerIndex。自動推進
  • getXXX()和setXXX()方法用於訪問資料,對writerIndex和readerIndex無影響

程式碼:

public static void byteBufRelativeAccess() {
    ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
    for (int i = 0; i < buffer.capacity(); i++) {
        byte b = buffer.getByte(i);// 不改變readerIndex值
        System.out.println((char) b);
    }
}

5.3.2 順序訪問索引

Netty的ByteBuf同時具有讀索引和寫索引,但JDK的ByteBuffer只有一個索引,所以JDK需要呼叫flip()方法在讀模式和寫模式之間切換。

  •  ByteBuf被讀索引和寫索引劃分成3個區域:可丟棄位元組區域,可讀位元組區域和可寫位元組區域

5.3.3 可丟棄位元組區域

可丟棄位元組區域是指:[0,readerIndex)之間的區域。可呼叫discardReadBytes()方法丟棄已經讀過的位元組。

  1. discardReadBytes()效果 ----- 將可讀位元組區域(CONTENT)[readerIndex, writerIndex)往前移動readerIndex位,同時修改讀索引和寫索引。

  2. discardReadBytes()方法會移動可讀位元組區域內容(CONTENT)。如果頻繁呼叫,會有多次資料複製開銷,對效能有一定的影響

5.3.4 可讀位元組區域

可讀位元組區域是指:[readerIndex, writerIndex)之間的區域。任何名稱以read和skip開頭的操作方法,都會改變readerIndex索引。

5.3.5 可寫位元組區域

可寫位元組區域是指:[writerIndex, capacity)之間的區域。任何名稱以write開頭的操作方法都將改變writerIndex的值。

5.3.6 索引管理

1. markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先備份當前的readerIndex,resetReaderIndex()則是將剛剛備份的readerIndex恢復回來。常用於dump ByteBuf的內容,又不想影響原來ByteBuf的readerIndex的值

2. readerIndex(int) ----- 設定readerIndex為固定的值

3. writerIndex(int) ----- 設定writerIndex為固定的值

4. clear() ----- 效果是: readerIndex=0, writerIndex(0)。不會清除記憶體

5. 呼叫clear()比呼叫discardReadBytes()輕量的多。僅僅重置readerIndex和writerIndex的值,不會拷貝任何記憶體,開銷較小。

5.3.7 查詢操作(indexOf)

查詢ByteBuf指定的值。類似於,String.indexOf("str")操作

1. 最簡單的方法 ----- indexOf()

2. 利用ByteProcessor作為引數來查詢某個指定的值。

程式碼:

public static void byteProcessor() {
    ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
    // 使用indexOf()方法來查詢
    buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8);
    // 使用ByteProcessor查詢給定的值
    int index = buffer.forEachByte(ByteProcessor.FIND_CR);
}

5.3.8 派生緩衝區 ----- 檢視

派生緩衝區為ByteBuf提供了一個訪問的檢視。檢視僅僅提供一種訪問操作,不做任何拷貝操作。下列方法,都會呈現給使用者一個檢視,以供訪問:

1. duplicate() 

2. slice()

3. slice(int, int)

4. Unpooled.unmodifiableBuffer(...)

5. Unpooled.wrappedBuffer(...)

6. order(ByteOrder)

7. readSlice(int)

理解

1. 上面的6中方法,都會返回一個新的ByteBuf例項,具有自己的讀索引和寫索引。但是,其內部儲存是與原物件是共享的。這就是檢視的概念

2. 請注意:如果你修改了這個新的ByteBuf例項的具體內容,那麼對應的源例項也會被修改,因為其內部儲存是共享的

3. 如果需要拷貝現有緩衝區的真實副本,請使用copy()或copy(int, int)方法。

4. 使用派生緩衝區,避免了複製記憶體的開銷,有效提高程式的效能

程式碼:

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); // return true
}

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); // return true
}

5.3.9 讀/寫操作

如上文所提到的,有兩種類別的讀/寫操作:

1. get()和set()操作 ----- 從給定的索引開始,並且保持索引不變

2. read()和write()操作 ----- 從給定的索引開始,並且根據已經訪問過的位元組數對索引進行訪問

3. 下圖給出get()操作API,對於set()操作、read()操作和write操作可參考書籍或API

5.3.10 更多的操作

下面的兩個方法操作字面意思較難理解,給出解釋:

1. hasArray() ----- 如果ByteBuf由一個位元組陣列支撐,則返回true。通俗的講:ByteBuf是堆緩衝區模式,則代表其內部儲存是由位元組陣列支撐的。如果還沒理解,可參考5.2.2章節

2. array() ----- 如果ByteBuf是由一個位元組陣列支撐澤返回陣列,否則丟擲UnsupportedOperationException異常。也就是,ByteBuf是堆緩衝區模式

5.4 ByteBufHolder介面

ByteBufHolder為Netty的高階特性提供了支援,如緩衝區池化,可以從池中借用ByteBuf,並且在需要時自動釋放。

1. ByteBufHolder是ByteBuf的容器,可以通過子類實現ByteBufHolder介面,根據自身需要新增自己需要的資料欄位。可以用於自定義緩衝區型別擴充套件欄位。

2. Netty提供了一個預設的實現DefaultByteBufHolder。

程式碼

public class CustomByteBufHolder extends DefaultByteBufHolder{

    private String protocolName;

    public CustomByteBufHolder(String protocolName, ByteBuf data) {
        super(data);
        this.protocolName = protocolName;
    }

    @Override
    public CustomByteBufHolder replace(ByteBuf data) {
        return new CustomByteBufHolder(protocolName, data);
    }

    @Override
    public CustomByteBufHolder retain() {
        super.retain();
        return this;
    }

    @Override
    public CustomByteBufHolder touch() {
        super.touch();
        return this;
    }

    @Override
    public CustomByteBufHolder touch(Object hint) {
        super.touch(hint);
        return this;
    }
    ...
}

5.5 ByteBuf分配

建立和管理ByteBuf例項的多種方式:按需分配(ByteBufAllocator)、Unpooled緩衝區和ByteBufUtil類

5.5.1 按序分配: ByteBufAllocator介面

Netty通過介面ByteBufAllocator實現了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator: 

1. ctx.channel().alloc().buffer() ----- 本質就是: ByteBufAllocator.DEFAULT

2. ByteBufAllocator.DEFAULT.buffer() ----- 返回一個基於堆或者直接記憶體儲存的Bytebuf。預設是堆記憶體

3. ByteBufAllocator.DEFAULT ----- 有兩種型別: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。對於Java程式,預設使用PooledByteBufAllocator(池化)。對於安卓,預設使用UnpooledByteBufAllocator(非池化)

4. 可以通過BootStrap中的Config為每個Channel提供獨立的ByteBufAllocator例項

解釋:

1. 上圖中的buffer()方法,返回一個基於堆或者直接記憶體儲存的Bytebuf ----- 預設是堆記憶體。原始碼: AbstractByteBufAllocator() { this(false); }

2. ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。預設是池化(PooledByteBufAllocator.DEFAULT)

5.5.2 Unpooled緩衝區 ----- 非池化

Unpooled提供靜態的輔助方法來建立未池化的ByteBuf。

注意:

1. 上圖的buffer()方法,返回一個未池化的基於堆記憶體儲存的ByteBuf

2. wrappedBuffer() ----- 建立一個檢視,返回一個包裝了給定資料的ByteBuf。非常實用

建立ByteBuf程式碼:

 public void createByteBuf(ChannelHandlerContext ctx) {
    // 1. 通過Channel建立ByteBuf
    ByteBuf buf1 = ctx.channel().alloc().buffer();
    // 2. 通過ByteBufAllocator.DEFAULT建立
    ByteBuf buf2 =  ByteBufAllocator.DEFAULT.buffer();
    // 3. 通過Unpooled建立
    ByteBuf buf3 = Unpooled.buffer();
}

5.5.3 ByteBufUtil類

ByteBufUtil類提供了用於操作ByteBuf的靜態的輔助方法: hexdump()和equals

1. hexdump() ----- 以十六進位制的表示形式列印ByteBuf的內容。非常有價值 

2. equals() ----- 判斷兩個ByteBuf例項的相等性

5.6 引用計數

Netty4.0版本中為ButeBuf和ButeBufHolder引入了引用計數技術。請區別引用計數和可達性分析演算法(jvm垃圾回收)

1. 誰負責釋放: 一般來說,是由最後訪問(引用計數)物件的那一方來負責將它釋放

2. buffer.release() ----- 引用計數減1 

3. buffer.retain() ----- 引用計數加1

4. buffer.refCnt() ----- 返回當前物件引用計數值

5. buffer.touch() ----- 記錄當前物件的訪問位置,主要用於除錯。

6. 引用計數並非僅對於直接緩衝區(direct Buffer)。ByteBuf的三種模式: 堆緩衝區(heap Buffer)、直接緩衝區(dirrect Buffer)和複合緩衝區(Composite Buffer)都使用了引用計數,某些時候需要程式設計師手動維護引用數值

程式碼:

public static void releaseReferenceCountedObject(){
    ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
    // 引用計數加1
    buffer.retain();
    // 輸出引用計數
    buffer.refCnt();
    // 引用計數減1
    buffer.release();
}

5.7 建議

1. 如果使用了Netty的ByteBuf,建議功能測試時,開啟記憶體檢測: -Dio.netty.leakDetectionLevel=paranoid

2. ByteBuf的三種模式: 堆緩衝區(heap Buffer)、直接緩衝區(dirrect Buffer)和複合緩衝區(Composite Buffer)都使用了引用計數,某些時候需要程式設計師手動維護引用數值。

附錄