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


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

 

其核心分為三部分,

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

    中間層為通用通信API;

    上層為可擴展的事件模型。

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

 

io.netty.buffer 包描述

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

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

  可以根據自身需求定義自己的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做簡單介紹,其實現類還需要讀者自己去慢慢摸索 。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM