Netty8# Netty之ByteBuf初探

前言

字節的流動形成了流,Netty作爲優秀的通信框架他的字節是如何流動的,本文就理一下這個事。梳理完Netty的字節流動與JDK提供的ByteBuffer一對比看下Netty方便在哪裏。本分從官方文檔概念原理入手梳理,然後看下源碼解讀下這些原理如何實現的,體驗一把Netty寫入數據自動擴容,探究下這個過程如何實現的。

一、基本概念 


1.ByteBuf創建
 

使用Unpooled類來創建ByteBuf,不建議使用ByteBuf的構造函數自己去創建。

2.讀寫索引
 

ByteBuf提供了兩個指針readerIndex和writerIndex,分別記錄讀、寫的開始位置。兩個指針將ByteBuf分成了三個區域。

3.discardable bytes
 

這個區間的範圍爲0~readerIndex,已經被讀過的、可廢棄的區域。通過調用discardReadBytes(),可以釋放discardable bytes區域。這個區域釋放後,可寫區域(writable bytes)部分增多。

4.readable bytes
 

可讀區域的範圍爲(writerIndex-readerIndex)

5.writable bytes
 

可寫區域的範圍爲(capacity-writerIndex)

6.清理索引
 

調用Buffer.clear()後,讀寫索引全部歸零,緩存buffer被釋放。


二、ByteBuf的構建 


接下來通過示例竄下上面的知識點,看下源碼是如何實現的,示例中將字符串寫入ByteBuf中,然後再讀出來打印。
@Test
public void testWriteUtf81() {
     String str1 = "瓜農";
     ByteBuf buf = Unpooled.buffer(1);
     buf.writeBytes(str1.getBytes(CharsetUtil.UTF_8));
     ByteBuf readByteBuf = ByteBufUtil.readBytes(UnpooledByteBufAllocator.DEFAULT,buf,str1.getBytes(CharsetUtil.UTF_8).length);
     System.out.print(readByteBuf.toString(CharsetUtil.UTF_8));
}

源碼解讀

public static ByteBuf buffer(int initialCapacity) {
 return ALLOC.heapBuffer(initialCapacity); // 註解@1
}

public ByteBuf heapBuffer(int initialCapacity) {
  return heapBuffer(initialCapacity, DEFAULT_MAX_CAPACITY); // 註解@2
}

InstrumentedUnpooledUnsafeHeapByteBuf(UnpooledByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
  super(alloc, initialCapacity, maxCapacity);
}

public UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
  super(maxCapacity);

  if (initialCapacity > maxCapacity) {
    throw new IllegalArgumentException(String.format(
      "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
  }

  this.alloc = checkNotNull(alloc, "alloc");
  setArray(allocateArray(initialCapacity)); // 註解@3
  setIndex(00); // 註解@4
}

註解@1 使用ByteBufAllocator來分配ByteBuf,默認爲UnpooledByteBufAllocator。

註解@2 initialCapacity爲初始容量例子中給的爲16,maxCapacity爲默認的DEFAULT_MAX_CAPACITY=Integer.MAX_VALUE。

註解@3 allocateArray()的方法如下,此時使用JDK的byte[]初始化緩存區。通過setArray(),UnpooledHeapByteBuf持有byte[]緩存區。

 protected byte[] allocateArray(int initialCapacity) {
   return new byte[initialCapacity];
 }

private void setArray(byte[] initialArray) {
  array = initialArray;
  tmpNioBuf = null;
}

註解@4 初始化readerIndex和writerIndex,均爲0。

小結 ByteBuf的構建通過Unpooled來分配,示例中通過UnpooledByteBufAllocator持有byte[]、 readerIndex、writerIndex、maxCapacity完成ByteBuf的初始化。示例中array數組大小爲16;readerIndex=writerIndex=0;maxCapacity=Integer.MAX_VALUE。


三、寫入數據 


public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
  ensureWritable(length); // 註解@5
  setBytes(writerIndex, src, srcIndex, length); // 註解@6
  writerIndex += length; // 註解@7
  return this;
}

註解@5 確保剩餘的空間能夠容納需寫入的數據。

具體邏輯如下:如果寫入的數據長度小於已經分配的容量空間capacity則允許直接返回;

如果寫入的數據長度超過允許的最大容量maxCapacity直接拋出IndexOutOfBoundsException拒絕;

如果寫入數據長度大於已經分配的空間capacity但是小於最大最大允許空間maxCapacity,則需要擴容。

final void ensureWritable0(int minWritableBytes) {
    final int writerIndex = writerIndex(); // 註解@5.1
    final int targetCapacity = writerIndex + minWritableBytes; // 註解@5.2
    if (targetCapacity <= capacity()) { // 註解@5.3
        ensureAccessible();
        return;
    }
    if (checkBounds && targetCapacity > maxCapacity) { // 註解@5.4 
        ensureAccessible();
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }

    // Normalize the target capacity to the power of 2.
    final int fastWritable = maxFastWritableBytes(); // 註解@5.5 
    int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
            : alloc().calculateNewCapacity(targetCapacity, maxCapacity); // 註解@5.6

    // Adjust to the new capacity.
    capacity(newCapacity); // 註解@5.7
}

註解@5.1  獲取當前寫索引

註解@5.2 計算需要的容量

註解@5.3 與當前已分配的容量capacity進行比較

註解@5.4 不能超過最大允許的容量maxCapacity

註解@5.5 fastWritable = capacity() - writerIndex

註解@5.6 newCapacity的判斷通常走到這裏應該爲,剩餘的空間不夠了。所以通常會進入alloc().calculateNewCapacity(targetCapacity, maxCapacity)。

public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
        // ...
        final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
        if (minNewCapacity == threshold) { // 註解@5.6.1
            return threshold;
        }
        // If over threshold, do not double but just increase by threshold.
        if (minNewCapacity > threshold) { // 註解@5.6.2
            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) { // 註解@5.6.3
            newCapacity <<= 1;
        }

        return Math.min(newCapacity, maxCapacity);
    }

註解@5.6.1 如果寫入的數據長度剛好爲4M則返回threshold=4M

註解@5.6.2 如果寫入的數據長度大於4M,newCapacity不再翻倍增長,通過minNewCapacity / threshold * threshold計算剛容下需要的數據即可。

註解@5.6.3 如果寫入的數據長度小於4M,則newCapacity從64翻倍增長(128、256、512...),直到newCapacity能夠容納需要寫入的數據。

註解@5.7 確定了要擴容的容量newCapacity後,我們看下如何擴容的。

public ByteBuf capacity(int newCapacity) {
    checkNewCapacity(newCapacity);
    byte[] oldArray = array;
    int oldCapacity = oldArray.length;
    if (newCapacity == oldCapacity) {
      return this;
    }

    int bytesToCopy;
    if (newCapacity > oldCapacity) {
      bytesToCopy = oldCapacity;
    } else {
      trimIndicesToCapacity(newCapacity);
      bytesToCopy = newCapacity;
    }
    byte[] newArray = allocateArray(newCapacity); // 註解@5.7.1
    System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy); // 註解@5.7.2
    setArray(newArray); // 註解@5.7.3
    freeArray(oldArray); // 註解@5.7.4
    return this;
}

註解@5.7.1 使用新的容量初始化newArray=new byte[initialCapacity]

註解@5.7.2 將舊的oldArray數據拷貝到新的newArray=new中

註解@5.7.3 將UnpooledHeapByteBuf的byte[]引用替換爲newArray

註解@5.7.4 oldArray清理操作

註解@6 寫入數據,通過System.arraycopy將數據寫入array中。

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

註解@7 移動writerIndex指針。

小結: 將上面例子的initialCapacity設置成1,促使寫入數據時擴充容量。下面運行時截圖:array被擴容到64,writerIndex從0位置移動到6.

在寫入數據時,判斷剩餘容量是否足夠;不夠則需要擴容,如果寫入的數據小於4M,則雙倍增長,直到容納寫寫入的數據。如果寫入的數據大於4M,通過(minNewCapacity / threshold * threshold)計算需要擴容的大小。


四、讀出數據

從buf中把剛纔寫入的數據(”瓜農“)讀出來,通過工具類ByteBufUtil.readBytes來實現。

ByteBuf readByteBuf = ByteBufUtil.readBytes(UnpooledByteBufAllocator.DEFAULT,buf,str1.getBytes(CharsetUtil.UTF_8).length);
System.out.print(readByteBuf.toString(CharsetUtil.UTF_8));
public static ByteBuf readBytes(ByteBufAllocator alloc, ByteBuf buffer, int length) {
    boolean release = true;
    ByteBuf dst = alloc.buffer(length); // 註解@8
    try {
        buffer.readBytes(dst); // 註解@9
        release = false;
        return dst;
    } finally {
        if (release) {
            dst.release();
        }
    }
}

註解@8 重新構造了一個ByteBuf(dst)用於存儲讀取的數據

註解@9 讀取數據,並移動讀索引。

@Override
public ByteBuf readBytes(ByteBuf dst, int length) {
    if (checkBounds) {
        if (length > dst.writableBytes()) {
            throw new IndexOutOfBoundsException(String.format(
                    "length(%d) exceeds dst.writableBytes(%d) where dst is: %s", length, dst.writableBytes(), dst));
        }
    }
    readBytes(dst, dst.writerIndex(), length); // 註解@9.1
    dst.writerIndex(dst.writerIndex() + length); // 註解@9.2
    return this;
}

註解@9.1 讀取字節到新的ByteBuf。

public ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {
    checkReadableBytes(length);
    getBytes(readerIndex, dst, dstIndex, length); // 註解@9.1.1
    readerIndex += length; // 註解@9.1.2
    return this;
}

註解@9.1.1 通過native api UNSAFE.copyMemory() 實現byte數組之間的拷貝

註解@9.1.2 源byteBuf讀索引readerIndex向前移動

註解@9.2 數據讀入新構建的緩存區dst,dst的寫索引向前移動

小結: 示例中通過構造一個新的ByteBuf(dst),將源ByteBuf(buf)的數據讀入到dst。數據讀取結束後,源ByteBuf(buf)readerIndex向前移動;ByteBuf(dst)的writerIndex向前移動。

本文分享自微信公衆號 - 瓜農老梁(gh_01130ae30a83)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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