1. 程式人生 > 其它 >Netty學習之核心元件ByteBuf及API

Netty學習之核心元件ByteBuf及API

Netty提供的ByteBuf不同於JDK中NIO的ByteBuffer,ByteBuf是netty中資料傳輸的容器,是Netty自己實現的,作為NIO ByteBuffer的替代品,提供了更好的API供開發者使用。相較於NIO的ByteBuffer更具有卓越的功能性和靈活性。具體NIO的ByteBuffer如何實現請參考IO模型之NIO程式碼及其實踐詳解

一、ByteBuf的API特點

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

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

二、ByteBuf類原理及使用

  1、ByteBuf工作原理

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

        

  • ByteBuf維護了readerIndex和writerIndex索引。
  • 當readerIndex > writerIndex時,則丟擲IndexOutOfBoundsException。
  • ByteBuf容量 = writerIndex。
  • ByteBuf可讀容量 = writerIndex - readerIndex。
  • readXXX()和writeXXX()方法將會推進其對應的索引,自動推進。
  • getXXX()和setXXX()方法對writerIndex和readerIndex無影響,不會改變index值。

  readerIndex和WriterIndex將整個ByteBuf分成了三個區域:可丟棄位元組、可讀位元組、可寫位元組,如下圖:

  當尚未讀取時,擁有可讀位元組區域以及可寫位元組區域。

        

  當已經讀過部分割槽域後,變成了可丟棄位元組、可讀位元組、可寫位元組三個區域。

            

  2、ByteBuf的使用模式

  ByteBuf本質是: 一個由不同的索引分別控制讀訪問和寫訪問的位元組陣列。ByteBuf共有三種模式: 堆緩衝區模式(Heap Buffer)、直接緩衝區模式(Direct Buffer)和複合緩衝區模式(Composite Buffer),相較於NIO的ByteBuffer多了一種複合緩衝區模式。

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

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

  三種ByteBuf使用區別對比:

          

三、ButeBuf的池化與非池化

  記憶體的申請和銷燬都有一定效能開銷,記憶體池化技術可以有效的減少相關開銷。Netty在4引入了該技術。Netty的池化分為物件池和記憶體池,對應的ByteBuf的堆緩衝區和直接緩衝區。

  是否使用池化取決於ByteBufAllocator使用的例項物件(參考分配方式ByteBufAllocator相關說明,本文後部分有說明)

  PooledByteBufAllocator可以通過ctx.alloc獲得,如下圖:

      

  Netty預設使用池化byteBuf,如果想要宣告不池化的可以使用Unpooled工具類。

四、位元組級操作

  4.1、隨機訪問索引

  ByteBuf的索引與普通的Java位元組陣列一樣。第一個位元組的索引是0,最後一個位元組索引總是capacity()-1。ByteBuf的API分為4大類:Get*、Set*、Read*、Write*。使用有以下兩條規則:

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

  4.2、順序訪問索引

  Netty的ByteBuf同時具有讀索引和寫索引,但JDK的ByteBuffer只有一個索引,所以JDK需要呼叫flip()方法在讀模式和寫模式之間切換(NIO方式)。ByteBuf被讀索引和寫索引劃分成3個區域:可丟棄位元組區域,可讀位元組區域和可寫位元組區域 ,如下圖:

        

  4.3、可丟棄位元組區域

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

  • discardReadBytes()效果: 將可讀位元組區域(CONTENT)[readerIndex, writerIndex)往前移動readerIndex位,同時修改讀索引和寫索引。
  • discardReadBytes()方法會移動可讀位元組區域內容(CONTENT)。如果頻繁呼叫,會有多次資料複製開銷,對效能有一定的影響。

  4.4、可讀位元組區域

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

  4.5、可寫位元組區域

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

  4.6、索引管理

  • markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先備份當前的readerIndex,resetReaderIndex()則是將剛剛備份的readerIndex恢復回來。常用於dump ByteBuf的內容,又不想影響原來ByteBuf的readerIndex的值
  • readerIndex(int) ----- 設定readerIndex為固定的值
  • writerIndex(int) ----- 設定writerIndex為固定的值
  • clear() ----- 效果是: readerIndex=0, writerIndex(0)。不會清除記憶體
  • 呼叫clear()比呼叫discardReadBytes()輕量的多。僅僅重置readerIndex和writerIndex的值,不會拷貝任何記憶體,開銷較小。

  4.7、查詢操作(indexOf)

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

  • 最簡單的方法 ----- indexOf()
  • 利用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);
}

  4.8、其餘訪問操作

  除去get、set、read、write類基本操作,還有一些其餘的有用操作,如下圖:

        

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

  • hasArray() :如果ByteBuf由一個位元組陣列支撐,則返回true。通俗的講:ByteBuf是堆緩衝區模式,則代表其內部儲存是由位元組陣列支撐的。
  • array() :如果ByteBuf是由一個位元組陣列支撐則返回陣列,否則丟擲UnsupportedOperationException異常。也就是,ByteBuf是堆緩衝區模式。

五、ByteBufHolder的使用

  我們時不時的會遇到這樣的情況:即需要另外儲存除有效的實際資料各種屬性值。HTTP響應就是一個很好的例子;與內容一起的位元組的還有狀態碼,cookies等。

  Netty 提供的 ByteBufHolder 可以對這種常見情況進行處理。 ByteBufHolder 還提供了對於 Netty 的高階功能,如緩衝池,其中儲存實際資料的 ByteBuf 可以從池中借用,如果需要還可以自動釋放。

  ByteBufHolder 有那麼幾個方法。到底層的這些支援接入資料和引用計數。如下圖所示:

        

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

六、ByteBuf分配

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

  1、按序分配: ByteBufAllocator介面

  Netty通過介面ByteBufAllocator實現了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator,是否使用池是由應用程式決定的:

  • ctx.channel().alloc().buffer() ----- 本質就是: ByteBufAllocator.DEFAULT
  • ByteBufAllocator.DEFAULT.buffer() ----- 返回一個基於堆或者直接記憶體儲存的Bytebuf。預設是堆記憶體
  • ByteBufAllocator.DEFAULT ----- 有兩種型別: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。對於Java程式,預設使用PooledByteBufAllocator(池化)。對於安卓,預設使用UnpooledByteBufAllocator(非池化)
  • 可以通過BootStrap中的Config為每個Channel提供獨立的ByteBufAllocator例項

  ByteBufAllocator提供的操作如下圖:

        

  注意:

  • 上圖中的buffer()方法,返回一個基於堆或者直接記憶體儲存的Bytebuf ----- 預設是堆記憶體。原始碼: AbstractByteBufAllocator() { this(false); }
  • ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。預設是池化(PooledByteBufAllocator.DEFAULT)
  • 通過一些方法接受整型引數允許使用者指定 ByteBuf 的初始和最大容量值。

  得到一個 ByteBufAllocator 的引用很簡單。你可以得到從 Channel (在理論上,每 Channel 可具有不同的 ByteBufAllocator ),或通過繫結到的 ChannelHandler 的 ChannelHandlerContext 得到它,如程式碼:

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); //1、Channel

ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc(); //2、 ChannelHandlerContext

  第一種是從 channel 獲得 ByteBufAllocator,第二種是從 ChannelHandlerContext 獲得 ByteBufAllocator。

  Netty 提供了兩種 ByteBufAllocator 的實現,一種是 PooledByteBufAllocator,用ByteBuf 例項池改進效能以及記憶體使用降到最低,此實現使用一個“jemalloc”記憶體分配。其他的實現不池化 ByteBuf 情況下,每次返回一個新的例項。Netty 預設使用 PooledByteBufAllocator,我們可以通過 ChannelConfig 或通過引導設定一個不同的實現來改變。

  2、Unpooled緩衝區:非池化

  Unpooled提供靜態的輔助方法來建立未池化的ByteBuf。其包含方法如下:

        

  注意:

  • 上圖的buffer()方法,返回一個未池化的基於堆記憶體儲存的ByteBuf
  • wrappedBuffer() :建立一個檢視,返回一個包裝了給定資料的ByteBuf。非常實用

  建立ByteBuf程式碼:

 public void createByteBuf(ChannelHandlerContext ctx) {
    // 1. 通過Channel建立ByteBuf,實際上也是使用ByteBufAllocator,因為ctx.channel().alloc()返回的就是一個ByteBufAllocator物件
    ByteBuf buf1 = ctx.channel().alloc().buffer();
    // 2. 通過ByteBufAllocator.DEFAULT建立
    ByteBuf buf2 =  ByteBufAllocator.DEFAULT.buffer();
    // 3. 通過Unpooled建立
    ByteBuf buf3 = Unpooled.buffer();
}

  3、ByteBufUtil類

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

  • hexdump() :以十六進位制的表示形式列印ByteBuf的內容,可以用於除錯程式時列印 ByteBuf 的內容。非十六進位制字串相比位元組而言對使用者更友好。 而且十六進位制版本可以很容易地轉換回實際位元組表示。
  • boolean equals(ByteBuf, ByteBuf) :判斷兩個ByteBuf例項的相等性,在 實現自己 ByteBuf 的子類時經常用到

七、派生緩衝區

  “派生的緩衝區”是代表一個專門的展示 ByteBuf 內容的“檢視”。這種檢視是由duplicate() 、slice()、slice(int, int)、Unpooled.unmodifiableBuffer(...)、Unpooled.wrappedBuffer(...)、order(ByteOrder)、readSlice(int) 方法建立的。所有這些都返回一個新的 ByteBuf 例項包括它自己的 reader, writer 和標記索引。然而,內部資料儲存共享就像在一個 NIO 的 ByteBuffer。這使得派生的緩衝區建立、修改其內容,以及修改其“源”例項更廉價。

  注意:

  • 上面的7中方法,都會返回一個新的ByteBuf例項,具有自己的讀索引和寫索引。但是,其內部儲存是與原物件是共享的。這就是檢視的概念
  • 請注意:如果你修改了這個新的ByteBuf例項的具體內容,那麼對應的源例項也會被修改,因為其內部儲存是共享的。
  • 如果需要拷貝現有緩衝區的真實副本,請使用copy()或copy(int, int)方法。
  • 使用派生緩衝區,避免了複製記憶體的開銷,有效提高程式的效能

  針對派生和複製區別,如下面程式碼所展示:

Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1

ByteBuf sliced = buf.slice(0, 14);          //2
System.out.println(sliced.toString(utf8));  //3

buf.setByte(0, (byte) 'J');                 //4
assert buf.getByte(0) == sliced.getByte(0);
  1. 建立一個 ByteBuf 儲存特定位元組串。
  2. 建立從索引 0 開始,並在 14 結束的 ByteBuf 的新 slice。
  3. 列印 Netty in Action
  4. 更新索引 0 的位元組。
  5. 斷言成功,因為資料是共享的,並以一個地方所做的修改將在其他地方可見。
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);     //1

ByteBuf copy = buf.copy(0, 14);               //2
System.out.println(copy.toString(utf8));      //3

buf.setByte(0, (byte) 'J');                   //4
assert buf.getByte(0) != copy.getByte(0);
  1. 建立一個 ByteBuf 儲存特定位元組串。
  2. 建立從索引0開始和 14 結束 的 ByteBuf 的段的拷貝。
  3. 列印 Netty in Action
  4. 更新索引 0 的位元組。
  5. .斷言成功,因為資料不是共享的,並以一個地方所做的修改將不影響其他。

  因此使用派生緩衝區可以儘可能避免複製記憶體,要想資料獨立,請使用copy。

八、引用計數

  引用計數是一種通過在某個物件所持有的資源不再被其他物件引用時釋放該物件所持有的資源來優化記憶體使用和效能的技術。Netty 在第4 版中為ByteBuf引入了引用計數技術,ByteBuf初始化引用數量為1,通過release 可以-1,為0時物件被回收。

        

  堆中物件即使不回收在gc執行時也會被回收,但是直接記憶體的物件如果不釋放,可能會引起記憶體的溢位。

  如果試圖訪問已經回收的物件會丟擲

      

  資源釋放的問題通常來講是由最後一個訪問該物件的事件處理器負責。執行writeAndFlush會自動釋放資源,同時如下圖中的SimpleChannelInboundHandler
的實現也是為了給我們實現了物件回收。省去一些通用程式碼。