Netty 之 ByteBuf 分析

概述

Netty 中的 ByteBuf 和 NIO 中的 ByteBuffer 的區別。

  • 1、Netty 中的 ByteBuf 支持動態的擴容和縮容。而 NIO 中的 ByteBuffer不支持。
  • 2、Netty 中的 ByteBuf 使用了 readIndex 和 writeIndex 來進行讀寫操作,而 NIO 中的 ByteBuffer 讀寫時,還需要 filp()、rewind() 等操作進行讀寫切換才能讀寫。
  • 3、Netty 中的 ByteBuf 使用了 markedReaderIndex 和 markedWriterIndex 來進行標記,讀寫時標記互不影響。而 NIO 中的 ByteBuffer 則只有一個 mark。讀寫切換時 mark都重置爲 -1。
  • 4、Netty 還支持 ByteBuf 的池化技術。
    比如:
    PooledHeapByteBuf
    PooledUnsafeHeapByteBuf
    PooledDirectByteBuf
    PooledUnsafeDirectByteBuf

下面我們簡單分析下 Netty 中的 ByteBuf。

writeByte() 方法

Netty 的 ByteBuf 支持動態擴容。在 put 操作的時候會對剩餘可用空間進行校驗。如果剩餘空間不足,則會自動擴容 。NIO 中的 ByteBuffer 不支持動態擴容。
Netty 中的 ByteBuf 還簡化 NIO 中的 ByteBuffer 讀寫問題。
NIO 中 ByteBuffer,使用 postion、limit、capacity、mark 來維護讀寫操作、每次讀寫還需要 flip 等操作,而 Netty 中的 ByteBuf 簡化了讀寫操作,用 readerIndex 和 writeIndex 來簡化讀寫操作、讀寫不需要來回切換。

public ByteBuf writeByte(int value) {
    // 確保 buffer 可寫,並且處理擴容、縮容
    ensureWritable0(1);
    // 寫入 value,並增加 writerIndex
    _setByte(writerIndex++, value);
    return this;
}
final void ensureWritable0(int minWritableBytes) {
    // 1、判斷是否可訪問
    ensureAccessible();
    // 2、如果寫入字節小於可寫字節數,則跳出檢查
    if (minWritableBytes <= writableBytes()) {
        return;
    }
    if (checkBounds) {
        // 3、如果寫入字節大於 Buffer 剩餘可寫入量,則拋出異常
        if (minWritableBytes > maxCapacity - writerIndex) {
            throw new IndexOutOfBoundsException(String.format(
                    "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                    writerIndex, minWritableBytes, maxCapacity, this));
        }
    }

    // 4、計算 Buffer 的容量
    int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);

    // 5、調整 Buffer 的容量大小 (擴大或減小 capacity)
    capacity(newCapacity);
}

1、判斷是否可訪問
2、如果寫入字節小於可寫字節數,則跳出檢查
3、如果寫入字節大於 Buffer 剩餘可寫入量,則拋出異常
4、計算 Buffer 的容量
5、調整 Buffer 的容量大小 (擴大或減小 capacity)

計算 Buffer 擴容大小

/**
 * minNewCapacity 需要的支持的最小容量(寫索引+當前寫入字節)
 * maxCapacity 最大容量
 */
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
    // 參數合法性校驗
    if (minNewCapacity < 0) {
        throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");
    }
    if (minNewCapacity > maxCapacity) {
        throw new IllegalArgumentException(String.format(
                "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
                minNewCapacity, maxCapacity));
    }
    
    // 容量計算閾值,如果 Buffer 容量小於4M,則每次都擴容2倍,大於4M,則每次擴容4M
    final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
    // 如果需要寫入的容量大小等於 threshold,則直接返回 threshold值。
    if (minNewCapacity == threshold) {
        return threshold;
    }

    // 如果 minNewCapacity 超過 threshold, 就不再每次擴容2倍
    if (minNewCapacity > threshold) {
        int newCapacity = minNewCapacity / threshold * threshold;
        // 這塊邏輯大概意思:需要擴容的最小容量+4M,如果超過 maxCapacity,則把設置爲 maxCapacity
        if (newCapacity > maxCapacity - threshold) {
            newCapacity = maxCapacity;
        } 
        // 否則,每次擴容4M
        else {
            newCapacity += threshold;
        }
        return newCapacity;
    }

    // 不超過 threshold 閾值. 則每次擴容兩倍,如果小於64的,則直接返回64。
    int newCapacity = 64;
    while (newCapacity < minNewCapacity) {
        newCapacity <<= 1;
    }

    return Math.min(newCapacity, maxCapacity);
}

這裏的主要邏輯爲:
1、threshold 設置爲 4M,當需要擴容時判斷 minNewCapacity 是否等於 threshold ,如果等於則使用 threshold 作爲緩衝區大小。
2、如果minNewCapacity 容量大於 threshold 容量,就不能採用每次擴容2倍的方式進行擴容,而是採用每次擴容 4M 的方式。擴容時還需要判斷是否達到最大值,如果達到最大值,則使用最大值。
3、如果 minNewCapacity 容量小於 threshold 容量,就成倍的進行擴容,最小容量爲64。

調整 Buffer 的容量大小

@Override
public ByteBuf capacity(int newCapacity) {
    // 檢查 newCapacity 是否在 0 - maxCapacity 之間
    checkNewCapacity(newCapacity);

    int oldCapacity = array.length;
    byte[] oldArray = array;
    // 如果 newCapacity 大於 oldCapacity 則擴容
    if (newCapacity > oldCapacity) {
        // 創建一個長度爲 newCapacity 的 byte[] 數組
        byte[] newArray = allocateArray(newCapacity);
        // 把數據 copy 到新的 byte[] 數組中
        System.arraycopy(oldArray, 0, newArray, 0, oldArray.length);
        // newArray 替換原來的 array
        setArray(newArray);
        // UnpooledHeapByteBuf.freeArray() 無操作,空方法。
        freeArray(oldArray);
    } 
    // 如果 newCapacity 小於 oldCapacity 則縮減容量
    else if (newCapacity < oldCapacity) {
        // 創建一個長度爲 newCapacity 的 byte[] 數組
        byte[] newArray = allocateArray(newCapacity);
        int readerIndex = readerIndex();
        // 如果讀索引小於 Buffer 大小
        if (readerIndex < newCapacity) {
            int writerIndex = writerIndex();
            // 寫索引大於 newCapacity, 則把寫索引設置爲 newCapacity
            if (writerIndex > newCapacity) {
                writerIndex(writerIndex = newCapacity);
            }
            // 把數據 copy 到新的 byte[] 數組中
            System.arraycopy(oldArray, readerIndex, newArray, readerIndex, writerIndex - readerIndex);
        } 
        // 如果讀索引大於 newCapacity,則說明無可用的讀寫。則把讀寫索引設置爲 newCapacity
        else {
            setIndex(newCapacity, newCapacity);
        }
        // newArray 替換原來的 array
        setArray(newArray);
        freeArray(oldArray);
    }
    return this;
}

discardReadBytes

discardReadBytes 就相當於 NIO ByteBuffer 中的 compact() 方法,壓縮緩衝區,從而可以在緩衝區中寫更多的數據。

調用 discardReadBytes() 方法時需要注意的是,每次 discard 操作,都會發生字節數組的內存複製,所以頻繁的調用會導致性能下降,因此在調用之前確認下是否需要每次調用。

  • discardReadBytes() 之前
 
    +-------------------+------------------+------------------+
    | discardable bytes |  readable bytes  |  writable bytes  |
    +-------------------+------------------+------------------+
    |                   |                  |                  |
    0      <=      readerIndex   <=   writerIndex    <=    capacity
  • discardReadBytes() 之後
   +------------------+--------------------------------------+
   |  readable bytes  |    writable bytes (got more space)   |
   +------------------+--------------------------------------+
   |                  |                                      |
readerIndex (0) <= writerIndex (decreased)        <=        capacity

通過discard前後可用看出,discard後將會有更多的空間可用寫入數據。

/**
 * 丟棄掉已經讀過的數據,相當於 NIO ByteBuffer.compact() 方法。
 */
@Override
public ByteBuf discardReadBytes() {
    // 判斷是否可訪問
    ensureAccessible();
    // 如果 readerIndex = 0,說明沒有已經讀取過的數據,不需要 discard
    if (readerIndex == 0) {
        return this;
    }
    // 如果 readerIndex != writerIndex,則需要調整讀寫索引,並移動數據
    if (readerIndex != writerIndex) {
        // 複製readerIndex和writerIndex 之間的數據,前移到0的座標位置。
        setBytes(0, this, readerIndex, writerIndex - readerIndex);
        // 修改寫索引的值
        writerIndex -= readerIndex;
        // 調整 marker 值
        adjustMarkers(readerIndex);
        // 讀索引設置爲0
        readerIndex = 0;
    } 
    // 如果讀寫索引相等,說明沒有要讀取的數據,則直接把 讀寫索引置位0即可。相當於清空索引。
    else {
        // 調整 marker 值
        adjustMarkers(readerIndex);
        // 讀寫索引直接設置爲0,即可。
        writerIndex = readerIndex = 0;
    }
    return this;
}

調整 讀標記 和 寫標記

Netty 的 ByteBuf 使用了 markedReaderIndex 和 markedWriterIndex 來分別代表讀寫的標記。讀寫標記會不影響。

protected final void adjustMarkers(int decrement) {
    int markedReaderIndex = this.markedReaderIndex;
    // 如果讀標記小於discard的大小,讀標記設置爲0。
    if (markedReaderIndex <= decrement) {
        this.markedReaderIndex = 0;
        int markedWriterIndex = this.markedWriterIndex;
        // 如果寫標記小於 decrement,則把寫標記置位0,否則爲 寫標記減去 decrement 
        if (markedWriterIndex <= decrement) {
            this.markedWriterIndex = 0;
        } else {
            this.markedWriterIndex = markedWriterIndex - decrement;
        }
    } else {
        this.markedReaderIndex = markedReaderIndex - decrement;
        markedWriterIndex -= decrement;
    }
}

clear() 方法

public ByteBuf clear() {
    // 設置讀寫索引都爲0
    readerIndex = writerIndex = 0;
    return this;
}

只需要把 readerIndex 和 writerIndex 都設置爲0即可,而不需要清楚 Buffer 裏面的數據。因爲再寫的時候會覆蓋掉。

  • clear() 之前
+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0      <=      readerIndex   <=   writerIndex    <=    capacity
  • clear() 之後
+---------------------------------------------------------+
|             writable bytes (got more space)             |
+---------------------------------------------------------+
|                                                         |
0 = readerIndex = writerIndex            <=            capacity
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章