自頂向下深入分析Netty(九)--ByteBuf

Netty架構模式

在本節(jié)之前,該系列文章已經(jīng)自頂向下分析了Netty的基本組件:EventLoop,ChannelChannelHandler,而本節(jié)將分析最后一個組件:字節(jié)緩沖區(qū)ByteBuf,可認為是圖中subReactorreadsend之間的部分。

9.1 ByteBuf總述

引入緩沖區(qū)是為了解決速度不匹配的問題,在網(wǎng)絡通訊中,CPU處理數(shù)據(jù)的速度大大快于網(wǎng)絡傳輸數(shù)據(jù)的速度,所以引入緩沖區(qū),將網(wǎng)絡傳輸?shù)臄?shù)據(jù)放入緩沖區(qū),累積足夠的數(shù)據(jù)再送給CPU處理。

9.1.1 緩沖區(qū)的使用

ByteBuf是一個可存儲字節(jié)的緩沖區(qū),其中的數(shù)據(jù)可提供給ChannelHandler處理或者將用戶需要寫入網(wǎng)絡的數(shù)據(jù)存入其中,待時機成熟再實際寫到網(wǎng)絡中。由此可知,ByteBuf有讀操作和寫操作,為了便于用戶使用,該緩沖區(qū)維護了兩個索引:讀索引和寫索引。一個ByteBuf緩沖區(qū)示例如下:

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

可知,ByteBuf由三個片段構成:廢棄段、可讀段和可寫段。其中,可讀段表示緩沖區(qū)實際存儲的可用數(shù)據(jù)。當用戶使用readXXX()或者skip()方法時,將會增加讀索引。讀索引之前的數(shù)據(jù)將進入廢棄段,表示該數(shù)據(jù)已被使用。此外,用戶可主動使用discardReadBytes()清空廢棄段以便得到跟多的可寫空間,示意圖如下:

清空前:
    +-------------------+------------------+------------------+
    | discardable bytes |  readable bytes  |  writable bytes  |
    +-------------------+------------------+------------------+
    |                   |                  |                  |
    0      <=      readerIndex   <=   writerIndex    <=    capacity
清空后:
    +------------------+--------------------------------------+
    |  readable bytes  |    writable bytes (got more space)   |
    +------------------+--------------------------------------+
    |                  |                                      |
readerIndex (0) <= writerIndex (decreased)       <=       capacity

對應可寫段,用戶可使用writeXXX()方法向緩沖區(qū)寫入數(shù)據(jù),也將增加寫索引。

9.1.2 讀寫索引的非常規(guī)使用

用戶在必要時可以使用clear()方法清空緩沖區(qū),此時緩沖區(qū)的寫索引和讀索引都將置0,但是并不清除緩沖區(qū)中的實際數(shù)據(jù)。如果需要循環(huán)使用一個緩沖區(qū),這個方法很有必要。
此外,用戶可以使用mark()reset()標記并重置讀索引和寫索引。想象這樣的情形:一個數(shù)據(jù)需要寫到寫索引為4的位置,之后的另一個數(shù)據(jù)才寫0-3索引,此時可以先mark標記0索引,然后byteBuf.writeIndex(4),寫入第一個數(shù)據(jù),之后reset重置,寫入第二個數(shù)據(jù)。用戶可根據(jù)不同的業(yè)務,合理使用這兩個方法。
需要說明的一點是:用戶使用toString(Charset)將緩沖區(qū)的字節(jié)數(shù)據(jù)轉為字符串時,并不會增加讀索引。另外,toString()只是覆蓋Object的常規(guī)方法,僅僅表示緩沖區(qū)的常規(guī)信息,并不會轉化其中的字節(jié)數(shù)據(jù)。

9.1.3 ByteBuf的底層及派生

容易想到ByteBuf緩沖區(qū)的底層數(shù)據(jù)結構是一個字節(jié)數(shù)組。從操作系統(tǒng)的角度理解,緩沖區(qū)的區(qū)別在于字節(jié)數(shù)組是在用戶空間還是內核空間。如果位于用戶空間,對于JAVA也就是位于堆,此時可使用JAVA的基本數(shù)據(jù)類型byte[]表示,用戶可使用array()直接取得該字節(jié)數(shù)組,使用hasArray()判定該緩沖區(qū)是否是用戶空間緩沖區(qū)。如果位于內核空間,JAVA程序將不能直接進行操作,此時可委托給JDK NIO中的直接緩沖區(qū)DirectByteBuffer由其操作內核字節(jié)數(shù)組,用戶可使用nioBuffer()取得直接緩沖區(qū),使用nioBufferCount()判定底層是否有直接緩沖區(qū)。
用戶可在已有緩沖區(qū)上創(chuàng)建視圖即派生緩沖區(qū),這些視圖維護各自獨立的寫索引、讀索引以及標記索引,但他們和原生緩沖區(qū)共享想用的內部字節(jié)數(shù)據(jù)。創(chuàng)建視圖即派生緩沖區(qū)的方法有:duplicate(),slice()以及slice(int,int)。如果想拷貝緩沖區(qū),也就是說期望維護特有的字節(jié)數(shù)據(jù)而不是共享字節(jié)數(shù)據(jù),此時可使用copy()方法。

9.2 ByteBuf VS ByteBuffer

也許你已經(jīng)發(fā)現(xiàn)了ByteBufByteBuffer在命名上有極大的相似性,JDK的NIO包中既然已經(jīng)有字節(jié)緩沖區(qū)ByteBuffer 的實現(xiàn),為什么Netty還要重復造輪子呢?一個很大的原因是:ByteBuffer對程序員并不友好。
考慮這樣的需求,向緩沖區(qū)寫入兩個字節(jié)0x01和0x02,然后讀取出這兩個字節(jié)。如果使用ByteBuffer,代碼是這樣的:

    ByteBuffer buf = ByteBuffer.allocate(4);
    buf.put((byte) 1);
    buf.put((byte) 2);

    buf.flip(); // 從寫模式切換為讀模式
    System.out.println(buf.get());  // 取出0x01
    System.out.println(buf.get());  // 取出0x02

對于熟悉Netty的ByteBuf的你來說,或許只是多了一行buf.flip()用于將緩沖區(qū)從寫模式卻換為讀模式。但事實并不如此,注意示例中申請了4個字節(jié)的空間,此時理應可以繼續(xù)寫入數(shù)據(jù)。不幸的是,如果再次調用buf.put((byte)3),將拋出java.nio.BufferOverflowException。而要正確達到該目的,需要調用buf.clear()清空整個緩沖區(qū)或者buf.compact()清除已經(jīng)讀過的數(shù)據(jù)。
這個操作雖然有些繁瑣,但并不是不能忍受,那么繼續(xù)上個例子,考慮這樣取數(shù)據(jù)的操作:

    buf.flip();
    System.out.println(buf.get(0));
    System.out.println(buf.get(1));
    
    System.out.println(buf.get());
    System.out.println(buf.get());

通過之前的分析,聰明的你也許已經(jīng)發(fā)現(xiàn)get()操作會增加讀索引,那么get(index)操作也會增加讀索引嗎?答案是:并不會,所以這個代碼示例是正確的,將輸出0 1 0 1的結果。什么?get()get(0)居然是兩個不一樣的操作,前者會增加讀索引而后者并不會。是的,可以掀桌子了。此外,get()的方法名本身就很有迷惑性,很自然的會認為與數(shù)組的get()一致,但是卻有一個極大的副作用:增加索引,所以合理的名字應該是:getAndIncreasePosition。
又引入了一個新名詞position,事實上ByteBuffer中并沒有讀索引和寫索引的說法,這兩個索引被統(tǒng)一稱為position。在讀寫模式切換時,該值將會改變,正好與事實上的讀索引與寫索引對應。但愿這樣的說法,并沒有讓你覺得頭暈。
如果我們使用Netty的ByteBuf,感覺世界清靜了很多:

    ByteBuf buf2 = Unpooled.buffer(4);
    buf2.writeByte(1);
    buf2.writeByte(2);

    System.out.println(buf2.readByte());
    System.out.println(buf2.readByte());
    buf2.writeByte(3);
    buf2.writeByte(4);

當然,如果不幸分配到了噩夢模式,必須使用ByteBuffer,那么謹記這四個步驟:

  1. 寫入數(shù)據(jù)到ByteBuffer
  2. 調用flip()方法
  3. ByteBuffer中讀取數(shù)據(jù)
  4. 調用clear()方法或者compact()方法
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容