哪里描述不正確望指正. 歡迎轉載
大綱
知識點 | 概括 |
---|---|
ByteBuf 的數據模型 |
描述ByteBuf 基本概念 |
來自不同地區的ByteBuf
|
介紹存儲在不同內存區域的ByteBuf 及其優缺點 |
byte 級別的操作 |
ByteBuf 基本的讀寫查操作 |
派生ByteBuf
|
為ByetBuf 創建不同類型的副本 |
多種方式分配ByteBuf
|
介紹池化/非池化ByteBuf 以及如何分配池化/非池化的ByteBuf
|
釋放ByteBuf
|
基于引用計數, 釋放資源 |
數據容器的選擇
- Java NIO使用
ByteBuffer.class
- Netty使用
ByteBuf.class
Netty官方聲稱ByteBuf
比ByteBuffer
更加方便使用
- 讀和寫使用了不同的索引(讀寫模式切換不需要調用
ByteBuffer::filp
方法) - 支持方法的鏈式調用
- 支持引用計數(釋放ByteBuf資源)
- 支持池化(避免冗余)
- 可以被用戶自定義的緩沖區類型擴展
- 通過內置的復合緩沖區類型實現了透明的零拷貝
- 容量可以按需增長
ByteBuf
的數據模型
與NIO
ByteBuffer
不同的是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組成, 我們可以將header
ByteBuf
和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
會減少, 從而將可丟棄字節拋棄, 不過這會導致內存復制, 因為要把readerIndex
到writerIndex
之間的內容往左移動. 這里要注意的是writerIndex
到capacity
之間的數據不會移動也不會改變, 除非ByteBuf
容量很緊湊, 否則應該少用該方法
read*()
和skip*()
方法會增加當前readerIndex
. 注意特例readBytes(ByteBuf dest)
方法(將讀取的byte
寫入dest
)會增加當前ByteBuf
的readerIndex
也會增加dest
的writerIndex
, 但readBytes(ByteBuf dest, int dstIndex, int length )
不會改變dest
的writerIndex
, 因為指定了下標參數(具體請查看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::readableBytes
和ByteBuf::writableBytes
是否大于0, 否則拋出IndexOutOfBoundException
- 管理索引(
readerIndex
,writerIndex
)
在操作
ByteBuf
的時候, 可能需要暫時存下當前索引的位置, 稍后返回該索引位置. 或者需要根據協議自定義跳轉到指定索引位置
- 保存和重置索引:
ByteBuf::markReaderIndex
和ByteBuf::resetReaderIndex
, 同理有ByteBuf::markWriterIndex
和ByteBuf::resetWriterIndex
- 將索引移動到指定位置:
ByteBuf::readerIndex
和ByteBuf::writerIndex
(都接收int參數) - 清空
ByteBuf
:ByteBuf::clear
(注意此方法不是將內容刪除,而是將readerIndex
和writerIndex
重置為0)
- 查找
簡單查找:
ByteBuf::indexOf
, 接收一個byte
參數, 返回第一個出現的index
高級查找:
已經在新版本中被棄用, 所以這里只關注ByteBufProcessor
ByteProcessor
ByteBufProcessor
接口定義了多個常量并且內部有兩個實現類IndexOfProcessor
和IndexNotOfProcessor
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
返回readerIndex
至writerIndex
之間的內容 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
-
ByteBufAllocator
一般從Channel::alloc
或者ChannelHandlerContext::alloc
中獲取
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
...
ByteBufAllocator
提供了一系列方法, 可以用來創建來自不同地區的ByteBuf
, 基于堆內存, 基于直接內存, 聚合器, 還有阻塞型(I/O)的ByteBuf
ByteBufAllocator
有兩個實現類PooledByteBufAllocator
和UnpooledByteBufAllocator
PooledByteBufAllocator
池化了ByteBuf
以用來減少內存碎片, 提高性能
UnpooledByteBufAllocator
創建的ByteBuf
都是一個新實例
- Netty默認使用了池化的
Allocator
, 但我們可以在引導中自定使用池化還是非池化分配器
-
Unpooled
當無法在Channel
或者ChannelHandlerContext
獲取ByteBufAllocator
時, 推薦使用Unpooled
(靜態工具類)來創建未池化的ByteBuf
釋放ByteBuf
- 引用計數
Netty在第4版中為
ByteBuf
和ByteBufHolder
引入了引用計數技術, 它們都實現了ReferenceCounted
接口
當一個對象所持有的資源被多個對象引用, 假設引用計數為n, 每個對象釋放引用之后, 引用計數減1, 當引用計數等于0時, 說明對象已經沒有用途, 對象持有的資源會被釋放.
- 釋放引用
realease()