Netty系列(二):談談ByteBuf

前言

在網絡傳輸過程當中,字節是最基本也是最小的單元。JAVA NIO有提供一個ByteBuffer容器去裝載這些數據,可是用起來會有點複雜,常常要在讀寫間進行切換以及不支持動態擴展等等。而netty爲咱們提供了一個ByteBuf組件,功能是很強大的,本文主要對ByteBuf進行一些講解,中間會穿插着和ByteBuffer進行對比。java


優點

ByteBuf與ByteBuffer的相比的優點:git

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

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


讀寫索引

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

bytebuffer.png
bytebuffer.png

能夠根據下面簡單的代碼自行測試一下:app

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

能夠根據下面簡單的代碼自行測試一下:jvm

 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完美的解決了這一問題,支持動態擴展其容量。ide


零拷貝

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()方法的調用,得出來的結果是:指向的仍是原來分配的空間地址,也就證實了零拷貝的觀點。ui


支持引用計數

看一段簡單的代碼段:

 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

相關文章
相關標籤/搜索