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

NIO 介紹

NIO,能夠說是New IO,也能夠說是non-blocking IO,具體怎麼解釋均可以。java

NIO 1是在JSR51裏面定義的,在JDK1.4中引入,由於BolckingIO不支持高併發網絡編程,這也是Java1.4之前被人詬病的緣由。NIO 2是在JSR203中定義的,在JDK1.7中引入,這是JavaNIO整個的發展歷程。NIO 1和NIO 2並非一個新舊替代的關係,而是一個補充的關係,NIO 2補充了1中缺乏的一些東西。咱們能夠看一下兩個的內容:git

NIO 1(本系列文章只介紹NIO 1):編程

Buffers

Channelssegmentfault

Selectors數組

NIO 2:緩存

Update

New File System API(引入文件系統API,以前的NIO 1當中,在Linux中操做文件都須要有讀寫權限,操做各類屬性等等,在Java中是沒辦法操做的,因此這裏引入了一個文件API)網絡

Asynchronous IO(即 常說的 AIO)併發

BIO與NIO的比較

BIO:函數

面向流(Stream Oriented)(鏈接創建的時候就能夠得到InputStream和OutputStream,就能夠經過流來進行操做,流是沒有緩衝的,效率比較低)

阻塞IO(Blocking IO)高併發

NIO:

面向緩衝區(Buffer Oriented)(讀寫都是經過Buffer來進行的,要讀數據就得先把數據讀到Buffer中去,寫數據也要先把數據寫到Buffer中,再從Buffer寫到網絡中去)

非阻塞IO(Non Blocking IO)(IO複用模型)

選擇器(Selectors)

NIO Buffer 學習

上面說了,NIO三個最重要的概念就是Buffer,Channel,Selector,Buffer緩衝區是用來放數據,Channel就是通道,能夠把數據寫到別的地方,Selector就是一個多路複用器,用來實現線程複用。下面會一個一個說。NIO不像BIO那麼簡單,甚至比起來要複雜的多。因此要一塊一塊的學習。下面先學習Buffer。

一個Buffer本質上是內存中的一塊,能夠將數據寫入這塊內存,也能夠從這塊內存中讀取數據。JavaNIO中定義了七種類型的Buffer:

file

能夠看到幾種基本類型對應的都有Buffer(除了布爾類型),能夠存放不一樣類型的原始數據。

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

file

最好理解的是capacity,表明Buffer的容量,申請一個容量爲1024的Buffer,那麼capacity就是1024。沒有特殊狀況,capacity永遠不會變。一旦Buffer的數據大小達到了capacity,須要清空Buffer,才能重新寫入值。

position就是表示下一個能夠操做(讀或者寫)的位置。JavaNIO不少人詬病比較複雜的一個地方就是,把讀操做和寫操做混在一塊兒,同一種操做能夠分紅讀模式和寫模式,從讀模式到寫模式須要本身手動去切換(執行flip),就是說沒有分開的讀指針和寫指針,就一個操做指針position,在寫模式下,position表示下一個能夠寫入的位置,在讀模式下,position表示下一個能夠讀的位置。兩種模式在切換的時候,position都會歸零,這樣就能夠從頭開始操做。好比在寫模式下,position從0寫到了5,那麼切換到讀模式,position會變成0,從第一位開始讀。

limit表示一個限制的最大位置,在寫模式下,limit表明的是最大能寫入的數據的位置,這個時候limit等於capacity。寫結束後切換到讀模式,此時的limit等於Buffer中實際的數據大小,由於Buffer不必定被寫滿,好比寫模式下在capacity爲10的Buffer中寫入了五個數據,那麼切換到讀模式,capacity仍是10,limit變爲5,position天然歸0,值變爲0。

Buffer的建立

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

Non-direct ByteBuffer

HeapByteBuffer,標準的java類(表示在堆上建立了一個Buffer,就是一個普通的Java類,在堆上申請內存存放Buffer實例)

維護一份byte[] 在JVM堆上

建立開銷小(heap申請內存是很快的,因此建立開銷很小)

拷貝到臨時DirectByteBuffer,但臨時緩衝區使用緩存。彙集寫/發散讀時沒有緩存臨時緩衝區 (也就是前面的線程模型提到的當作數據拷貝的時候,並不能直接從堆上直接寫到內核態緩衝區發送出去,必需要在native中申請一塊內存,先把數據拷貝到native中去,而後再發送)

能夠自動GC(垃圾回收)

Direct ByteBuffe

底層存儲在非JVM堆上,經過native代碼操做(數據存儲在堆以外,也就是JVM以外的普通內存空間,Java經過JNI調用c函數malloc申請的nvtive內存,也就表明JVM是沒法回收的)

-XX:MaxDirectMemorySize=<size>

建立開銷大(須要調用c函數申請,建立開銷大)

無需臨時緩衝區作拷貝(數據原本就在native中,能夠直接發送)

須要本身GC,每次建立或者釋放都須要調用一次System.gc()

咱們來看一下代碼,建立Buffer有兩種方法,allocate/allocateDirect方法,從名字就能看出各自建立的是上面哪一種Buffer。或者藉助數組建立(使用warp)。咱們先用allocate建立:

ByteBuffer buffer0 = ByteBuffer.allocate(10);

能夠看到,很簡單,而後用allocateDirect方法建立:

ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);

而後用第三種,根據一個數組去建立:

byte[] bytes = new byte[10];

ByteBuffer buffer2 = ByteBuffer.wrap(bytes);

根據數組建立的時候,還能夠設置偏移量(新Buffer的位置)和長度:

byte[] bytes2 = new byte[10];

//指定範圍

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

上面一共用四種方法建立了Buffer,咱們來打印出四種buffer的信息,包括數組的信息,position,limit,capacity三個信息,以及剩餘可操做的空間(每一個buffer都打印):

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底層是不是以數組的形式存儲的,是就爲true,對於HeapByteBuffer,底層都是數組,因此weitrue,DirectByteBuffer底層不是以數組形式存儲的,因此爲false,使用warp建立的Buffer實際上返回的也是HeapByteBuffer,也在堆上,咱們先來看buffer0的打印結果

file

前兩行打印出了數組和數組的偏移量,後面打印出一些屬性,由於尚未作任何操做,因此position是0,capacity理所固然是10,可操做的最後位置limit這時候等於capacity,也是10,可操做的剩餘空間大小也是10,因此打印出了上面的結果。根據上面建立出的實際狀況,buffer0,buffer1,buffer2打印的結果應該是同樣的,實際也是如此:

file

buffer3有些不一樣,咱們建立的時候設置了前提條件,由於偏移量爲2,因此position的起始位置是從2開始的,長度設置成了3,因此可操做的最大位置limit是5(2加3),由於是根據數據byte2建立的,因此總的容量仍是10,因此capacity仍是10,剩餘可操做的空間天然就是長度3,因此buffer3的打印結果以下:

file

Buffer的訪問

上面寫了代碼說了怎麼建立Buffer,下面看怎麼訪問Buffer。先建立一個buffer:

ByteBuffer buffer = ByteBuffer.allocate(10);

而後看一個打印Buffer信息的方法:

file

而後咱們打印一下剛剛建立的Buffer:

printBuffer(buffer);

打印結果:

file

上面打印的信息很簡單,再也不解釋,下面看第一個操做,開始向Buffer中寫入數據,而後打印(注意寫入的操做):

file

咱們向Buffer中寫入了五個數據,來看看Buffer的信息如何變化:

file

position變成了5,其它信息並未改變,下一步,咱們轉換buffer的狀態,由寫模式改成讀模式,而後打印:

file

打印結果:

file

能夠看到,模式轉換後,position歸零,limit到了已寫入數據的末尾,capacity天然不變,如今讀取兩個元素(注意讀操做):

file

打印結果:

file

注意上面position的變化,下面咱們來標記一下當前buffer的位置,這樣進行屢次操做後,還能夠回到標記時的狀態:

file

打印結果天然沒什麼變化:

file

再次讀取兩個數據:

file

查看打印結果:

file

position再次變化,下面恢復到mark以前的位置:

file

打印結果:

file

下面來對buffer進行壓縮操做,也就是將 position 與 limit之間的數據複製到buffer的開始位置,複製後 position = limit -position,limit = capacity,但若是position 與limit 之間沒有數據的話發,就不會進行復制:

file

打印結果:

file

能夠看到,壓縮後,將未讀過的數據直接移到了開始位置,position直接移到了這些數據的末尾,而且切換到寫模式,因此position等於未操做的數據空間長度,也就是limit減去原來的position,capacity回到了空間最後的位置,下面咱們狀況buffer:

file

打印結果:

file

能夠看到,clear直接把buffer回到了初始狀態。經過上面幾種操做,你們能夠對ByteBuffer幾種指針變化的流程有所瞭解。

Slice切片複製Buffer操做

這種複製操做有點相似視圖的概念,是一種淺複製,調用該方法獲得的新緩衝區所操做的數組仍是原始緩衝區中的那個數組,不過,經過slice建立的新緩衝區只能操做原始緩衝區中數組剩餘的數據,即索引爲調用slice方法時原始緩衝區的position到limit索引之間的數據,超出這個範圍的數據經過slice建立的新緩衝區沒法操做到。咱們給定一個Buffer,放滿數據,而後打印:

file

打印結果:

file

而後手動指定position和limit的位置:

file

打印結果:

file

而後根據定位到的位置,切分buffer,切分後的新buffer 的position爲0,limit和capacity都爲原來的position和limit之間的長度,打印新buffer:

file

打印結果:

file

循環切分出來的新buffer,一個一個讀取出來,而後乘以11,而後放到原位置:

file

打印結果:

file

能夠看到由於讀操做因此position發生變化,手動定位原來buffer的開始和結束位置,循環讀取打印原來buffer中的每一個數據,發現切分的buffer修改的同時,原來的buffer也修改了,說明slice也是淺拷貝:

file

打印結果:

file

Duplicate複製Buffer操做

Duplicate表示的也是淺拷貝,也就是隻複製飲用,對象實例仍是指向一個。咱們建立一個buffer,放慢數據,而後打印:

file

打印結果:

file

轉換讀寫狀態,而後打印:

file

打印結果:

file

手動定位位置到3和6,mark一次,而後又單獨定位position到5:

file

打印結果:

file

淺複製一份,包括原來的position和limit的位置也一塊兒複製了過來,原來的buffer清空,position和limit的位置復位(注意clear只是指針復位,數據還在):

file

拷貝出來的position和limit的位置仍是最後清空前的位置,打印結果:

file

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

file

打印結果:

file

asReadOnlyBuffer操做與Duplicate同樣,只是前者是隻讀的。

缺點

NIO編程負責的一個緣由就是Buffer複雜,Buffer指針變來變去仍是比較複雜的,並且原本就一個指針,讀寫模式還有互相轉換,這種要本身當心的控制才行。模式搞錯了會出大問題。

代碼地址:https://gitee.com/blueses/net... 02

相關文章
相關標籤/搜索