Netty源碼分析:AbstractByteBuf

Netty源碼分析:AbstractByteBuf

ByteBuf與Java NIO ByteBuffer類似,由於ByteBuffer存在一定的缺陷,具體缺陷如下:

1)ByteBuffer長度固定,一旦分配,則容量不能動態擴展和收縮

2)ByteBuffer只有一個標識位置的指針,讀寫的時候需要手動的調用flip()方法來進行從寫到讀模式的切換,否則讀出來的內容就是錯誤的。

而Netty實現的ByteBuf類似ByteBuffer,但其克服了以上兩個缺點,其支持動態擴容和收縮,利用兩個標識位置的指針來協助讀寫操作。

先看一個簡單的例子

    public class TestByteBuf {

        public static void main(String[] args) {
            //1 分配一個ByteBuf,然後寫入數據到此Buf
            ByteBuf byteBuf = Unpooled.buffer(256);
            String content = "wojiushimogui";
            byteBuf.writeBytes(content.getBytes());
            System.out.println("1、readerIndex:"+byteBuf.readerIndex());
            System.out.println("1、writerIndex:"+byteBuf.writerIndex());

            //2 開始讀數據
            int len = 5;
            byte[] readContent = new byte[len];
            byteBuf.readBytes(readContent);
            System.out.println("2、readContent:"+new String(readContent));
            System.out.println("2、readerIndex:"+byteBuf.readerIndex());
            System.out.println("2、writerIndex:"+byteBuf.writerIndex());

            //3 重用緩存區
            byteBuf.discardReadBytes();

            System.out.println("3、readerIndex:"+byteBuf.readerIndex());
            System.out.println("3、writerIndex:"+byteBuf.writerIndex());

        }
    }
    /*輸出
    1、readerIndex:0
    1、writerIndex:13
    2、readContent:wojiu
    2、readerIndex:5
    2、writerIndex:13
    3、readerIndex:0
    3、writerIndex:8
    *
    * */

上面就是關於ByteBuf的一個簡單例子,其中readerIndex是讀索引,二writerIndex爲寫索引,ByteBuf就是利用這兩個索引來進行緩存的讀寫的。

ByteBuf的主要類繼承關係

ByteBuf的主要類繼承關係如下:

從內存分配的角度看,ByteBuf可以分爲兩類:

(1)堆內存字節緩存區

優點:內存的分配和回收速度快,可以被JVM自動回收。
缺點:如果是進行Socket的I/O讀寫,則需要額外做一次內存複製,即將堆內存對應的緩衝區複製到內核Channel中,因此性能會有一定程度的下降。

(2)直接內存字節緩存區

其優點和缺點剛好與“堆內存字節緩存區”相反,即優點:如果進行Socket的I/O讀寫,則不需要進行復制,而由於其是在堆外內存分配,因此相比堆內存其分配和回收就慢一些。

從內存回收的角度看,ByteBuf可以分爲兩類:

(1)基於對象池的ByteBuf

優點:可以重用ByteBuf對象。即可以提升內存的使用效率。
缺點:內存池的管理和維護添加了複雜性。

(2)普通ByteBuf

其優缺點與ByteBuf相反。

以上兩種類型進行兩兩組合就構成了多種多樣的ByteBuf,供用戶選擇使用。

本篇博文主要介紹AbstractByteBuf這個抽象類,這個類是後面其他子類的基礎。

AbstractByteBuf

ByteBuf這個抽象類定義了許多方法,是標準。首先先看第一個子類:AbstractByteBuf,這個類定義了一些公共屬性(如下),這些公共屬性包括:讀索引、寫索引、mark以及最大容量等。

    public abstract class AbstractByteBuf extends ByteBuf {

        static final ResourceLeakDetector<ByteBuf> leakDetector = new ResourceLeakDetector<ByteBuf>(ByteBuf.class);

        int readerIndex;
        int writerIndex;
        private int markedReaderIndex;
        private int markedWriterIndex;

        private int maxCapacity;

        private SwappedByteBuf swappedBuf;

注意這裏的讀、寫索引是netty實現的ByteBuf對Java NIO ByteBuffer的position改善,即使用讀、寫兩個標識位置的指針來支持緩存的讀寫,而不像Java NIO ByteBuffer這個類只有position這個標識,我們經常要通過調用flip()函數來進行寫模式到讀模式的切換否則就讀出來的數據是錯誤的。具體關於ByteBuffer的解析可以看這篇博文[Java源碼分析》:Java NIO 之 Buffer]
(http://blog.csdn.net/u010412719/article/details/52775637).

在AbstractByteBuf中並沒有定義ByteBuf的緩衝區實現,這是因爲AbstractByteBuf並不清楚子類到底基於何種實現,可能是基於byte數組或者是DirectBytebBuffer。

AbstractByteBuf中定義了讀寫操作方法,在本博文的開始的demo中,就有用到,例如:byteBuf.writeBytes(content.getBytes());byteBuf.readBytes(readContent);下面進行介紹。

1) writeBytes(byte[] src)

下面就以這個函數爲例來介紹writeBytes方法。

    @Override
    public ByteBuf writeBytes(byte[] src) {
        writeBytes(src, 0, src.length);
        return this;
    }

這個方法調用瞭如下的重載方法,這個名爲writeByte還有很多重載方法,這裏不進行介紹。

    @Override
    public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
        ensureAccessible();
        ensureWritable(length);
        setBytes(writerIndex, src, srcIndex, length);
        writerIndex += length;
        return this;
    }

首先通過調用ensureAccessible()方法來確保該ByteBuf實例是存活的,是可以訪問的。

    protected final void ensureAccessible() {
    //根據refCnt()的源碼註釋得到:refCnt方法返回的是ByteBuf對象的引用計數,如果返回值爲零,則說明該對象被摧毀。
        if (refCnt() == 0) {
            throw new IllegalReferenceCountException(0);
        }
    }   

然後通過調用ensureWritable方法來確保可寫,該函數的源碼如下:

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

該函數是如何來判斷是否可寫呢?進行了如下判斷:
如果可寫容量大於等於minWritableBytes,則說明可寫,直接返回。如果小於,則首先判斷是否超過了最大可以容量,如果超過則跑異常,如果沒有則擴容。其中首先調用如下的calculateNewCapacity函數來計算自動擴容後的容量,入參爲滿足要求的最小容量。

    private int calculateNewCapacity(int minNewCapacity) {
        final int maxCapacity = this.maxCapacity;
        final int threshold = 1048576 * 4; // 4 MiB page

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

        // If over threshold, do not double but just increase by threshold.
        if (minNewCapacity > threshold) {
            int newCapacity = minNewCapacity / threshold * threshold;
            if (newCapacity > maxCapacity - threshold) {
                newCapacity = maxCapacity;
            } else {
                newCapacity += threshold;
            }
            return newCapacity;
        }

        // Not over threshold. Double up to 4 MiB, starting from 64.
        int newCapacity = 64;
        while (newCapacity < minNewCapacity) {
            newCapacity <<= 1;
        }

        return Math.min(newCapacity, maxCapacity);
    }

在calculateNewCapacity函數中, 有一個閾值:4MB,然後進行了如下的判斷來求新的容量:

(1)如果需要的新容量剛好等於此閾值,則直接使用此閾值作爲新容量然後返回。如果不等於,則會出現兩種情況,第一種是大於,第二種是小於。 這兩種對應了兩種分配內存的策略。

(2) 如果是大於閾值,則採用的是以閾值threshold爲基礎歩進的方式來得到新容量,其中與最大容量進行了比較,以防超過最大容量。

(3)如果是小於閾值,則是以64爲基礎來進行倍增,即64–>128–>256…,直到滿足最小容量要求的容量作爲新容量。

這兩種分配內存的策略結合值得我們學習,很經典,爲什麼這麼說呢?先倍增後步進帶來的好處爲:當內存比較小的時候倍增操作並不會帶來太多的內存浪費,當時在內存增大到一定閾值之後,如果繼續倍增則可能會帶來額外的內存浪費。

在調用 calculateNewCapacity 計算完目標容量之後,則需要重新創建新的緩衝區,並將原緩衝區的內容複製到新創建的ByteBuf中。這些都是調用capacity(newCapacity)中來完成的。由於不同的子類會對應不同的複製操作,所以AbstractByteBuf類中的該方法是一個抽象方法,留給子類自己來實現。

這裏我們看下UnpooledHeapByteBuf子類的該方法的實現:

下面的代碼比較簡單,就是簡單的new的一個新的數組,然後進行了複製。

    @Override
    public ByteBuf capacity(int newCapacity) {
        ensureAccessible();
        if (newCapacity < 0 || newCapacity > maxCapacity()) {
            throw new IllegalArgumentException("newCapacity: " + newCapacity);
        }

        int oldCapacity = array.length;
        if (newCapacity > oldCapacity) { //擴容
            byte[] newArray = new byte[newCapacity];
            System.arraycopy(array, 0, newArray, 0, array.length);
            setArray(newArray);
        } else if (newCapacity < oldCapacity) { //內存的收縮
            byte[] newArray = new byte[newCapacity];
            int readerIndex = readerIndex();
            if (readerIndex < newCapacity) {
                int writerIndex = writerIndex();
                if (writerIndex > newCapacity) {
                    writerIndex(writerIndex = newCapacity);
                }
                System.arraycopy(array, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
            } else {
                setIndex(newCapacity, newCapacity);
            }
            setArray(newArray);
        }
        return this;
    }

到這裏,在writeBytes方法中就調用ensureWritable(length)方法確保了ByteBuf可寫,繼續看writeBytes方法,該方法調用了setBytes(writerIndex, src, srcIndex, length);來完成數據的寫入,具體是如何寫入的,在AbstractByteBuf類中也沒有進行實現,這也是因爲由於不同的子類會對應不同的複製操作,所以AbstractByteBuf類中的該方法是一個抽象方法,留給子類自己來實現。

同樣這裏看下UnpooledHeapByteBuf子類的該方法的實現:

該方法首先首先調用checkSrcIndex方法來檢查目標數組的可讀空間是否夠,也再一次檢查是否ByteBuf可寫容量是否夠,如果不夠則跑異常。然後調用System.arraycopy將原數組的數據寫入到ByteBuf中。

    @Override
    public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
        checkSrcIndex(index, length, srcIndex, src.length);
        System.arraycopy(src, srcIndex, array, index, length);
        return this;
    } 

2)readBytes(byte[] dst)

上面分析了writeBytes(byte[] src)方法,這一節就來分析下從緩存中讀取數據的方法:readBytes(byte[] dst),其源代碼如下:

    @Override
    public ByteBuf readBytes(byte[] dst) {
        readBytes(dst, 0, dst.length);
        return this;
    } 

    @Override
    public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
        checkReadableBytes(length);
        getBytes(readerIndex, dst, dstIndex, length);
        readerIndex += length;
        return this;
    }

首先調用 checkReadableBytes方法檢查ByteBuf是否還有長度爲minimumReadableBytes的內容可讀。

    protected final void checkReadableBytes(int minimumReadableBytes) {
        ensureAccessible();
        if (minimumReadableBytes < 0) {
            throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");
        }
        if (readerIndex > writerIndex - minimumReadableBytes) {
            throw new IndexOutOfBoundsException(String.format(
                    "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
                    readerIndex, minimumReadableBytes, writerIndex, this));
        }
    } 

校驗通過之後,readBytes方法調用getBytes方法來從當前的讀索引開始,複製length個字節到目標byte數組中。由於不同的子類會對應不同的複製操作,所以AbstractByteBuf類中的該方法是一個抽象方法,留給子類自己來實現。

這裏還是看下UnpooledHeapByteBuf子類的該方法的實現:

該方法首先調用checkDstIndex方法來檢查目標數組的存儲空間是否夠用,也再一次檢查了ByteBuf的可讀內容是否夠。然後調用 System.arraycopy完成從ByteBuf中讀取內容到源數組中。

    @Override
    public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
        checkDstIndex(index, length, dstIndex, dst.length);
        System.arraycopy(array, index, dst, dstIndex, length);
        return this;
    } 

3) discardReadBytes()

該方法是用來重用緩衝區的。即將0~readerIndex-1之間這個區域重用起來,具體做法就是將[readerIndex,writerIndex)之間的數據拷貝到[0,writerIndex-readerIndex)的位置。具體代碼如下:

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

        if (readerIndex != writerIndex) {
            setBytes(0, this, readerIndex, writerIndex - readerIndex);
            writerIndex -= readerIndex;
            adjustMarkers(readerIndex);
            readerIndex = 0;
        } else {
            adjustMarkers(readerIndex);
            writerIndex = readerIndex = 0;
        }
        return this;
    }

由於是想將[readerIndex,writerIndex)之間的數據拷貝到[0,writerIndex-readerIndex)的位置,就有如下幾種情況:

1)首先判斷readerIndex是否等於0,如果等於0,則說明沒有可以重用的空間,直接返回。否則則說明有可以重用的緩衝區。繼續進行如下的判斷。

2)如果讀索引不等於0且不等於寫索引,則說明此緩衝區中既有已經讀取過的被丟棄的緩衝區也有還沒有讀取的可讀緩衝區,則調用setBytes方法將[readerIndex,writerIndex)之間的數據拷貝到[0,writerIndex-readerIndex)的位置,然後調整讀寫索引以及mark。

3)如果讀索引不等於0但等於寫索引,則說明緩衝區中則沒有可讀的數據了,則不需要進行數據的拷貝,只需要調整讀寫索引以及mark即可。

其中,上面所用到的調整mark的函數的代碼如下:

    protected final void adjustMarkers(int decrement) {
        int markedReaderIndex = this.markedReaderIndex;
        if (markedReaderIndex <= decrement) {
            this.markedReaderIndex = 0;
            int markedWriterIndex = this.markedWriterIndex;
            if (markedWriterIndex <= decrement) {
                this.markedWriterIndex = 0;
            } else {
                this.markedWriterIndex = markedWriterIndex - decrement;
            }
        } else {
            this.markedReaderIndex = markedReaderIndex - decrement;
            markedWriterIndex -= decrement;
        }
    } 

adjustMarkers函數中調整markedReaderIndex、markedWriterIndex的邏輯如下:由於markedReaderIndex、markedWriterIndex都不能小於0,因此首先判斷markedReaderIndex、markedWriterIndex是否小於需要減少的decrement,如果小於等於,則設置markedReaderIndex、markedWriterIndex爲零,如果不小於,則設置其爲當前值減去decrement之後的新值。

對於setBytes方法,由於不同的子類會對應不同的複製操作,所以AbstractByteBuf類中的該方法是一個抽象方法,留給子類自己來實現。 下面是UnpooledHeapByteBuf子類的該方法的實現:

    @Override
    public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) {
        checkSrcIndex(index, length, srcIndex, src.capacity());
        if (src.hasMemoryAddress()) {
            PlatformDependent.copyMemory(src.memoryAddress() + srcIndex, array, index, length);
        } else  if (src.hasArray()) {
            setBytes(index, src.array(), src.arrayOffset() + srcIndex, length);
        } else {
            src.getBytes(srcIndex, array, index, length);
        }
        return this;
    }

小結

本篇博文就從一個Demo中用到的三個方法(writeBytes、readBytes、discardReadBytes)入手,簡單分析了下AbstractByteBuf這個類,後面將會對其他子類進行分析。

參考資料

1、《Netty權威指南》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章