在本節(jié)之前,該系列文章已經(jīng)自頂向下分析了Netty的基本組件:
EventLoop
认罩,Channel
和ChannelHandler
衫冻,而本節(jié)將分析最后一個(gè)組件:字節(jié)緩沖區(qū)ByteBuf
,可認(rèn)為是圖中subReactor
與read
和send
之間的部分眷蜈。
9.1 ByteBuf總述
引入緩沖區(qū)是為了解決速度不匹配的問題沪哺,在網(wǎng)絡(luò)通訊中,CPU處理數(shù)據(jù)的速度大大快于網(wǎng)絡(luò)傳輸數(shù)據(jù)的速度酌儒,所以引入緩沖區(qū)辜妓,將網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)放入緩沖區(qū),累積足夠的數(shù)據(jù)再送給CPU處理。
9.1.1 緩沖區(qū)的使用
ByteBuf
是一個(gè)可存儲(chǔ)字節(jié)的緩沖區(qū)籍滴,其中的數(shù)據(jù)可提供給ChannelHandler
處理或者將用戶需要寫入網(wǎng)絡(luò)的數(shù)據(jù)存入其中酪夷,待時(shí)機(jī)成熟再實(shí)際寫到網(wǎng)絡(luò)中。由此可知异逐,ByteBuf
有讀操作和寫操作捶索,為了便于用戶使用,該緩沖區(qū)維護(hù)了兩個(gè)索引:讀索引和寫索引灰瞻。一個(gè)ByteBuf
緩沖區(qū)示例如下:
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
可知,ByteBuf
由三個(gè)片段構(gòu)成:廢棄段辅甥、可讀段和可寫段酝润。其中,可讀段表示緩沖區(qū)實(shí)際存儲(chǔ)的可用數(shù)據(jù)璃弄。當(dāng)用戶使用readXXX()
或者skip()
方法時(shí)要销,將會(huì)增加讀索引。讀索引之前的數(shù)據(jù)將進(jìn)入廢棄段夏块,表示該數(shù)據(jù)已被使用疏咐。此外,用戶可主動(dòng)使用discardReadBytes()
清空廢棄段以便得到跟多的可寫空間脐供,示意圖如下:
清空前:
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
清空后:
+------------------+--------------------------------------+
| readable bytes | writable bytes (got more space) |
+------------------+--------------------------------------+
| | |
readerIndex (0) <= writerIndex (decreased) <= capacity
對(duì)應(yīng)可寫段浑塞,用戶可使用writeXXX()
方法向緩沖區(qū)寫入數(shù)據(jù),也將增加寫索引政己。
9.1.2 讀寫索引的非常規(guī)使用
用戶在必要時(shí)可以使用clear()
方法清空緩沖區(qū)酌壕,此時(shí)緩沖區(qū)的寫索引和讀索引都將置0,但是并不清除緩沖區(qū)中的實(shí)際數(shù)據(jù)歇由。如果需要循環(huán)使用一個(gè)緩沖區(qū)卵牍,這個(gè)方法很有必要。
此外沦泌,用戶可以使用mark()
和reset()
標(biāo)記并重置讀索引和寫索引糊昙。想象這樣的情形:一個(gè)數(shù)據(jù)需要寫到寫索引為4的位置,之后的另一個(gè)數(shù)據(jù)才寫0-3索引谢谦,此時(shí)可以先mark標(biāo)記0索引释牺,然后byteBuf.writeIndex(4)
,寫入第一個(gè)數(shù)據(jù)他宛,之后reset重置船侧,寫入第二個(gè)數(shù)據(jù)。用戶可根據(jù)不同的業(yè)務(wù)厅各,合理使用這兩個(gè)方法镜撩。
需要說明的一點(diǎn)是:用戶使用toString(Charset)
將緩沖區(qū)的字節(jié)數(shù)據(jù)轉(zhuǎn)為字符串時(shí),并不會(huì)增加讀索引。另外袁梗,toString()
只是覆蓋Object
的常規(guī)方法宜鸯,僅僅表示緩沖區(qū)的常規(guī)信息,并不會(huì)轉(zhuǎn)化其中的字節(jié)數(shù)據(jù)遮怜。
9.1.3 ByteBuf的底層及派生
容易想到ByteBuf
緩沖區(qū)的底層數(shù)據(jù)結(jié)構(gòu)是一個(gè)字節(jié)數(shù)組淋袖。從操作系統(tǒng)的角度理解,緩沖區(qū)的區(qū)別在于字節(jié)數(shù)組是在用戶空間還是內(nèi)核空間锯梁。如果位于用戶空間即碗,對(duì)于JAVA也就是位于堆,此時(shí)可使用JAVA的基本數(shù)據(jù)類型byte[]
表示陌凳,用戶可使用array()
直接取得該字節(jié)數(shù)組剥懒,使用hasArray()
判定該緩沖區(qū)是否是用戶空間緩沖區(qū)。如果位于內(nèi)核空間合敦,JAVA程序?qū)⒉荒苤苯舆M(jìn)行操作初橘,此時(shí)可委托給JDK NIO中的直接緩沖區(qū)DirectByteBuffer
由其操作內(nèi)核字節(jié)數(shù)組,用戶可使用nioBuffer()
取得直接緩沖區(qū)充岛,使用nioBufferCount()
判定底層是否有直接緩沖區(qū)保檐。
用戶可在已有緩沖區(qū)上創(chuàng)建視圖即派生緩沖區(qū),這些視圖維護(hù)各自獨(dú)立的寫索引崔梗、讀索引以及標(biāo)記索引夜只,但他們和原生緩沖區(qū)共享想用的內(nèi)部字節(jié)數(shù)據(jù)。創(chuàng)建視圖即派生緩沖區(qū)的方法有:duplicate()
炒俱,slice()
以及slice(int,int)
盐肃。如果想拷貝緩沖區(qū),也就是說期望維護(hù)特有的字節(jié)數(shù)據(jù)而不是共享字節(jié)數(shù)據(jù)权悟,此時(shí)可使用copy()
方法砸王。
9.2 ByteBuf VS ByteBuffer
也許你已經(jīng)發(fā)現(xiàn)了ByteBuf
和ByteBuffer
在命名上有極大的相似性,JDK的NIO包中既然已經(jīng)有字節(jié)緩沖區(qū)ByteBuffer
的實(shí)現(xiàn)峦阁,為什么Netty還要重復(fù)造輪子呢谦铃?一個(gè)很大的原因是:ByteBuffer
對(duì)程序員并不友好。
考慮這樣的需求榔昔,向緩沖區(qū)寫入兩個(gè)字節(jié)0x01和0x02驹闰,然后讀取出這兩個(gè)字節(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
對(duì)于熟悉Netty的ByteBuf
的你來說嘹朗,或許只是多了一行buf.flip()
用于將緩沖區(qū)從寫模式卻換為讀模式。但事實(shí)并不如此诵肛,注意示例中申請(qǐng)了4個(gè)字節(jié)的空間屹培,此時(shí)理應(yīng)可以繼續(xù)寫入數(shù)據(jù)。不幸的是,如果再次調(diào)用buf.put((byte)3)
褪秀,將拋出java.nio.BufferOverflowException
蓄诽。而要正確達(dá)到該目的,需要調(diào)用buf.clear()
清空整個(gè)緩沖區(qū)或者buf.compact()
清除已經(jīng)讀過的數(shù)據(jù)媒吗。
這個(gè)操作雖然有些繁瑣仑氛,但并不是不能忍受,那么繼續(xù)上個(gè)例子闸英,考慮這樣取數(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()
操作會(huì)增加讀索引,那么get(index)
操作也會(huì)增加讀索引嗎自阱?答案是:并不會(huì)嚎莉,所以這個(gè)代碼示例是正確的,將輸出0 1 0 1
的結(jié)果沛豌。什么?get()
與get(0)
居然是兩個(gè)不一樣的操作赃额,前者會(huì)增加讀索引而后者并不會(huì)加派。是的,可以掀桌子了跳芳。此外芍锦,get()
的方法名本身就很有迷惑性,很自然的會(huì)認(rèn)為與數(shù)組的get()
一致飞盆,但是卻有一個(gè)極大的副作用:增加索引娄琉,所以合理的名字應(yīng)該是:getAndIncreasePosition
。
又引入了一個(gè)新名詞position
吓歇,事實(shí)上ByteBuffer
中并沒有讀索引和寫索引的說法,這兩個(gè)索引被統(tǒng)一稱為position
女气。在讀寫模式切換時(shí),該值將會(huì)改變炼鞠,正好與事實(shí)上的讀索引與寫索引對(duì)應(yīng)。但愿這樣的說法轰胁,并沒有讓你覺得頭暈谒主。
如果我們使用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);
當(dāng)然,如果不幸分配到了噩夢(mèng)模式,必須使用ByteBuffer
姿现,那么謹(jǐn)記這四個(gè)步驟:
- 寫入數(shù)據(jù)到
ByteBuffer
- 調(diào)用
flip()
方法 - 從
ByteBuffer
中讀取數(shù)據(jù) - 調(diào)用
clear()
方法或者compact()
方法