Netty之Unpooled_Bytebuf


前言

計算機存儲基本單位是字節(byte),傳輸基本單位是bit(位),JAVA NIO提供了ByteBuffer等七種容器來提升傳輸時的效率,但是在使用時比較復雜,經常要進行讀寫切換,主要缺點如下:

(1)ByteBuffer長度固定,一旦分配完成,它的容量不能動態擴展和收縮,當需要編碼的對象大於ByteBuffer的容量時,會發生索引越界異常;

(2)ByteBuffer只有一個標識位置的指針position,讀寫的時候需要手工調用flip()和rewind()等,使用者必須小心謹慎地處理這些API,否則很容易導致程序處理失敗;

(3)ByteBuffer的API功能有限,一些高級和實用的特性它不支持,需要使用者自己編程實現。

為了彌補這些不足,Netty提供了自己的ByteBuffer實現—— ByteBuf

ByteBuf介紹

ByteBuf是Netty.Buffer中的類,主要特征如下:

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

ByteBuf通過兩個位置指針來協助緩沖區的讀寫操作,分別是讀操作(使用readerIndex)、寫操作(使用writerIndex)。

readerIndex和writerIndex的取值一開始都是0,隨着數據的寫入writerIndex會增加,讀取數據會使readerIndex增加,但是它不會超過writerIndex。在讀取之后,0~readerIndex的就被視為discard的,調discardReadBytes方法,可以釋放這部分空間,它的作用類似ByteBuffer的compact方法。readerIndex和writerIndex之間的數據是可讀取的,等價於ByteBuffer position和limit之間的數據。writerIndex和capacity之間的空間是可寫的,等價於ByteBuffer limit和capacity之間的可用空間。

由於寫操作不修改readerIndex指針,讀操作不修改writerIndex指針,因此讀寫之間不再需要調整位置指針,這極大地簡化了緩沖區的讀寫操作,避免了由於遺漏或者不熟悉flip()操作導致的功能異常。

傳統JAVA NIO ByteBuffer圖示:

ByteBuf讀寫示意圖:

ByteBuf零拷貝

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

以前的做法

ByteBuffer header = ByteBuffer.allocate(1);
    header.put("a".getBytes()); 
    header.flip(); 
ByteBuffer body = ByteBuffer.allocate(1);
    body.put("b".getBytes()); 
	body.flip(); ByteBuffer message = ByteBuffer.allocate(header.remaining() + body.remaining()); 			message.put(header); 
    message.put(body);
    message.flip();
while (message.hasRemaining()){
		System.err.println((char)message.get());
        }

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

netty提供的方法

CompositeByteBuf messageBuf = Unpooled.compositeBuffer(); 
ByteBuf headerBuf = Unpooled.buffer(1); 
	headerBuf.writeByte('a');
ByteBuf bodyBuf = Unpooled.buffer(1);
	bodyBuf.writeByte('b'); 
	messageBuf.addComponents(headerBuf, bodyBuf); 
for (ByteBuf buf : messageBuf) {
    System.out.println((char)buf.readByte()); 
    System.out.println(buf.toString());10        
}

這里通過CompositeByteBuf 對象將headerBuf 與bodyBuf組合到了一起,也得到了完整的消息體,但是並未進行內存上的拷貝。可以注意下我在上面代碼段中進行的buf.toString()方法的調用,得出來的結果是:指向的還是原來分配的空間地址,也就證明了零拷貝的觀點。

ByteBuf引用計數

retain和lock類似,release和unlock類似,內部維護一個計數器,計數器到0的時候就表示已經釋放掉了。往一個已經被release掉的buffer中去寫數據,會拋出IllegalReferenceCountException: refCnt: 0異常。

ByteBuf buffer = Unpooled.buffer(1);
int i = buffer.refCnt();         
System.err.println("refCnt : " + i);    //refCnt : 1 
buffer.retain(); 
buffer.retain(); 
buffer.retain(); 
buffer.retain(); 
i = buffer.refCnt();
System.err.println("refCnt : " + i);      //refCnt : 5
boolean release = buffer.release();
i = buffer.refCnt();
System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 4 ===== false
release = buffer.release(4);
i = buffer.refCnt();
System.err.println("refCnt : " + i + " ===== " + release);      //refCnt : 0 ===== true

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

ByteBuf支持池

Netty對ByteBuf的分配提供了池支持,具體的類是PooledByteBufAllocator。用這個分配器去分配ByteBuf可以提升性能以及減少內存碎片。Netty中默認用PooledByteBufAllocator當做ByteBuf的分配器。PooledByteBufAllocator對象可以從Channel中或者綁定了Channel的ChannelHandlerContext中去獲取到。

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();

ByteBuf API介紹

創建ByteBuf

// 創建一個heapBuffer,是在堆內分配的 
ByteBuf heapBuf = Unpooled.buffer(5); 
if (heapBuf.hasArray()) { 
    byte[] array = heapBuf.array(); 
    int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); 
    int length = heapBuf.readableBytes(); 
    handleArray(array, offset, length); 
} 
// 創建一個directBuffer,是分配的堆外內存
ByteBuf directBuf = Unpooled.directBuffer();
if (!directBuf.hasArray()) {
    int length = directBuf.readableBytes();
    byte[] array = new byte[length];
    directBuf.getBytes(directBuf.readerIndex(), array);
    handleArray(array, 0, length);
}

這兩者的主要區別:

a. 分配的堆外內存空間,在進行網絡傳輸時就不用進行拷貝,直接被網卡使用。但是這些空間想要被jvm所使用,必須拷貝到堆內存中。
b. 分配和釋放堆外內存相比堆內存而言,代價是相當昂貴的。
c. 使用這兩者buffer中的數據的方式也略有不同,見上面的代碼段。

讀寫數據(readByte writeByte)

ByteBuf heapBuf = Unpooled.buffer(5);
heapBuf.writeByte(1);
System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1
heapBuf.readByte();
System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1
heapBuf.setByte(2, 2);
System.err.println("writeIndex : " + heapBuf.writerIndex());//writeIndex : 1
heapBuf.getByte(2);
System.err.println("readIndex : " + heapBuf.readerIndex());//readIndex : 1

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

discardReadBytes方法

先看一張圖:

從上面的圖中可以觀察到,調用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)方法,使用方法和上面介紹的一致,但是會進行內存復制工作,效率很低。

ByteBuf heapBuf = Unpooled.buffer(5); 
heapBuf.writeByte(1); 
heapBuf.writeByte(1); 
heapBuf.writeByte(1); 
heapBuf.writeByte(1); 
// 直接拷貝整個buffer 
ByteBuf duplicate = heapBuf.duplicate(); 
duplicate.setByte(0, 2); 
System.err.println("duplicate: " + duplicate.getByte(0) + "====heapBuf: " + heapBuf.getByte(0));//duplicate: 2====heapBuf: 2
// 拷貝buffer中已經寫了的數據
ByteBuf slice = heapBuf.slice();
System.err.println("slice capacity: " + slice.capacity());//slice capacity: 4
slice.setByte(2, 5);
ByteBuf slice1 = heapBuf.slice(0, 3);
System.err.println("slice1 capacity: "+slice1.capacity());//slice1 capacity: 3
System.err.println("duplicate: " + duplicate.getByte(2) + "====heapBuf: " + heapBuf.getByte(2));//duplicate: 5====heapBuf: 5復制代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM