ByteBuf : Netty的數據容器類

哪里描述不正確望指正. 歡迎轉載
大綱

知識點 概括
ByteBuf的數據模型 描述ByteBuf基本概念
來自不同地區的ByteBuf 介紹存儲在不同內存區域的ByteBuf及其優缺點
byte級別的操作 ByteBuf基本的讀寫查操作
派生ByteBuf ByetBuf創建不同類型的副本
多種方式分配ByteBuf 介紹池化/非池化ByteBuf以及如何分配池化/非池化的ByteBuf
釋放ByteBuf 基于引用計數, 釋放資源
數據容器的選擇
  • Java NIO使用ByteBuffer.class
  • Netty使用ByteBuf.class

Netty官方聲稱ByteBufByteBuffer更加方便使用

  • 讀和寫使用了不同的索引(讀寫模式切換不需要調用ByteBuffer::filp方法)
  • 支持方法的鏈式調用
  • 支持引用計數(釋放ByteBuf資源)
  • 支持池化(避免冗余)
  • 可以被用戶自定義的緩沖區類型擴展
  • 通過內置的復合緩沖區類型實現了透明的零拷貝
  • 容量可以按需增長

ByteBuf的數據模型

ByteBuf的數據模型.png

與NIOByteBuffer不同的是NettyByteBuf維護了兩個索引

  • readerIndex
    調用read*()方法時, index+
    調用get*()方法時, index不變
  • writerIndex
    調用write*()方法時, index+
    調用set*()方法時, index不變
    調用skip*()方法時, index+

index的變化只與read*()write*()skip*()方法有關


來自不同地區的ByteBuf
  • 堆緩沖區

將數據存儲在 JVM 的堆空間中, 該模式被稱為支撐數組(backing array), 最常用.

  • 優點
    能在沒有使用池化的情況下提供快速的分配和釋放(在堆上直接調用JAVA的API, 不用調用操作系統的接口)
  • 缺點
    傳輸時, 會先拷貝到直接緩沖區

聲明一個支撐數組

ByteBuf buf = new UnpooledHeapByteBuf(ByteBufAllocator.DEFAULT, initalCapacity, maxCapacity);
//默認capacity是256
ByteBuf buf = Unpooled.buffer(capacity);
//建議池化方式創建, 下文將講到如何分配ByteBuf

判斷ByteBuf是否在JVM heap中, 并處理數組

ByteBuf heapBuf = Unpooled.copiedBuffer("Hello Netty", CharsetUtil.UTF_8);
if (heapBuf.hasArray()) { //檢查支撐數組(true)
  byte[] array = heapBuf.array();
  handleArray(array);//處理
} 

如果ByteBuf::hasArray返回false時再次訪問ByteBuf::array則拋出異常UnsupportedOperationException
調用ByteBuf::array之前總是要判斷ByteBuf::hasArray

  • 直接緩沖區

通過本地調用來為ByteBuf分配內存(非JVM heap)

  • 優點
    直接緩沖區對于網絡數據傳輸是理想的選擇, 傳輸時ByteBuf不用經過中間緩存區
  • 缺點
    分配和釋放代價大(調用操作系統API), 下文中池化的緩沖區可以緩解這個缺點

聲明一個直接緩沖區數組

ByteBuf buf = new UnpooledDirectByteBuf(ByteBufAllocator.DEFAULT, initialCapacity, maxCapacity);
//默認capacity是256
ByteBuf buf = Unpooled.directBuffer(capacity);
//建議池化方式創建, 下文將講到如何分配ByteBuf

直接緩沖區的用法ByteBuf就如同其名字, 可以直接傳輸(無需經過中間緩存區)

  • 復合緩沖區

聚合多個ByteBuf, Netty 通過一個 ByteBuf.class 子類——CompositeByteBuf.class——實現了這個模式,將多個ByteBuf聚合在一起提供統一操作

  • CompositeByteBuf中可能同時存在直接內存分配和非直接內存分配. 如果只有一個ByteBuf則調用CompositeByteBuf::hasArray時相當于調用ByteBuf::hasArray, 否則存在多個實例時, 調用CompositeByteBuf::hasArray時直接返回false
  • CompositeByteBuf.class內部維護了一個arrayList用來存放ByteBuf

使用場景

Http請求由header和(1或n個)content組成, 我們可以將headerByteBuf和contentByteBuf聚合為一個CompositeByteBuf

image.png

構建我們的Http請求主體

class HttpMessage{
  //聚合器
  CompositeByteBuf message;
  //使用不同內存模型的ByteBuf
  ByteBuf header;
  ByteBuf content;
  public HttpMessage(){
    message = Unpooled.compositeBuffer();
    header = Unpooled.directBuffer();
    content = Unpooled.buffer();
    //聚合
    message.addComponents(header, content);
    //也可以移除
    //message.removeComponent(0);
  }
}

訪問我們的聚合器

//循環遍歷每個component
message.forEach(byteBuf -> System.out.println(byteBuf.toString(CharsetUtil.UTF_8)));
//逐個獲取component
ByteBuf header2 = message.component(0);
ByteBuf content2 = message.component(1);
//處理消息
handle(header2, content2);
//...

byte級別的操作

在高并發項目開發中, 如果能讓我們靈活地操作字節(分配和釋放), 系統性能和吞吐量將會有所提升

  • 隨機讀寫
    ByteBuf::getByte(int)讀取任意位置的字節
    ByteBuf::setByte(int)寫入任意位置的字節
  • 順序讀寫

由于ByteBuf有兩個索引, 所以一個ByteBuf的數據可被兩個索引拆分為三個部分, 對應順序訪問中三個重要的概念

  • 可丟棄字節
  • 可讀字節
  • 可寫字節
    image.png

丟棄可丟棄字節, 通過調用ByteBuf::discardReadBytes方法, readerIndex會變為0, writerIndex會減少, 從而將可丟棄字節拋棄, 不過這會導致內存復制, 因為要把readerIndexwriterIndex之間的內容往左移動. 這里要注意的是writerIndexcapacity之間的數據不會移動也不會改變, 除非ByteBuf容量很緊湊, 否則應該少用該方法

read*()skip*()方法會增加當前readerIndex. 注意特例readBytes(ByteBuf dest)方法(將讀取的byte寫入dest)會增加當前ByteBufreaderIndex也會增加destwriterIndex, 但readBytes(ByteBuf dest, int dstIndex, int length )不會改變destwriterIndex, 因為指定了下標參數(具體請查看netty官方文檔)

同上, writeBytes(ByteBuf dest)如果沒有自定下標參數, 同樣會增加dest的writerIndex

ByteBuf提供了一系列的字節級別讀寫, 舉個簡單的例子
readByte: 讀取1個byte 然后 readerIndex+1
readInt: 讀取4個byte 然后readerIndex+4
writeByte:寫入1個byte 然后 writerIndex+1
writeInt:寫入4個byte 然后 writerIndex+4
注意: 為了養成一個好的習慣, 讀寫時要隨時注意ByteBuf::readableBytesByteBuf::writableBytes是否大于0, 否則拋出IndexOutOfBoundException

  • 管理索引(readerIndex,writerIndex)

在操作ByteBuf的時候, 可能需要暫時存下當前索引的位置, 稍后返回該索引位置. 或者需要根據協議自定義跳轉到指定索引位置

  1. 保存和重置索引: ByteBuf::markReaderIndexByteBuf::resetReaderIndex, 同理有ByteBuf::markWriterIndexByteBuf::resetWriterIndex
  2. 將索引移動到指定位置: ByteBuf::readerIndexByteBuf::writerIndex(都接收int參數)
  3. 清空ByteBuf: ByteBuf::clear(注意此方法不是將內容刪除,而是將readerIndexwriterIndex重置為0)
  • 查找

簡單查找: ByteBuf::indexOf, 接收一個byte參數, 返回第一個出現的index

高級查找: ByteBufProcessor已經在新版本中被棄用, 所以這里只關注ByteProcessor

ByteBufProcessor接口定義了多個常量并且內部有兩個實現類IndexOfProcessorIndexNotOfProcessor
IndexOfProcessor用來查找第一個出現的字符下標, IndexNotOfProcessor用來查找第一個不一樣的字符下標.
為了區分上面兩個類的作用, 簡單舉個例子:
假設我們有ByteBuf內容: Netty

//找出'N'出現的第一個下標
int indexOf = buf.forEachByte(new ByteProcessor.IndexOfProcessor((byte)'N'));

上面語句的結果: indexOf = 0

//找出第一個不是'N'的下標
int indexNotOf = buf.forEachByte(new ByteProcessor.IndexNotOfProcessor(((byte) 'N')));

上面語句的結果: indexNotOf = 1
ByteBufProcessor還定義了一些常量, 下面簡單地示范

//查找 LF ('\n')
buf.forEachByte(ByteProcessor.FIND_LF);

派生ByteBuf

以下方法用來獲取ByteBuf的一個新實例, 這些實例擁有自己獨立的索引(標記和讀寫指針), 但數據內容共享(同一個引用)

  • ByteBuf::duplicate返回所有內容
  • Bytebuf::slice返回readerIndexwriterIndex之間的內容
  • Bytebuf::slice(int, int)
  • ByteBuf::order
  • Unpooled::unmodifiableBuffer

如果要生成一個數據獨立的副本, 使用ByteBuf::copy

另外一種派生ByteBuf的方式是ByteBufHolder. 每個ByteBufHolder維護一個ByteBuf, 我們可以將ByteBufHolder看作一個池, 用戶可以向ByteBufHolder借用ByteBuf, 并在沒用的時候釋放. 關于ByteBufHolder的實際用途, 本人目前尚未了解清楚, 希望了解的讀者可以向我反饋


多種方式分配ByteBuf
  • 什么是池化/非池化的ByteBuf?
    Netty預先向操作系統申請一塊內存, 用來存放池化的數據, 當我們創建一個池化的ByteBuf時, 一般不會創建新的ByteBuf, 而是復用了之前創建好的, 當我們的ByteBuf使用完, 釋放完之后, ByteBuf不會被銷毀, 而是被Netty放回了池中, 等待下一個請求, 繼續分配這個ByteBuf, 這就是池化的ByteBuf
    而非池化的ByteBuf, 每次被請求時, 都會創建新的, 被釋放時, 對象都會被銷毀

  • 為什么要使用池化的ByteBuf?
    池化ByteBuf是為了避免創建/銷毀ByteBuf造成的開銷, 在許多Netty應用中, 一個請求所生成的ByteBuf在邏輯上是轉瞬即逝的, 請求返回之后, 這些數據就成了GC回收的目標, 如果沒有及時清理, 那么內存將會無限增加(Netty太快了).

下面介紹兩種分配ByteBuf的途徑, 通過獲取到的分配器, 我們就可以根據需求分配池化/非池化的ByteBuf

  1. ByteBufAllocator
    一般從Channel::alloc或者ChannelHandlerContext::alloc中獲取
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
...

ByteBufAllocator提供了一系列方法, 可以用來創建來自不同地區的ByteBuf, 基于堆內存, 基于直接內存, 聚合器, 還有阻塞型(I/O)的ByteBuf

ByteBufAllocator有兩個實現類PooledByteBufAllocatorUnpooledByteBufAllocator
PooledByteBufAllocator池化了ByteBuf以用來減少內存碎片, 提高性能
UnpooledByteBufAllocator創建的ByteBuf都是一個新實例

  • Netty默認使用了池化的Allocator, 但我們可以在引導中自定使用池化還是非池化分配器
  1. Unpooled
    當無法在Channel或者ChannelHandlerContext獲取ByteBufAllocator時, 推薦使用Unpooled(靜態工具類)來創建未池化的ByteBuf

釋放ByteBuf
  • 引用計數

Netty在第4版中為ByteBufByteBufHolder引入了引用計數技術, 它們都實現了ReferenceCounted接口
當一個對象所持有的資源被多個對象引用, 假設引用計數為n, 每個對象釋放引用之后, 引用計數減1, 當引用計數等于0時, 說明對象已經沒有用途, 對象持有的資源會被釋放.

  • 釋放引用
    realease()
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容