netty系列之:netty中的ByteBuf詳解

hjavn發表於2021-08-04

工具與資源中心

幫助開發者更加高效的工作,提供圍繞開發者全生命週期的工具與資源
developer.aliyun.com/tool?spm=a1z3...

netty中用於進行資訊承載和交流的類叫做ByteBuf,從名字可以看出這是Byte的快取區,那麼ByteBuf都有哪些特性呢?一起來看看。

netty提供了一個io.netty.buffer的包,該包裡面定義了各種型別的ByteBuf和其衍生的型別。

netty Buffer的基礎是ByteBuf類,這是一個抽象類,其他的Buffer類基本上都是由該類衍生而得的,這個類也定義了netty整體Buffer的基調。

先來看下ByteBuf的定義:

public abstract class ByteBuf implements ReferenceCounted, Comparable {

ByteBuf實現了兩個介面,分別是ReferenceCounted和Comparable。Comparable是JDK自帶的介面,表示該類之間是可以進行比較的。而ReferenceCounted表示的是物件的引用統計。當一個ReferenceCounted被例項化之後,其引用count=1,每次呼叫retain() 方法,就會增加count,呼叫release() 方法又會減少count。當count減為0之後,物件將會被釋放,如果試圖訪問被釋放過後的物件,則會報訪問異常。

如果一個物件實現了ReferenceCounted,並且這個物件裡面包含的其他物件也實現了ReferenceCounted,那麼當容器物件的count=0的時候,其內部的其他物件也會被呼叫release()方法進行釋放。

綜上,ByteBuf是一個可以比較的,可以計算引用次數的物件。他提供了序列或者隨機的byte訪問機制。

注意的是,雖然JDK中有自帶的ByteBuffer類,但是netty中的 ByteBuf 算是對Byte Buffer的重新實現。他們沒有關聯關係。

建立一個Buff

ByteBuf是一個抽象類,並不能直接用來例項化,雖然可以使用ByteBuf的子類進行例項化操作,但是netty並不推薦。netty推薦使用io.netty.buffer.Unpooled來進行Buff的建立工作。Unpooled是一個工具類,可以為ByteBuf分配空間、拷貝或者封裝操作。

下面是建立幾個不同ByteBuf的例子:

import static io.netty.buffer.Unpooled.*; ByteBuf heapBuffer = buffer(128); ByteBuf directBuffer = directBuffer(256); ByteBuf wrappedBuffer = wrappedBuffer(new byte[128], new byte[256]); ByteBuf copiedBuffer = copiedBuffer(ByteBuffer.allocate(128));

上面我們看到了4種不同的buff構建方式,普通的buff、directBuffer、wrappedBuffer和copiedBuffer。

普通的buff是固定大小的堆buff,而directBuffer是固定大小的direct buff。direct buff使用的是堆外記憶體,省去了資料到核心的拷貝,因此效率比普通的buff要高。

wrappedBuffer是對現有的byte arrays或者byte buffers的封裝,可以看做是一個檢視,當底層的資料發生變化的時候,Wrapped buffer中的資料也會發生變化。

Copied buffer是對現有的byte arrays、byte buffers 或者 string的深拷貝,所以它和wrappedBuffer是不同的,Copied buffer和原資料之間並不共享資料。

隨機訪問Buff

熟悉集合的朋友應該都知道,要想隨機訪問某個集合,一定是通過index來訪問的,ByteBuf也一樣,可以通過capacity或得其容量,然後通過getByte方法隨機訪問其中的byte,如下所示:

//隨機訪問 ByteBuf buffer = heapBuffer; for (int i = 0; i < buffer.capacity(); i ++) { byte b = buffer.getByte(i); System.out.println((char) b); }

序列讀寫

讀寫要比訪問複雜一點,ByteBuf 提供了兩個index用來定位讀和寫的位置,分別是readerIndex 和 writerIndex ,兩個index分別控制讀和寫的位置。

下圖顯示的一個buffer被分成了三部分,分別是可廢棄的bytes、可讀的bytes和可寫的bytes。

+——————-+——————+——————+ | discardable bytes | readable bytes | writable bytes | | | (CONTENT) | | +——————-+——————+——————+ | | | | 0 <= readerIndex <= writerIndex <= capacity

上圖還表明了readerIndex、writerIndex和capacity的大小關係。

其中readable bytes是真正的內容,可以通過呼叫read* 或者skip* 的方法來進行訪問或者跳過,呼叫這些方法的時候,readerIndex會同步增加,如果超出了readable bytes的範圍,則會丟擲IndexOutOfBoundsException。預設情況下readerIndex=0。

下面是一個遍歷readable bytes的例子:

//遍歷readable bytes while (directBuffer.isReadable()) { System.out.println(directBuffer.readByte()); }

首先通過判斷是否是readable來決定是否呼叫readByte方法。

Writable bytes是一個未確定的區域,等待被填充。可以通過呼叫write*方法對其操作,同時writerIndex 會同步更新,同樣的,如果空間不夠的話,也會丟擲IndexOutOfBoundsException。預設情況下 新分配的writerIndex =0 ,而wrapped 或者copied buffer的writerIndex=buf的capacity。

下面是一個使用writable Byte的例子:

//寫入writable bytes while (wrappedBuffer.maxWritableBytes() >= 4) { wrappedBuffer.writeInt(new Random().nextInt()); }

Discardable bytes是已經被讀取過的bytes,初始情況下它的值=0,每當readerIndex右移的時候,Discardable bytes的空間就會增加。如果想要完全刪除或重置Discardable bytes,則可以呼叫discardReadBytes()方法,該方法會將Discardable bytes空間刪除,將多餘的空間放到writable bytes中,如下所示:

呼叫 discardReadBytes() 之前: +——————-+——————+——————+ | discardable bytes | readable bytes | writable bytes | +——————-+——————+——————+ | | | | 0 <= readerIndex <= writerIndex <= capacity 呼叫 discardReadBytes()之後: +——————+————————————–+ | readable bytes | writable bytes (got more space) | +——————+————————————–+ | | |

readerIndex (0) <= writerIndex (decreased) <= capacity

注意,雖然writable bytes變多了,但是其內容是不可控的,並不能保證裡面的內容是空的或者不變。

呼叫clear()方法會將readerIndex 和 writerIndex 清零,注意clear方法只會設定readerIndex 和 writerIndex 的值,並不會清空content,看下面的示意圖:

呼叫 clear()之前: +——————-+——————+——————+ | discardable bytes | readable bytes | writable bytes | +——————-+——————+——————+ | | | | 0 <= readerIndex <= writerIndex <= capacity 呼叫 clear()之後: +———————————————————+ | writable bytes (got more space) | +———————————————————+ | | 0 = readerIndex = writerIndex <= capacity

搜尋

ByteBuf提供了單個byte的搜尋功能,如 indexOf(int, int, byte) 和 bytesBefore(int, int, byte)兩個方法。

如果是要對ByteBuf遍歷進行搜尋處理的話,可以使用 forEachByte(int, int, ByteProcessor),這個方法接收一個ByteProcessor用於進行復雜的處理。

其他衍生buffer方法

ByteBuf還提供了很多方法用來建立衍生的buffer,如下所示:

duplicate() slice() slice(int, int) readSlice(int) retainedDuplicate() retainedSlice() retainedSlice(int, int) readRetainedSlice(int)

要注意的是,這些buf是建立在現有buf基礎上的衍生品,他們的底層內容是一樣的,只有readerIndex, writerIndex 和做標記的index不一樣。所以他們和原buf是有共享資料的。如果你希望的是新建一個全新的buffer,那麼可以使用copy()方法或者前面提到的Unpooled.copiedBuffer。

在前面小節中,我們講到ByteBuf是一個ReferenceCounted,這個特徵在衍生buf中就用到了。我們知道呼叫retain() 方法的時候,引用count會增加,但是對於 duplicate(), slice(), slice(int, int) 和 readSlice(int) 這些方法來說,雖然他們也是引用,但是沒有呼叫retain()方法,這樣原始資料會在任意一個Buf呼叫release()方法之後被回收。

如果不想有上面的副作用,那麼可以將方法替換成retainedDuplicate(), retainedSlice(), retainedSlice(int, int) 和 readRetainedSlice(int) ,這些方法會呼叫retain()方法以增加一個引用。

和現有JDK型別的轉換

之前提到了ByteBuf 是對ByteBuffer的重寫,他們是不同的實現。雖然這兩個不同,但是不妨礙將ByteBuf轉換ByteBuffer。

當然,最簡單的轉換是把ByteBuf轉換成byte陣列byte[]。要想轉換成byte陣列,可以先呼叫hasArray() 進行判斷,然後再呼叫array()方法進行轉換。

同樣的ByteBuf還可以轉換成為ByteBuffer ,可以先呼叫 nioBufferCount()判斷能夠轉換成為 ByteBuffers的個數,再呼叫nioBuffer() 進行轉換。

返回的ByteBuffer是對現有buf的共享或者複製,對返回之後buffer的position和limit修改不會影響到原buf。

最後,使用toString(Charset) 方法可以將ByteBuf轉換成為String。

ByteBuf是netty的底層基礎,是傳輸資料的承載物件,深入理解ByteBuf就可以搞懂netty的設計思想,非常不錯。

本文的例子可以參考:learn-netty4

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章