
在本節(jié)之前,該系列文章已經(jīng)自頂向下分析了Netty的基本組件:
EventLoop,Channel和ChannelHandler,而本節(jié)將分析最后一個組件:字節(jié)緩沖區(qū)ByteBuf,可認為是圖中subReactor與read和send之間的部分。
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)了ByteBuf和ByteBuffer在命名上有極大的相似性,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,那么謹記這四個步驟:
- 寫入數(shù)據(jù)到
ByteBuffer - 調用
flip()方法 - 從
ByteBuffer中讀取數(shù)據(jù) - 調用
clear()方法或者compact()方法