深刻理解Netty中的Buffer

1. 引言

上一篇文章咱們概要介紹了Netty的原理及結構,下面幾篇文章咱們開始對Netty的各個模塊進行比較詳細的分析。Netty的結構最底層是buffer模塊,這部分也相對獨立,咱們就先從buffer講起。程序員

2. Netty的4W1H

2.1 What(什麼是Buffer)

buffer中文名又叫緩衝區,按照維基百科的解釋,是」在數據傳輸時,在內存裏開闢的一塊臨時保存數據的區域」。它實際上是一種化同步爲異步的機制,能夠解決數據傳輸的速率不對等以及不穩定的問題。算法

根據這個定義,咱們能夠知道涉及I/O(特別是I/O寫)的地方,基本會有buffer的存在。就Java來講,咱們很是熟悉的Old I/O–InputStream&OutputStream系列API,基本都是在內部使用到了buffer。Java課程老師就教過,outputStream.write()只將內容寫入了buffer,必須調用outputStream.flush(),才能保證數據寫入生效!編程

而NIO中則直接將buffer這個概念封裝成了對象,其中最經常使用的大概是ByteBuffer了。因而使用方式變爲了:將數據寫入Buffer,flip()一下,而後將數據讀出來。因而,buffer的概念更加深刻人心了!緩存

Netty中的buffer也不例外。不一樣的是,Netty的buffer專爲網絡通信而生,因此它又叫ChannelBuffer(好吧其實沒有什麼因果關係…)。咱們下面就來說講Netty中的buffer。固然,關於Netty,咱們必須講講它的所謂」Zero-Copy-Capable」機制。性能優化

2.2 When & Where(TCP/IP協議與buffer)

TCP/IP協議是目前的主流網絡協議。它是一個多層協議,最下層是物理層,最上層是應用層(HTTP協議等),而在Java開發中,通常只接觸TCP以上,即傳輸層和應用層的內容。這就是Netty的主要應用場景。網絡

TCP報文有個比較大的特色,就是它傳輸的時候,會先把應用層的數據項拆開成字節,而後按照本身的傳輸須要,選擇合適數量的字節進行傳輸。什麼叫」本身的傳輸須要」?首先TCP包有最大長度限制,那麼太大的數據項確定是要拆開的。其次由於TCP以及下層協議會附加一些協議頭信息,若是數據項過小,那麼可能報文大部分都是沒有價值的頭信息,這樣傳輸是很不划算的。所以有了收集必定數量的小數據,並打包傳輸的Nagle算法(這個東東在HTTP協議裏會很討厭,Netty裏能夠用setOption(「tcpNoDelay」, true)關掉它)。app

這麼說可能太抽象了一點,咱們舉個例子吧:框架

發送時,咱們這樣分3次寫入(‘|’表示兩個buffer的分隔):異步

+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+

接收時,可能變成了這樣:tcp

+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+

很好懂吧?但是,說了這麼多,跟buffer有個什麼關係呢?別急,咱們來看下面一部分。

2.3 Why(分層思想)

咱們先回到以前的messageReceived方法:

public void messageReceived(
        ChannelHandlerContext ctx, MessageEvent e) {
    // Send back the received message to the remote peer.
    transferredBytes.addAndGet(((ChannelBuffer) e.getMessage()).readableBytes());
    e.getChannel().write(e.getMessage());
}

這裏MessageEvent.getMessage()默認的返回值是一個ChannelBuffer。咱們知道,業務中須要的」Message」,實際上是一條應用層級別的完整消息,而通常的buffer工做在傳輸層,與」Message」是不能對應上的。那麼這個ChannelBuffer是什麼呢?

來一個官方給的一段代碼,我想這個答案就很明顯了:

requestPart1 = buffer1.silice(OFFSET_PAYLOAD,buffer1.readableBytes() - OFFSET_PAYLOAD);

requestPart2 = buffer2.silice(OFFSET_PAYLOAD,buffer2.readableBytes() - OFFSET_PAYLOAD);

request = ChannelBuffers.wrappedBuffer(requestPart1,requestPart2);

輸入圖片說明

這裏能夠看到,TCP層HTTP報文被分紅了兩個ChannelBuffer,這兩個Buffer對咱們上層的邏輯(HTTP處理)是沒有意義的。可是兩個ChannelBuffer被組合起來,就成爲了一個有意義的HTTP報文,這個報文對應的ChannelBuffer,纔是能稱之爲」Message」的東西。這裏用到了一個詞」Virtual Buffer」,也就是所謂的」Zero-Copy-Capable Byte Buffer」了。是否是頓時以爲豁然開朗了?

我這裏總結一下,若是要說NIO的Buffer和Netty的ChannelBuffer最大的區別的話,就是前者僅僅是傳輸上的Buffer,然後者實際上是傳輸Buffer和抽象後的邏輯Buffer的結合。延伸開來講,NIO僅僅是一個網絡傳輸框架,而Netty是一個網絡應用框架,包括網絡以及應用的分層結構。

固然,使用ChannelBuffer表示」Message」,不失爲一個比較實用的方法,可是使用一個對象來表示解碼後的Message可能更符合習慣一點。在Netty裏,MessageEvent.getMessage()是能夠存放一個POJO的,這樣子抽象程度又高了一些,這個咱們在之後講到ChannelPipeline的時候會說到。

2.4 How(ChannelBuffer實現原理)

好了,終於來到了代碼實現部分。之因此囉嗦了這麼多,由於我以爲,關於」Zero-Copy-Capable Rich Byte Buffer」,理解爲何須要它,比理解它是怎麼實現的,可能要更重要一點。

關於代碼閱讀,我想可能不少朋友跟我同樣,喜歡」順藤摸瓜」式讀代碼–找到一個入口,而後順着查看它的調用,直到理解清楚。很幸運,ChannelBuffers(注意有s!)就是這樣一根」藤」,它是全部ChannelBuffer實現類的入口,它提供了不少靜態的工具方法來建立不一樣的Buffer,靠「順藤摸瓜」式讀代碼方式,大體能把各類ChannelBuffer的實現類摸個遍。先列一下ChannelBuffer相關類圖。

輸入圖片說明

此外還有WrappedChannelBuffer系列也是繼承自AbstractChannelBuffer,圖放到了後面。

3. Buffer源碼解讀

3.1 ChannelBuffer中的readerIndex和writerIndex

Netty中的buffer是徹底從新實現的,與NIO ByteBuffer與ByteBuffer不一樣的是,它內部保存了一個讀指針readerIndex和一個寫指針writerIndex,能夠同時進行讀和寫,而不須要使用flip()進行讀寫切換。AbstactChannelBuffer類裏面包含了主要的讀寫邏輯,貼一段代碼,讓你們能看的更明白一點:

public void writeByte(int value) {
setByte(writerIndex ++, value);
}

public byte readByte() {
if (readerIndex == writerIndex) {
throw new IndexOutOfBoundsException("Readable byte limit exceeded: "
+ readerIndex);
}
return getByte(readerIndex ++);
}

public int writableBytes() {
return capacity() - writerIndex;
}

public int readableBytes() {
return writerIndex - readerIndex;
}

這裏readerIndex老是小於writerIndex。我以爲這樣的方式很是天然,比單指針與flip()要更加好理解一些。AbstactChannelBuffer還有兩個相應的mark指針markedReaderIndex和markedWriterIndex,跟NIO的原理同樣,做標記用,這裏再也不贅述了。

3.2 字節序Endianness與HeapChannelBuffer

HeapChannelBuffer是最經常使用的Buffer,跟NIO HeapByteBuffer做用至關,其底層也是一個byte[]。

HeapChannelBuffer有兩個子類:BigEndianHeapChannelBuffer和LittleEndianHeapChannelBuffer。這裏有個很基礎的概念:字節序(ByteOrder/Endianness)。字節序規定了多於一個字節的數字(int啊long什麼的),如何在內存中表示。BIG_ENDIAN(大端序)表示高位在前,按照大端序,整型數12會被存儲爲0 0 0 12這樣四個字節,而LITTLE_ENDIAN則正好相反。可能搞C/C++的程序員對這個會比較熟悉,而Javaer則比較陌生一點,由於Java已經把內存給管理好了。可是在網絡編程方面,根據協議的不一樣,不一樣的字節序也可能會被用到。目前大部分協議仍是採用大端序,可參考RFC1700。

瞭解了這些知識,咱們也很容易就知道爲何會有BigEndianHeapChannelBuffer和LittleEndianHeapChannelBuffer了。

3.3 DynamicChannelBuffer

DynamicChannelBuffer是一個很方便的Buffer,之因此叫Dynamic是由於它的長度會根據內容的長度來擴充,你能夠像使用ArrayList同樣,無須關心其容量。DynamicChannelBuffer實現自動擴容的核心在於ensureWritableBytes方法,算法很簡單:在寫入前作容量檢查,容量不夠時,新建一個容量x2的buffer,跟ArrayList的擴容是相同的。貼一段代碼吧(爲了代碼易懂,這裏我刪掉了一些邊界檢查,只保留主邏輯):

public void writeByte(int value) {
    ensureWritableBytes(1);
    super.writeByte(value);
}

public void ensureWritableBytes(int minWritableBytes) {
    if (minWritableBytes <= writableBytes()) {
        return;
    }

    int newCapacity = capacity();
    int minNewCapacity = writerIndex() + minWritableBytes;
    while (newCapacity < minNewCapacity) {
        newCapacity <<= 1;
    }

    ChannelBuffer newBuffer = factory().getBuffer(order(), newCapacity);
    newBuffer.writeBytes(buffer, 0, writerIndex());
    buffer = newBuffer;
}

3.4 CompositeChannelBuffer

CompositeChannelBuffer是由多個ChannelBuffer組合而成的,能夠看作一個總體進行讀寫。這裏有一個技巧:CompositeChannelBuffer並不會開闢新的內存並直接複製全部ChannelBuffer內容,而是直接保存了全部ChannelBuffer的引用,並在子ChannelBuffer裏進行讀寫,從而實現了」Zero-Copy-Capable」。來段簡略版的代碼,應該更能說明其原理:

public class CompositeChannelBuffer{

    //components保存全部內部ChannelBuffer
    private ChannelBuffer[] components;
    //indices記錄在整個CompositeChannelBuffer中,每一個components的起始位置
    private int[] indices;
    //緩存上一次讀寫的componentId
    private int lastAccessedComponentId;

    public byte getByte(int index) {
        //經過indices中記錄的位置索引到對應第幾個子Buffer
        int componentId = componentId(index);
        return components[componentId].getByte(index - indices[componentId]);
    }

    public void setByte(int index, int value) {
        int componentId = componentId(index);
        components[componentId].setByte(index - indices[componentId], value);
    }

}

查找componentId的算法再次不做介紹了,你們本身實現起來也不會太難。值得一提的是,基於ChannelBuffer連續讀寫的特性,使用了順序查找(而不是二分查找),而且用lastAccessedComponentId來進行緩存。

3.5 ByteBufferBackedChannelBuffer

前面說ChannelBuffer是本身的實現的,其實只說對了一半。ByteBufferBackedChannelBuffer就是封裝了NIO ByteBuffer的類,用於實現堆外內存的Buffer(使用NIO的DirectByteBuffer)。固然,其實它也能夠放其餘的ByteBuffer的實現類。代碼實現就不說了,也沒啥可說的。

3.6 WrappedChannelBuffer

輸入圖片說明

WrappedChannelBuffer都是幾個對已有ChannelBuffer進行包裝,完成特定功能的類。代碼不貼了,實現都比較簡單,列一下功能吧。

輸入圖片說明

至此Netty 3.7的buffer部分咱們基本瞭解了,相關內容仍是比較簡單的,也沒有太多費腦細胞的地方。

Netty 4.0以後就不一樣了,ChannelBuffer更名ByteBuf,成爲了單獨項目buffer,而且爲了性能優化,加入了BufferPool之類的機制,已經變得比較複雜了(本質倒沒怎麼變)。性能優化是個比較複雜的事情,研究源碼時,建議先避開這些東西,瞭解其總體結構,等到須要深刻時再對算法進行細緻研究。舉個例子,Netty4.0裏爲了優化,將Map換成了Java 8裏6000行的ConcurrentHashMapV8,大家感覺一下…

下篇文章咱們開始講Channel。

相關文章
相關標籤/搜索