
👆關注微信公眾號,獲取更多編程內容
Java的NIO模塊提供了ByteBuffer作為其字節存儲容器,但是這個類的使用過於復雜,因此Netty實現了ByteBuf來替換NIO的ByteBuffer類,ByteBuf具有以下的特點:
- 自定義用戶緩沖區域的類型
- 實現字節區域的深淺拷貝
- 容量可按需增長
- 在讀寫模式直接不需要像JDK的ByteBuffer那樣調用flip()方法切換
- 讀寫使用不同的索引,即readIndex和writeIndex
- 支持方法鏈式調用
- 支持引用計數和池化操作
下面我們將深入學習Netty的核心之一的ByteBuf的內部。
1. 讀索引和寫索引
ByteByf內部有三個主要的標記分別是readInde、writeIndex以及capacity,分別用來標記度索引,寫索引,以及容器容量.其基本數據結果如下圖所示:
當構建一個新的非Wrap和Copy類型的ByteBuf的時候readIndex = writeIndex = 0
在后面的讀取操作,主要分為兩類: 相對操作和絕對操作
-
相對操作: 在讀寫操作的時候移動readIndex或者writeIndex,主要是read() 或者skip()或者write*()操作方法,這些操作在使用的時候會移動這兩個索引。
-
絕對操作: 絕對操作主要是是通過索引隨機讀取(這里的索引並非readIndex和writeIndex,值得是字節的索引位置),通過這種操作並不會移動readIndex或者writeIndex,比如getByte(int index)、setByte(int index,int value)等;
如果readIndex > write 或者 write > capacity 的時候將會拋出IndexOutOfBoundException
2. 緩存區類型
ByteBuf實現的類型主要有三種:
- 堆緩沖區
- 直接緩沖區(非堆緩沖)
- 復合緩沖區
堆內存緩沖區由JVM進行管理,因此創建和釋放非常的方便,但是在進行數據交互的時候,需要從JVM內存中拷貝到操作系統直接內存中,性能略低。同樣的對於直接緩沖器而言,其讀寫數據非常方便,不需要再次拷貝數據,但是JVM對其管理(創建和釋放)相對於JVM堆內存而言,都不是太容易。
所以綜合而言,針對后端的數據處理業務,則推薦使用HeapByteBuf,對IO線程交互等操作,推薦使用DirectByteBuf.
2.1 堆緩沖區
堆緩沖區是ByteBuf最常見的實現模式,其實將數據存儲在JVM的堆內存中,內部實現是array數據,所以這種模式也成為數組支撐
,可以通過hasArray()
方法判斷其內部實現是否是數組支撐,如果是的話,我們可以安全的使用arrayOffset()
方法來獲取偏移量結合readableBytes()來獲取其底層實現,即獲取byte[],比如:
Charset charset = Charset.forName("utf-8");
ByteBuf stringByteBuf = Unpooled.copiedBuffer("hello world", charset);
if (stringByteBuf.hasArray()) {
byte[] array = stringByteBuf.array();
// as this, arrayOffset === 0
int startIndex = stringByteBuf.arrayOffset() + stringByteBuf.readerIndex();
// return value = writeIndex - readIndex
int length = stringByteBuf.readableBytes();
byte[] newBytes = Arrays.copyOfRange(array, startIndex, length);
System.out.println("數組支撐轉換:" + new String(newBytes,charset));
}
如果不是數組支撐實現的話,調用
arrayOffset()
方法將會拋出UnsupportOperationException.
2.2 直接緩沖區
直接緩沖區是ByteBuf的另外一種實現的模式,但是其內存分配是操作系統實現的,且內存並非在堆內存上。JavaDoc的
文檔指出
直接緩沖區的內容將會駐留在常規的會被垃圾回收的堆內存之外
這也就說明了為什么直接緩沖區是對於網絡數據傳輸的最理想的方式,但是直接緩沖區相對於堆緩沖區來說,其分配和釋放都比較昂貴,如果需要進行直接緩沖區的數據字節操作,你首先需要的是進入數據的復制操作,下面的代碼是基於直接緩沖區的讀操作:
Charset charset = Charset.forName("utf-8");
ByteBuf stringByteBuf = Unpooled.copiedBuffer("hello world", charset);
if(!stringByteBuf.hasArray()){
int lenght = stringByteBuf.readableBytes();
byte[] bytes = new byte[lenght];
stringByteBuf.getBytes(stringByteBuf.readerIndex(),bytes);
System.out.println(new String(bytes,charset));
}
2.3 復合緩沖區
復合緩沖區比較復雜,其主要是CompositeByteBuf實現,后面專門開一個文件學習這個實現模式.
3 字節讀寫操作
ByteBuf是一個抽象類,不能使用new關鍵字創建,我們可以使用Unpooled創建,如下:
// 創建ByteBuf ,capacity設置為10
ByteBuf byteBuf = Unpooled.buffer(10);
3.1 寫操作
ByteBuf提供了多個writeByte() 重載操作,我們可以使用這個進行寫數據。使用writeByte()方法寫入數據會自動移動writeInde,如下:
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("寫數據之前ByteBuf的writeIndex=" + buffer.writerIndex());
for (int i = 0; i < buffer.capacity(); i++) {
buffer.writeByte(i);
}
System.out.println("寫數據之后ByteBuf的writeIndex=" + buffer.writerIndex());
同時ByteBuf也提供了set(int index,int value)按索引寫入的方法,給定索引和數據,按索引寫入,如下:
ByteBuf buffer = Unpooled.buffer(10);
System.out.println("寫數據之前ByteBuf的writeIndex=" + buffer.writerIndex());
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
System.out.println("寫數據之后ByteBuf的writeIndex=" + buffer.writerIndex());
寫操作的時候,如果writeIndex > capaction 則會按指數擴容
3.2 讀操作
同樣的,讀操作也有兩種方式: 一種是按順序讀取,另外一種是指定索引讀取. 按順序則通過readByte()
讀取,readByte讀取每次讀取一個字節,同時readIndex會自動移動,按指定索引讀取,則不會移動readIndex.
3.2.1 按順序讀取
ByteBuf可以通過readByte()方法讀取,如果可讀字節耗盡,那么將會拋出IndexOutOfBoundException異常,所以在每次readByte()的時候需要判斷是否可讀,示例代碼如下:
ByteBuf buffer = Unpooled.buffer(10);
// 寫數據
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
// 循環遍歷,有數據繼續讀取
while (byteBuf.isReadable()) {
System.out.print(byteBuf.readByte());
}
3.2.2 按索引讀取
使用getByte(int index)可以獲取數據,但是readIndex不會移動,但是要求readIndex小於writeIndex.
ByteBuf buffer = Unpooled.buffer(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
for (int i = 0; i < buffer.capacity(); i++) {
System.out.print(buffer.getByte(i));
}
3.3 搜索操作
ByteBuf最簡單的搜索方法就是indexOf()
方法,indexOf() 不存在的時候返回的索引為 -1 .
ByteBuf buffer = Unpooled.buffer(6);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.setByte(i,i);
}
int index = buffer.indexOf(0,buffer.capacity(),Byte.valueOf("5"));
System.out.println("查找到索引位置 = " + index);
index = buffer.indexOf(0,buffer.capacity(),Byte.valueOf("9"));
System.out.println("查找到索引位置 = " + index);
3.4 索引管理
ByteBuf 提供了mark()和reset()方法來標記和重置readIndex和writeIndex這兩個索引,分別是:
- markReadIndex()
- markWriteIndex()
- resetReadIndex()
- resetWriteIndex()
若要重置readIndex和writeIndex,可以使用clean()方法,clean() 方法的實現為readIndex = writeIndex = 0
,但是不會清除內容,測試代碼如下:
// 申請空間
ByteBuf byteBuf = Unpooled.buffer(10);
// 寫入7個數據
for (int i = 0; i < 7; i++) {
byteBuf.writeByte(i);
}
// 輸出數據
while (byteBuf.isReadable()) {
System.out.print(byteBuf.readByte());
System.out.print(",");
}
System.out.println();
// 打印當前索引位置
int writeIndex = byteBuf.writerIndex();
// 重置索引
byteBuf.clear();
// 寫入兩個數據
byteBuf.writeByte(10).writeByte(10);
// 將writeIndex索引設置到{writeIndex}
byteBuf.writerIndex(writeIndex);
// 輸出數據,可見0和1被覆蓋了,其他的還是原來的數據
while (byteBuf.isReadable()) {
System.out.print(byteBuf.readByte());
System.out.print(",");
}
3.5 復制操作
某些場景下,我們需要復制某個ByetBuf,ByteBuf提供了兩種復制機制:淺拷貝和深拷貝.
-
淺拷貝 一般使用duplicate()方法,該方法返回一個新的ByteBuf實例, 它具有自己的讀索引和寫索引和標記索引。但其存儲和源對象共享內容,所以修改拷貝實例,原實例也會修改,需要注意。
-
若要創建新的完全真實的副本實例,可使用copy() 方法或copy(int,int)創建完全獨立的副本。