1. 程式人生 > >Java NIO 詳解(一)

Java NIO 詳解(一)

一、基本概念描述

1.1 I/O簡介

I/O即輸入輸出,是計算機與外界世界的一個藉口。IO操作的實際主題是作業系統。在java程式設計中,一般使用流的方式來處理IO,所有的IO都被視作是單個位元組的移動,通過stream物件一次移動一個位元組。流IO負責把物件轉換為位元組,然後再轉換為物件。

關於Java IO相關知識請參考我的另一篇文章:Java IO 詳解

1.2 什麼是NIO

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

在Java API中提供了兩套NIO,一套是針對標準輸入輸出NIO

,另一套就是網路程式設計NIO,本篇文章重點介紹標NIO,關於網路程式設計NIO請見Java NIO詳解(二)

1.3 流與塊的比較

NIO和IO最大的區別是資料打包和傳輸方式。IO是以的方式處理資料,而NIO是以的方式處理資料。

面向流的IO一次一個位元組的處理資料,一個輸入流產生一個位元組,一個輸出流就消費一個位元組。為流式資料建立過濾器就變得非常容易,連結幾個過濾器,以便對資料進行處理非常方便而簡單,但是面向流的IO通常處理的很慢。

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

二、NIO基礎

BufferChannel是標準NIO中的核心物件(網路NIO中還有個Selector核心物件,具體請參考Java NIO詳解(二)),幾乎每一個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主要有如下幾種:

這裡寫圖片描述

2.3 關於Channel

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

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

正如上面提到的,所有資料都通過Buffer物件處理,所以,您永遠不會將位元組直接寫入到Channel中,相反,您是將資料寫入到Buffer中;同樣,您也不會從Channel中讀取位元組,而是將資料從Channel讀入Buffer,再從Buffer獲取這個位元組。

因為Channel是雙向的,所以Channel可以比流更好地反映出底層作業系統的真實情況。特別是在Unix模型中,底層作業系統通常都是雙向的。

這裡寫圖片描述

在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();  
  • 1
  • 2

第二步:建立緩衝區

ByteBuffer buffer = ByteBuffer.allocate( 1024 );
  • 1

第三步:將資料從通道讀到緩衝區

fc.read( buffer );
  • 1

3.2 寫入資料到檔案

類似於從檔案讀資料,第一步:獲取一個通道

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

第二步:建立緩衝區,將資料放入緩衝區

ByteBuffer buffer = ByteBuffer.allocate( 1024 );

for (int i=0; i<message.length; ++i) {
 buffer.put( message[i] );
}
buffer.flip();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

第三步:把緩衝區資料寫入通道中

fc.write( buffer );
  • 1

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();
}     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

四、需要注意的點

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

4.1 檢查狀態

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

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

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;
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這裡寫圖片描述

在上面的FileCopy程式中,寫入資料之前我們呼叫了buffer.flip();方法,這個方法把當前的指標位置position設定成了limit,再將當前指標position指向資料的最開始端,我們現在可以將資料從緩衝區寫入通道了。 position 被設定為 0,這意味著我們得到的下一個位元組是第一個位元組。 limit 已被設定為原來的 position,這意味著它包括以前讀到的所有位元組,並且一個位元組也不多。

clear方法

先看一下clear的原始碼:

 public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這裡寫圖片描述

在上面的FileCopy程式中,寫入資料之後也就是讀資料之前,我們呼叫了 buffer.clear();方法,這個方法重設緩衝區以便接收更多的位元組。上圖顯示了在呼叫 clear() 後緩衝區的狀態。

--------------------- 本文來自 Heaven-Wang 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/suifeng3051/article/details/48160753?utm_source=copy