ByteBuf和相關輔助類


當我們進行數據傳輸的時候,往往需要使用到緩沖區,常用的緩沖區就是JDK NIO類庫提供的java.nio.Buffer。

實際上,7種基礎類型(Boolean除外)都有自己的緩沖區實現,對於NIO編程而言,我們主要使用的是ByteBuffer。從功能角度而言,ByteBuffer完全可以滿足NIO編程的需要,但是由於NIO編程的復雜性,ByteBuffer也有其局限性,它的主要缺點如下。

(1)ByteBuffer長度固定,一旦分配完成,它的容量不能動態擴展和收縮,當需要編碼的POJO對象大於ByteBuffer的容量時,會發生索引越界異常;

(2)ByteBuffer只有一個標識位置的指針position,讀寫的時候需要手工調用flip()和rewind()等,使用者必須小心謹慎地處理這些API,否則很容易導致程序處理失敗;

(3)ByteBuffer的API功能有限,一些高級和實用的特性它不支持,需要使用者自己編程實現。

為了彌補這些不足,Netty提供了自己的ByteBuffer實現——ByteBuf。

ByteBuf的工作原理

ByteBuf通過兩個位置指針來協助緩沖區的讀寫操作,讀操作使用readerIndex,寫操作使用writerIndex。

readerIndex和writerIndex的取值一開始都是0,隨着數據的寫入writerIndex會增加,讀取數據會使readerIndex增加,但是它不會超過writerIndex。在讀取之后,0~readerIndex的就被視為discard的,調用discardReadBytes方法,可以釋放這部分空間,它的作用類似ByteBuffer的compact方法。readerIndex和writerIndex之間的數據是可讀取的,等價於ByteBuffer position和limit之間的數據。writerIndex和capacity之間的空間是可寫的,等價於ByteBuffer limit和capacity之間的可用空間。

由於寫操作不修改readerIndex指針,讀操作不修改writerIndex指針,因此讀寫之間不再需要調整位置指針,這極大地簡化了緩沖區的讀寫操作,避免了由於遺漏或者不熟悉flip()操作導致的功能異常。

初始分配的ByteBuf如圖:

寫入N個字節之后的ByteBuf如圖:

讀取M(<N)個字節之后的ByteBuf如圖:

調用discardReadBytes操作之后的ByteBuf如圖:

調用clear操作之后的ByteBuf如圖:

ByteBuf是如何實現動態擴展

通常情況下,當我們對ByteBuffer進行put操作的時候,如果緩沖區剩余可寫空間不夠,就會發生BufferOverflowException異常。為了避免發生這個問題,通常在進行put操作的時候會對剩余可用空間進行校驗,如果剩余空間不足,需要重新創建一個新的ByteBuffer,並將之前的ByteBuffer復制到新創建的ByteBuffer中,最后釋放老的ByteBuffer,代碼示例如下。 

        if(this.buffer.remaining() < needSize) {
            int toBeExtSize = needSize < 128 ? needSize : 128;
            ByteBuffer tmpBuffer = ByteBuffer.allocate(this.buffer.capacity() + toBeExtSize);
            this.buffer.flip();
            tmpBuffer.put(this.buffer);
            this.buffer = tmpBuffer;
        }

從示例代碼可以看出,為了防止ByteBuffer溢出,每進行一次put操作,都需要對可用空間進行校驗,這導致了代碼冗余,稍有不慎,就可能引入其他問題。為了解決這個問題,ByteBuf對write操作進行了封裝,由ByteBuf的write操作負責進行剩余可用空間的校驗,如果可用緩沖區不足,ByteBuf會自動進行動態擴展,對於使用者而言,不需要關心底層的校驗和擴展細節,只要不超過設置的最大緩沖區容量即可。當可用空間不足時,ByteBuf會幫助我們實現自動擴展。

AbstractByteBuf

    @Override
    public ByteBuf writeByte(int value) {
        ensureWritable(1);
        setByte(writerIndex++, value);
        return this;
    }

    @Override
    public ByteBuf ensureWritable(int minWritableBytes) {
        if (minWritableBytes < 0) {
            throw new IllegalArgumentException(String.format(
                    "minWritableBytes: %d (expected: >= 0)", minWritableBytes));
        }
        if (minWritableBytes <= writableBytes()) {
            return this;
        }
        if (minWritableBytes > maxCapacity - writerIndex) {
            throw new IndexOutOfBoundsException(String.format(
                    "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                    writerIndex, minWritableBytes, maxCapacity, this));
        }
        // Normalize the current capacity to the power of 2.
        int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);
        // Adjust to the new capacity.
        capacity(newCapacity);
        return this;
    }

通過源碼分析,我們發現當進行write操作時會對需要write的字節進行校驗,如果可寫的字節數小於需要寫入的字節數,並且需要寫入的字節數小於可寫的最大字節數時,對緩沖區進行動態擴展。無論緩沖區是否進行了動態擴展,從功能角度看使用者並不感知,這樣就簡化了上層的應用。

ByteBuf源碼思路

由於NIO的Channel讀寫的參數都是ByteBuffer,因此,Netty的ByteBuf接口必須提供API方便的將ByteBuf轉換成ByteBuffer,或者將ByteBuffer包裝成ByteBuf。考慮到性能,應該盡量避免緩沖區的復制,內部實現的時候可以考慮聚合一個ByteBuffer的私有指針用來代表ByteBuffer。

從內存分配的角度看,ByteBuf可以分為兩類。

(1)堆內存(HeapByteBuf)字節緩沖區:特點是內存的分配和回收速度快,可以被JVM自動回收;缺點就是如果進行Socket的I/O讀寫,需要額外做一次內存復制,將堆內存對應的緩沖區復制到內核Channel中,性能會有一定程度的下降。

(2)直接內存(DirectByteBuf)字節緩沖區:非堆內存,它在堆外進行內存分配,相比於堆內存,它的分配和回收速度會慢一些,但是將它寫入或者從Socket Channel中讀取時,由於少了一次內存復制,速度比堆內存快。

正是因為各有利弊,所以Netty提供了多種ByteBuf供開發者使用,經驗表明,ByteBuf的最佳實踐是在I/O通信線程的讀寫緩沖區使用DirectByteBuf,后端業務消息的編解碼模塊使用HeapByteBuf,這樣組合可以達到性能最優。

從內存回收角度看,ByteBuf也分為兩類:基於對象池的ByteBuf和普通ByteBuf。

兩者的主要區別就是基於對象池的ByteBuf可以重用ByteBuf對象,它自己維護了一個內存池,可以循環利用創建的ByteBuf,提升內存的使用效率,降低由於高負載導致的頻繁GC。測試表明使用內存池后的Netty在高負載、大並發的沖擊下內存和GC更加平穩。盡管推薦使用基於內存池的ByteBuf,但是內存池的管理和維護更加復雜,使用起來也需要更加謹慎,因此,Netty提供了靈活的策略供使用者來做選擇。

內存池原理分析 

Arena本身是指一塊區域,在內存管理中,Memory Arena是指內存中的一大塊連續的區域,PoolArena就是Netty的內存池實現類。

為了集中管理內存的分配和釋放,同時提高分配和釋放內存時候的性能,很多框架和應用都會通過預先申請一大塊內存,然后通過提供相應的分配和釋放接口來使用內存。這樣一來,對內存的管理就被集中到幾個類或者函數中,由於不再頻繁使用系統調用來申請和釋放內存,應用或者系統的性能也會大大提高。在這種設計思路下,預先申請的那一大塊內存就被稱為Memory Arena。

不同的框架,Memory Arena的實現不同,Netty的PoolArena是由多個Chunk組成的大塊內存區域,而每個Chunk則由一個或者多個Page組成,因此,對內存的組織和管理也就主要集中在如何管理和組織Chunk和Page了。

PoolChunk 

Chunk主要用來組織和管理多個Page的內存分配和釋放,在Netty中,Chunk中的Page被構建成一棵二叉樹。假設一個Chunk由16個Page組成,那么這些Page將會被按照下圖所示的形式組織起來。 

Page的大小是4個字節,Chunk的大小是64個字節(4×16)。整棵樹有5層,第1層(也就是葉子節點所在的層)用來分配所有Page的內存,第4層用來分配2個Page的內存,依次類推。

每個節點都記錄了自己在整個Memory Arena中的偏移地址,當一個節點代表的內存區域被分配出去之后,這個節點就會被標記為已分配,自這個節點以下的所有節點在后面的內存分配請求中都會被忽略。舉例來說,當我們請求一個16字節的存儲區域時,上面這個樹中的第3層中的4個節點中的一個就會被標記為已分配,這就表示整個Memroy Arena中有16個字節被分配出去了,新的分配請求只能從剩下的3個節點及其子樹中尋找合適的節點。

對樹的遍歷采用深度優先的算法,但是在選擇哪個子節點繼續遍歷時則是隨機的,並不像通常的深度優先算法中那樣總是訪問左邊的子節點。

PoolSubpage 

對於小於一個Page的內存,Netty在Page中完成分配。每個Page會被切分成大小相等的多個存儲塊,存儲塊的大小由第一次申請的內存塊大小決定。假如一個Page是8個字節,如果第一次申請的塊大小是4個字節,那么這個Page就包含2個存儲塊;如果第一次申請的是8個字節,那么這個Page就被分成1個存儲塊。 

一個Page只能用於分配與第一次申請時大小相同的內存,比如,一個4字節的Page,如果第一次分配了1字節的內存,那么后面這個Page只能繼續分配1字節的內存,如果有一個申請2字節內存的請求,就需要在一個新的Page中進行分配。

Page中存儲區域的使用狀態通過一個long數組來維護,數組中每個long的每一位表示一個塊存儲區域的占用情況:0表示未占用,1表示以占用。對於一個4字節的Page來說,如果這個Page用來分配1個字節的存儲區域,那么long數組中就只有一個long類型的元素,這個數值的低4位用來指示各個存儲區域的占用情況。對於一個128字節的Page來說,如果這個Page也是用來分配1個字節的存儲區域,那么long數組中就會包含2個元素,總共128位,每一位代表一個區域的占用情況。

內存回收策略

無論是Chunk還是Page,都通過狀態位來標識內存是否可用,不同之處是Chunk通過在二叉樹上對節點進行標識實現,Page是通過維護塊的使用狀態標識來實現。

對於使用者來說,不需要關心內存池的實現細節,也不需要與這些類庫打交道,只需要按照API說明正常使用即可。

輔助類功能介紹

ByteBufHolder 

ByteBufHolder是ByteBuf的容器,在Netty中,它非常有用,例如HTTP協議的請求消息和應答消息都可以攜帶消息體,這個消息體在NIO ByteBuffer中就是個ByteBuffer對象,在Netty中就是ByteBuf對象。由於不同的協議消息體可以包含不同的協議字段和功能,因此,需要對ByteBuf進行包裝和抽象,不同的子類可以有不同的實現。為了滿足這些定制化的需求,Netty抽象出了ByteBufHolder對象,它包含了一個ByteBuf,另外還提供了一些其他實用的方法,使用者繼承ByteBufHolder接口后可以按需封裝自己的實現。

ByteBufAllocator 

ByteBufAllocator是字節緩沖區分配器,按照Netty的緩沖區實現不同,共有兩種不同的分配器:基於內存池的字節緩沖區分配器和普通的字節緩沖區分配器。

CompositeByteBuf

CompositeByteBuf允許將多個ByteBuf的實例組裝到一起,形成一個統一的視圖,有點類似於數據庫將多個表的字段組裝到一起統一用視圖展示。

CompositeByteBuf在一些場景下非常有用,例如某個協議POJO對象包含兩部分:消息頭和消息體,它們都是ByteBuf對象。當需要對消息進行編碼的時候需要進行整合,如果使用JDK的默認能力,有以下兩種方式:

 (1)將某個ByteBuffer復制到另一個ByteBuffer中,或者創建一個新的ByteBuffer,將兩者復制到新建的ByteBuffer中;

(2)通過List或數組等容器,將消息頭和消息體放到容器中進行統一維護和處理。

上面的做法非常別扭,實際上我們遇到的問題跟數據庫中視圖解決的問題一致——緩沖區有多個,但是需要統一展示和處理,必須有存放它們的統一容器。為了解決這個問題,Netty提供了CompositeByteBuf。

它定義了一個Component類型的集合,實際上Component就是ByteBuf的包裝實現類,它聚合了ByteBuf對象,維護了在集合中的位置偏移量信息等

ByteBufUtil 

ByteBufUtil是一個非常有用的工具類,它提供了一系列靜態方法用於操作ByteBuf對象。

其中最有用的方法就是對字符串的編碼和解碼,具體如下。

(1)encodeString(ByteBufAllocator alloc, CharBuffer src, Charset charset):對需要編碼的字符串src按照指定的字符集charset進行編碼,利用指定的ByteBufAllocator生成一個新的ByteBuf;

(2)decodeString(ByteBuffer src, Charset charset):使用指定的ByteBuffer和charset進行對ByteBuffer進行解碼,獲取解碼后的字符串。

還有一個非常有用的方法就是hexDump,它能夠將參數ByteBuf的內容以十六進制字符串的方式打印出來,用於輸出日志或者打印碼流,方便問題定位,提升系統的可維護性。hexDump包含了一系列的方法,參數不同,輸出的結果也不同。

 

 


免責聲明!

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



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