Netty提供的ByteBuf不同於JDK中NIO的ByteBuffer,ByteBuf是netty中數據傳輸的容器,是Netty自己實現的,作為NIO ByteBuffer的替代品,提供了更好的API供開發者使用。相較於NIO的ByteBuffer更具有卓越的功能性和靈活性。具體NIO的ByteBuffer如何實現請參考IO模型之NIO代碼及其實踐詳解。
一、ByteBuf的API特點
ByteBuf提供讀訪問索引(readerIndex)和寫訪問索引(writerIndex)來控制字節數組。ByteBuf API具有以下優點:
- 允許用戶自定義緩沖區類型擴展
- 通過內置的復合緩沖區類型實現透明的零拷貝
- 容量可按需增長
- 讀寫這兩種模式之間不需要調用類似於JDK的ByteBuffer的flip()方法進行切換
- 讀和寫使用不同的索引
- 支持方法的鏈式調用
- 支持引用計數
- 支持池化
二、ByteBuf類原理及使用
1、ByteBuf工作原理
ByteBuf維護兩個不同的索引: 讀索引(readerIndex)和寫索引(writerIndex)。如下圖:
- ByteBuf維護了readerIndex和writerIndex索引。
- 當readerIndex > writerIndex時,則拋出IndexOutOfBoundsException。
- ByteBuf容量 = writerIndex。
- ByteBuf可讀容量 = writerIndex - readerIndex。
- readXXX()和writeXXX()方法將會推進其對應的索引,自動推進。
- getXXX()和setXXX()方法對writerIndex和readerIndex無影響,不會改變index值。
readerIndex和WriterIndex將整個ByteBuf分成了三個區域:可丟棄字節、可讀字節、可寫字節,如下圖:
當尚未讀取時,擁有可讀字節區域以及可寫字節區域。
當已經讀過部分區域后,變成了可丟棄字節、可讀字節、可寫字節三個區域。
2、ByteBuf的使用模式
ByteBuf本質是: 一個由不同的索引分別控制讀訪問和寫訪問的字節數組。ByteBuf共有三種模式: 堆緩沖區模式(Heap Buffer)、直接緩沖區模式(Direct Buffer)和復合緩沖區模式(Composite Buffer),相較於NIO的ByteBuffer多了一種復合緩沖區模式。
2.1、堆緩沖區模式(Heap Buffer)
堆緩沖區模式又稱為:支撐數組(backing array)。將數據存放在JVM的堆空間,通過將數據存儲在數組中實現。
- 堆緩沖的優點: 由於數據存儲在Jvm堆中可以快速創建和快速釋放,並且提供了數組直接快速訪問的方法。
- 堆緩沖的缺點: 每次數據與I/O進行傳輸時,都需要將數據拷貝到直接緩沖區。
代碼如下:
public static void heapBuffer() { // 創建Java堆緩沖區 ByteBuf heapBuf = Unpooled.buffer(); if (heapBuf.hasArray()) { // 是數組支撐 byte[] array = heapBuf.array(); int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); int length = heapBuf.readableBytes(); handleArray(array, offset, length); } }
2.2、直接緩沖區模式(Direct Buffer)
Direct Buffer屬於堆外分配的直接內存,不會占用堆的容量。適用於套接字傳輸過程,避免了數據從內部緩沖區拷貝到直接緩沖區的過程,性能較好。
- Direct Buffer的優點: 使用Socket傳遞數據時性能很好,避免了數據從Jvm堆內存拷貝到直接緩沖區的過程,提高了性能。
- Direct Buffer的缺點: 相對於堆緩沖區而言,Direct Buffer分配內存空間和釋放更為昂貴。
對於涉及大量I/O的數據讀寫,建議使用Direct Buffer。而對於用於后端的業務消息編解碼模塊建議使用Heap Buffer。
代碼如下:
public static void directBuffer() { ByteBuf directBuf = Unpooled.directBuffer(); if (!directBuf.hasArray()) { int length = directBuf.readableBytes(); byte[] array = new byte[length]; directBuf.getBytes(directBuf.readerIndex(), array); handleArray(array, 0, length); } }
2.3、復合緩沖區模式(Composite Buffer)
Composite Buffer是Netty特有的緩沖區。本質上類似於提供一個或多個ByteBuf的組合視圖,可以根據需要添加和刪除不同類型的ByteBuf。
- 想要理解Composite Buffer,請記住:它是一個組合視圖。它提供一種訪問方式讓使用者自由的組合多個ByteBuf,避免了拷貝和分配新的緩沖區。
- Composite Buffer不支持訪問其支撐數組。因此如果要訪問,需要先將內容拷貝到堆內存中,再進行訪問
- 下圖是將兩個ByteBuf:頭部+Body組合在一起,沒有進行任何復制過程。僅僅創建了一個視圖
代碼如下:
public static void byteBufComposite() { // 復合緩沖區,只是提供一個視圖 CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct ByteBuf bodyBuf = Unpooled.directBuffer(); // can be backing or direct messageBuf.addComponents(headerBuf, bodyBuf); messageBuf.removeComponent(0); // remove the header for (ByteBuf buf : messageBuf) { System.out.println(buf.toString()); } }
三種ByteBuf使用區別對比:
三、ButeBuf的池化與非池化
內存的申請和銷毀都有一定性能開銷,內存池化技術可以有效的減少相關開銷。Netty在4引入了該技術。Netty的池化分為對象池和內存池,對應的ByteBuf的堆緩沖區和直接緩沖區。
是否使用池化取決於ByteBufAllocator使用的實例對象(參考分配方式ByteBufAllocator相關說明,本文后部分有說明)
PooledByteBufAllocator可以通過ctx.alloc獲得,如下圖:
Netty默認使用池化byteBuf,如果想要聲明不池化的可以使用Unpooled工具類。
四、字節級操作
4.1、隨機訪問索引
ByteBuf的索引與普通的Java字節數組一樣。第一個字節的索引是0,最后一個字節索引總是capacity()-1。ByteBuf的API分為4大類:Get*、Set*、Read*、Write*。使用有以下兩條規則:
- readXXX()和writeXXX()方法將會推進其對應的索引readerIndex和writerIndex。自動推進
- getXXX()和setXXX()方法用於訪問數據,對writerIndex和readerIndex無影響
代碼如下:
public static void byteBufRelativeAccess() { ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere for (int i = 0; i < buffer.capacity(); i++) { byte b = buffer.getByte(i);// 不改變readerIndex值 System.out.println((char) b); } }
4.2、順序訪問索引
Netty的ByteBuf同時具有讀索引和寫索引,但JDK的ByteBuffer只有一個索引,所以JDK需要調用flip()方法在讀模式和寫模式之間切換(NIO方式)。ByteBuf被讀索引和寫索引划分成3個區域:可丟棄字節區域,可讀字節區域和可寫字節區域 ,如下圖:
4.3、可丟棄字節區域
可丟棄字節區域是指:[0,readerIndex]之間的區域。可調用discardReadBytes()方法丟棄已經讀過的字節。
- discardReadBytes()效果: 將可讀字節區域(CONTENT)[readerIndex, writerIndex)往前移動readerIndex位,同時修改讀索引和寫索引。
- discardReadBytes()方法會移動可讀字節區域內容(CONTENT)。如果頻繁調用,會有多次數據復制開銷,對性能有一定的影響。
4.4、可讀字節區域
可讀字節區域是指:[readerIndex, writerIndex]之間的區域。任何名稱以read和skip開頭的操作方法,都會改變readerIndex索引。
4.5、可寫字節區域
可寫字節區域是指:[writerIndex, capacity]之間的區域。任何名稱以write開頭的操作方法都將改變writerIndex的值。
4.6、索引管理
- markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先備份當前的readerIndex,resetReaderIndex()則是將剛剛備份的readerIndex恢復回來。常用於dump ByteBuf的內容,又不想影響原來ByteBuf的readerIndex的值
- readerIndex(int) ----- 設置readerIndex為固定的值
- writerIndex(int) ----- 設置writerIndex為固定的值
- clear() ----- 效果是: readerIndex=0, writerIndex(0)。不會清除內存
- 調用clear()比調用discardReadBytes()輕量的多。僅僅重置readerIndex和writerIndex的值,不會拷貝任何內存,開銷較小。
4.7、查找操作(indexOf)
查找ByteBuf指定的值。類似於,String.indexOf("str")操作
- 最簡單的方法 ----- indexOf()
- 利用ByteProcessor作為參數來查找某個指定的值。
代碼如下:
public static void byteProcessor() { ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere // 使用indexOf()方法來查找 buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8); // 使用ByteProcessor查找給定的值 int index = buffer.forEachByte(ByteProcessor.FIND_CR); }
4.8、其余訪問操作
除去get、set、read、write類基本操作,還有一些其余的有用操作,如下圖:
下面的兩個方法操作字面意思較難理解,給出解釋:
- hasArray() :如果ByteBuf由一個字節數組支撐,則返回true。通俗的講:ByteBuf是堆緩沖區模式,則代表其內部存儲是由字節數組支撐的。
- array() :如果ByteBuf是由一個字節數組支撐則返回數組,否則拋出UnsupportedOperationException異常。也就是,ByteBuf是堆緩沖區模式。
五、ByteBufHolder的使用
我們時不時的會遇到這樣的情況:即需要另外存儲除有效的實際數據各種屬性值。HTTP響應就是一個很好的例子;與內容一起的字節的還有狀態碼,cookies等。
Netty 提供的 ByteBufHolder 可以對這種常見情況進行處理。 ByteBufHolder 還提供了對於 Netty 的高級功能,如緩沖池,其中保存實際數據的 ByteBuf 可以從池中借用,如果需要還可以自動釋放。
ByteBufHolder 有那么幾個方法。到底層的這些支持接入數據和引用計數。如下圖所示:
ByteBufHolder是ByteBuf的容器,可以通過子類實現ByteBufHolder接口,根據自身需要添加自己需要的數據字段。可以用於自定義緩沖區類型擴展字段。Netty提供了一個默認的實現DefaultByteBufHolder:
public class CustomByteBufHolder extends DefaultByteBufHolder{ private String protocolName; public CustomByteBufHolder(String protocolName, ByteBuf data) { super(data); this.protocolName = protocolName; } @Override public CustomByteBufHolder replace(ByteBuf data) { return new CustomByteBufHolder(protocolName, data); } @Override public CustomByteBufHolder retain() { super.retain(); return this; } @Override public CustomByteBufHolder touch() { super.touch(); return this; } @Override public CustomByteBufHolder touch(Object hint) { super.touch(hint); return this; } ... }
六、ByteBuf分配
創建和管理ByteBuf實例的多種方式:按需分配(ByteBufAllocator)、Unpooled緩沖區和ByteBufUtil類。
1、按序分配: ByteBufAllocator接口
Netty通過接口ByteBufAllocator實現了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator,是否使用池是由應用程序決定的:
- ctx.channel().alloc().buffer() ----- 本質就是: ByteBufAllocator.DEFAULT
- ByteBufAllocator.DEFAULT.buffer() ----- 返回一個基於堆或者直接內存存儲的Bytebuf。默認是堆內存
- ByteBufAllocator.DEFAULT ----- 有兩種類型: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。對於Java程序,默認使用PooledByteBufAllocator(池化)。對於安卓,默認使用UnpooledByteBufAllocator(非池化)
- 可以通過BootStrap中的Config為每個Channel提供獨立的ByteBufAllocator實例
ByteBufAllocator提供的操作如下圖:
注意:
- 上圖中的buffer()方法,返回一個基於堆或者直接內存存儲的Bytebuf ----- 缺省是堆內存。源碼: AbstractByteBufAllocator() { this(false); }
- ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。默認是池化(PooledByteBufAllocator.DEFAULT)
- 通過一些方法接受整型參數允許用戶指定 ByteBuf 的初始和最大容量值。
得到一個 ByteBufAllocator 的引用很簡單。你可以得到從 Channel (在理論上,每 Channel 可具有不同的 ByteBufAllocator ),或通過綁定到的 ChannelHandler 的 ChannelHandlerContext 得到它,如代碼:
Channel channel = ...; ByteBufAllocator allocator = channel.alloc(); //1、Channel ChannelHandlerContext ctx = ...; ByteBufAllocator allocator2 = ctx.alloc(); //2、 ChannelHandlerContext
第一種是從 channel 獲得 ByteBufAllocator,第二種是從 ChannelHandlerContext 獲得 ByteBufAllocator。
Netty 提供了兩種 ByteBufAllocator 的實現,一種是 PooledByteBufAllocator,用ByteBuf 實例池改進性能以及內存使用降到最低,此實現使用一個“jemalloc”內存分配。其他的實現不池化 ByteBuf 情況下,每次返回一個新的實例。Netty 默認使用 PooledByteBufAllocator,我們可以通過 ChannelConfig 或通過引導設置一個不同的實現來改變。
2、Unpooled緩沖區:非池化
Unpooled提供靜態的輔助方法來創建未池化的ByteBuf。其包含方法如下:
注意:
- 上圖的buffer()方法,返回一個未池化的基於堆內存存儲的ByteBuf
- wrappedBuffer() :創建一個視圖,返回一個包裝了給定數據的ByteBuf。非常實用
創建ByteBuf代碼:
public void createByteBuf(ChannelHandlerContext ctx) { // 1. 通過Channel創建ByteBuf,實際上也是使用ByteBufAllocator,因為ctx.channel().alloc()返回的就是一個ByteBufAllocator對象 ByteBuf buf1 = ctx.channel().alloc().buffer(); // 2. 通過ByteBufAllocator.DEFAULT創建 ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(); // 3. 通過Unpooled創建 ByteBuf buf3 = Unpooled.buffer(); }
3、ByteBufUtil類
ByteBufUtil類提供了用於操作ByteBuf的靜態的輔助方法: hexdump()和equals
- hexdump() :以十六進制的表示形式打印ByteBuf的內容,可以用於調試程序時打印 ByteBuf 的內容。非十六進制字符串相比字節而言對用戶更友好。 而且十六進制版本可以很容易地轉換回實際字節表示。
- boolean equals(ByteBuf, ByteBuf) :判斷兩個ByteBuf實例的相等性,在 實現自己 ByteBuf 的子類時經常用到
七、派生緩沖區
“派生的緩沖區”是代表一個專門的展示 ByteBuf 內容的“視圖”。這種視圖是由duplicate() 、slice()、slice(int, int)、Unpooled.unmodifiableBuffer(...)、Unpooled.wrappedBuffer(...)、 order(ByteOrder)、readSlice(int) 方法創建的。所有這些都返回一個新的 ByteBuf 實例包括它自己的 reader, writer 和標記索引。然而,內部數據存儲共享就像在一個 NIO 的 ByteBuffer。這使得派生的緩沖區創建、修改其內容,以及修改其“源”實例更廉價。
注意:
- 上面的7中方法,都會返回一個新的ByteBuf實例,具有自己的讀索引和寫索引。但是,其內部存儲是與原對象是共享的。這就是視圖的概念
- 請注意:如果你修改了這個新的ByteBuf實例的具體內容,那么對應的源實例也會被修改,因為其內部存儲是共享的。
- 如果需要拷貝現有緩沖區的真實副本,請使用copy()或copy(int, int)方法。
- 使用派生緩沖區,避免了復制內存的開銷,有效提高程序的性能
針對派生和復制區別,如下面代碼所展示:
Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1 ByteBuf sliced = buf.slice(0, 14); //2 System.out.println(sliced.toString(utf8)); //3 buf.setByte(0, (byte) 'J'); //4 assert buf.getByte(0) == sliced.getByte(0);
- 創建一個 ByteBuf 保存特定字節串。
- 創建從索引 0 開始,並在 14 結束的 ByteBuf 的新 slice。
- 打印 Netty in Action
- 更新索引 0 的字節。
- 斷言成功,因為數據是共享的,並以一個地方所做的修改將在其他地方可見。
Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //1 ByteBuf copy = buf.copy(0, 14); //2 System.out.println(copy.toString(utf8)); //3 buf.setByte(0, (byte) 'J'); //4 assert buf.getByte(0) != copy.getByte(0);
- 創建一個 ByteBuf 保存特定字節串。
- 創建從索引0開始和 14 結束 的 ByteBuf 的段的拷貝。
- 打印 Netty in Action
- 更新索引 0 的字節。
- .斷言成功,因為數據不是共享的,並以一個地方所做的修改將不影響其他。
因此使用派生緩沖區可以盡可能避免復制內存,要想數據獨立,請使用copy。
八、引用計數
引用計數是一種通過在某個對象所持有的資源不再被其他對象引用時釋放該對象所持有的資源來優化內存使用和性能的技術。Netty 在第4 版中為ByteBuf引入了引用計數技術,ByteBuf初始化引用數量為1,通過release 可以-1,為0時對象被回收。
堆中對象即使不回收在gc執行時也會被回收,但是直接內存的對象如果不釋放,可能會引起內存的溢出。
如果試圖訪問已經回收的對象會拋出
資源釋放的問題通常來講是由最后一個訪問該對象的事件處理器負責。執行writeAndFlush會自動釋放資源,同時如下圖中的SimpleChannelInboundHandler
的實現也是為了給我們實現了對象回收。省去一些通用代碼。