自頂向下深入分析Netty(九)--ByteBuf源碼分析

9.4 ByteBuf源碼分析

9.4.1 類圖

ByteBuf的子類實現非常多,其中關鍵的實現類如下:


ByteBuf類圖

可以使用兩種方式對ByteBuf進行分類:按底層實現方式和按是否使用對象池。

  1. 按底層實現
    1. HeapByteBuf
      HeapByteBuf的底層實現為JAVA堆內的字節數組。堆緩沖區與普通堆對象類似,位于JVM堆內存區,可由GC回收,其申請和釋放效率較高。常規JAVA程序使用建議使用該緩沖區。
    2. DirectByteBuf
      DirectByteBuf的底層實現為操作系統內核空間的字節數組。直接緩沖區的字節數組位于JVM堆外的NATIVE堆,由操作系統管理申請和釋放,而DirectByteBuf的引用由JVM管理。直接緩沖區由操作系統管理,一方面,申請和釋放效率都低于堆緩沖區,另一方面,卻可以大大提高IO效率。由于進行IO操作時,常規下用戶空間的數據(JAVA即堆緩沖區)需要拷貝到內核空間(直接緩沖區),然后內核空間寫到網絡SOCKET或者文件中。如果在用戶空間取得直接緩沖區,可直接向內核空間寫數據,減少了一次拷貝,可大大提高IO效率,這也是常說的零拷貝。
    3. CompositeByteBuf
      CompositeByteBuf,顧名思義,有以上兩種方式組合實現。這也是一種零拷貝技術,想象將兩個緩沖區合并為一個的場景,一般情況下,需要將后一個緩沖區的數據拷貝到前一個緩沖區;而使用組合緩沖區則可以直接保存兩個緩沖區,因為其內部實現組合兩個緩沖區并保證用戶如同操作一個普通緩沖區一樣操作該組合緩沖區,從而減少拷貝操作。
  2. 按是否使用對象池
    1. UnpooledByteBuf
      UnpooledByteBuf為不使用對象池的緩沖區,不需要創建大量緩沖區對象時建議使用該類緩沖區。
    2. PooledByteBuf
      PooledByteBuf為對象池緩沖區,當對象釋放后會歸還給對象池,所以可循環使用。當需要大量且頻繁創建緩沖區時,建議使用該類緩沖區。Netty4.1默認使用對象池緩沖區,4.0默認使用非對象池緩沖區。

9.4.2 關鍵類分析

9.4.2.1 ByteBuf

ByteBuf被定義為抽象類,但其中并未實現任何方法,故可看做一個接口,該接口擴展了ReferenceCounted實現引用計數。該類最重要的方法如下:

    int getInt(int index);
    ByteBuf setInt(int index, int value);
    int readInt();
    ByteBuf writeInt(int value);

這些方法從緩沖區取得或設置一個4字節整數,區別在于getInt()setInt()不會改變索引readInt()writeInt()分別會將讀索引和寫索引增加4,因為int占4個字節。該類方法有大量同類,可操作布爾數Boolean,字節Byte,字符Char,2字節短整數Short,3字節整數Medium,4字節整數Int,8字節長整數Long,4字節單精度浮點數Float,8字節雙精度浮點數Double以及字節數組ByteArray

該類的一些方法遵循這樣一個準則:空參數的方法類似常規getter方法,帶參數的方法類似常規setter方法。比如capacity()表示緩沖區當前容量,capacity(int newCapacity)表示設置新的緩沖區容量。在此列出這些方法的帶參數形式:

    ByteBuf capacity(int newCapacity); // 設置緩沖區容量
    ByteBuf order(ByteOrder endianness); // 設置緩沖區字節序
    ByteBuf readerIndex(int readerIndex); // 設置緩沖區讀索引
    ByteBuf writerIndex(int writerIndex); // 設置緩沖區寫索引

上述后兩個方法,可操作讀寫索引,除此之外,還有以下方法可以操作讀寫索引:

    ByteBuf setIndex(int readerIndex, int writerIndex); // 設置讀寫索引
    ByteBuf markReaderIndex();  // 標記讀索引,寫索引可類比
    ByteBuf resetReaderIndex(); // 重置為標記的讀索引
    ByteBuf skipBytes(int length); // 略過指定字節(增加讀索引)
    ByteBuf clear(); // 讀寫索引都置0

緩沖區可寫可讀性判斷,以可讀性為例:

    int readableBytes(); // 可讀的字節數
    boolean isReadable(); // 是否可讀
    boolean isReadable(int size); // 指定的字節數是否可讀

堆緩沖區和直接緩沖區的判斷,有以下方法:

    boolean hasArray(); // 判斷底層實現是否為字節數組
    byte[] array(); // 返回底層實現的字節數組
    int arrayOffset();  // 底層字節數組的首字節位置
    
    boolean isDirect(); // 判斷底層實現是否為直接ByteBuffer
    boolean hasMemoryAddress(); // 底層直接ByteBuffer是否有內存地址
    long memoryAddress(); // 直接ByteBuffer的首字節內存地址

這一組方法主要用于區分緩沖區的底層實現,前3個方法用于對底層實現為字節數組的堆緩沖區進行操作,后三個方法用于底層為NIO Direct ByteBuffer的緩沖區進行操作。
使用緩沖區時,經常需要在緩沖區查找某個特定字節或對某個字節進行操作,為此ByteBuf提供了一系列方法:

    // 首個特定字節的絕對位置
    int indexOf(int fromIndex, int toIndex, byte value);
    // 首個特定字節的相對位置,相對讀索引
    int bytesBefore(byte value);
    int bytesBefore(int length, byte value);
    int bytesBefore(int index, int length, byte value);
    
    // processor返回false時的首個位置
    int forEachByte(ByteBufProcessor processor);
    int forEachByte(int index, int length, ByteBufProcessor processor);
    int forEachByteDesc(ByteBufProcessor processor);
    int forEachByteDesc(int index, int length, ByteBufProcessor processor);

接口ByteBufProcessorprocess(byte)方法定義的不符合常規,返回true時表示希望繼續處理下一個字節,返回false時表示希望停止處理返回當前位置。由此,當我們希望查找到換行符時,代碼如下:

    public boolean process(byte value) throws Exception {
        return value != '\r' && value != '\n';
    }

這正是Netty默認實現的一個ByteBufProcessor FIND_CRLF,可見當我們期望查找換行符時,process的代碼卻要表達非換行符,此處設計不太合理,不人性化。作為折中,ByteBufProcessor中提供了大量默認的處理器,可滿足大量場景的需求。如果確有必要實現自己的處理器,一定注意實現方法需要反義。
復制或截取緩沖區也是一個頻繁操作,ByteBuf提供了以下方法:

    ByteBuf copy();
    ByteBuf copy(int index, int length);
    ByteBuf slice();
    ByteBuf slice(int index, int length);
    ByteBuf duplicate();

其中copy()方法生成的ByteBuf完全獨立于原ByteBuf,而slice()duplicate()方法生成的ByteBuf與原ByteBuf共享相同的底層實現,只是各自維護獨立的索引和標記,使用這兩個方法時,特別需要注意結合使用場景確定是否調用retain()增加引用計數。
有關JAVA NIO中的ByteBuffer的方法:

    int nioBufferCount();
    ByteBuffer nioBuffer();
    ByteBuffer nioBuffer(int index, int length);
    ByteBuffer internalNioBuffer(int index, int length);
    ByteBuffer[] nioBuffers();
    ByteBuffer[] nioBuffers(int index, int length);

這些方法在進行IO的寫操作時會大量使用,一般情況下,用戶很少調用這些方法。
最后需要注意的是toString()方法:

    String toString();
    String toString(Charset charset);
    String toString(int index, int length, Charset charset);

不帶參數的toString()方法是JAVA中Object的標準重載方法,返回ByteBuf的JAVA描述;帶參數的方法則返回使用指定編碼集編碼的緩沖區字節數據的字符形式。
不再列出ByteBuf繼承自ReferenceCounted接口的方法,可見ByteBuf是一個含有過多的抽象方法的抽象類(接口)。此處,Netty使用了聚合的設計方法,將ByteBuf的子類可能使用的方法都集中到了基類。再加上使用工廠模式生成ByteBuf,給用戶程序員帶來了極大便利:完全不用接觸具體的子類,只需要使用頂層接口進行操作。

了解了ByteBuf的所有方法,那么我們逐個擊破,繼續分析AbstractByteBuf

9.4.2.2 AbstractByteBuf

抽象基類AbstractByteBuf中定義了ByteBuf的通用操作,比如讀寫索引以及標記索引的維護、容量擴增以及廢棄字節丟棄等等。首先看其中的私有變量:

    int readerIndex; // 讀索引
    int writerIndex; // 寫索引
    private int markedReaderIndex; // 標記讀索引
    private int markedWriterIndex; // 標記寫索引

    private int maxCapacity; // 最大容量

與變量相關的setter和getter方法不再分析,分析第一個關鍵方法:計算容量擴增的方法calculateNewCapacity(minNewCapacity),其中參數表示擴增所需的最小容量:

    private int calculateNewCapacity(int minNewCapacity) {
        final int maxCapacity = this.maxCapacity;
        final int threshold = 1048576 * 4; // 4MB的閾值

        if (minNewCapacity == threshold) {
            return threshold;
        }

        // 所需的最小容量超過閾值4MB,每次增加4MB
        if (minNewCapacity > threshold) {
            int newCapacity = (minNewCapacity / threshold) * threshold;
            if (newCapacity > maxCapacity - threshold) {
                newCapacity = maxCapacity; // 超過最大容量不再擴增
            } else {
                newCapacity += threshold; // 增加4MB
            }
            return newCapacity;
        }

        // 此時所需的最小容量小于閾值4MB,容量翻倍
        int newCapacity = 64;
        while (newCapacity < minNewCapacity) {
            newCapacity <<= 1; // 使用移位運算表示*2
        }

        return Math.min(newCapacity, maxCapacity);
    }

可見ByteBuf的最小容量為64B,當所需的擴容量在64B和4MB之間時,翻倍擴容;超過4MB之后,則每次擴容增加4MB,且最終容量(小于maxCapacity時)為4MB的最小整數倍。容量擴增的具體實現與ByteBuf的底層實現緊密相關,最終實現的容量擴增方法capacity(newCapacity)由底層實現。
接著分析丟棄已讀字節方法discardReadBytes()

    public ByteBuf discardReadBytes() {
        if (readerIndex == 0) {
            return this;
        }

        if (readerIndex != writerIndex) {
            // 將readerIndex之后的數據移動到從0開始
            setBytes(0, this, readerIndex, writerIndex - readerIndex);
            writerIndex -= readerIndex; // 寫索引減少readerIndex
            adjustMarkers(readerIndex); // 標記索引對應調整
            readerIndex = 0; // 讀索引置0
        } else {
            // 讀寫索引相同時等同于clear操作
            adjustMarkers(readerIndex);
            writerIndex = readerIndex = 0;
        }
        return this;
    }

只需注意其中的setBytes(),從一個源數據ByteBuf中復制數據到ByteBuf中,在本例中數據源ByteBuf就是它本身,所以是將readerIndex之后的數據移動到索引0開始,也就是丟棄readerIndex之前的數據。adjustMarkers()重新調節標記索引,方法實現簡單,不再進行細節分析。需要注意的是:讀寫索引不同時,頻繁調用discardReadBytes()將導致數據的頻繁前移,使性能損失。由此,提供了另一個方法discardSomeReadBytes(),當讀索引超過容量的一半時,才會進行數據前移,核心實現如下:

    if (readerIndex >= capacity() >>> 1) {
        setBytes(0, this, readerIndex, writerIndex - readerIndex);
        writerIndex -= readerIndex;
        adjustMarkers(readerIndex);
        readerIndex = 0;
    }

當然,如果并不想丟棄字節,只期望讀索引前移,可使用方法skipBytes():

    public ByteBuf skipBytes(int length) {
        checkReadableBytes(length);
        readerIndex += length;
        return this;
    }

接下來以getInt()readInt()為例,分析常用的數據獲取方法。

    public int getInt(int index) {
        checkIndex(index, 4);   // 索引正確性檢查
        return _getInt(index);
    }

    protected abstract int _getInt(int index);

該方法對索引進行正確性檢查,然后將實際操作交給子類負責具體實現的_getInt()方法。

    public int readInt() {
        checkReadableBytes0(4); // 檢查索引
        int v = _getInt(readerIndex);
        readerIndex += 4;   // 讀索引增加
        return v;
    }

可見readIntgetInt的最大區別在于是否自動維護讀索引,readInt將增加讀索引,getInt則不會對索引產生任何影響。
數據設置方法setInt()writeInt()的實現可對應類比,代碼如下:

    public ByteBuf setInt(int index, int value) {
        checkIndex(index, 4);
        _setInt(index, value);
        return this;
    }

    protected abstract void _setInt(int index, int value);
    
    public ByteBuf writeInt(int value) {
        ensureAccessible();
        ensureWritable0(4);
        _setInt(writerIndex, value);
        writerIndex += 4;
        return this;
    }

此外,在AbstractByteBuf中實現了ByteBuf中很多和索引相關的無參方法,比如copy():

    public ByteBuf copy() {
        return copy(readerIndex, readableBytes());
    }

具體實現只對無參方法設定默認索引,然后委托給有參方法由子類實現。
最后再分析一下檢索字節的方法,比如indexOf()bytesBefore()forEachByte()等,其中的實現最后都委托給forEachByte(),核心代碼如下:

    private int forEachByteAsc0(int start, int end, 
            ByteBufProcessor processor) throws Exception {
        for (; start < end; ++start) {
            if (!processor.process(_getByte(start))) {
                return start;
            }
        }

        return -1;
    }

原理也很簡單,從頭開始使用_getByte()取出每個字節進行比較。if語句中的邏輯非!符號取反,這個設計并不好,容易讓人產生誤解。比如,indexOf()查找一個字節的ByteBufProcessor實現如下:

    private static class IndexOfProcessor implements ByteBufProcessor {
        private final byte byteToFind;

        public IndexOfProcessor(byte byteToFind) {
            this.byteToFind = byteToFind;
        }

        @Override
        public boolean process(byte value) {
            // 期望找到某個字節,但此處需要使用!=
            return value != byteToFind;
        }
    }

此外的其他方法不再分析,接著分析與引用計數相關的AbstractReferenceCountedByteBuf

9.4.2.3 AbstractReferenceCountedByteBuf

從名字可以推斷,該抽象類實現引用計數相關的功能。引用計數的功能簡單理解就是:當需要使用一個對象時,計數加1;不再使用時,計數減1。如何實現計數功能呢?考慮到引用計數的多線程使用情形,一般情況下,我們會選擇簡單的AtomicInteger作為計數,使用時加1,釋放時減1。這樣的實現是沒有問題的,但Netty選擇了另一種內存效率更高的實現方式:volatile + FieldUpdater
首先看使用的成員變量:

    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
            AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");

    private volatile int refCnt = 1;    // 實際的引用計數值,創建時為1

增加引用計數的方法:

    private ByteBuf retain0(int increment) {
        for (;;) {
            int refCnt = this.refCnt;
            final int nextCnt = refCnt + increment;

            if (nextCnt <= increment) {
                throw new IllegalReferenceCountException(refCnt, increment);
            }
            if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
                break;
            }
        }
        return this;
    }
    
    public ByteBuf retain() {
        return retain0(1);
    }

實現較為簡單,只需注意compareAndSet(obj, expect, update)。這是一個原子操作,當前字段的實際值如果與expect相同,則會將字段值更新為update;否則,更新失敗返回false。所以,代碼中使用for(;;)循環直到該字段的新值被更新。
減少引用計數的方法也類似:

    private boolean release0(int decrement) {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt < decrement) {
                throw new IllegalReferenceCountException(refCnt, -decrement);
            }

            if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
                if (refCnt == decrement) {
                    deallocate();
                    return true;
                }
                return false;
            }
        }
    }
    
    public boolean release() {
        return release0(1);
    }

至此,ByteBuf的抽象層代碼分析完畢。稍作休整,進入正題:ByteBuf的具體實現子類。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容