netty(十一)初識Netty - ByteBuf 創建與讀寫 一、ByteBuf使用

本章節主要學習一下netty當中的ByteBuf,ByteBuf是對字節數據的封裝。

接下來主要學習Bytebuf的使用以及其細節。

一、ByteBuf使用

1.1 創建ByteBuf

通常可以使用如下方式創建:

    public static void main(String[] args) {
        //創建一個容量爲10的ByteBuf
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
        System.out.println(buf);
    }

查看結果:

PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 10)

由結果我們看到初始化了一個池化的直接內存buf,也就是說如果我們使用默認類型創建,會返回一個直接內存。

ridx表示read index,即讀取位置是0;
widx表示write index,即寫入位置是0;
cap表示容量,是10;

1.1.1 直接內存 和 堆內存

  • 使用如下方式創建一個堆內存:

//創建一個堆內存buf
ByteBuf heapBuffer = ByteBufAllocator.DEFAULT.heapBuffer(10);

關於堆內存是通過jvm進行回收的,所以我們不需要過多的干預。

  • 使用如下方式創建一個直接內存

//創建一個直接內存buf
ByteBuf directBuffer = ByteBufAllocator.DEFAULT.directBuffer(10);

  • 直接內存的特性:
    • 直接內存創建和銷燬的代價昂貴,但讀寫性能高(少一次內存複製),適合配合池化功能一起用
    • 直接內存對 GC 壓力小,因爲這部分內存不受 JVM 垃圾回收的管理,但也要注意及時主動釋放

關於Netty中直接內存的介紹,在後面涉及到再繼續介紹,此處簡單帶過。

1.1.2 池化 和 非池化

無論在任何使用池的地方,例如數據庫連接池,線程池等等。

池化最大的意義莫過於可以重用資源

在Netty當中,引入池化機制,使得我們可以重用ByteBuf。

那麼池化對我們使用ByteBuf提供了哪些優點

  • 使我們不必在每次都得創建新的 ByteBuf 實例,這個操作對直接內存代價昂貴;即使是使用堆內存,也會增加 GC 壓力。
  • 可以重用池中 ByteBuf 實例,並且採用了與 jemalloc(http://jemalloc.net/) 類似的內存分配算法提升分配效率。
  • 高併發時,池化功能更節約內存,減少內存溢出的可能

1.1.3 ByteBuf的組成

其源碼如下:

public abstract class ByteBuf extends Object implements ReferenceCounted, Comparable<ByteBuf>

如上所示,是一個抽象類,繼承自Object,實現了ReferenceCounted,Comparable<ByteBuf>。

ReferenceCounted是用於ByteBuf的回收工作,從其名稱就能看出,是引用計數法,refCnt是引用數值;其提供了方法retain(),計數+1;release()方法,計數減1,當最終引用計數是0時,資源將被釋放。此處不多做介紹。

Comparable主要用作比較。

就像普通的原始字節數組一樣, ByteBuf使用從零開始的索引 。 這意味着第一個字節的索引始終爲0 ,最後一個字節的索引始終爲capacity - 1 。

ByteBuf提供了兩個指針變量來支持順序讀寫操作readerIndex用於讀操作和writerIndex用於寫操作。

下面通過圖的形式展示緩衝區的變化過程:

如上所示的緩衝區劃分方式,相比於之前學習的NIO中的ByteBuffer有一定的優勢:

  • 在ByteBuffer中,讀寫通過position這一個指針,需要通過flip()去切換;而在ByteBuf中,讀寫指針區分了,使用更加方便。
  • ByteBuf引入了自動擴容的能力。

1.2 寫入ByteBuf

關於ByteBuf的寫入,提供了很多的方法,我直接將圖粘在下方:

1.2.1 寫入示例

    public static void main(String[] args) {

        //申請長度是10的buffer
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
        //寫入5個長度的字節數組
        byte[] bytes = new byte[]{1, 2, 3, 4, 5};
        byteBuf.writeBytes(bytes);
        System.out.println(byteBuf);

        //再次寫入5個
        byteBuf.writeBytes(bytes);
        System.out.println(byteBuf);
    }

結果:

PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 10, cap: 10)

比較簡單,關於其他方式,不演示了。

1.2.2 大端寫入和小端寫入

在前面的圖中有一些方法是帶有LE的,可以成爲小端寫入,比如

writeIntLE(int value)

其中的LE是 Little Endian 的縮寫,可以理解爲小字節序、低字節序。

與之相對應的就是 Big Endian,大字節序或高字節序。高字節序沒有特別標註,因爲在網絡編程中通常使用的都是高字節序。如下面的代碼就是高字節序:

writeInt(int value)

舉例:寫入兩個數字888和888L

其二進制表示就是:

| 0000 0000 | 0000 0000 | 0000 0011 | 0111 1000 |
| 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0000 | 0000 0011 | 0111 1000 |

將其轉換成字節數組就是

[0,0,3,120]
[0,0,0,0,0,0,3,120]

有如下測試代碼:

    public static void main(String[] args) {
        int a = 888;

        //大端寫入
        ByteBuf byteBuf1 = ByteBufAllocator.DEFAULT.buffer();
        byteBuf1.writeInt(a);
        byte[] bytes1 = new byte[4];
        byteBuf1.readBytes(bytes1);
        System.out.println(Arrays.toString(bytes1));

        //小端寫入
        ByteBuf byteBuf2 = ByteBufAllocator.DEFAULT.buffer();
        byteBuf2.writeIntLE(a);
        byte[] bytes2 = new byte[4];
        byteBuf2.readBytes(bytes2);
        System.out.println(Arrays.toString(bytes2));

        long b = 888L;
        //大端寫入
        ByteBuf byteBuf3 = ByteBufAllocator.DEFAULT.buffer();
        byteBuf3.writeLong(b);
        byte[] bytes3 = new byte[8];
        byteBuf3.readBytes(bytes3);
        System.out.println(Arrays.toString(bytes3));

        //小端寫入
        ByteBuf byteBuf4 = ByteBufAllocator.DEFAULT.buffer();
        byteBuf4.writeLongLE(b);
        byte[] bytes4 = new byte[8];
        byteBuf4.readBytes(bytes4);
        System.out.println(Arrays.toString(bytes4));
    }

結果:

[0, 0, 3, 120]
[120, 3, 0, 0]
[0, 0, 0, 0, 0, 0, 3, 120]
[120, 3, 0, 0, 0, 0, 0, 0]

結論:int佔4個字節,long佔8個字節,大端寫入是從左向右寫;小端寫入從右向左寫。

網絡傳輸通常使用大端傳輸(Big Endian)

1.2.3 set方式的寫入

還有一些列以set命名的寫入方式,以下面爲例:

public abstract ByteBuf setBytes(int index, byte[] src)

這一類方法要求我們指定index,從此index開始寫入數據, 此方法不會修改此緩衝區的readerIndex或writerIndex 。

示例代碼如下:

    public static void main(String[] args) {

        //申請長度是10的buffer
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
        //寫入5個長度的字節數組
        byte[] bytes = new byte[]{1, 2, 3, 4, 5};
        //設置從index是5的位置開始寫入
        byteBuf.setBytes(5, bytes);
        System.out.println(byteBuf);

        //手動設置index寫入位置到10
        byteBuf.writerIndex(10);
        System.out.println(byteBuf);

        //長度10的字節數組進行讀取
        byte[] readBytes = new byte[10];
        byteBuf.readBytes(readBytes);
        System.out.println(Arrays.toString(readBytes));
    }

結果:

PooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10)
PooledUnsafeHeapByteBuf(ridx: 0, widx: 10, cap: 10)
[0, 0, 0, 0, 0, 1, 2, 3, 4, 5]

1.2.4 擴容

此處我們繼續使用在1.2.1小節中的代碼,繼續增加5個長度:

    public static void main(String[] args) {

        //申請長度是10的buffer
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
        //寫入5個長度的字節數組
        byte[] bytes = new byte[]{1, 2, 3, 4, 5};
        byteBuf.writeBytes(bytes);
        System.out.println(byteBuf);

        //再次寫入5個
        byteBuf.writeBytes(bytes);
        System.out.println(byteBuf);

        //再次寫入5個
        byteBuf.writeBytes(bytes);
        System.out.println(byteBuf);
}

結果:

PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 10, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 15, cap: 16)

如上所示發現最後一條的容量變成了16。可是我們初始給的只有10,此處就是自動進行了擴容操作。

AbstractByteBuf中的ensureWritable0方法就是擴容方法,其擴容代碼如下所示:

    final void ensureWritable0(int minWritableBytes) {
        //獲取當前寫入下標
        final int writerIndex = writerIndex();
        //預計寫入後的下標
        final int targetCapacity = writerIndex + minWritableBytes;
        //如果小於初始容量,不擴容
        if (targetCapacity <= capacity()) {
            ensureAccessible();
            return;
        }
        //超過最大容量,跑出異常
        if (checkBounds && targetCapacity > maxCapacity) {
            ensureAccessible();
            throw new IndexOutOfBoundsException(String.format(
                    "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                    writerIndex, minWritableBytes, maxCapacity, this));
        }

        // 將目標容量歸一化爲2的冪。
        //此處用初始化ByteBuf時指定的默認大小,會根據初始賦值計算出2的冪
        //會用該默認大小 減去 寫入位置的下標,得到能寫入的大小的最大值
        final int fastWritable = maxFastWritableBytes();
        //如果能寫入的最大值 大於 需要寫入的容量,則將能寫入的值 加上 當前寫入下標做爲新的容量
        //否則使用calculateNewCapacity去分配
        int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
                : alloc().calculateNewCapacity(targetCapacity, maxCapacity);

        // Adjust to the new capacity.
        capacity(newCapacity);
    }

maxFastWritableBytes內部會獲取一個maxLength,這個值是在allocate緩衝區是計算出來的2的冪,且小於512的,用它減去當前寫入index的位置。

    public int maxFastWritableBytes() {
        return Math.min(maxLength, maxCapacity()) - writerIndex;
    }

下面看下初始化時如何計算出的這個默認值:

int normalizeCapacity(int reqCapacity) {
        checkPositiveOrZero(reqCapacity, "reqCapacity");
        // 如果此處大於chunkSize(16777217),則將這個值作爲分配的值
        if (reqCapacity >= chunkSize) {
            return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
        }

        // >= 512
        if (!isTiny(reqCapacity)) {
            // 翻倍(乘以2)
            int normalizedCapacity = reqCapacity;
            normalizedCapacity --;
            normalizedCapacity |= normalizedCapacity >>>  1;
            normalizedCapacity |= normalizedCapacity >>>  2;
            normalizedCapacity |= normalizedCapacity >>>  4;
            normalizedCapacity |= normalizedCapacity >>>  8;
            normalizedCapacity |= normalizedCapacity >>> 16;
            normalizedCapacity ++;

            if (normalizedCapacity < 0) {
                normalizedCapacity >>>= 1;
            }
            assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;

            return normalizedCapacity;
        }

        if (directMemoryCacheAlignment > 0) {
            return alignCapacity(reqCapacity);
        }

        // Quantum-spaced
        if ((reqCapacity & 15) == 0) {
            return reqCapacity;
        }

        return (reqCapacity & ~15) + 16;
    }

在上面的代碼中,有一個默認值chunkSize,如果分配的值大於chunkSize(16777217),則將這個值作爲分配的值,而在後面擴容時ensureWritable0中的判斷:

int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable : this.alloc().calculateNewCapacity(targetCapacity, this.maxCapacity);

此時會走重新計算容量的方法:

his.alloc().calculateNewCapacity(targetCapacity, this.maxCapacity);

具體代碼如下:

    public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
        checkPositiveOrZero(minNewCapacity, "minNewCapacity");
        //如果需要的容量大於最大容量,則拋出異常
        if (minNewCapacity > maxCapacity) {
            throw new IllegalArgumentException(String.format(
                    "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
                    minNewCapacity, maxCapacity));
        }
        //定義一頁4M的閾值
        final int threshold = CALCULATE_THRESHOLD; // 4 MiB page

        //如果需要的容量等於頁閾值,就返回該閾值
        if (minNewCapacity == threshold) {
            return threshold;
        }

        // 如果需要容量大於 頁閾值
        if (minNewCapacity > threshold) {
            //計算最小需要幾個頁閾值的大小,並賦予一個新的容量值
            int newCapacity = minNewCapacity / threshold * threshold;
            //如果新容量,比 最大容量 - 一個分頁還要大,即剩餘不到一個頁,則賦予最大容量
            if (newCapacity > maxCapacity - threshold) {
                newCapacity = maxCapacity;
            } else {
                //在賦予的新容量基礎上 在加一個頁容量
                newCapacity += threshold;
            }
            return newCapacity;
        }

        // 如果沒有超過頁閾值,則從64開始,最大增加到4
        int newCapacity = 64;
        while (newCapacity < minNewCapacity) {
            //左移一位,即*2
            newCapacity <<= 1;
        }
        // 取新賦予容量和最大值中的最小值
        return Math.min(newCapacity, maxCapacity);
    }

關於擴容的問題就看到這裏了。

1.3 讀取ByteBuf

1.3.1 代碼演示

關於讀取方法都是和寫入相對應的,這裏不列舉了,直接上示例代碼

    public static void main(String[] args) {
        //申請長度是10的buffer
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);

        //寫入5個長度的字節數組
        byte[] bytes = new byte[]{1, 2, 3, 4, 5};
        byteBuf.writeBytes(bytes);
        System.out.println(byteBuf);

        //直接讀取下一個字節
        System.out.println(byteBuf.readByte());
        System.out.println(byteBuf);

        System.out.println(byteBuf.readByte());
        System.out.println(byteBuf);

        //讀取自己數組
        
        byteBuf.readBytes(new byte[3]);
        System.out.println(byteBuf);
    }

結果:

PooledUnsafeDirectByteBuf(ridx: 0, widx: 5, cap: 10)
1
PooledUnsafeDirectByteBuf(ridx: 1, widx: 5, cap: 10)
2
PooledUnsafeDirectByteBuf(ridx: 2, widx: 5, cap: 10)
PooledUnsafeDirectByteBuf(ridx: 5, widx: 5, cap: 10)

1.3.2 重複讀取

1)使用get開頭的方法,不會修改index。

2)使用mark:

    public static void main(String[] args) {
        //申請長度是10的buffer
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);

        //寫入5個長度的字節數組
        byte[] bytes = new byte[]{1, 2, 3, 4, 5};
        byteBuf.writeBytes(bytes);

        //設置一個mark
        byteBuf.markReaderIndex();

        //讀取自己數組
        System.out.println(byteBuf.readByte());
        System.out.println(byteBuf);

        //重置到mark
        byteBuf.resetReaderIndex();

        //讀取自己數組
        System.out.println(byteBuf.readByte());
        System.out.println(byteBuf);

    }

結果:

1
PooledUnsafeDirectByteBuf(ridx: 1, widx: 5, cap: 10)
1
PooledUnsafeDirectByteBuf(ridx: 1, widx: 5, cap: 10)

限於篇幅原因,先寫到這,後面繼續更新ByteBuf的文章。有學到幫忙點個贊啦~~

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