Java NIO筆記

refer to original

文章目錄

1.Java NIO Tutorial

Java NIO(New IO)是一個可以替代標準Java IO API的IO API(從Java 1.4開始),Java NIO提供了與標準IO不同的IO工作方式。

Java NIO: Channels and Buffers(通道和緩衝區)

標準的IO基於字節流和字符流進行操作的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。

Java NIO: Non-blocking IO(非阻塞IO)

Java NIO可以讓你非阻塞的使用IO,例如:當線程從通道讀取數據到緩衝區時,線程還是可以進行其他事情。當數據被寫入到緩衝區時,線程可以繼續處理它。從緩衝區寫入通道也類似。

Java NIO: Selectors(選擇器)

Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個的線程可以監聽多個數據通道。

2.Java NIO Overview

Java NIO 由以下幾個核心部分組成:

  • Channels
  • Buffers
  • Selectors

雖然Java NIO 中除此之外還有很多類和組件,但在我看來,Channel,Buffer 和 Selector 構成了核心的API。其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類。因此我將主要介紹這三個組件在本段,其他組件將在本系列文章的其他章節介紹

Channels and Buffers

基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。如圖所示:

圖示

Java NIO: Channels read data into Buffers, and Buffers write data into Channels

JAVA NIO中的一些主要Channel的實現:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

更多詳情見NIO-Channel-UML

正如你所看見的一樣,這些channel覆蓋了UDP、TCP 網絡 IO、文件IO。

這些類還附帶了一些有趣的接口,但是爲了簡單起見,我將把它們排除在這個Java NIO概述之外。在本Java NIO教程的其他文本中,將在相關的地方對它們進行解釋。

Java NIO裏關鍵的Buffer實現:

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

更多詳情見NIO-Buffer-UML

這些Buffer包含通過IO傳輸的所有基本數據類型:byte, short, int, long, float, double 和 char。

Java NIO還有一個MappedByteBuffer,它與內存映射文件一起使用。不過,我將把這個緩衝區從這個概述中去掉。

Selectors

Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。

如圖所示

圖示

要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。

3.Java NIO Channel

Java NIO通道與streams相似,但有一些不同:

  • 既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的(read or write)。
  • 通道可以異步地讀寫。
  • 通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。

正如上面所說,從通道讀取數據到緩衝區,從緩衝區寫入數據到通道。如下圖所示:

image

Java NIO: Channels read data into Buffers, and Buffers write data into Channels

Channel Implementations

這些是Java NIO中最重要的通道的實現:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 從文件中讀寫數據。

DatagramChannel 能通過UDP讀寫網絡中的數據。

SocketChannel 能通過TCP讀寫網絡中的數據。

ServerSocketChannel可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。

Basic Channel Example

下面是一個使用FileChannel讀取數據到Buffer中的示例:

String path = "/Users/nsh/data";
FileInputStream fileInputStream = new FileInputStream(new File(path));
//RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
//FileChannel inChannel = aFile.getChannel();
try {
    FileChannel inChannel = fileInputStream.getChannel();
    ByteBuffer buf = ByteBuffer.allocate(32);

    int bytesRead = 0;
    while (bytesRead != -1) {
        bytesRead = inChannel.read(buf);
        if (bytesRead == -1) {
            break;
        }

        buf.flip();
        while (buf.hasRemaining()) {
            System.out.print((char) buf.get());
        }
        System.out.println();
        buf.clear();
    }
}catch (Exception e){
    e.printStackTrace();
}finally {
    fileInputStream.close();
}

注意 buf.flip() 的調用,首先讀取數據到Buffer,然後反轉Buffer,接着再從Buffer中讀取數據。下一節會深入講解Buffer的更多細節。

4.Java NIO Buffer

Java NIO中的Buffer用於和NIO通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。

緩衝區本質上(essentially)是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝(wrapped)成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

Basic Buffer Usage

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

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

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

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

下面是一個使用Buffer的例子:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

Buffer Capacity, Position and Limit

緩衝區本質上是一塊可以寫入數據,然後可以從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。

爲了理解Buffer的工作原理,需要熟悉它的三個屬性:

  • capacity
  • position
  • limit

position和limit的含義取決於Buffer處在讀模式還是寫模式。不管Buffer處在什麼模式,capacity的含義總是一樣的。

這裏有一個關於capacity,position和limit在讀寫模式中的說明,詳細的解釋在插圖後面。

圖示

Capacity

作爲一個內存塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往裏寫capacity個byte、long,char等類型。一旦Buffer滿了,需要將其清空(通過讀數據或者清除數據)才能繼續寫數據往裏寫數據。

Position

當你寫數據到Buffer中時,position表示當前的位置。初始的position值爲0.當一個byte、long等數據寫到Buffer後, position會向前移動到下一個可插入數據的Buffer單元。position最大可爲capacity – 1.

當讀取數據時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置爲0. 當從Buffer的position處讀取數據時,position向前移動到下一個可讀的位置。

Limit

在寫模式下,Buffer的limit表示你最多能往Buffer裏寫多少數據。 寫模式下,limit等於Buffer的capacity。

當切換Buffer到讀模式時, limit表示你最多能讀到多少數據。因此,當切換Buffer到讀模式時,limit會被設置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數據(limit被設置成已寫數據的數量,這個值在寫模式下就是position)

Buffer Types

Java NIO 有以下Buffer類型

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

如你所見,這些Buffer類型代表了不同的數據類型。換句話說,就是可以通過char,short,int,long,float 或 double類型來操作緩衝區中的字節。

MappedByteBuffer 有些特別,在涉及它的專門章節中再講。

Allocating a Buffer

To obtain a Buffer object you must first allocate it. Every Buffer class has an allocate() method that does this. Here is an example showing the allocation of a ByteBuffer, with a capacity of 48 bytes:

ByteBuffer buf = ByteBuffer.allocate(48);

Here is an example allocating a CharBuffer with space for 1024 characters:

CharBuffer buf = CharBuffer.allocate(1024);

Writing Data to a Buffer

寫數據到Buffer有兩種方式:

  1. 從Channel寫到Buffer。
  2. 通過Buffer的put()方法寫到Buffer裏。

Here is an example showing how a Channel can write data into a Buffer:

int bytesRead = inChannel.read(buf); //read into buffer.

Here is an example that writes data into a Buffer via the put() method:

buf.put(127);

put方法有很多版本,允許你以不同的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc。

flip()

lip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成之前position的值。

換句話說,position現在用於標記讀的位置,limit表示之前寫進了多少個byte、char等 —— 現在能讀取多少個byte、char等。

Reading Data from a Buffer

從Buffer中讀取數據有兩種方式:

  1. 從Buffer讀取數據到Channel。
  2. 使用get()方法從Buffer中讀取數據。

Here is an example of how you can read data from a buffer into a channel:

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);

Here is an example that reads data from a Buffer using the get() method:

byte aByte = buf.get();    

get方法有很多版本,允許你以不同的方式從Buffer中讀取數據。例如,從指定position讀取,或者從Buffer中讀取數據到字節數組。更多Buffer實現的細節參考JavaDoc。

rewind()

Buffer.rewind()將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)。

clear() and compact()

一旦讀完Buffer中的數據,需要讓Buffer準備好再次被寫入。可以通過clear()或compact()方法來完成。

如果調用的是clear()方法,position將被設回0,limit被設置成 capacity的值。換句話說,Buffer 被清空了。Buffer中的數據並未清除,只是這些標記告訴我們可以從哪裏開始往Buffer裏寫數據。

如果Buffer中有一些未讀的數據,調用clear()方法,數據將“被遺忘”,意味着不再有任何標記會告訴你哪些數據被讀過,哪些還沒有。

如果Buffer中仍有未讀的數據,且後續還需要這些數據,但是此時想要先先寫些數據,那麼使用compact()方法。

compact()方法將所有未讀的數據拷貝到Buffer起始處。然後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法一樣,設置成capacity。現在Buffer準備好寫數據了,但是不會覆蓋未讀的數據。

mark() and reset()

通過調用Buffer.mark()方法,可以標記Buffer中的一個特定position。之後可以通過調用Buffer.reset()方法恢復到這個position。例如:

buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset();  //set position back to mark.   

equals() and compareTo()

可以使用equals()和compareTo()方法兩個Buffer。

equals()

當滿足下列條件時,表示兩個Buffer相等:

  1. 有相同的類型(byte、char、int等)。
  2. Buffer中剩餘的byte、char等的個數相等。
  3. Buffer中所有剩餘的byte、char等都相同。

如你所見,equals只是比較Buffer的一部分,不是每一個在它裏面的元素都比較。實際上,它只比較Buffer中的剩餘元素。

compareTo()

compareTo()方法比較兩個Buffer的剩餘元素(byte、char等), 如果滿足下列條件,則認爲一個Buffer“小於”另一個Buffer:

  1. 第一個不相等的元素小於另一個Buffer中對應的元素 。
  2. 所有元素都相等,但第一個Buffer比另一個先耗盡(第一個Buffer的元素1. 數比另一個少)。

5.Java NIO Scatter / Gather

Java NIO開始支持scatter/gather,scatter/gather用於描述從Channel中讀取或者寫入到Channel的操作

scatter從Channel中讀取是指在讀操作時將讀取的數據寫入多個buffer中。因此,Channel將從Channel中讀取的數據“scatter”到多個Buffer中。

gather寫入Channel是指在寫操作時將多個buffer的數據寫入同一個Channel,因此,Channel 將多個Buffer中的數據“gather”後發送到Channel。

scatter / gather經常用於需要將傳輸的數據分開處理的場合,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中,這樣你可以方便的處理消息頭和消息體。

Scattering Reads

A “scattering read” reads data from a single channel into multiple buffers. Here is an illustration(圖示) of that principle:

圖示

Java NIO: Scattering Read

Here is a code example that shows how to perform a scattering read:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

注意buffer首先被插入到數組,然後再將數組作爲channel.read() 的輸入參數。read()方法按照buffer在數組中的順序將從channel中讀取的數據寫入到buffer,當一個buffer被寫滿後,channel緊接着向另一個buffer中寫。

Scattering Reads在移動下一個buffer前,必須填滿當前的buffer,這也意味着它不適用於動態消息(譯者注:消息大小不固定)。換句話說,如果存在消息頭和消息體,消息頭必須完成填充(例如 128byte),Scattering Reads才能正常工作。

Gathering Writes

A “gathering write” writes data from multiple buffers into a single channel. Here is an illustration of that principle:

image

Java NIO: Gathering Write

Here is a code example that shows how to perform a gathering write:

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據纔會被寫入。因此,如果一個buffer的容量爲128byte,但是僅僅包含58byte的數據,那麼這58byte的數據將被寫入到channel中。因此與Scattering Reads相反,Gathering Writes能較好的處理動態消息。

6.Java NIO Channel to Channel Transfers

In Java NIO you can transfer data directly from one channel to another, if one of the channels is a FileChannel. The FileChannel class has a transferTo() and a transferFrom() method which does this for you.

transferFrom()

FileChannel的transferFrom()方法可以將數據從源通道傳輸到FileChannel中。下面是一個簡單的例子:

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

toChannel.transferFrom(fromChannel, position, count);

方法的輸入參數position表示從position處開始向目標文件寫入數據,count表示最多傳輸的字節數。如果源通道的剩餘空間小於 count 個字節,則所傳輸的字節數要小於請求的字節數。

此外要注意,在SoketChannel的實現中,SocketChannel只會傳輸此刻準備好的數據(可能不足count字節)。因此,SocketChannel可能不會將請求的所有數據(count個字節)全部傳輸到FileChannel中。

transferTo()

The transferTo() method transfer from a FileChannel into some other channel. Here is a simple example:

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);

是不是發現這個例子和前面那個例子特別相似?除了調用方法的FileChannel對象不一樣外,其他的都一樣。

上面所說的關於SocketChannel的問題在transferTo()方法中同樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿。

7.Java NIO Selector

Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否爲諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。

Why Use a Selector?

僅用單個線程來處理多個Channels的好處(advantage)是,只需要更少的線程來處理通道。

事實上,可以只用一個線程處理所有的通道。對於操作系統來說,線程之間上下文切換的開銷很大,而且每個線程都要佔用系統的一些資源(如內存)。因此,使用的線程越少越好。

但是,需要記住,現代的操作系統和CPU在多任務方面表現的越來越好,所以多線程的開銷隨着時間的推移,變得越來越小了。實際上,如果一個CPU有多個內核,不使用多任務可能是在浪費CPU能力。不管怎麼說,關於那種設計的討論應該放在另一篇不同的文章中。在這裏,只要知道使用Selector能夠處理多個通道就足夠了。

Here is an illustration of a thread using a Selector to handle 3 Channel’s:
image

Java NIO: A Thread uses a Selector to handle 3 Channel’s

Creating a Selector

You create a Selector by calling the Selector.open() method, like this:

Selector selector = Selector.open();

Registering Channels with the Selector

爲了將Channel和Selector配合使用,必須將channel註冊到selector上。通過SelectableChannel.register()方法來實現,如下:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

與Selector一起使用時,Channel必須處於非阻塞模式下。這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式。而套接字通道都可以。

注意register()方法的第二個參數。這是一個“interest set”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同類型的事件:

  1. Connect
  2. Accept
  3. Read
  4. Write

通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱爲“連接就緒”。一個server socket channel準備好接收新進入的連接稱爲“接收就緒”。一個有數據可讀的通道可以說是“讀就緒”。等待寫數據的通道可以說是“寫就緒”。

這四種事件用SelectionKey的四個常量來表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量連接起來,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

在下面還會繼續提到interest set。

SelectionKey

在上一小節中,當向Selector註冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object(附加的對象) (optional)

I’ll describe these properties below.

Interest Set

就像Registering Channels with the Selector一節中所描述的,interest set是你所選擇的感興趣的事件集合。可以通過SelectionKey讀寫interest set,像這樣:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;   

可以看到,用“&”操作interest set和給定的SelectionKey常量,可以確定某個確定的事件是否在interest set中。

Ready Set

ready set是通道已經準備就緒的操作的集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:

int readySet = selectionKey.readyOps();

可以用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布爾類型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel + Selector

從SelectionKey訪問Channel和Selector很簡單。如下:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();   

Attaching Objects

可以將一個對象或者更多信息附着到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集數據的某個對象。使用方法如下:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

還可以在用register()方法向Selector註冊Channel的時候附加對象。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Selecting Channels via a Selector

一旦向Selector註冊了一或多個通道,就可以調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如連接、接受、讀或寫)已經準備就緒的那些通道。換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。

下面是select()方法:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()阻塞到至少有一個通道在你註冊的事件上就緒了。

select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(參數)。

selectNow()不會阻塞,不管什麼通道就緒都立刻返回(此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。)

select()方法返回的int值表示有多少通道已經就緒。亦即,自上次調用select()方法後有多少通道變成就緒狀態。如果調用select()方法,因爲有一個通道變成就緒狀態,返回了1,若再次調用select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了。

selectedKeys()

一旦調用了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過調用selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:

Set<SelectionKey> selectedKeys = selector.selectedKeys(); 

當像Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 對象。這個對象代表了註冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些對象。

可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

這個循環遍歷已選擇鍵集中的每個鍵,並檢測各個鍵所對應的通道的就緒事件。

注意每次迭代末尾的keyIterator.remove()調用。Selector不會自己從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。

SelectionKey.channel()方法返回的通道需要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。

wakeUp()

某個線程調用select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。

如果有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會立即“醒來(wake up)”。

close()

用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey實例無效。通道本身並不會關閉。

Full Selector Example

這裏有一個完整的示例,打開一個Selector,註冊一個通道註冊到這個Selector上(通道的初始化過程略去),然後持續監控這個Selector的四種事件(接受,連接,讀,寫)是否就緒。

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {

  int readyChannels = selector.selectNow();

  if(readyChannels == 0) continue;

  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}

8.Java NIO FileChannel

Java NIO中的FileChannel是一個連接到文件的通道。可以通過文件通道讀寫文件。

FileChannel無法設置爲非阻塞模式,它總是運行在阻塞模式下。

Opening a FileChannel

在使用FileChannel之前,必須先打開它。但是,我們無法直接打開一個FileChannel,需要通過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是通過RandomAccessFile打開FileChannel的示例:

RandomAccessFile aFile     = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel      inChannel = aFile.getChannel();

Reading Data from a FileChannel

調用多個read()方法之一從FileChannel中讀取數據。如:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);

首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。

然後,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。如果返回-1,表示到了文件末尾。

Writing Data to a FileChannel

使用FileChannel.write()方法向FileChannel寫數據,該方法的參數是一個Buffer。如:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

注意FileChannel.write()是在while循環中調用的。因爲無法保證write()方法一次能向FileChannel寫入多少字節,因此需要重複調用write()方法,直到Buffer中已經沒有尚未寫入通道的字節。

Closing a FileChannel

When you are done using a FileChannel you must close it. Here is how that is done:

channel.close();

FileChannel Position

有時可能需要在FileChannel的某個特定位置進行數據的讀/寫操作。可以通過調用position()方法獲取FileChannel的當前位置。

也可以通過調用position(long pos)方法設置FileChannel的當前位置。

Here are two examples:

long pos channel.position();

channel.position(pos +123);

如果將位置設置在文件結束符之後,然後試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標誌。

如果將位置設置在文件結束符之後,然後向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能導致“文件空洞”,磁盤上物理文件中寫入的數據間有空隙。

FileChannel Size

FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:

long fileSize = channel.size(); 

FileChannel Truncate

可以使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度後面的部分將被刪除。如:

channel.truncate(1024);

這個例子截取文件的前1024個字節。

FileChannel Force

FileChannel.force()方法將通道里尚未寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操作系統會將數據緩存在內存中,所以無法保證寫入到FileChannel裏的數據一定會即時寫到磁盤上。要保證這一點,需要調用force()方法。

force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。

下面的例子同時將文件數據和元數據強制寫到磁盤上:

channel.force(true);

9.Java NIO SocketChannel

Java NIO中的SocketChannel是一個連接到TCP網絡套接字的通道。可以通過以下2種方式創建SocketChannel:

  1. 打開一個SocketChannel並連接到互聯網上的某臺服務器。
  2. 一個新連接到達ServerSocketChannel時,會創建一個SocketChannel。

Opening a SocketChannel

Here is how you open a SocketChannel:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

Closing a SocketChannel

You close a SocketChannel after use by calling the SocketChannel.close() method. Here is how that is done:

socketChannel.close();

Reading from a SocketChannel

To read data from a SocketChannel you call one of the read() methods. Here is an example:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = socketChannel.read(buf);

首先,分配一個Buffer。從SocketChannel讀取到的數據將會放到這個Buffer中。

然後,調用SocketChannel.read()。該方法將數據從SocketChannel 讀到Buffer中。read()方法返回的int值表示讀了多少字節進Buffer裏。如果返回的是-1,表示已經讀到了流的末尾(連接關閉了)。

Writing to a SocketChannel

寫數據到SocketChannel用的是SocketChannel.write()方法,該方法以一個Buffer作爲參數。示例如下:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

注意SocketChannel.write()方法的調用是在一個while循環中的。Write()方法無法保證能寫多少字節到SocketChannel。所以,我們重複調用write()直到Buffer沒有要寫的字節爲止。

Non-blocking Mode

可以設置 SocketChannel 爲非阻塞模式(non-blocking mode).設置之後,就可以在異步模式下調用connect(), read() 和write()了。

connect()

如果SocketChannel在非阻塞模式下,此時調用connect(),該方法可能在連接建立之前就返回了。爲了確定連接是否建立,可以調用finishConnect()的方法。像這樣:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...    
}

write()

非阻塞模式下,write()方法在尚未寫出任何內容時可能就返回了。所以需要在循環中調用write()。前面已經有例子了,這裏就不贅述了。

read()

非阻塞模式下,read()方法在尚未讀取到任何數據時可能就返回了。所以需要關注它的int返回值,它會告訴你讀取了多少字節。

Non-blocking Mode with Selectors

非阻塞模式與選擇器搭配會工作的更好,通過將一或多個SocketChannel註冊到Selector,可以詢問選擇器哪個通道已經準備好了讀取,寫入等。Selector與SocketChannel的搭配使用會在後面詳講。

10.Java NIO ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一個可以監聽新進來的TCP連接的通道, 就像標準IO中的ServerSocket一樣。ServerSocketChannel類在 java.nio.channels包中。

Here is an example:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();

    //do something with socketChannel...
}

Opening a ServerSocketChannel

通過調用 ServerSocketChannel.open() 方法來打開ServerSocketChannel.如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

Closing a ServerSocketChannel

通過調用ServerSocketChannel.close() 方法來關閉ServerSocketChannel. 如:

serverSocketChannel.close(); 

Listening for Incoming Connections

通過 ServerSocketChannel.accept() 方法監聽新進來的連接。當 accept()方法返回的時候,它返回一個包含新進來的連接的 SocketChannel。因此, accept()方法會一直阻塞到有新連接到達。

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    //do something with socketChannel...
}

當然,也可以在while循環中使用除了true以外的其它退出準則。

Non-blocking Mode

ServerSocketChannel可以設置成非阻塞模式。在非阻塞模式下,accept() 方法會立刻返回,如果還沒有新進來的連接,返回的將是null。 因此,需要檢查返回的SocketChannel是否是null.如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
    }
}

11.Non-blocking Server

12.Java NIO DatagramChannel

Java NIO中的DatagramChannel是一個能收發UDP包的通道。因爲UDP是無連接的網絡協議,所以不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。

Opening a DatagramChannel

Here is how you open a DatagramChannel:

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));

這個例子打開的 DatagramChannel可以在UDP端口9999上接收數據包。

Receiving Data

You receive data from a DatagramChannel by calling its receive() method, like this:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();

channel.receive(buf);

receive()方法會將接收到的數據包內容複製到指定的Buffer. 如果Buffer容不下收到的數據,多出的數據將被丟棄。

Sending Data

You can send data via a DatagramChannel by calling its send() method, like this:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));

這個例子發送一串字符到”jenkov.com”服務器的UDP端口80。 因爲服務端並沒有監控這個端口,所以什麼也不會發生。也不會通知你發出的數據包是否已收到,因爲UDP在數據傳送方面沒有任何保證。

Connecting to a Specific Address

可以將DatagramChannel“連接”到網絡中的特定地址的。由於UDP是無連接的,連接到特定地址並不會像TCP通道那樣創建一個真正的連接。而是鎖住DatagramChannel ,讓其只能從特定地址收發數據。

Here is an example:

channel.connect(new InetSocketAddress("jenkov.com", 80));

當連接後,也可以使用read()和write()方法,就像在用傳統的通道一樣。只是在數據傳送方面沒有任何保證。這裏有幾個例子:

int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf);

13.Java NIO Pipe

Java NIO 管道是2個線程之間的單向數據連接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。
這裏是Pipe原理的圖示:
image

Java NIO: Pipe Internals

Creating a Pipe

You open a Pipe by calling the Pipe.open() method. Here is how that looks:

Pipe pipe = Pipe.open();

Writing to a Pipe

To write to a Pipe you need to access the sink channel. Here is how that is done:

Pipe.SinkChannel sinkChannel = pipe.sink();

You write to a SinkChannel by calling it’s write() method, like this:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    sinkChannel.write(buf);
}

Reading from a Pipe

To read from a Pipe you need to access the source channel. Here is how that is done:

Pipe.SourceChannel sourceChannel = pipe.source();

To read from the source channel you call its read() method like this:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);

The int returned by the read() method tells how many bytes were read into the buffer.

14.Java NIO vs. IO

當學習了Java NIO和IO的API後,一個問題馬上涌入腦海:

我應該何時使用IO,何時使用NIO呢?在本文中,我會盡量清晰地解析Java NIO和IO的差異、它們的使用場景,以及它們如何影響您的代碼設計。

Main Differences Betwen Java NIO and IO

下表總結了Java NIO和IO之間的主要差別,我會更詳細地描述表中每部分的差異。

NIO IO
Buffer oriented Stream oriented
Non blocking IO Blocking IO
Selectors

Stream Oriented vs. Buffer Oriented

Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。

Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。

Java NIO的緩衝導向方法略有不同。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏尚未處理的數據。

Blocking vs. Non-blocking IO

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了。

Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。

非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。

線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

Selectors

Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。

How NIO and IO Influences Application Design

無論您選擇IO或NIO工具箱,可能會影響您應用程序設計的以下幾個方面:

  1. 對NIO或IO類的API調用。
  2. 數據處理。
  3. 用來處理數據的線程數。

The API Calls

當然,使用NIO的API調用時看起來與使用IO時有所不同,但這並不意外,因爲並不是僅從一個InputStream逐字節讀取,而是數據必須先讀入緩衝區再處理。

The Processing of Data

使用純粹的NIO設計相較IO設計,數據處理也受到影響。

在IO設計中,我們從InputStream或 Reader逐字節讀取數據。假設你正在處理一基於行的文本數據流,例如:

Name: Anna
Age: 25
Email: [email protected]
Phone: 1234567890

該文本行的流可以這樣處理:

InputStream input = ... ; // get the InputStream from the client socket

BufferedReader reader = new BufferedReader(new InputStreamReader(input));

String nameLine   = reader.readLine();
String ageLine    = reader.readLine();
String emailLine  = reader.readLine();
String phoneLine  = reader.readLine();

請注意處理狀態由程序執行多久決定。換句話說,一旦reader.readLine()方法返回,你就知道肯定文本行就已讀完, readline()阻塞直到整行讀完,這就是原因。你也知道此行包含名稱;同樣,第二個readline()調用返回的時候,你知道這行包含年齡等。 正如你可以看到,該處理程序僅在有新數據讀入時運行,並知道每步的數據是什麼。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據(大多如此)。下圖也說明了這條原則:

image

Java IO: Reading data from a blocking stream.

A NIO implementation would look different. Here is a simplified example:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

注意第二行,從通道讀取字節到ByteBuffer。當這個方法調用返回時,你不知道你所需的所有數據是否在緩衝區內。你所知道的是,該緩衝區包含一些字節,這使得處理有點困難。
假設第一次 read(buffer)調用後,讀入緩衝區的數據只有半行,例如,“Name:An”,你能處理數據嗎?顯然不能,需要等待,直到整行數據讀入緩存,在此之前,對數據的任何處理毫無意義。

所以,你怎麼知道是否該緩衝區包含足夠的數據可以處理呢?好了,你不知道。發現的方法只能查看緩衝區中的數據。其結果是,在你知道所有數據都在緩衝區裏之前,你必須檢查幾次緩衝區的數據。這不僅效率低下,而且可以使程序設計方案雜亂不堪。例如:

ByteBuffer buffer = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buffer);

while(! bufferFull(bytesRead) ) {
    bytesRead = inChannel.read(buffer);
}

bufferFull()方法必須跟蹤有多少數據讀入緩衝區,並返回真或假,這取決於緩衝區是否已滿。換句話說,如果緩衝區準備好被處理,那麼表示緩衝區滿了。

bufferFull()方法掃描緩衝區,但必須使緩衝區保持與調用bufferFull()方法之前相同的狀態。否則,下一個讀入緩衝區的數據可能不會在正確的位置讀入。這並非不可能,但這是另一個需要注意的問題。

如果緩衝區已滿,它可以被處理。如果它不滿,並且在你的實際案例中有意義,你或許能處理其中的部分數據。但是許多情況下並非如此。下圖展示了“緩衝區數據循環就緒”:
image

Java NIO: Reading data from a channel until all needed data is in buffer

Summary

NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。

如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,實現NIO的服務器可能是一個優勢。同樣,如果你需要維持許多打開的連接到其他計算機上,如P2P網絡中,使用一個單獨的線程來管理你所有出站連接,可能是一個優勢。一個線程多個連接的設計方案如下圖所示:
image

Java NIO: A single thread managing multiple connections.

如果你有少量的連接使用非常高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能非常契合。下圖說明了一個典型的IO服務器設計:
image

Java IO: A classic IO server design - one connection handled by one thread.

15.Java NIO Path

Java Path接口是Java NIO 2更新的一部分,Java NIO在Java 6和Java 7中接收到了這個更新。

Java Path接口被添加到Java 7中的Java NIO中。Path接口位於java.nio.file包中,因此java Path接口的完全限定名是java.nio.file.Path。

Java Path實例實例表示文件系統中的路徑。路徑可以指向文件或目錄。路徑可以是絕對路徑,也可以是相對路徑。絕對路徑包含從文件系統根目錄到它指向的文件或目錄的完整路徑。相對路徑包含文件或目錄相對於其他路徑的路徑。相對路徑聽起來可能有點混亂(confusing)。別擔心。我將在本Java NIO Path教程的後面更詳細地解釋相對路徑。

在某些操作系統中,不要將文件系統路徑與path環境變量混淆。java.nio.file.Path接口與Path環境變量無關。

在許多方面,java.nio.file.Path接口與java.io.file類類似,但有一些細微的區別。不過,在許多情況下,可以使用Path接口來替換File類的使用。

Creating a Path Instance

要使用java.nio.file.Path實例,必須創建Path實例。使用名爲Paths的類(java.nio.file.Paths)中的靜態方法Paths.get()創建Paths實例。下面是一個Java path.get()示例:

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathExample {

    public static void main(String[] args) {

        Path path = Paths.get("c:\\data\\myfile.txt");

    }
}

Notice the two import statements at the top of the example. To use the Path interface and the Paths class we must first import them.

其次,注意Paths.get(“c:\\data\\myfile.txt”)方法調用。創建路徑實例的是對Paths.get()方法的調用。換句話說,Path.get()方法是路徑實例的工廠方法。

Creating an Absolute Path

創建絕對路徑是通過調用Paths.get()factory方法完成的,該方法使用絕對文件作爲參數。下面是創建表示絕對路徑的路徑實例的示例:

Path path = Paths.get("c:\\data\\myfile.txt");

絕對路徑是c:\\ data\\myfile.txt。在Java字符串中,\\是必需的,因爲\是轉義字符,這意味着下面的字符告訴字符串中這個位置真正要定位的字符。通過編寫,告訴Java編譯器在字符串中寫入一個字符。
上面的路徑是Windows文件系統路徑。在Unix系統(Linux、MacOS、FreeBSD等)上,上述絕對路徑可能如下所示:

Path path = Paths.get("/home/jakobjenkov/myfile.txt");

絕對路徑現在是/home/jakobjenkov/myfile.txt
如果在Windows計算機上使用此類路徑(以/開頭的路徑),則該路徑將被解釋爲相對於當前驅動器。例如,路徑

/home/jakobjenkov/myfile.txt

可能被解釋爲位於C驅動器上。然後路徑將對應於此完整路徑:

C:/home/jakobjenkov/myfile.txt

Creating a Relative Path

相對路徑是指從一個路徑(基本路徑)指向一個目錄或文件的路徑。相對路徑的完整路徑(絕對路徑)是通過將基路徑與相對路徑組合而得到的。
Java NIO Path 類也可以用於處理相對路徑。使用Paths.get(basePath,relativePath)方法創建相對路徑。以下是Java中的兩個相對路徑示例:

Path projects = Paths.get("d:\\data", "projects");

Path file     = Paths.get("d:\\data","projects\\a-project\\myfile.txt");

第一個示例創建一個Java路徑實例,該實例指向路徑(目錄)d:\\ data\\projects。第二個示例創建一個路徑實例,該實例指向路徑(文件)d:\\data\\projects\a-project\\myfile.txt
使用相對路徑時,可以在路徑字符串內使用兩個特殊代碼。這些代碼是:

.
..

這個 .代碼表示“當前目錄”。例如,如果創建如下相對路徑:

Path currentDir = Paths.get(".");
System.out.println(currentDir.toAbsolutePath());

Path.normalize()

Path接口的normalize()方法可以規範化路徑。規範化意味着它刪除所有的.還有..在路徑字符串中間編碼,並解析路徑字符串引用的路徑。下面是一個Java Path.normalize()示例:

String originalPath ="d:\\data\\projects\\a-project\\..\\another-project";

Path path1 = Paths.get(originalPath);
System.out.println("path1 = " + path1);

Path path2 = path1.normalize();
System.out.println("path2 = " + path2);

此Path示例首先創建帶有..的路徑字符串在中間。然後,該示例從該路徑字符串創建一個Path實例,並輸出該Path實例(實際上它輸出Path.toString())。

然後,該示例對創建的Path實例調用normalize(),該實例將返回一個新的Path實例。這個新的、規範化的Path實例也會被打印出來。
以下是從上述示例打印的輸出:

path1 = d:\data\projects\a-project\..\another-project
path2 = d:\data\projects\another-project

如您所見,規範化路徑不包含a-project\..部分,因爲這是多餘的。刪除的部分不會向最終絕對路徑添加任何內容。

16.Java NIO Files

Java NIO Files類(java.nio.file.Files)提供了幾種在文件系統中操作文件的方法。本Java NIO文件教程將介紹這些方法中最常用的方法。Files類包含許多方法,因此如果需要這裏沒有描述的方法,可以查看JavaDoc。

java.nio.file.Files類與java.nio.file.Path實例一起工作,因此在使用Files類之前,您需要了解Path類。

Files.exists()

The Files.exists() method checks if a given Path exists in the file system.

Path path = Paths.get("data/logging.properties");

boolean pathExists =
        Files.exists(path,
            new LinkOption[]{ LinkOption.NOFOLLOW_LINKS});

Files.createDirectory()

The Files.createDirectory() method creates a new directory from a Path instance.

Path path = Paths.get("data/subdir");

try {
    Path newDir = Files.createDirectory(path);
} catch(FileAlreadyExistsException e){
    // the directory already exists.
} catch (IOException e) {
    //something else went wrong
    e.printStackTrace();
}

Files.copy()

The Files.copy() method copies a file from one path to another.

Path sourcePath      = Paths.get("data/logging.properties");
Path destinationPath = Paths.get("data/logging-copy.properties");

try {
    Files.copy(sourcePath, destinationPath);
} catch(FileAlreadyExistsException e) {
    //destination file already exists
} catch (IOException e) {
    //something else went wrong
    e.printStackTrace();
}

Overwriting Existing Files

It is possible to force the Files.copy() to overwrite an existing file.

Path sourcePath      = Paths.get("data/logging.properties");
Path destinationPath = Paths.get("data/logging-copy.properties");

try {
    Files.copy(sourcePath, destinationPath,
            StandardCopyOption.REPLACE_EXISTING);
} catch(FileAlreadyExistsException e) {
    //destination file already exists
} catch (IOException e) {
    //something else went wrong
    e.printStackTrace();
}

Files.move()

Java NIO文件類還包含一個用於將文件從一個路徑移動到另一個路徑的函數。移動文件和重命名文件是一樣的,只是移動文件既可以將其移動到不同的目錄,也可以在同一操作中更改其名稱。是的,java.io.File類也可以使用其renameTo()方法來實現這一點,但是現在在java.nio.File.Files類中也有了文件移動功能。

Path sourcePath      = Paths.get("data/logging-copy.properties");
Path destinationPath = Paths.get("data/subdir/logging-moved.properties");

try {
    Files.move(sourcePath, destinationPath,
            StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    //moving file failed.
    e.printStackTrace();
}

Files.delete()

The Files.delete() method can delete a file or directory.

Path path = Paths.get("data/subdir/logging-moved.properties");

try {
    Files.delete(path);
} catch (IOException e) {
    //deleting file failed
    e.printStackTrace();
}

Files.walkFileTree()

Files.walkFileTree()方法包含遞歸(recursively)遍歷目錄樹的功能。walkFileTree()方法接受一個Path實例和一個FileVisitor作爲參數。路徑實例指向要遍歷的目錄。在遍歷期間調用FileVisitor。

public interface FileVisitor {

    public FileVisitResult preVisitDirectory(
        Path dir, BasicFileAttributes attrs) throws IOException;

    public FileVisitResult visitFile(
        Path file, BasicFileAttributes attrs) throws IOException;

    public FileVisitResult visitFileFailed(
        Path file, IOException exc) throws IOException;

    public FileVisitResult postVisitDirectory(
        Path dir, IOException exc) throws IOException {

}

您必須自己實現FileVisitor接口,並將實現的實例傳遞給walkFileTree()方法。在目錄遍歷期間,FileVisitor實現的每個方法都將在不同的時間被調用。如果不需要hook into所有這些方法,可以擴展SimpleFileVisitor類,該類包含FileVisitor接口中所有方法的默認實現。

Files.walkFileTree(path, new FileVisitor<Path>() {
  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    System.out.println("pre visit dir:" + dir);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    System.out.println("visit file: " + file);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
    System.out.println("visit file failed: " + file);
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    System.out.println("post visit directory: " + dir);
    return FileVisitResult.CONTINUE;
  }
});

在遍歷期間,在不同的時間調用FileVisitor實現中的每個方法:

preVisitDirectory()方法是在訪問任何目錄之前調用的。postVisitDirectory()方法是在訪問目錄之後調用的。

在文件遍歷期間,將爲訪問的每個文件調用visitFile()方法。它僅文件調用目錄不會調用。如果訪問文件失敗,則調用visitFileFailed()方法。例如,如果您沒有正確的權限,或者發生了其他問題。

四個方法中的每一個都返回一個FileVisitResult枚舉實例。FileVisitResult枚舉包含以下四個選項:

  • CONTINUE
  • TERMINATE
  • SKIP_SIBLINGS
  • SKIP_SUBTREE

通過返回其中一個值,被調用的方法可以決定如何繼續文件遍歷。

CONTINUE:意味着文件遍歷應繼續正常進行。
TERMINATE:意味着文件遍歷現在應該終止。
SKIP_SIBLINGS:意味着文件遍歷應繼續,但不訪問此文件或目錄的任何同級。
SKIP_SUBTREE:表示文件遍歷應該繼續,但不訪問此目錄中的條目。此值只有在從preVisitDirectory()返回時才具有函數。如果從任何其他方法返回,它將被解釋爲CONTINUE。

Searching For Files

Here is a walkFileTree() that extends SimpleFileVisitor to look for a file named README.txt :

Path rootPath = Paths.get("data");
String fileToFind = File.separator + "README.txt";

try {
  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
    
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
      String fileString = file.toAbsolutePath().toString();
      //System.out.println("pathString = " + fileString);

      if(fileString.endsWith(fileToFind)){
        System.out.println("file found at path: " + file.toAbsolutePath());
        return FileVisitResult.TERMINATE;
      }
      return FileVisitResult.CONTINUE;
    }
  });
} catch(IOException e){
    e.printStackTrace();
}

Deleting Directories Recursively

Files.walkFileTree()還可用於刪除包含所有文件和子目錄的目錄。Files.delete()方法僅在目錄爲空時刪除該目錄。通過遍歷所有目錄並刪除每個目錄中的所有文件(在visitFile()中),然後刪除目錄本身(在postVisitDirectory()中),可以刪除包含所有子目錄和文件的目錄。

Path rootPath = Paths.get("data/to-delete");

try {
  Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
      System.out.println("delete file: " + file.toString());
      Files.delete(file);
      return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
      Files.delete(dir);
      System.out.println("delete dir: " + dir.toString());
      return FileVisitResult.CONTINUE;
    }
  });
} catch(IOException e){
  e.printStackTrace();
}

Additional Methods in the Files Class

java.nio.file.Files類包含許多其他有用的函數,例如用於創建符號鏈接、確定文件大小、設置文件權限等的函數。有關這些方法的更多信息,請查看java.nio.file.Files類的JavaDoc。

17.Java NIO AsynchronousFileChannel

在Java 7中,異步文件通道被添加到Java NIO中。AsynchronousFileChannel使異步讀取數據和將數據寫入文件成爲可能。本教程將解釋如何使用AsynchronousFileChannel。

Creating an AsynchronousFileChannel

You create an AsynchronousFileChannel via its static method open(). Here is an example of creating an AsynchronousFileChannel:

Path path = Paths.get("data/test.xml");

AsynchronousFileChannel fileChannel =
   AsynchronousFileChannel.open(path, StandardOpenOption.READ);

open()方法的第一個參數是指向AsynchronousFileChannel要關聯的文件的路徑實例。

第二個參數是一個或多個打開選項,告訴AsynchronousFileChannel要對基礎文件執行哪些操作。在本例中,我們使用StandardOpenOption.READ,這意味着將打開文件進行讀取。

Reading Data

可以通過兩種方式從異步文件通道讀取數據。每種讀取數據的方法都調用AsynchronousFileChannel的read()方法之一。以下各節將介紹兩種讀取數據的方法。

Reading Data Via a Future

從AsynchronousFileChannel讀取數據的第一種方法是調用返回Future的read()方法。下面是調用read()方法的方式:

Future<Integer> operation = fileChannel.read(buffer, 0);

此版本的read()方法將ByteBuffer作爲第一個參數。從AsynchronousFileChannel讀取的數據將被tebuffer讀入此。第二個參數是文件中開始讀取的字節位置。

read()方法立即返回,即使讀取操作尚未完成。通過調用read()方法返回的Future實例的isDone()方法,可以檢查讀取操作何時完成。

AsynchronousFileChannel fileChannel = 
    AsynchronousFileChannel.open(path, StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;

Future<Integer> operation = fileChannel.read(buffer, position);

while(!operation.isDone());

buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();

本例創建一個AsynchronousFileChannel,然後創建一個ByteBuffer,該ByteBuffer作爲參數傳遞給read()方法,其位置爲0。調用read()之後,示例循環,直到返回的Future的isDone()方法返回true。當然,這不是一個非常有效的CPU使用-但不知何故,你需要等到讀操作完成。

讀取操作完成後,將數據讀入ByteBuffer,然後讀入字符串並打印到System.out。

Reading Data Via a CompletionHandler

從AsynchronousFileChannel讀取數據的第二種方法是調用以CompletionHandler作爲參數的read()方法版本。下面是如何調用此read()方法:

fileChannel.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println("result = " + result);

        attachment.flip();
        byte[] data = new byte[attachment.limit()];
        attachment.get(data);
        System.out.println(new String(data));
        attachment.clear();
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {

    }
});

讀取操作完成後,將調用CompletionHandler的completed()方法。當參數傳遞給completed()方法時,會傳遞一個整數,告訴讀取了多少字節,以及傳遞給read()方法的“附件”。“attachment”是read()方法的第三個參數。在本例中,數據也被讀入ByteBuffer。您可以自由選擇要附加的對象。

如果讀取操作失敗,將改爲調用CompletionHandler的failed()方法。

Writing Data

與讀取一樣,您可以用兩種方式將數據寫入異步文件通道。每種寫入數據的方法都調用AsynchronousFileChannel的write()方法之一。以下各節將介紹兩種數據寫入方法。

Writing Data Via a Future

AsynchronousFileChannel還允許您異步寫入數據。下面是完整的Java AsynchronousFileChannel編寫示例:

Path path = Paths.get("data/test-write.txt");
AsynchronousFileChannel fileChannel = 
    AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;

buffer.put("test data".getBytes());
buffer.flip();

Future<Integer> operation = fileChannel.write(buffer, position);
buffer.clear();

while(!operation.isDone());

System.out.println("Write done");

首先,異步文件通道以寫模式打開。然後創建ByteBuffer並將一些數據寫入其中。然後ByteBuffer中的數據被寫入文件。最後,示例檢查返回的Future以查看寫入操作何時完成。

注意,該文件必須在該代碼生效之前已經存在。如果文件不存在,Read()方法將拋出java. NIO.FIL.NUUCHFILExeExchange。

You can make sure that the file the Path points to exists with the following code:

if(!Files.exists(path)){
    Files.createFile(path);
}

Writing Data Via a CompletionHandler

您還可以使用CompletionHandler將數據寫入AsynchronousFileChannel,以告訴您何時完成寫入而不是Future。下面是使用CompletionHandler將數據寫入AsynchronousFileChannel的示例:

Path path = Paths.get("data/test-write.txt");
if(!Files.exists(path)){
    Files.createFile(path);
}
AsynchronousFileChannel fileChannel = 
    AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;

buffer.put("test data".getBytes());
buffer.flip();

fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        System.out.println("bytes written: " + result);
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        System.out.println("Write failed");
        exc.printStackTrace();
    }
});

當寫入操作完成時,將調用CompletionHandler的completed()方法。如果由於某種原因寫操作失敗,將調用failed()方法。
注意ByteBuffer是如何被用作附件的-傳遞給CompletionHandler方法的對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章