Netty系列(二):談談ByteBuf

mars_jun發表於2019-01-22

前言

在網路傳輸過程中,位元組是最基本也是最小的單元。JAVA NIO有提供一個ByteBuffer容器去裝載這些資料,但是用起來會有點複雜,經常要在讀寫間進行切換以及不支援動態擴充套件等等。而netty為我們提供了一個ByteBuf元件,功能是很強大的,本文主要對ByteBuf進行一些講解,中間會穿插著和ByteBuffer進行對比。


優勢

ByteBuf與ByteBuffer的相比的優勢:

  1. 讀和寫用不同的索引。
  2. 讀和寫可以隨意的切換,不需要呼叫flip()方法。
  3. 容量能夠被動態擴充套件,和StringBuilder一樣。
  4. 用其內建的複合緩衝區可實現透明的零拷貝。
  5. 支援方法鏈。
  6. 支援引用計數。count == 0,release。
  7. 支援池。

下面將會對每一種優勢進行詳細的解讀。


讀寫索引

ByteBuffer讀寫同用position索引,利用flip()方法切換讀寫模式,而ByteBuf讀寫分不同的索引,讀用readIndex,寫用writeIndex,這樣可以更加方便我們進行操作,省去了flip這一步驟。ByteBuffer與ByteBuf兩種讀寫模型會在下面用圖解形式給大家進行說明。

bytebuffer.png
bytebuffer.png

可以根據下面簡單的程式碼自行測試一下:

1        ByteBuffer byteBuffer = ByteBuffer.allocate(8);
2        System.err.println("startPosition: " + byteBuffer.position() + ",limit: " + byteBuffer.limit() + ",capacity: " + byteBuffer.capacity());
3        byteBuffer.put("abc".getBytes());
4        System.err.println("writePosition: " + byteBuffer.position() + ",limit: " + byteBuffer.limit() + ",capacity: " + byteBuffer.capacity());
5        byteBuffer.flip();
6        System.err.println("readPosition: " + byteBuffer.position() + ",limit: " + byteBuffer.limit() + ",capacity: " + byteBuffer.capacity());
複製程式碼

bytebuf.png
bytebuf.png

可以根據下面簡單的程式碼自行測試一下:

 1        ByteBuf heapBuffer = Unpooled.buffer(8);
2        int startWriterIndex = heapBuffer.writerIndex();
3        System.err.println("startWriterIndex: " + startWriterIndex);
4        int startReadIndex = heapBuffer.readerIndex();
5        System.err.println("startReadIndex: " + startReadIndex);
6        System.err.println("capacity: " + heapBuffer.capacity());
7        System.err.println("========================");
8        for (int i = 0; i < 3; i++) {
9            heapBuffer.writeByte(i);
10        }
11        int writerIndex = heapBuffer.writerIndex();
12        System.err.println("writerIndex: " + writerIndex);
13        heapBuffer.readBytes(2);
14        int readerIndex = heapBuffer.readerIndex();
15        System.err.println("readerIndex: " + readerIndex);
16        System.err.println("capacity: " + heapBuffer.capacity());
複製程式碼

動態擴充套件

ByteBuffer是不支援動態擴充套件的,給定一個具體的capacity,一旦put進去的資料超過其容量,就會丟擲java.nio.BufferOverflowException異常,而ByteBuf完美的解決了這一問題,支援動態擴充套件其容量。


零拷貝

netty提供了CompositeByteBuf類實現零拷貝。大多數情況下,在進行網路資料傳輸時我們會將訊息分為訊息頭head訊息體body,甚至還會有其他部分,這裡我們簡單的分為兩部分來進行探討:

以前的做法
 1        ByteBuffer header = ByteBuffer.allocate(1);
2        header.put("a".getBytes());
3        header.flip();
4        ByteBuffer body = ByteBuffer.allocate(1);
5        body.put("b".getBytes());
6        body.flip();
7        ByteBuffer message = ByteBuffer.allocate(header.remaining() + body.remaining());
8        message.put(header);
9        message.put(body);
10        message.flip();
11        while (message.hasRemaining()){
12            System.err.println((char)message.get());
13        }
複製程式碼

這樣為了得到完整的訊息體相當於對記憶體進行了多餘的兩次拷貝,造成了很大的資源的浪費。

netty提供的方法
 1        CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
2        ByteBuf headerBuf = Unpooled.buffer(1);
3        headerBuf.writeByte('a');
4        ByteBuf bodyBuf = Unpooled.buffer(1);
5        bodyBuf.writeByte('b');
6        messageBuf.addComponents(headerBuf, bodyBuf);
7        for (ByteBuf buf : messageBuf) {
8            System.out.println((char)buf.readByte());
9            System.out.println(buf.toString());
10        }
複製程式碼

這裡通過CompositeByteBuf 物件將headerBuf 與bodyBuf組合到了一起,也得到了完整的訊息體,但是並未進行記憶體上的拷貝。可以注意下我在上面程式碼段中進行的buf.toString()方法的呼叫,得出來的結果是:指向的還是原來分配的空間地址,也就證明了零拷貝的觀點。


支援引用計數

看一段簡單的程式碼段:

 1        ByteBuf buffer = Unpooled.buffer(1);
2        int i = buffer.refCnt();
3        System.err.println("refCnt : " + i);    //refCnt : 1
4        buffer.retain();
5        buffer.retain();
6        buffer.retain();
7        buffer.retain();
8        i = buffer.refCnt();
9        System.err.println("refCnt : " + i);      //refCnt : 5
10        boolean release = buffer.release();
11        i = buffer.refCnt();
12        System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 4 ===== false
13        release = buffer.release(4);
14        i = buffer.refCnt();
15        System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 0 ===== true
複製程式碼

這裡我感覺就是AQS差不多的概念,retain和lock類似,release和unlock類似,內部維護一個計數器,計數器到0的時候就表示已經釋放掉了。往一個已經被release掉的buffer中去寫資料,會丟擲IllegalReferenceCountException: refCnt: 0異常。

Netty in Action一書中對其的介紹是:

The idea behind reference counting isn’t particularly complex; mostly it involves
tracking the number of active references to a specified object. A ReferenceCounted
implementation instance will normally start out with an active reference count of 1. As long as the reference count is greater than 0, the object is guaranteed not to be released.When the number of active references decreases to 0, the instance will be released. Note that while the precise meaning of release may be implementation-specific, at the very least an object that has been released should no longer be available for use.

引用計數器實現的原理並不複雜,僅僅只是涉及到一個指定物件的活動引用,物件被初始化後引用計數值為1。只要引用計數大於0,這個物件就不會被釋放,當引用計數減到為0時,這個例項就會被釋放,被釋放的物件不應該再被使用。


支援池

Netty對ByteBuf的分配提供了池支援,具體的類是PooledByteBufAllocator。用這個分配器去分配ByteBuf可以提升效能以及減少記憶體碎片。Netty中預設用PooledByteBufAllocator當做ByteBuf的分配器。PooledByteBufAllocator物件可以從Channel中或者繫結了Channel的ChannelHandlerContext中去獲取到。

1Channel channel = ...;
2ByteBufAllocator allocator = channel.alloc();
3....
4ChannelHandlerContext ctx = ...;
5ByteBufAllocator allocator2 = ctx.alloc();
複製程式碼

API介紹(介紹容易混淆的幾個)

建立ByteBuf
 1        // 建立一個heapBuffer,是在堆內分配的
2        ByteBuf heapBuf = Unpooled.buffer(5);
3        if (heapBuf.hasArray()) {
4            byte[] array = heapBuf.array();
5            int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
6            int length = heapBuf.readableBytes();
7            handleArray(array, offset, length);
8        }
9        // 建立一個directBuffer,是分配的堆外記憶體
10        ByteBuf directBuf = Unpooled.directBuffer();
11        if (!directBuf.hasArray()) {
12            int length = directBuf.readableBytes();
13            byte[] array = new byte[length];
14            directBuf.getBytes(directBuf.readerIndex(), array);
15            handleArray(array0, length);
16        }
複製程式碼

這兩者的主要區別:
a. 分配的堆外記憶體空間,在進行網路傳輸時就不用進行拷貝,直接被網路卡使用。但是這些空間想要被jvm所使用,必須拷貝到堆記憶體中。
b. 分配和釋放堆外記憶體相比堆記憶體而言,是相當昂貴的。
c. 使用這兩者buffer中的資料的方式也略有不同,見上面的程式碼段。


讀寫資料(readByte writeByte)
1        ByteBuf heapBuf = Unpooled.buffer(5);
2        heapBuf.writeByte(1);
3        System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1
4        heapBuf.readByte();
5        System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1
6        heapBuf.setByte(22);
7        System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1
8        heapBuf.getByte(2);
9        System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1
複製程式碼

進行readByte和writeByte方法的呼叫時會改變readIndex和writeIndex的值,而呼叫set和get方法時不會改變readIndex和writeIndex的值。上面的測試案例中列印的writeIndex和readIndex均為1,並未在呼叫set和get方法後被改變。


discardReadBytes方法

先看一張圖:

discardReadBytes.png
discardReadBytes.png

從上面的圖中可以觀察到,呼叫discardReadBytes方法後,readIndex置為0,writeIndex也往前移動了Discardable bytes長度的距離,擴大了可寫區域。但是這種做法會嚴重影響效率,它進行了大量的拷貝工作。如果要進行資料的清除操作,建議使用clear方法。呼叫clear()方法將會將readIndex和writeIndex同時置為0,不會進行記憶體的拷貝工作,同時要注意,clear方法不會清除記憶體中的內容,只是改變了索引位置而已。


Derived buffers

這裡介紹三個方法(淺拷貝):

duplicate():直接拷貝整個buffer。
slice():拷貝buffer中已經寫了的資料。
slice(index,length): 拷貝buffer中從index開始,長度為length的資料。
readSlice(length): 從當前readIndex讀取length長度的資料。

我對上面這幾個方法的形容雖然是拷貝,但是這幾個方法並沒有實際意義上去複製一個新的buffer出來,它和原buffer是共享資料的。所以說呼叫這些方法消耗是很低的,並沒有開闢新的空間去儲存,但是修改後會影響原buffer。這種方法也就是我們們俗稱的淺拷貝。
要想進行深拷貝,這裡可以呼叫copy()和copy(index,length)方法,使用方法和上面介紹的一致,但是會進行記憶體複製工作,效率很低。
測試demo:

 1        ByteBuf heapBuf = Unpooled.buffer(5);
2        heapBuf.writeByte(1);
3        heapBuf.writeByte(1);
4        heapBuf.writeByte(1);
5        heapBuf.writeByte(1);
6        // 直接拷貝整個buffer
7        ByteBuf duplicate = heapBuf.duplicate();
8        duplicate.setByte(02);
9        System.err.println("duplicate: " + duplicate.getByte(0) + "====heapBuf: " + heapBuf.getByte(0));//duplicate: 2====heapBuf: 2
10        // 拷貝buffer中已經寫了的資料
11        ByteBuf slice = heapBuf.slice();
12        System.err.println("slice capacity: " + slice.capacity());//slice capacity: 4
13        slice.setByte(25);
14        ByteBuf slice1 = heapBuf.slice(03);
15        System.err.println("slice1 capacity: "+slice1.capacity());//slice1 capacity: 3
16        System.err.println("duplicate: " + duplicate.getByte(2) + "====heapBuf: " + heapBuf.getByte(2));//duplicate: 5====heapBuf: 5
複製程式碼

上面的所有測試程式碼均可以在我的github中獲取(netty中的buffer模組)。

End

相關文章