標籤:
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建立一個組合bufferByteBuf 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的bufferb.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做簡單介紹,其實作類別還需要讀者自己去慢慢摸索 。
深入netty源碼解析之一資料結構