Netty實戰(五)

来源:https://www.cnblogs.com/kimi77/archive/2023/05/27/17436565.html
-Advertisement-
Play Games

# 一、什麼是ByteBuf 我們前面說過,網路數據的基本單位總是位元組。Java NIO 提供了 ByteBuffer 作為它的位元組容器,但是這個類使用起來過於複雜,而且也有些繁瑣。**ByteBuffer 替代品是 ByteBuf**,一個強大的實現,既解決了 JDK API 的局限性,又為網路應 ...


一、什麼是ByteBuf

我們前面說過,網路數據的基本單位總是位元組。Java NIO 提供了 ByteBuffer 作為它的位元組容器,但是這個類使用起來過於複雜,而且也有些繁瑣。ByteBuffer 替代品是 ByteBuf,一個強大的實現,既解決了 JDK API 的局限性,又為網路應用程式的開發者提供了更好的 API。

下麵我們將會說明和 JDK 的 ByteBuffer 相比,ByteBuf 的卓越功能性和靈活性

二、 ByteBuf 的 API

Netty 的數據處理 API 通過兩個組件暴露——abstract class ByteBufinterface ByteBufHolder

下麵是一些 ByteBuf API 的優點:

  • 它可以被用戶自定義的緩衝區類型擴展;
  • 通過內置的複合緩衝區類型實現了透明的零拷貝;
  • 容量可以按需增長(類似於 JDK 的 StringBuilder);
  • 在讀和寫這兩種模式之間切換不需要調用 ByteBuffer 的 flip()方法;
  • 讀和寫使用了不同的索引;
  • 支持方法的鏈式調用;
  • 支持引用計數;
  • 支持池化。

其他類可用於管理 ByteBuf 實例的分配,以及執行各種針對於數據容器本身和它所持有的數據的操作。

三、ByteBuf 類——Netty 的數據容器

因為所有的網路通信都涉及位元組序列的移動,所以高效易用的數據結構明顯是必不可少的。Netty 的 ByteBuf 實現滿足並超越了這些需求。

3.1 ByteBuf如何工作?

ByteBuf 維護了兩個不同的索引:一個用於讀取,一個用於寫入。

當你從 ByteBuf 讀取時,它的 readerIndex 將會被遞增已經被讀取的位元組數。
當你寫入 ByteBuf 時,它的writerIndex 也會被遞增。

ByteBuf 的佈局結構和狀態如下:
在這裡插入圖片描述
那麼這兩個索引之間有什麼關係?如果讀取位元組直到 readerIndex 達到和 writerIndex 同樣的值時會發生什麼?

在那時,我們將會到達“可以讀取的”數據的末尾。就如同試圖讀取超出數組末尾的數據一樣,試圖讀取超出該點的數據將會觸發IndexOutOfBoundsException。

名稱以 read 或者 write 開頭的 ByteBuf 方法,將會推進其對應的索引,而名稱以 set 或者 get 開頭的操作則不會。

後面的這些方法將在作為一個參數傳入的一個相對索引上執行操作。可以指定 ByteBuf 的最大容量。試圖移動寫索引(即 writerIndex)超過這個值將會觸發一個異常。(預設的限制是Integer.MAX_VALUE。)

PS:也就是說用戶直接或者間接使 capacity(int)或者 ensureWritable(int)方法來增加超過該最大容量時拋出異常。

3.2 ByteBuf 的使用模式

首先我們記住ByteBuf:一個由不同的索引分別控制讀訪問和寫訪問的位元組數組。

3.2.1 堆緩衝區

最常用的 ByteBuf 模式是將數據存儲在 JVM 的堆空間中。這種模式被稱為支撐數組(backing array),它能在沒有使用池化的情況下提供快速的分配和釋放。這種方式,非常適合於有遺留的數據需要處理的情況。

代碼展示:

ByteBuf heapBuf = ...;
//檢查ByteBuf是否有一個支撐數組
if (heapBuf.hasArray()) {
//如果有,則獲取對該數組的引用
byte[] array = heapBuf.array();
//計算第一個位元組的偏移量
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
//獲取可讀位元組數
int length = heapBuf.readableBytes();
//使用數組、偏移量和長度作為參數調用你的方法
handleArray(array, offset, length);
}

PS:當 hasArray()方法返回 false 時,嘗試訪問支撐數組將觸發一個 UnsupportedOperationException。這個模式類似於 JDK 的 ByteBuffer 的用法。

3.2.2 直接緩衝區

直接緩衝區是另外一種 ByteBuf 模式。

我們都希望用於對象創建的記憶體分配永遠都來自於堆中。但這並不是必須的,因為ByteBuffer 類允許 JVM 實現通過本地調用來分配記憶體。

這主要是為了避免在每次調用本地 I/O 操作之前(或者之後)將緩衝區的內容複製到一個中間緩衝區(或者從中間緩衝區把內容複製到緩衝區)。

直接緩衝區的內容將駐留在常規的會被垃圾回收的堆之外(它的內容可能不會被堆回收)。如果你的數據包含在一個在堆上分配的緩衝區中,那麼在通過套接字發送它之前,JVM將會在內部把你的緩衝區複製到一個直接緩衝區中。

直接緩衝區的主要缺點是:
1、相對於基於堆的緩衝區,它們的分配和釋放都較為昂貴。
2、因為數據不是在堆上,所以你不得不進行一次複製,如代碼所示。

ByteBuf directBuf = ...;
//檢查 ByteBuf 是否由數組支撐。如果不是,則這是一個直接緩衝區
if (!directBuf.hasArray()) {
//獲取位元組可讀數
int length = directBuf.readableBytes();
//分配一個新的數組來保存具有該長度的位元組數據
byte[] array = new byte[length];
//將位元組複製到該數組
directBuf.getBytes(directBuf.readerIndex(), array);
//開始調用
handleArray(array, 0, length);
}

顯然,與使用支撐數組相比,這涉及的工作更多。因此,如果事先知道容器中的數據將會被作為數組來訪問,你可能更願意使用堆記憶體。

3.2.3 複合緩衝區

第三種也是最後一種模式使用的是複合緩衝區,它為多個 ByteBuf 提供一個聚合視圖。

在這裡你可以根據需要添加或者刪除 ByteBuf 實例,這是一個 JDK 的 ByteBuffer 實現完全缺失的特性。Netty 通過一個 ByteBuf 子類——CompositeByteBuf——實現了這個模式,它提供了一個將多個緩衝區表示為單個合併緩衝區的虛擬表示。

註意:CompositeByteBuf 中的 ByteBuf 實例可能同時包含直接記憶體分配和非直接記憶體分配。如果其中只有一個實例,那麼對 CompositeByteBuf 上的 hasArray()方法的調用將返回該組件上的 hasArray()方法的值;否則它將返回 false

舉個粒子:
假設我們有一條由頭部和主體兩部分組成並通過 HTTP 協議傳輸的消息。且這兩部分分別由應用程式的不同模塊產生,然後在消息被髮送的時候組裝。該應用程式可以選擇為多個消息重用相同的消息主體。當這種情況發生時,對於每個消息都將會創建一個新的頭部。

如果我們不想為每個消息都重新分配這兩個緩衝區,那麼使用 CompositeByteBuf 是一個完美的選擇。

如圖為持有一個頭部和主體的 CompositeByteBuf:
在這裡插入圖片描述
JDK 的 ByteBuffer針對我們這個需求實現代碼會像下麵這樣:

// 使用數組保存消息部分
ByteBuffer[] message = new ByteBuffer[] { header, body };
// 創建一個新的ByteBuffer,並使用copy合併頭和正文
ByteBuffer message2 =ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();

使用 CompositeByteBuf 的複合緩衝區模式則是這樣:

CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ...; // 可以是堆緩衝或直接緩衝
ByteBuf bodyBuf = ...; // 可以是堆緩衝或直接緩衝
//將 ByteBuf 實例追加到 CompositeByteBuf
messageBuf.addComponents(headerBuf, bodyBuf);
.....
//刪除位於索引位置為 0(第一個組件)的 ByteBuf
messageBuf.removeComponent(0); // 移除頭部
//迴圈遍歷所有的 ByteBuf 實例
for (ByteBuf buf : messageBuf) {
System.out.println(buf.toString());
}

CompositeByteBuf 不支持訪問其支撐數組,因此訪問 CompositeByteBuf 中的數據類似於(訪問)直接緩衝區的模式,像下麵這樣:

CompositeByteBuf compBuf = Unpooled.compositeBuffer();
//獲得可讀位元組數
int length = compBuf.readableBytes();
//分配一個具有可讀位元組數長度的新數組
byte[] array = new byte[length];
//將數組讀到該數組中
compBuf.getBytes(compBuf.readerIndex(), array);
//調用使用
handleArray(array, 0, array.length);

Netty使用了CompositeByteBuf來優化套接字的I/O操作,儘可能地消除了由JDK的緩衝區實現所導致的性能以及記憶體使用率的懲罰。
這種優化發生在Netty的核心代碼中,因此不會被暴露出來,但是我們應該知道它所帶來的影響。

四、位元組級操作

ByteBuf 提供了許多超出基本讀、寫操作的方法用於修改它的數據。

4.1 隨機訪問索引

如同在普通的 Java 位元組數組中一樣,ByteBuf 的索引是從零開始的:第一個位元組的索引是0,最後一個位元組的索引總是 capacity() - 1。對存儲機制的封裝使得遍歷 ByteBuf 的內容非常簡單。

像這樣的:

ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);
System.out.println((char)b);
}

需要註意的是,使用那些需要一個索引值參數的方法(的其中)之一來訪問數據既不會改變readerIndex 也不會改變 writerIndex。如果有需要,也可以通過調用 readerIndex(index)或者 writerIndex(index)來手動移動這兩者。

4.2 順序訪問索引

雖然 ByteBuf 同時具有讀索引和寫索引,但是 JDK 的 ByteBuffer 卻只有一個索引,這也就是為什麼必須調用 flip()方法來在讀模式和寫模式之間進行切換的原因。

以下展示了ByteBuf 是如何被它的兩個索引劃分成 3 個區域的:
在這裡插入圖片描述

4.3 可丟棄位元組

可丟棄位元組的分段包含了已經被讀過的位元組。通過調用 discardReadBytes()方法,可以丟棄它們並回收空間。這個分段的初始大小為 0,存儲在 readerIndex 中,會隨著 read 操作的執行而增加(get*操作不會移動 readerIndex)。
在這裡插入圖片描述
像圖中所展示的緩衝區上調用discardReadBytes()方法後的結果。可以看到,可丟棄位元組分段中的空間已經變為可寫的了。註意,在調用discardReadBytes()之後,對可寫分段的內容並沒有任何的保證 (因為只是移動了可以讀取的位元組以及 writerIndex,而沒有對所有可寫入的位元組進行擦除寫)。

有的同學可能會傾向於頻繁地調用 discardReadBytes()方法以確保可寫分段的最大化,但是請註意,這將極有可能會導致記憶體複製,因為可讀位元組(圖中標記為 CONTENT 的部分)必須被移動到緩衝區的開始位置。建議只在有真正需要的時候才這樣做,例如,當記憶體非常寶貴的時候。

4.4 可讀位元組

ByteBuf 的可讀位元組分段存儲了實際數據。新分配的、包裝的或者複製的緩衝區的預設的readerIndex 值為 0。任何名稱以 read 或者 skip 開頭的操作都將檢索或者跳過位於當前readerIndex 的數據,並且將它增加已讀位元組數。

如果被調用的方法需要一個 ByteBuf 參數作為寫入的目標,並且沒有指定目標索引參數,那麼該目標緩衝區的 writerIndex 也將被增加。例如:

readBytes(ByteBuf dest);

如果嘗試在緩衝區的可讀位元組數已經耗盡時從中讀取數據,那麼將會引發一個 IndexOutOfBoundsException。

下麵是一個讀取所有數據的代碼示例:

ByteBuf buffer = ...;
while (buffer.isReadable()) {
System.out.println(buffer.readByte());
}

4.5 可寫位元組

可寫位元組分段是指一個擁有未定義內容的、寫入就緒的記憶體區域。新分配的緩衝區的writerIndex 的預設值為 0。任何名稱以 write 開頭的操作都將從當前的 writerIndex 處開始寫數據,並將它增加已經寫入的位元組數。

如果寫操作的目標也是 ByteBuf,並且沒有指定源索引的值,則源緩衝區的 readerIndex 也同樣會被增加相同的大小。這個調用如下所示:

writeBytes(ByteBuf dest);

如果嘗試往目標寫入超過目標容量的數據,將會引發一個IndexOutOfBoundException(PS:在往 ByteBuf 中寫入數據時,其將首先確保目標 ByteBuf 具有足夠的可寫入空間來容納當前要寫入的數據,如果沒有,則將檢查當前的寫索引以及最大容量是否可以在擴展後容納該數據,可以則會分配並調整容量,否則就會拋出該異常。)

下麵代碼是一個用隨機整數值填充緩衝區,直到它空間不足為止的例子。writeableBytes()方法在這裡被用來確定該緩衝區中是否還有足夠的空間。

// 用隨機整數填充緩衝區的可寫位元組。
ByteBuf buffer = ...;
while (buffer.writableBytes() >= 4) {
buffer.writeInt(random.nextInt());
}

4.6 索引管理

JDK 的 InputStream 定義了 mark(int readlimit)和 reset()方法,這些方法分別被用來將流中的當前位置標記為指定的值,以及將流重置到該位置。(說白了就是給指定位置寫值,比如有四個位置可以指定第一個位置寫 “作”,第二個位置寫“者”,第三個位置寫“很”,第四個位置寫“帥”)。

也可以通過調用 readerIndex(int)或者 writerIndex(int)來將索引移動到指定位置。試圖將任何一個索引設置到一個無效的位置都將導致一個 IndexOutOfBoundsException。

可以通過調用 clear()方法來將 readerIndex 和 writerIndex 都設置為 0。註意,這並不會清除記憶體中的內容。

clear()未被調用:
在這裡插入圖片描述

clear()調用之後:
在這裡插入圖片描述
調用 clear()比調用 discardReadBytes()輕量得多,因為它將只是重置索引而不會複製任何的記憶體。

4.7 查找操作

在ByteBuf中有多種可以用來確定指定值的索引的方法。最簡單的是使用indexOf()方法。
較複雜的查找可以通過那些需要一個io.netty.util.ByteProcessor,這個介面只定義了一個方法:

boolean process(byte value)

ByteProcessor針對一些常見的值定義了許多便利的方法。假設你的應用程式需要和所謂的包含有以NULL結尾的內容的Flash套接字作為參數的方法達成。可以調用它將檢查輸入值是否是正在查找的值。

forEachByte(ByteBufProcessor.FIND_NUL)

展示一個查找回車符(\r)的例子:

ByteBuf buffer = ...;
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);

4.8 派生緩衝區

派生緩衝區為 ByteBuf 提供了以專門的方式來呈現其內容的視圖。這類視圖是通過以下方法被創建的:

  • duplicate();
  • slice();
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。

每個這些方法都將返回一個新的 ByteBuf 實例,它具有自己的讀索引、寫索引和標記索引。其內部存儲和 JDK 的 ByteBuffer 一樣也是共用的。這使得派生緩衝區的創建成本是很低廉的,但是這也意味著,如果你修改了它的內容,也同時修改了其對應的源實例,所以要小心。

ByteBuf 複製:

如果需要一個現有緩衝區的真實副本,請使用 copy()或者 copy(int, int)方法。不同於派生緩衝區,由這個調用所返回的 ByteBuf 擁有獨立的數據副本。

看段代碼:

1、使用 slice(int, int)方法來操作 ByteBuf 的一個分段

Charset utf8 = Charset.forName("UTF-8");
//創建一個用於保存給定字元串的位元組的 ByteBuf
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
//創建該 ByteBuf 從索引 0 開始到索引 15結束的一個新切片
ByteBuf sliced = buf.slice(0, 15);
//將列印“Netty in Action”
System.out.println(sliced.toString(utf8));
//更新索引 0 處的位元組
buf.setByte(0, (byte)'J');
//將會成功,因為數據是共用的,對其中一個所做的更改對另外一個也是可見的
assert buf.getByte(0) == sliced.getByte(0);

2、利用副本切片來操作 ByteBuf 的一個分段

Charset utf8 = Charset.forName("UTF-8");
//創建 ByteBuf 以保存所提供的字元串的位元組
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
//創建該 ByteBuf 從索引 0 開始到索引 15結束的分段的副本
ByteBuf copy = buf.copy(0, 15);
//將列印“Netty in Action”
System.out.println(copy.toString(utf8));
////更新索引 0 處的位元組
buf.setByte(0, (byte) 'J');
//將會失敗,因為數據不是共用的
assert buf.getByte(0) != copy.getByte(0);

4.9 讀/寫操作

正如我們所提到過的,有兩種類別的讀/寫操作:

  • get()和 set()操作,從給定的索引開始,並且保持索引不變;
  • read()和 write()操作,從給定的索引開始,並且會根據已經訪問過的位元組數對索引進行調整。

下麵是部分常用的get()方法:

名 稱 描 述
getBoolean(int) 返回給定索引處的 Boolean 值
getByte(int) 返回給定索引處的位元組
getUnsignedByte(int) 將給定索引處的無符號位元組值作為 short 返回
getMedium(int) 返回給定索引處的 24 位的中等 int 值
getUnsignedMedium(int) 返回給定索引處的無符號的 24 位的中等 int 值
getInt(int) 返回給定索引處的 int 值
getUnsignedInt(int) 將給定索引處的無符號 int 值作為 long 返回
getLong(int) 返回給定索引處的 long 值
getShort(int) 返回給定索引處的 short 值
getUnsignedShort(int) 將給定索引處的無符號 short 值作為 int 返回
getBytes(int, ...) 將該緩衝區中從給定索引開始的數據傳送到指定的目的地

下麵是部分常用的set()方法:

名 稱 描 述
setBoolean(int, boolean) 設定給定索引處的 Boolean 值
setByte(int index, int value) 設定給定索引處的位元組值
setMedium(int index, int value) 設定給定索引處的 24 位的中等 int 值
setInt(int index, int value) 設定給定索引處的 int 值
setLong(int index, long value) 設定給定索引處的 long 值
setShort(int index, int value) 設定給定索引處的 short 值

get()和 set()方法的用法大家可以參照下麵的例子:

Charset utf8 = Charset.forName("UTF-8");
//創建一個新的 ByteBuf以保存給定字元串的位元組
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
//列印第一個字元'N'
System.out.println((char)buf.getByte(0));
//存儲當前的 readerIndex和 writerIndex
int readerIndex = buf.readerIndex();
int writerIndex = buf.writerIndex();
//將索引 0 處的位元組更新為字元'B'
buf.setByte(0, (byte)'B');
////列印第一個字元'B'
System.out.println((char)buf.getByte(0));
//將會成功,因為這些操作並不會修改相應的索引
assert readerIndex == buf.readerIndex();
assert writerIndex == buf.writerIndex();

然後我們看一下read()操作,其作用於當前的 readerIndex 或 writerIndex。這些方法將用於從 ByteBuf 中讀取數據,如同它是一個流。

常用的read()方法:

名 稱 描 述
readBoolean() 返回當前 readerIndex 處的 Boolean,並將 readerIndex 增加 1
readByte() 返回當前 readerIndex 處的位元組,並將 readerIndex 增加 1
readUnsignedByte() 將當前 readerIndex 處的無符號位元組值作為 short 返回,並將readerIndex 增加 1
readMedium() 返回當前 readerIndex 處的 24 位的中等 int 值,並將 readerIndex增加 3
readUnsignedMedium() 返回當前 readerIndex 處的 24 位的無符號的中等 int 值,並將readerIndex 增加 3
readInt() 返回當前 readerIndex 的 int 值,並將 readerIndex 增加 4
readUnsignedInt() 將當前 readerIndex 處的無符號的 int 值作為 long 值返回,並將readerIndex 增加 4
readLong() 返回當前 readerIndex 處的 long 值,並將 readerIndex 增加 8
readShort() 返回當前 readerIndex 處的 short 值,並將 readerIndex 增加 2
readUnsignedShort() 將當前 readerIndex 處的無符號 short 值作為 int 值返回,並將readerIndex 增加 2
readBytes(ByteBuf byte[] destination,int dstIndex [,intlength]) 將當前 ByteBuf 中從當前 readerIndex 處開始的(如果設置了,length 長度的位元組)數據傳送到一個目標 ByteBuf 或者 byte[],從目標的 dstIndex 開始的位置。本地的 readerIndex 將被增加已經傳輸的位元組數

常見的writer()方法(PS:幾乎每個 read()方法都有對應的 write()方法,用於將數據追加到 ByteBuf 中。註意:下列所列出的這些方法的參數是需要寫入的值,而不是索引值。)

名 稱 描 述
writeBoolean(boolean) 在當前 writerIndex 處寫入一個 Boolean,並將 writerIndex 增加 1
writeByte(int) 在當前 writerIndex 處寫入一個位元組值,並將 writerIndex 增加 1
writeMedium(int) 在當前 writerIndex 處寫入一個中等的 int 值,並將 writerIndex增加 3
writeInt(int) 在當前 writerIndex 處寫入一個 int 值,並將 writerIndex 增加 4
writeLong(long) 在當前 writerIndex 處寫入一個 long 值,並將 writerIndex 增加 8
writeShort(int) 在當前 writerIndex 處寫入一個 short 值,並將 writerIndex 增加 2
writeBytes(sourceByteBuf byte[][,int srcIndex,int length]) 從當前 writerIndex 開始,傳輸來自於指定源(ByteBuf 或者 byte[])的數據。如果提供了 srcIndex 和 length,則從 srcIndex 開始讀取,並且處理長度為 length 的位元組。當前 writerIndex 將會被增加所寫入的位元組數

ByteBuf 上的 read()和 write()操作大家可以參照下列的代碼:

Charset utf8 = Charset.forName("UTF-8");
//創建一個新的ByteBuf 以保存給定字元串的位元組
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
//列印第一個字元'N'
System.out.println((char)buf.readByte());
//存儲當前的存儲當前的 readerIndex
int readerIndex = buf.readerIndex();
////存儲當前的存儲當前的 writerIndex
int writerIndex = buf.writerIndex();
//將字元'?'追加到緩衝區
buf.writeByte((byte)'?');
assert readerIndex == buf.readerIndex();
//將會成功,因為 writeByte()方法移動了 writerIndex
assert writerIndex != buf.writerIndex();

4.10 更多的操作

ByteBuf 還為我們提供了其他有用操作:

名 稱 描 述
readableBytes() 返回可被讀取的位元組數
writableBytes() 返回可被寫入的位元組數
capacity() 返回 ByteBuf 可容納的位元組數。在此之後,它會嘗試再次擴展直到達到 maxCapacity()
maxCapacity() 返回 ByteBuf 可以容納的最大位元組數
hasArray() 如果 ByteBuf 由一個位元組數組支撐,則返回 true
array() 如果 ByteBuf 由一個位元組數組支撐則返回該數組;否則,它將拋出一個UnsupportedOperationException 異常

五、ByteBufHolder 介面

為了處理我們除了數據負載之外,還需要存儲各種屬性值的需求,Netty 提供了 ByteBufHolder。

ByteBufHolder 也為 Netty 的高級特性提供了支持,如緩衝區池化,其中可以從池中借用 ByteBuf,並且在需要時自動釋放。ByteBufHolder 只有幾種用於訪問底層數據和引用計數的方法。

ByteBufHolder 的常見操作如下:

名 稱 描 述
content() 返回由這個 ByteBufHolder 所持有的 ByteBuf
copy() 返回這個 ByteBufHolder 的一個深拷貝,包括一個其所包含的 ByteBuf 的非共用拷貝
duplicate() 返回這個 ByteBufHolder 的一個淺拷貝,包括一個其所包含的 ByteBuf 的共用拷貝

如果想要實現一個將其有效負載存儲在 ByteBuf 中的消息對象,那麼 ByteBufHolder 將是個不錯的選擇。

六、ByteBuf 分配

6.1 按需分配:ByteBufAllocator 介面

為了降低分配和釋放記憶體的開銷,Netty 通過 interface ByteBufAllocator 實現了(ByteBuf 的)池化,它可以用來分配我們所描述過的任意類型的 ByteBuf 實例。

使用池化是特定於應用程式的決定,其並不會以任何方式改變 ByteBuf API。

下麵列出了 ByteBufAllocator 常見的一些操作:

名 稱 描 述
buffer()、buffer(int initialCapacity)、buffer(int initialCapacity, int maxCapacity) 返回一個基於堆或者直接記憶體存儲的 ByteBuf
heapBuffer()、heapBuffer(int initialCapacity)、heapBuffer(int initialCapacity, int maxCapacity) 返回一個基於堆記憶體存儲的ByteBuf
directBuffer()、directBuffer(int initialCapacity)、directBuffer(int initialCapacity, int maxCapacity) 返回一個基於直接記憶體存儲的ByteBuf
compositeBuffer()、compositeBuffer(int maxNumComponents)、compositeDirectBuffer()、compositeDirectBuffer(int maxNumComponents)、compositeHeapBuffer()、compositeHeapBuffer(int maxNumComponents) 返回一個可以通過添加最大到指定數目的基於堆的或者直接記憶體存儲的緩衝區來擴展的CompositeByteBuf
ioBuffer() 返回一個用於套接字的 I/O 操作的 ByteBuf

可以通過 Channel(每個都可以有一個不同的 ByteBufAllocator 實例)或者綁定到ChannelHandler 的 ChannelHandlerContext 獲取一個到 ByteBufAllocator 的引用。

大家可以參考下麵的代碼:

Channel channel = ...;
//從 Channel 獲取一個到ByteBufAllocator 的引用
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
//從 ChannelHandlerContext 獲取一個到 ByteBufAllocator 的引用
ByteBufAllocator allocator2 = ctx.alloc();
...

Netty提供了兩種ByteBufAllocator的實現:PooledByteBufAllocator和UnpooledByteBufAllocator。

前者池化了ByteBuf的實例以提高性能並最大限度地減少記憶體碎片。

後者的實現不池化ByteBuf實例,並且在每次它被調用時都會返回一個新的實例。

雖然Netty預設使用了PooledByteBufAllocator,但這可以很容易地通過ChannelConfig API或者在引導你的應用程式時指定一個不同的分配器來更改。

6.2 Unpooled 緩衝區

可能某些情況下,未能獲取一個到 ByteBufAllocator 的引用。對於這種情況,Netty 提供了一個簡單的稱為 Unpooled 的工具類,它提供了靜態的輔助方法來創建未池化的 ByteBuf實例。

下麵是寫常用的Unpooled方法:

名 稱 描 述
buffer()、buffer(int initialCapacity)、buffer(int initialCapacity, int maxCapacity) 返回一個未池化的基於堆記憶體存儲的ByteBuf
directBuffer()、directBuffer(int initialCapacity)、directBuffer(int initialCapacity, int maxCapacity) 返回一個未池化的基於直接記憶體存儲的 ByteBuf
wrappedBuffer() 返回一個包裝了給定數據的 ByteBuf
copiedBuffer() 返回一個複製了給定數據的 ByteBuf

Unpooled 類還使得 ByteBuf 同樣可用於那些並不需要 Netty 的其他組件的非網路項目,使得其能得益於高性能的可擴展的緩衝區 API。

6.3 ByteBufUtil 類

ByteBufUtil 提供了用於操作 ByteBuf 的靜態的輔助方法。因為這個 API 是通用的,並且和池化無關,所以這些方法已然在分配類的外部實現。

這些靜態方法中最有價值方法:

  • hexdump()方法:它以十六進位的表示形式列印ByteBuf 的內容。這在各種情況下都很有用,例如,出於調試的目的記錄 ByteBuf 的內容。十六進位的表示通常會提供一個比位元組值的直接表示形式更加有用的日誌條目,此外,十六進位的版本還可以很容易地轉換回實際的位元組表示。
  • boolean equals(ByteBuf, ByteBuf):它被用來判斷兩個 ByteBuf實例的相等性。如果你實現自己的 ByteBuf 子類,你可能會發現 ByteBufUtil 的其他有用方法。

七、引用計數

引用計數是一種通過在某個對象所持有的資源不再被其他對象引用時釋放該對象所持有的資源來優化記憶體使用和性能的技術。

引用計數背後的想法並不是特別的複雜;它主要涉及跟蹤到某個特定對象的活動引用的數量。一個 ReferenceCounted 實現的實例將通常以活動的引用計數為 1 作為開始。只要引用計數大於 0,就能保證對象不會被釋放。當活動引用的數量減少到 0 時,該實例就會被釋放。

PS,雖然釋放的確切語義可能是特定於實現的,但是至少已經釋放的對象應該不可再用了。

引用計數對於池化實現(如 PooledByteBufAllocator)來說是至關重要的,它降低了記憶體分配的開銷。

引用計數演示代碼:

Channel channel = ...;
//從 Channel 獲取ByteBufAllocator
ByteBufAllocator allocator = channel.alloc();
....
//從 ByteBufAllocator分配一個 ByteBuf
ByteBuf buffer = allocator.directBuffer();
//檢查引用計數是否為預期的 1
assert buffer.refCnt() == 1;
...

釋放引用計數:

ByteBuf buffer = ...;
//減少到該對象的活動引用。當減少到 0 時,該對象被釋放,並且該方法返回 true
boolean released = buffer.release();
..

試圖訪問一個已經被釋放的引用計數的對象,將會導致一個 IllegalReferenceCountException。註意,一個特定的(ReferenceCounted 的實現)類,可以用它自己的獨特方式來定義它的引用計數規則。例如,我們可以設想一個類,其 release()方法的實現總是將引用計數設為
零,而不用關心它的當前值,從而一次性地使所有的活動引用都失效


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 設計一個業務改動信息時的自定義記錄,例如新增、修改、刪除數據等。並且記錄的規則可以通過配置的方式控制。大家需要根據各自業務場景參考,歡迎討論。偽代碼如下: 實體類: @TableName("tbl_user") User{ String id String name Integer age Stri ...
  • > 本文首發於公眾號:Hunter後端 > 原文鏈接:[Python連接es筆記一之連接與查詢es](https://mp.weixin.qq.com/s/smp3VvWD6ChuFVuotQ9_zg) 有幾種方式在 Python 中配置與 es 的連接,最簡單最有用的方法就是定義一個預設的連接,如 ...
  • 在Python軟體開發中,tkinter中command功能的作用是為按鈕、菜單等組件綁定回調函數,用戶操作該組件時會觸發相應的函數執行。 本文涵蓋了各種組件和功能: 1、為Button組件(按鈕)綁定回調函數 import tkinter as tk def say_hello(): print( ...
  • ## IO流(input/output) ​ 數據運輸的載體或者中間鍵 ### 位元組流 #### 輸入位元組流(FileInputStream) ​ 以位元組為最小單元,讀取任何類型的文件,但是要註意字元集類型的轉換。 ```Java public static void testFileInputSt ...
  • 哈嘍大家好,今天我們來獲取一下某個生活平臺網站數據,進行可視化分析。 採集58的數據可以使用Python的requests庫和beautifulsoup庫,數據可視化分析可以使用matplotlib庫和seaborn庫。下麵是一個簡單的例子: 1、首先導入需要使用的模塊 import request ...
  • 摘要:如果希望將 JSON 文件導入到 Redis 中,首先要做的就是連接到 redis 服務。 本文分享自華為雲社區《Python將JSON格式文件導入 redis,多種方法》,作者: 夢想橡皮擦 。 在導入前需要先確定你已經安裝 Redis,並且可以啟動相關服務。 windows 上啟動 red ...
  • 好久都沒有寫點東西了,是時候有點寫東西的必要了。 去年下年底離職了,躺了幾個月,最近又兜兜轉轉換了一家公司繼續當牛馬了,前段時間八股文背了好多,難受呀,不過我也趁著前段時間自己也整理了屬於我自己的八股文,有好幾萬字吧,哈哈哈,以後就不用到處去找八股文了。 說回正題,這個group_concat的問題 ...
  • 數據類型是電腦編程中將不同類型的數據值分類和定義的方式。 通過數據類型,可以確定數據的存儲方式和記憶體占用量,瞭解不同類型的數據進行各種運算的能力。 使用`pandas`進行數據分析時,最常用到的幾種類型是: 1. 字元串類型,各類文本內容都是字元串類型 2. 數值類型,包括整數和浮點數,可用於計算 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...