Netty快速入門(03)Java NIO 介紹-Buffer

NIO 介紹

NIO,可以說是New IO,也可以說是non-blocking IO,具體怎么解釋都可以。

NIO 1是在JSR51里面定義的,在JDK1.4中引入,因為BolckingIO不支持高并發(fā)網(wǎng)絡(luò)編程,這也是Java1.4以前被人詬病的原因。NIO 2是在JSR203中定義的,在JDK1.7中引入,這是JavaNIO整個的發(fā)展歷程。NIO 1和NIO 2并不是一個新舊替代的關(guān)系,而是一個補(bǔ)充的關(guān)系,NIO 2補(bǔ)充了1中缺少的一些東西。我們可以看一下兩個的內(nèi)容:

NIO 1(這里的介紹只介紹NIO 1):

Buffers

Channels

Selectors

NIO 2:

Update

New File System API(引入文件系統(tǒng)API,之前的NIO 1當(dāng)中,在Linux中操作文件都需要有讀寫權(quán)限,操作各種屬性等等,在Java中是沒辦法操作的,所以這里引入了一個文件API)

Asynchronous IO(即 常說的 AIO)






BIO與NIO的比較

BIO:

面向流(Stream Oriented)(連接建立的時候就可以獲得InputStream和OutputStream,就可以通過流來進(jìn)行操作,流是沒有緩沖的,效率比較低)

阻塞IO(Blocking IO)

NIO:

面向緩沖區(qū)(Buffer Oriented)(讀寫都是通過Buffer來進(jìn)行的,要讀數(shù)據(jù)就得先把數(shù)據(jù)讀到Buffer中去,寫數(shù)據(jù)也要先把數(shù)據(jù)寫到Buffer中,再從Buffer寫到網(wǎng)絡(luò)中去)

非阻塞IO(Non Blocking IO)(IO復(fù)用模型)

選擇器(Selectors)







NIO Buffer 學(xué)習(xí)? ??

上面說了,NIO三個最重要的概念就是Buffer,Channel,Selector,Buffer緩沖區(qū)是用來放數(shù)據(jù),Channel就是通道,可以把數(shù)據(jù)寫到別的地方,Selector就是一個多路復(fù)用器,用來實現(xiàn)線程復(fù)用。下面會一個一個說。NIO不像BIO那么簡單,甚至比起來要復(fù)雜的多。所以要一塊一塊的學(xué)習(xí)。下面先學(xué)習(xí)Buffer。


一個Buffer本質(zhì)上是內(nèi)存中的一塊,可以將數(shù)據(jù)寫入這塊內(nèi)存,也可以從這塊內(nèi)存中讀取數(shù)據(jù)。JavaNIO中定義了七種類型的Buffer:

可以看到幾種基本類型對應(yīng)的都有Buffer(除了布爾類型),可以存放不同類型的原始數(shù)據(jù)。



Buffer有三大核心概念:position,limit,capacity。

最好理解的是capacity,代表Buffer的容量,申請一個容量為1024的Buffer,那么capacity就是1024。沒有特殊情況,capacity永遠(yuǎn)不會變。一旦Buffer的數(shù)據(jù)大小達(dá)到了capacity,需要清空Buffer,才能從新寫入值。

position就是表示下一個可以操作(讀或者寫)的位置。JavaNIO很多人詬病比較復(fù)雜的一個地方就是,把讀操作和寫操作混在一起,同一種操作可以分成讀模式和寫模式,從讀模式到寫模式需要自己手動去切換(執(zhí)行flip),就是說沒有分開的讀指針和寫指針,就一個操作指針position,在寫模式下,position表示下一個可以寫入的位置,在讀模式下,position表示下一個可以讀的位置。兩種模式在切換的時候,position都會歸零,這樣就可以從頭開始操作。比如在寫模式下,position從0寫到了5,那么切換到讀模式,position會變成0,從第一位開始讀。

limit表示一個限制的最大位置,在寫模式下,limit代表的是最大能寫入的數(shù)據(jù)的位置,這個時候limit等于capacity。寫結(jié)束后切換到讀模式,此時的limit等于Buffer中實際的數(shù)據(jù)大小,因為Buffer不一定被寫滿,比如寫模式下在capacity為10的Buffer中寫入了五個數(shù)據(jù),那么切換到讀模式,capacity還是10,limit變?yōu)?,position自然歸0,值變?yōu)?。






Buffer的創(chuàng)建

Buffer大致分為兩種類型,一種是Direct Buffer,一種是non-direct Buffer,也叫HeapBuffer。下面看一下比較:

Non-direct ByteBuffer

HeapByteBuffer,標(biāo)準(zhǔn)的java類(表示在堆上創(chuàng)建了一個Buffer,就是一個普通的Java類,在堆上申請內(nèi)存存放Buffer實例)

維護(hù)一份byte[] 在JVM堆上

創(chuàng)建開銷?。╤eap申請內(nèi)存是很快的,所以創(chuàng)建開銷很?。?/p>

拷貝到臨時DirectByteBuffer,但臨時緩沖區(qū)使用緩存。聚集寫/發(fā)散讀時沒有緩存臨時緩沖區(qū) (也就是前面的線程模型提到的當(dāng)做數(shù)據(jù)拷貝的時候,并不能直接從堆上直接寫到內(nèi)核態(tài)緩沖區(qū)發(fā)送出去,必須要在native中申請一塊內(nèi)存,先把數(shù)據(jù)拷貝到native中去,然后再發(fā)送)

可以自動GC(垃圾回收)

Direct ByteBuffe

底層存儲在非JVM堆上,通過native代碼操作(數(shù)據(jù)存儲在堆之外,也就是JVM之外的普通內(nèi)存空間,Java通過JNI調(diào)用c函數(shù)malloc申請的nvtive內(nèi)存,也就代表JVM是無法回收的)

-XX:MaxDirectMemorySize=<size>

創(chuàng)建開銷大(需要調(diào)用c函數(shù)申請,創(chuàng)建開銷大)

無需臨時緩沖區(qū)做拷貝(數(shù)據(jù)本來就在native中,可以直接發(fā)送)

需要自己GC,每次創(chuàng)建或者釋放都需要調(diào)用一次System.gc()

我們來看一下代碼,創(chuàng)建Buffer有兩種方法,allocate/allocateDirect方法,從名字就能看出各自創(chuàng)建的是上面哪種Buffer?;蛘呓柚鷶?shù)組創(chuàng)建(使用warp)。我們先用allocate創(chuàng)建:

ByteBuffer buffer0 = ByteBuffer.allocate(10);

可以看到,很簡單,然后用allocateDirect方法創(chuàng)建:

ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);

然后用第三種,根據(jù)一個數(shù)組去創(chuàng)建:

byte[] bytes = new byte[10];

ByteBuffer buffer2 = ByteBuffer.wrap(bytes);

根據(jù)數(shù)組創(chuàng)建的時候,還可以設(shè)置偏移量(新Buffer的位置)和長度:

byte[] bytes2 = new byte[10];

//指定范圍

ByteBuffer buffer3 = ByteBuffer.wrap(bytes2, 2, 3);

上面一共用四種方法創(chuàng)建了Buffer,我們來打印出四種buffer的信息,包括數(shù)組的信息,position,limit,capacity三個信息,以及剩余可操作的空間(每個buffer都打?。?/p>

if (buffer0.hasArray()) {

????????System.out.println("buffer0 array: " + buffer0.array());

????????System.out.println("Buffer0 array offset: " + buffer0.arrayOffset());

}

System.out.println("Position: " + buffer0.position());

System.out.println("Limit: " + buffer0.limit());

System.out.println("Capacity: " + buffer0.capacity());

System.out.println("Remaining: " + buffer0.remaining());


hasArray()判斷表示的是,buffer底層是否是以數(shù)組的形式存儲的,是就為true,對于HeapByteBuffer,底層都是數(shù)組,所以weitrue,DirectByteBuffer底層不是以數(shù)組形式存儲的,所以為false,使用warp創(chuàng)建的Buffer實際上返回的也是HeapByteBuffer,也在堆上,我們先來看buffer0的打印結(jié)果

前兩行打印出了數(shù)組和數(shù)組的偏移量,后面打印出一些屬性,因為還沒有做任何操作,所以position是0,capacity理所當(dāng)然是10,可操作的最后位置limit這時候等于capacity,也是10,可操作的剩余空間大小也是10,所以打印出了上面的結(jié)果。根據(jù)上面創(chuàng)建出的實際情況,buffer0,buffer1,buffer2打印的結(jié)果應(yīng)該是一樣的,實際也是如此:

buffer3有些不同,我們創(chuàng)建的時候設(shè)置了前提條件,因為偏移量為2,所以position的起始位置是從2開始的,長度設(shè)置成了3,所以可操作的最大位置limit是5(2加3),因為是根據(jù)數(shù)據(jù)byte2創(chuàng)建的,所以總的容量還是10,所以capacity還是10,剩余可操作的空間自然就是長度3,所以buffer3的打印結(jié)果如下:






Buffer的訪問

上面寫了代碼說了怎么創(chuàng)建Buffer,下面看怎么訪問Buffer。先創(chuàng)建一個buffer:

ByteBuffer buffer = ByteBuffer.allocate(10);

然后看一個打印Buffer信息的方法:

然后我們打印一下剛剛創(chuàng)建的Buffer:

printBuffer(buffer);

打印結(jié)果:

上面打印的信息很簡單,不再解釋,下面看第一個操作,開始向Buffer中寫入數(shù)據(jù),然后打?。ㄗ⒁鈱懭氲牟僮鳎?/p>

我們向Buffer中寫入了五個數(shù)據(jù),來看看Buffer的信息如何變化:

position變成了5,其它信息并未改變,下一步,我們轉(zhuǎn)換buffer的狀態(tài),由寫模式改為讀模式,然后打印:

打印結(jié)果:

可以看到,模式轉(zhuǎn)換后,position歸零,limit到了已寫入數(shù)據(jù)的末尾,capacity自然不變,現(xiàn)在讀取兩個元素(注意讀操作):

打印結(jié)果:

注意上面position的變化,下面我們來標(biāo)記一下當(dāng)前buffer的位置,這樣進(jìn)行多次操作后,還可以回到標(biāo)記時的狀態(tài):

打印結(jié)果自然沒什么變化:

再次讀取兩個數(shù)據(jù):

查看打印結(jié)果:

position再次變化,下面恢復(fù)到mark之前的位置:

打印結(jié)果:

下面來對buffer進(jìn)行壓縮操作,也就是將 position 與 limit之間的數(shù)據(jù)復(fù)制到buffer的開始位置,復(fù)制后 position = limit -position,limit = capacity,但如果position 與limit 之間沒有數(shù)據(jù)的話發(fā),就不會進(jìn)行復(fù)制:

打印結(jié)果:

可以看到,壓縮后,將未讀過的數(shù)據(jù)直接移到了開始位置,position直接移到了這些數(shù)據(jù)的末尾,并且切換到寫模式,所以position等于未操作的數(shù)據(jù)空間長度,也就是limit減去原來的position,capacity回到了空間最后的位置,下面我們情況buffer:

打印結(jié)果:

可以看到,clear直接把buffer回到了初始狀態(tài)。通過上面幾種操作,大家可以對ByteBuffer幾種指針變化的流程有所了解。








Slice切片復(fù)制Buffer操作

這種復(fù)制操作有點(diǎn)類似視圖的概念,是一種淺復(fù)制,調(diào)用該方法得到的新緩沖區(qū)所操作的數(shù)組還是原始緩沖區(qū)中的那個數(shù)組,不過,通過slice創(chuàng)建的新緩沖區(qū)只能操作原始緩沖區(qū)中數(shù)組剩余的數(shù)據(jù),即索引為調(diào)用slice方法時原始緩沖區(qū)的position到limit索引之間的數(shù)據(jù),超出這個范圍的數(shù)據(jù)通過slice創(chuàng)建的新緩沖區(qū)無法操作到。我們給定一個Buffer,放滿數(shù)據(jù),然后打?。?/p>

打印結(jié)果:

然后手動指定position和limit的位置:

打印結(jié)果:

然后根據(jù)定位到的位置,切分buffer,切分后的新buffer 的position為0,limit和capacity都為原來的position和limit之間的長度,打印新buffer:

打印結(jié)果:

循環(huán)切分出來的新buffer,一個一個讀取出來,然后乘以11,然后放到原位置:

打印結(jié)果:

可以看到因為讀操作所以position發(fā)生變化,手動定位原來buffer的開始和結(jié)束位置,循環(huán)讀取打印原來buffer中的每個數(shù)據(jù),發(fā)現(xiàn)切分的buffer修改的同時,原來的buffer也修改了,說明slice也是淺拷貝:

打印結(jié)果:








Duplicate復(fù)制Buffer操作

Duplicate表示的也是淺拷貝,也就是只復(fù)制飲用,對象實例還是指向一個。我們創(chuàng)建一個buffer,放慢數(shù)據(jù),然后打?。?/p>

打印結(jié)果:

轉(zhuǎn)換讀寫狀態(tài),然后打?。?/p>

打印結(jié)果:

手動定位位置到3和6,mark一次,然后又單獨(dú)定位position到5:

打印結(jié)果:

淺復(fù)制一份,包括原來的position和limit的位置也一起復(fù)制了過來,原來的buffer清空,position和limit的位置復(fù)位(注意clear只是指針復(fù)位,數(shù)據(jù)還在):

拷貝出來的position和limit的位置還是最后清空前的位置,打印結(jié)果:

拷貝出來的也清空,position和limit的位置也回到最左和最右:

打印結(jié)果:

asReadOnlyBuffer操作與Duplicate一樣,只是前者是只讀的。








缺點(diǎn)

NIO編程負(fù)責(zé)的一個原因就是Buffer復(fù)雜,Buffer指針變來變?nèi)ミ€是比較復(fù)雜的,而且本來就一個指針,讀寫模式還有互相轉(zhuǎn)換,這種要自己小心的控制才行。模式搞錯了會出大問題。



代碼地址:https://gitee.com/blueses/netty-demo??02

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容