【轉】Netty那點事(二)Netty中的buffer

來源:互聯網
上載者:User

標籤:style   http   os   java   使用   io   strong   ar   資料   

【原文】https://github.com/code4craft/netty-learning/blob/master/posts/ch2-buffer.md上一篇文章我們概要介紹了Netty的原理及結構,下面幾篇文章我們開始對Netty的各個模組進行比較詳細的分析。Netty的結構最底層是buffer機制,這部分也相對獨立,我們就先從buffer講起。What:buffer二三事

buffer中文名又叫緩衝區,按照維基百科的解釋,是"在資料轉送時,在記憶體裡開闢的一塊臨時儲存資料的地區"。它其實是一種化同步為非同步機制,可以解決資料轉送的速率不對等以及不穩定的問題。

根據這個定義,我們可以知道涉及I/O(特別是I/O寫)的地方,基本會有Buffer了。就Java來說,我們非常熟悉的Old I/O--InputStream&OutputStream系列API,基本都是在內部使用到了buffer。Java課程老師就教過,必須調用OutputStream.flush(),才能保證資料寫入生效!

而NIO中則直接將buffer這個概念封裝成了對象,其中最常用的大概是ByteBuffer了。於是使用方式變為了:將資料寫入Buffer,flip()一下,然後將資料讀出來。於是,buffer的概念更加深入人心了!

Netty中的buffer也不例外。不同的是,Netty的buffer專為網路通訊而生,所以它又叫ChannelBuffer(好吧其實沒有什麼因果關係…)。我們下面就來講講Netty中得buffer。當然,關於Netty,我們必須講講它的所謂"Zero-Copy-Capable"機制。

When & Where:TCP/IP協議與buffer

TCP/IP協議是目前的主流網路通訊協定。它是一個多層協議,最下層是物理層,最上層是應用程式層(HTTP協議等),而做Java應用開發,一般只接觸TCP以上,即傳輸層和應用程式層的內容。這也是Netty的主要應用情境。

TCP報文有個比較大的特點,就是它傳輸的時候,會先把應用程式層的資料項目拆開成位元組,然後按照自己的傳輸需要,選擇合適數量的位元組進行傳輸。什麼叫"自己的傳輸需要"?首先TCP包有最大長度限制,那麼太大的資料項目肯定是要拆開的。其次因為TCP以及下層協議會附加一些協議頭資訊,如果資料項目太小,那麼可能報文大部分都是沒有價值的頭資訊,這樣傳輸是很不划算的。因此有了收集一定數量的小資料,並打包傳輸的Nagle演算法(這個東東在HTTP協議裡會很討厭,Netty裡可以用setOption("tcpNoDelay", true)關掉它)。

這麼說可能太學院派了一點,我們舉個例子吧:

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

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

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

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

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

Why:Buffer中的分層思想

我們先回到之前的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是什麼呢?

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

這裡可以看到,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是一個網路應用程式框架,包括網路以及應用的分層結構。

當然,在Netty裡,預設使用ChannelBuffer表示"Message",不失為一個比較實用的方法,但是MessageEvent.getMessage()是可以存放一個POJO的,這樣子抽象程度又高了一些,這個我們在以後講到ChannelPipeline的時候會說到。

How:Netty中的ChannelBuffer及實現

好了,終於來到了代碼實現部分。之所以囉嗦了這麼多,因為我覺得,關於"Zero-Copy-Capable Rich Byte Buffer",理解為什麼需要它,比理解它是怎麼實現的,可能要更重要一點。

我想可能很多朋友跟我一樣,喜歡"順藤摸瓜"式讀代碼--找到一個入口,然後順著查看它的調用,直到理解清楚。很幸運,ChannelBuffers(注意有s!)就是這樣一根"藤",它是所有ChannelBuffer實作類別的入口,它提供了很多靜態工具方法來建立不同的Buffer,靠“順藤摸瓜”式讀代碼方式,大致能把各種ChannelBuffer的實作類別摸個遍。先列一下ChannelBuffer相關類圖。

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

ChannelBuffer中的readerIndex和writerIndex

開始以為Netty的ChannelBuffer是對NIO ByteBuffer的一個封裝,其實不是的,它是把ByteBuffer重新實現了一遍

以最常用的HeapChannelBuffer為例,其底層也是一個byte[],與ByteBuffer不同的是,它是可以同時進行讀和寫的,而不需要使用flip()進行讀寫切換。ChannelBuffer讀寫的核心代碼在AbstactChannelBuffer裡,這裡通過readerIndex和writerIndex兩個整數,分別指向當前讀的位置和當前寫的位置,並且,readerIndex總是小於writerIndex的。貼兩段代碼,讓大家能看的更明白一點:

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

我倒是覺得這樣的方式非常自然,比單指標與flip()要更加好理解一些。AbstactChannelBuffer還有兩個相應的mark指標markedReaderIndexmarkedWriterIndex,跟NIO的原理是一樣的,這裡不再贅述了。

位元組序Endianness與HeapChannelBuffer

在建立Buffer時,我們注意到了這樣一個方法:public static ChannelBuffer buffer(ByteOrder endianness, int capacity);,其中ByteOrder是什麼意思呢?

這裡有個很基礎的概念:位元組序(ByteOrder/Endianness)。它規定了多餘一個位元組的數字(int啊long什麼的),如何在記憶體中表示。BIG_ENDIAN(大端序)表示高位在前,整型數12會被儲存為0 0 0 12四位元組,而LITTLE_ENDIAN則正好相反。可能搞C/C++的程式員對這個會比較熟悉,而Javaer則比較陌生一點,因為Java已經把記憶體給管理好了。但是在網路編程方面,根據協議的不同,不同的位元組序也可能會被用到。目前大部分協議還是採用大端序,可參考RFC1700。

瞭解了這些知識,我們也很容易就知道為什麼會有BigEndianHeapChannelBufferLittleEndianHeapChannelBuffer了!

DynamicChannelBuffer

DynamicChannelBuffer是一個很方便的Buffer,之所以叫Dynamic是因為它的長度會根據內容的長度來擴充,你可以像使用ArrayList一樣,無須關心其容量。實現自動擴容的核心在於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;    }
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來進行緩衝。

ByteBufferBackedChannelBuffer

前面說ChannelBuffer是自己的實現的,其實只說對了一半。ByteBufferBackedChannelBuffer就是封裝了NIO ByteBuffer的類,用於實現堆外記憶體的Buffer(使用NIO的DirectByteBuffer)。當然,其實它也可以放其他的ByteBuffer的實作類別。代碼實現就不說了,也沒啥可說的。

WrappedChannelBuffer

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

類名 入口 功能
SlicedChannelBuffer ChannelBuffer.slice()
ChannelBuffer.slice(int,int)
某個ChannelBuffer的一部分
TruncatedChannelBuffer ChannelBuffer.slice()
ChannelBuffer.slice(int,int)
某個ChannelBuffer的一部分, 可以理解為其實位置為0的SlicedChannelBuffer
DuplicatedChannelBuffer ChannelBuffer.duplicate() 與某個ChannelBuffer使用同樣的儲存, 區別是有自己的index
ReadOnlyChannelBuffer ChannelBuffers.unmodifiableBuffer(ChannelBuffer) 唯讀,你懂的

可以看到,關於實現方面,Netty 3.7的buffer相關內容還是比較簡單的,也沒有太多費腦細胞的地方。

而Netty 4.0之後就不同了。4.0,ChannelBuffer改名ByteBuf,成了單獨項目buffer,並且為了效能最佳化,加入了BufferPool之類的機制,已經變得比較複雜了(本質倒沒怎麼變)。效能最佳化是個很複雜的事情,研究源碼時,建議先避開這些東西,除非你對演算法情有獨鐘。舉個例子,Netty4.0裡為了最佳化,將Map換成了Java 8裡6000行的ConcurrentHashMapV8,你們感受一下…

參考資料:

    • TCP/IP協議 http://zh.wikipedia.org/zh-cn/TCP/IP%E5%8D%8F%E8%AE%AE
    • Data_buffer http://en.wikipedia.org/wiki/Data_buffer
    • Endianness http://en.wikipedia.org/wiki/Endianness

【轉】Netty那點事(二)Netty中的buffer

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.