深刻netty源碼解析之一數據結構

Netty是一個異步事件驅動的網絡應用框架,它適用於高性能協議的服務端和客戶端的快速開發和維護。其架構以下所示:java

 

其核心分爲三部分,api

    最低層爲支持零拷貝功能的自定義Byte buffer;數組

    中間層爲通用通訊API;網絡

    上層爲可擴展的事件模型。數據結構

如今咱們從最低層的支持零拷貝功能的自定義Byte buffer開始,它包含在io.netty.buffer包內。架構

 

io.netty.buffer 包描述app

io.netty.buffer 包中包含了Netty底層的數據結構。框架

在java nio中byteBuffer表現了底層二進制和文本信息的基礎數據結構,io.netty.buffer抽象了byteBuffer api,netty使用本身的Buffer API去提供NIO的byteBuffer來表示字節序列。它的buffer Api跟使用ByteBuffer相比有顯著的優點。netty的buffer類型:byteBuf的設計從根本上解決了byteBuffer出現的問題,而且知足了網絡應用開發者的平常需求。下面列舉一些比較酷的特性:dom

  能夠根據自身需求定義本身的buffer類型異步

  透明的零拷貝時經過內置的複合buffer類型來實現。

      像StringBuffer同樣,支持動態buffer類型根據須要擴展buffer容量。

      再也不須要調用flip()方法。

      一般狀況比ByteBuffer更快。

      擴展性更好

ByteBuf爲優化快速協議實現提供,它提供了豐富的一組操做。例如,它提供了多種操做方式去獲取unsigned 值和string及對於buffer中的特定字節序列的檢索,你亦能夠經過擴展或者包裝已經存在的Buffer類型來增長更便利的獲取方法。自定義的Buffer類型仍然要繼承自ByteBuf接口而不是引進一個不兼容的新類型。

透明的零拷貝

爲了提高網絡應用的性能到極致,你須要下降內存複製操做進行的次數。固然你也能夠設置一組能夠切分的buffer,用它們來組合中一個完整的消息。Netty提供了一個組合buffer,這個組合buffer支持你從任意數目的已存在bufer中不使用內存拷貝來建立一個新的buffer。例如,一個消息由兩部分組成:頭部和內容。在一個模塊化的應用中,當發送消息時,這兩部分能夠有不一樣模塊產生和後面的組合。

+--------+----------+
| header | body |
+--------+----------+

若你使用ByteBuffer(java NIO),你必須建立一個新的大的Buffer,而後將這頭部和內容拷貝到新建立的buffer中,或者你能夠在NIO中使用寫操做集中操做,但若你使用複合Buffer做爲一個ByteBuffer數組而不是僅僅一個Buffer時,破壞了抽象類型而且引入了一個複雜的狀態管理。並且,若你不從NIO channel中讀取或者寫入時就不會起做用。

//組合類型和組件類型不匹配

ByteBuffer[] message = new ByteBuffer[] { header, body };

相反,ByteBuf沒有這種問題,由於它的高擴展性和內置的組合buffer類型。

//組合類型和組件類型不匹配
ByteBuf message = Unpooled.wrappedBuffer(header, body);

//所以,你能夠經過混和一個組合buffer和一個普通buffer建立一個組合buffer

ByteBuf messageWithFooter = Unpooled.wrappedBuffer(message, footer);

//因爲組合buffer仍然是一個ByteBuf,你能夠很容易的獲取它的內容,即使你要獲取的區域跨越多個組件,和獲取簡單Buffer的獲取方式也是同樣的。

//實例中獲取的unsigned整型跨越了內容和尾部。
messageWithFooter.getUnsignedInt(
messageWithFooter.readableBytes() - footer.readableBytes() - 1);

 

容量自動擴充(Automatic Capacity Extension)

 許多協議定義了消息的長度,這意味着在建立消息以前無法決定消息的長度或者不容易精確計算消息的長度。就像你剛開始建立一個string同樣。咱們一般估計字符串的長度,而後使用StringBuffer來根據須要去擴充。

 

//建立新的動態buffer。在內部,爲避免潛在的浪費內存空間,真正的buffer將延後建立。

ByteBuf b = Unpooled.buffer(4);

//當第一次嘗試去寫的時候,纔會在內部建立一個容量爲4的buffer

b.writeByte('1');

b.writeByte('2');
b.writeByte('3');
b.writeByte('4');

//當要寫的字節數超過初始化的容量4時,在內部,buffer自動從新分配一個更大的容量

b.writeByte('5');

 

更好的性能

在絕大部分狀況下,繼承自ByteBuf的buffer實現對字節數組(例如byte[])的包裝是很是輕量級的。不像ByteBuffer,ByteBuf沒有複雜的邊界檢查和索引補償,於是,對JVM來講,更容易優化獲取buffer的方式。

更復雜的buffer實現僅僅用在切分或者組合buffer,而且複雜buffer的性能和ByteBuffer同樣。

 

ByteBuf的繼承關係

 

進入ByteBuf來看:

Byte提供了對字節序列的隨機或者順序獲取方式,能夠讀取0個或者多個字節。

這個接口提供了對一個或者多個基本字節數組(byte[])和普通的NIO ByteBuffer的抽象試圖。

 建立一個Buffer

     建議經過使用helper方法unpooled來建立一個新的buffer,而不是調用一個buffer實現的構造方法。

  索引的隨機訪問

  同普通的字節數組同樣,ByteBuf使用基於0的索引方法,這意味着字節數組的第一個字節的索引爲0,數組的最後一個字節索引爲容量-1.例如,爲便利一個buffer的全部字節,不用考慮它的內部實現,你能夠這樣作:

 buffer = ...;
  for (int i = 0; i < buffer.capacity(); i ++) {
      byte b = buffer.getByte(i);
      System.out.println((char) b);
  }

 索引的順序獲取

 ByteBuf提供了兩個指針來支持順序讀取和寫入:readerIndex()用來讀操做,writerIndex()用來寫操做。下圖展現了一個buffer是如何經過2個指針來劃分爲3個區域的:

       +-------------------+------------------+------------------+
       | 可丟棄字節  |  可讀字節 |  可寫字節  |
       |            |  (內容)   |          |
       +-------------------+-------------- +------------------+
       |            |           |          |
       0      <=  讀索引   <=  寫索引 <=   容量

可讀字節(真正的內容)

這個部分是數據真正存儲的區域,名稱以read或者skip開頭的全部操做都會從當前的讀索引處讀或者跳過數據,而且根據讀的字節數目遞增。若讀操做的參數一樣是一個ByteBuf而且沒有指明目的索引,指定buffer的寫索引將同步增長。

若是下面沒有內容了(接着讀取就會報越界異常),buffer新分配的默認值或者複製的buffer的可讀索引爲0.

//遍歷一個buffer的可讀字節

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

可寫字節

這個區域是須要填充的未定義空間。以write結尾的任何操做將在當前可寫索引處寫入數據,並根據寫入的字節數目增長可寫索引。若寫操做的參數是ByteBuf,而且沒有指明源索引,指定的Buffer 的可讀索引同步增長。

 

若沒有可寫入的內容(繼續的話會報越界異常)時,Buffer的默認值的寫索引是buffer的容量。

  // 用任意的整型來填充buffer的可寫區域.
  {@link ByteBuf} buffer = ...;
  while (buffer.maxWritableBytes() >= 4) {
      buffer.writeInt(random.nextInt());
  }

可丟棄的字節

  這個區域包含了讀操做已經讀過了的字節。初始化時該區域的容量爲0,但當讀操做進行時它的容量會逐漸達到寫索引。經過調用discardReadBytes()方法來聲明不用區域,以下圖描述所示:

  discardReadBytes()方法前:
 *      +-------------------+------------------+------------------+
 *      | discardable bytes |  readable bytes  |  writable bytes  |
 *      +-------------------+------------------+------------------+
 *      |                   |                  |                  |
 *      0      <=      readerIndex   <=   writerIndex    <=    capacity
 *
 *
  discardReadBytes()方法後
 *
 *      +------------------+--------------------------------------+
 *      |  readable bytes  |    writable bytes (got more space)   |
 *      +------------------+--------------------------------------+
 *      |                  |                                      |
 * readerIndex (0) <= writerIndex (decreased)        <=        capacity

請注意:在調用discardReadBytes()方法後,沒法保證可些字節的內容。可寫字節在大部分狀況下不會移動,甚至能夠根據不一樣buffer實現填充徹底不一樣的數據。

清除buffer索引

你能夠經過調用clear()方法來設置readerIndex()和writerIndex()的值爲0.clear()方法並無清除buffer中的內容而僅僅是將兩個指針的值設爲0.請注意:ByteBuf的clear()方法的語法和ByteBuffer的clear()操做時徹底不一樣的。

 * clear()調用前
 *
 *      +-------------------+------------------+------------------+
 *      | discardable bytes |  readable bytes  |  writable bytes  |
 *      +-------------------+------------------+------------------+
 *      |                   |                  |                  |
 *      0      <=      readerIndex   <=   writerIndex    <=    capacity
 *
 *
 * clear()調用後
 *
 *      +---------------------------------------------------------+
 *      |             writable bytes (got more space)             |
 *      +---------------------------------------------------------+
 *      |                                                         |
 *      0 = readerIndex = writerIndex            <=            capacity

檢索操做:

   對簡單的單字節檢索,使用indexOf()、bytesBefore()。bytesBefore()在處理null(結尾字符)時特別有用。

   對於複雜的檢索,使用ForEachByte()。

標籤 和重置

每一個buffer都有兩個索引標籤。一個用來存儲readerIndex,另外一個用來存儲writerIndex()。你也能夠經過調用reset方法來從新設置這兩個索引的位置。

除了沒有readLimit的inputStream的標籤和重置方法也一樣起做用。

源buffer

能夠經過調用duplicate()或者slice方法來建立一個已經存在buffer的視圖。源buffer擁有獨立的readerIndex、writeIndex和標籤索引,然而像NIO buffer那樣,共享別的一些內部數據。

當須要徹底拷貝一個已經存在buffer時,請調用copy()方法.

轉換到已存在的JDK類型

字節數組

  判斷一個buffer是否由字節數組組成,使用hasArray()方法判斷;

  若一個buffer由字節數組構成,能夠直接經過array()方法獲取;

NIO buffer

  判斷一個buffer是否能夠轉換成NIO的buffer,使用nioBufferCount()判斷

  若一個ByteBuf能夠轉換成NIO的byteBuffer,能夠經過nioBuffer方法獲取。

字符串

  將ByteBuf轉換成string的toString方法有不少個,請必定注意:toString不是一個轉換方法。

 I/O流

   請參考byteBufInputStream和ByteBufOutputStream.

小結:

  Netty底層的數據結構爲ByteBuf接口及其實現,抓住它們就獲取到了底層實現的精華,本文僅是針對ByteBuf作簡單介紹,其實現類還須要讀者本身去慢慢摸索 。

相關文章
相關標籤/搜索