Java NIO(一)

NIO 在《JAVA編程思想》中文版中稱之爲New IO,當然也有其他地方稱爲No Block IO,我本人看來,稱爲New IO更好。
NIO 在java1.4加入。

參考博客:

Java NIO 詳解(一)
理解Java NIO

一、基本概念

1.1 I/O簡介

I/O即輸入輸出,是計算機與外界世界的一個藉口。IO操作的實際主題是操作系統。在Java編程中,一般使用流的方式來處理IO,所有的IO都被視作是單個字節的移動,通過stream對象一次移動一個字節。流IO負責把對象轉換爲字節,然後再轉換爲對象。

1.2 什麼是NIO

NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。

在java API中提供了兩套NIO,一套是針對標準輸入輸出NIO,另一套就是網絡編程NIO,本篇文章重點介紹標NIO。

1.3 Why NIO

開始講NIO之前,瞭解爲什麼會有NIO,相比傳統流I/O的優勢在哪,它可以用來做什麼等等的問題,還是很有必要的。

傳統流I/O是基於字節的,所有I/O都被視爲單個字節的移動;
而NIO是基於塊的,大家可能猜到了,NIO的性能肯定優於流I/O。

沒錯!其性能的提高 要得益於其使用的結構更接近操作系統執行I/O的方式:通道和緩衝器。

我們可以把它想象成一個煤礦,通道是一個包含煤層(數據)的礦藏,而緩衝器則是派送 到礦藏的卡車。卡車載滿煤炭而歸,我們再從卡車上獲得煤炭。也就是說,我們並沒有直接和通道交互;我們只是和緩衝器交互,並把緩衝器派送到通道。通道要麼 從緩衝器獲得數據,要麼向緩衝器發送數據。(這段比喻出自Java編程思想)

NIO的主要應用在高性能、高容量服務端應用程序,典型的有Apache Mina就是基於它的。

1.4 流與塊的比較

NIO和IO最大的區別是數據打包和傳輸方式。IO是以流的方式處理數據,而NIO是以塊的方式處理數據。

面向流的IO一次一個字節的處理數據,一個輸入流產生一個字節,一個輸出流就消費一個字節。爲流式數據創建過濾器就變得非常容易,鏈接幾個過濾器,以便對數據進行處理非常方便而簡單,但是面向流的IO通常處理的很慢。

面向塊的IO系統以塊的形式處理數據。每一個操作都在一步中產生或消費一個數據塊。按塊要比按流快的多,但面向塊的IO缺少了面向流IO所具有的有雅興和簡單性。

二、 NIO基礎

Buffer和Channel是標準NIO中的核心對象(網絡NIO中還有個Selector核心對象,具體請參考[Java NIO詳解(二)] (http://blog.csdn.net/suifeng3051/article/details/48441629)),幾乎每一個IO操作中都會用到它們。

Channel是對原IO中流的模擬,任何來源和目的數據都必須通過一個Channel對象。一個Buffer實質上是一個容器對象,發給Channel的所有對象都必須先放到Buffer中;同樣的,從Channel中讀取的任何數據都要讀到Buffer中。

2.1 關於Buffer

Buffer是一個對象,它包含一些要寫入或讀出的數據。在NIO中,數據是放入buffer對象的,而在IO中,數據是直接寫入或者讀到Stream對象的。應用程序不能直接對 Channel 進行讀寫操作,而必須通過 Buffer 來進行,即 Channel 是通過 Buffer 來讀寫數據的。

在NIO中,所有的數據都是用Buffer處理的,它是NIO讀寫數據的中轉池。Buffer實質上是一個數組,通常是一個字節數據,但也可以是其他類型的數組。但一個緩衝區不僅僅是一個數組,重要的是它提供了對數據的結構化訪問,而且還可以跟蹤系統的讀寫進程。

使用 Buffer 讀寫數據一般遵循以下四個步驟:

  1. 寫入數據到 Buffer;
  2. 調用 flip() 方法;
  3. 從 Buffer 中讀取數據;
  4. 調用 clear() 方法或者 compact() 方法。

當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,需要通過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到 Buffer 的所有數據。

一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用 clear() 或 compact() 方法。clear() 方法會清空整個緩衝區。compact() 方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。

Buffer主要有如下幾種:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

2.2 關於Channle

Channel是一個對象,可以通過它讀取和寫入數據。可以把它看做IO中的流。但是它和流相比還有一些不同:

  1. Channel是雙向的,既可以讀又可以寫,而流是單向的
  2. Channel可以進行異步的讀寫
  3. 對Channel的讀寫必須通過buffer對象

正如上面提到的,所有數據都通過Buffer對象處理,所以,您永遠不會將字節直接寫入到Channel中,相反,您是將數據寫入到Buffer中;同樣,您也不會從Channel中讀取字節,而是將數據從Channel讀入Buffer,再從Buffer獲取這個字節

因爲Channel是雙向的,所以Channel可以比流更好地反映出底層操作系統的真實情況。特別是在Unix模型中,底層操作系統通常都是雙向的。
屏幕快照 2017-07-29 上午11.41.53

在Java NIO中Channel主要有如下幾種類型:

  • FileChannel:從文件讀取數據的
  • DatagramChannel:讀寫UDP網絡協議數據
  • SocketChannel:讀寫TCP網絡協議數據
  • ServerSocketChannel:可以監聽TCP連接

三、實踐:NIO中的讀寫

IO中的讀和寫,對應的是數據和Stream,NIO中的讀和寫,則對應的就是通道和緩衝區。
NIO中從通道中讀取:創建一個緩衝區,然後讓通道讀取數據到緩衝區。
NIO寫入數據到通道:創建一個緩衝區,用數據填充它,然後讓通道用這些數據來執行寫入。

3.1 從文件中讀取

我們已經知道,在NIO系統中,任何時候執行一個讀操作,您都是從Channel中讀取,而您不是直接從Channel中讀取數據,因爲所有的數據都必須用Buffer來封裝,所以您應該是從Channel讀取數據到Buffer。

因此,如果從文件讀取數據的話,需要如下三步:

  1. 從FileInputStream獲取Channel
  2. 創建Buffer
  3. 從Channel讀取數據到Buffer

第一步:獲取通道

FileInputStream fin = new FileInputStream( "readandshow.txt" );
FileChannel fc = fin.getChannel();  

第二步:創建緩衝區

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

第三步:將數據從通道讀到緩衝區

fc.read( buffer );

3.2 寫入數據到文件

類似:

第一步:獲取一個通道

FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
FileChannel fc = fout.getChannel();

第二步:創建緩衝區,將數據放倒緩衝區中

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {
 buffer.put( message[i] );
}
buffer.flip();

第三步:把緩衝區數據寫入通道中

fc.write( buffer );

3.3 讀寫結合

CopyFile是一個非常好的讀寫結合的例子,我們將通過CopyFile這個實力讓大家體會NIO的操作過程。CopyFile執行三個基本的操作:創建一個Buffer,然後從源文件讀取數據到緩衝區,然後再將緩衝區寫入目標文件。

/**
 * 用java NIO api拷貝文件
 * @param src
 * @param dst
 * @throws IOException
 */
public static void copyFileUseNIO(String src,String dst) throws IOException{
    //聲明源文件和目標文件
            FileInputStream fi=new FileInputStream(new File(src));
            FileOutputStream fo=new FileOutputStream(new File(dst));
            //獲得傳輸通道channel
            FileChannel inChannel=fi.getChannel();
            FileChannel outChannel=fo.getChannel();
            //獲得容器buffer
            ByteBuffer buffer=ByteBuffer.allocate(1024);
            while(true){
                //判斷是否讀完文件
                int eof =inChannel.read(buffer);
                if(eof==-1){
                    break;  
                }
                //重設一下buffer的position=0,limit=position
                buffer.flip();
                //開始寫
                outChannel.write(buffer);
                //寫完要重置buffer,重設position=0,limit=capacity
                buffer.clear();
            }
            inChannel.close();
            outChannel.close();
            fi.close();
            fo.close();
}     

四、需要注意的點

上面程序有三個地方需要注意

4.1 檢查狀態

當沒有更多的數據時,拷貝就算完成,此時 read() 方法會返回 -1 ,我們可以根據這個方法判斷是否讀完。

int r= fcin.read( buffer );
if (r==-1) {
     break;
     }

4.2 Buffer類的flip、clear方法

控制buffer狀態的三個變量

  • position:跟蹤已經寫了多少數據或讀了多少數據,它指向的是下一個字節來自哪個位置
  • limit:代表還有多少數據可以取出或還有多少空間可以寫入,它的值小於等於capacity。
  • capacity:代表緩衝區的最大容量,一般新建一個緩衝區的時候,limit的值和capacity的值默認是相等的。

flip、clear這兩個方法便是用來設置這些值的。

flip方法

我們先看一下flip的源碼:

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

注意,limit代表的含義。

20150902152400081

在上面的FileCopy程序中,寫入數據之前我們調用了buffer.flip();方法,這個方法把當前的指針位置limit設置成了position,再將當前指針position指向數據的最開始端,我們現在可以將數據從緩衝區寫入通道了。 position 被設置爲 0,這意味着我們得到的下一個字節是第一個字節。 limit 已被設置爲原來的 position,這意味着它包括以前讀到的所有字節,並且一個字節也不多。

clear方法

先看一下clear的源碼:

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

20150902153303447

在上面的FileCopy程序中,寫入數據之後也就是讀數據之前,我們調用了 buffer.clear();方法,這個方法重設緩衝區以便接收更多的字節。上圖顯示了在調用 clear() 後緩衝區的狀態。

五、細看flip和clear方法

緩衝區對象有四個基本屬性:

  • 容量Capacity:緩衝區能容納的數據元素的最大數量,在緩衝區創建時設定,無法更改
  • 上界Limit:緩衝區的第一個不能被讀或寫的元素的索引
  • 位置Position:下一個要被讀或寫的元素的索引
  • 標記Mark:備忘位置,調用mark()來設定mark=position,調用reset()設定position=mark

這四個屬性總是遵循這樣的關係:0<=mark<=position<=limit<=capacity。下圖是新創建的容量爲10的緩衝區邏輯視圖:

1b5a4a95089ff41a6b8eb41507425171a982561ef

buffer.put((byte)’H’).put((byte)’e’).put((byte)’l’).put((byte)’l’).put((byte)’o’);

五次調用put後的緩衝區:

2989fb6f6d640e6dd9cdf39a50291f54543ef20f0

調用絕對版本的put不影響position:

buffer.put(0,(byte)’M’).put((byte)’w’);

365e55a96fe6ad4ff8a47ef770127c1fb20568e69

現在緩衝區滿了,我們必須將其清空。我們想把這個緩衝區傳遞給一個通道,以使內容能被全部寫出,但現在執行get()無疑會取出未定義的數據。我們必須將 posistion設爲0,然後通道就會從正確的位置開始讀了,但讀到哪算讀完了呢?這正是limit引入的原因,它指明緩衝區有效內容的未端。這個操作 在緩衝區中叫做翻轉:buffer.flip()。

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

4e1e456a496324f680b72d61331cee50a6a356847

然後,調用clear使buffer處於創建時的狀態,接受新的數據。

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
發佈了48 篇原創文章 · 獲贊 11 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章