Java IO系列之二:NIO基本操做 深刻理解Java NIO

核心部分

   NIO( New Input/ Output) , 引入了一種基於通道和緩衝區的 I/O 方式,NIO 是一種同步非阻塞的 IO 模型。同步是指線程不斷輪詢 IO 事件是否就緒,非阻塞是指線程在等待 IO 的時候,能夠同時作其餘任務。同步的核心就是 Selector,Selector 代替了線程自己輪詢 IO 事件,避免了阻塞,同時減小了沒必要要的線程消耗;非阻塞的核心就是通道和緩衝區,當 IO 事件就緒時,能夠經過寫到緩衝區,保證 IO 的成功,而無需線程阻塞式地等待。html

  核心: Channel 、Buffer、Selector(通道、緩存區、選擇器)java

  Channel react

    • FileChannel:從文件中讀寫數據。
    • DatagramChannel:能經過UDP讀寫網絡中的數據。
    • SocketChannel:能經過TCP讀寫網絡中的數據。
    • ServerSocketChannel:能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。

  

1. 背景

  java.nio全稱java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,爲全部的原始類型(boolean類型除外)提供緩存支持的數據容器,使用它能夠提供非阻塞式的高伸縮性網絡。api

  在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 類, 引入了一種基於通道和緩衝區的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆的 DirectByteBuffer 對象做爲這塊內存的引用進行操做,避免了在 Java 堆和 Native 堆中來回複製數據數組

  NIO 是一種同步非阻塞的 IO 模型。同步是指線程不斷輪詢 IO 事件是否就緒非阻塞是指線程在等待 IO 的時候,能夠同時作其餘任務同步的核心就是 Selector,Selector 代替了線程自己輪詢 IO 事件,避免了阻塞同時減小了沒必要要的線程消耗;非阻塞的核心就是通道和緩衝區,當 IO 事件就緒時,能夠經過寫到緩衝區,保證 IO 的成功,而無需線程阻塞式地等待。緩存

2. 原理

  reactor(反應器)模式 使用單線程模擬多線程,提升資源利用率和程序的效率,增長系統吞吐量。服務器

  Java NIO: Channels and Buffers(通道和緩衝區)非阻塞的核心: 標準的IO基於字節流和字符流進行操做的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操做,數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。網絡

  Java NIO: Non-blocking IO(非阻塞IO): Java NIO可讓你非阻塞的使用IO,例如:當線程從通道讀取數據到緩衝區時,線程仍是能夠進行其餘事情。當數據被寫入到緩衝區時,線程能夠繼續處理它。從緩衝區寫入通道也相似。多線程

  Java NIO: Selectors(選擇器)同步的核心:Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(好比:鏈接打開,數據到達)。所以,單個的線程能夠監聽多個數據通道。app

3. 通道Channel

Java NIO的通道相似流,但又有些不一樣:

  • 既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向的。(可讀可寫,單向)
  • 通道能夠異步地讀寫。
  • 通道中的數據老是要先讀到一個Buffer,或者老是要從一個Buffer中寫入。(和Buffer交互)

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

Channel的實現

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

  • FileChannel:從文件中讀寫數據。
  • DatagramChannel:能經過UDP讀寫網絡中的數據。
  • SocketChannel:能經過TCP讀寫網絡中的數據。
  • ServerSocketChannel:能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。

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

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw"); FileChannel inChannel = aFile.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf); while (bytesRead != -1) { System.out.println("Read " + bytesRead); buf.flip(); while(buf.hasRemaining()){ System.out.print((char) buf.get()); } buf.clear(); bytesRead = inChannel.read(buf); } aFile.close();

注意 buf.flip() 的調用,首先讀取數據到Buffer,而後反轉Buffer,接着再從Buffer中讀取數據。

4. 緩存區Buffer

4.1 Buffer的基本用法

使用Buffer讀寫數據通常遵循如下四個步驟:

  1. 寫入數據到Buffer
  2. 調用flip()方法將Buffer模式切換到讀模式
  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();

4.2 Buffer的capacity,position和limit

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

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

  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)

  position和limit的含義取決於Buffer處在讀模式仍是寫模式。無論Buffer處在什麼模式,capacity的含義老是同樣的。

  

4.3 Buffer的8大類型

Java NIO 有如下Buffer類型

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

如你所見,這些Buffer類型表明了不一樣的數據類型。換句話說,就是能夠經過char,short,int,long,float 或 double類型來操做緩衝區中的字節。MappedByteBuffer 有些特別

4.4 Buffer的分配

要想得到一個Buffer對象首先要進行分配。 每個Buffer類都有一個allocate方法。

//分配48字節capacity的ByteBuffer的例子。

ByteBuffer buf = ByteBuffer.allocate(48);

//分配一個可存儲1024個字符的CharBuffer:

CharBuffer buf = CharBuffer.allocate(1024);

4.5 向Buffer中寫數據

寫數據到Buffer有兩種方式:

  • 從Channel寫到Buffer。
  • 經過Buffer的put()方法寫到Buffer裏。
//從Channel寫到Buffer的例子 int bytesRead = inChannel.read(buf); //read into buffer.
//經過put方法寫Buffer的例子: buf.put(127); put方法有不少版本,容許你以不一樣的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc。

4.6 flip()方法

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

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

4.7 從Buffer中讀取數據

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

  1. 從Buffer讀取數據到Channel。
  2. 使用get()方法從Buffer中讀取數據。
//從Buffer讀取數據到Channel的例子
int bytesWritten = inChannel.write(buf) //使用get()方法從Buffer中讀取數據的例子
byte aByte = buf.get(); get方法有不少版本,容許你以不一樣的方式從Buffer中讀取數據。例如,從指定position讀取,或者從Buffer中讀取數據到字節數組。更多Buffer實現的細節參考JavaDoc。

4.8 rewind()方法

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

4.9 clear()與compact()方法

  clear()方法會清空整個緩衝區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。

  一旦讀完Buffer中的數據,須要讓Buffer準備好再次被寫入。能夠經過clear()或compact()方法來完成。

  若是調用的是clear()方法,position將被設回0,limit被設置成 capacity的值。換句話說,Buffer 被清空了。Buffer中的數據並未清除,只是這些標記告訴咱們能夠從哪裏開始往Buffer裏寫數據。若是Buffer中有一些未讀的數據,調用clear()方法,數據將「被遺忘」,意味着再也不有任何標記會告訴你哪些數據被讀過,哪些尚未。

  若是Buffer中仍有未讀的數據,且後續還須要這些數據,可是此時想要先先寫些數據,那麼使用compact()方法。compact()方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法同樣,設置成capacity。如今Buffer準備好寫數據了,可是不會覆蓋未讀的數據。

4.10 mark()與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.

4.11 equals()與compareTo()方法

可使用equals()和compareTo()方法比較兩個Buffer。(注:剩餘元素是從 position到limit之間的元素)

equals()

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

  • 兩個對象類型相同。包含不一樣數據類型的 buffer 永遠不會相等,並且buffer 毫不會等於非 buffer對象。
  • 兩個對象都剩餘一樣數量(limit - position)的元素。Buffer 的容量不須要相同,並且緩衝區中剩餘數據的索引也沒必要相同。
  • 在每一個緩衝區中應被 get() 函數返回的剩餘數據元素序列([position, limit - 1] 位置對應的元素序列)必須一致。

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

compareTo()

compareTo()方法比較兩個Buffer的剩餘元素(byte、char等),按字典順序比較兩個字符串, 若是知足下列條件,則認爲一個Buffer「小於」另外一個Buffer:

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

與 equals() 類似,compareTo() 不容許不一樣對象間進行比較。但 compareTo()更爲嚴格:若是你傳遞一個類型錯誤的對象,它會拋出 ClassCastException 異常,但 equals() 只會返回 false。 

if (buffer1.compareTo(buffer2) < 0) {  
    // do sth, it means buffer2 < buffer1,not buffer1 < buffer2  
    doSth();  
}  

5.  Scatter/Gather

Java NIO開始支持scatter/gather,scatter/gather用於描述從Channel(譯者注:Channel在中文常常翻譯爲通道)中讀取或者寫入到Channel的操做。
分散(scatter)從Channel中讀取是指在讀操做時將讀取的數據寫入多個buffer中。所以,Channel將從Channel中讀取的數據「分散(scatter)」到多個Buffer中。
彙集(gather)寫入Channel是指在寫操做時將多個buffer的數據寫入同一個Channel,所以,Channel 將多個Buffer中的數據「彙集(gather)」後發送到Channel。

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

  

Scattering Reads(對應buffer的寫模式)
  Scattering Reads是指數據從一個channel讀取到多個buffer中。以下圖描述:

    Java NIO: Scattering Read

  Java NIO: 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,這也意味着它不適用於動態消息(譯者注:消息大小不固定)。
換句話說,若是存在消息頭和消息體,消息頭必須完成填充(例如 128
byte),Scattering Reads才能正常工做。

 

Gathering Writes(對應buffer的讀模式)

  Gathering Writes是指數據從多個buffer寫入到同一個channel。以下圖描述:

     Java NIO: Gathering Write

  Java NIO: 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.  Channel to Channel Transfers

在Java NIO中,若是兩個通道中有一個是FileChannel,那你能夠直接將數據從一個channel傳輸到另一個channel。

6.1 transferFrom()

FileChannel的transferFrom()方法能夠將數據從源通道傳輸到FileChannel中(譯者注:這個方法在JDK文檔中的解釋爲將字節從給定的可讀取字節通道傳輸到此通道的文件中)。下面是一個簡單的例子:

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(position, count, fromChannel);
方法的輸入參數
  position表示從position處開始向目標文件寫入數據,
  count表示最多傳輸的字節數。若是源通道的剩餘空間小於 count 個字節,則所傳輸的字節數要小於請求的字節數。
此外要注意,在SoketChannel的實現中,SocketChannel只會傳輸此刻準備好的數據(可能不足count字節)。
所以,SocketChannel可能不會將請求的全部數據(count個字節)所有傳輸到FileChannel中。

6.2 transferTo()

transferTo()方法將數據從FileChannel傳輸到其餘的channel中。下面是一個簡單的例子:

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);

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

7. Selector

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

7.1 爲何使用Selector?

  僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。(可是,須要記住,現代的操做系統和CPU在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個CPU有多個內核,不使用多任務多是在浪費CPU能力。)

下面是單線程使用一個Selector處理3個channel的示例圖:

 Java NIO: Selectors

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

7.2 Selector的建立

經過調用Selector.open()方法建立一個Selector,以下:

Selector selector = Selector.open();

7.3 向Selector註冊通道

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

channel.configureBlocking(false);//與Selector一塊兒使用時,Channel必須處於非阻塞模式下。 SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

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

  注意register()方法的第二個參數。這是一個「interest集合」,意思是在經過Selector監聽Channel時對什麼事件感興趣。能夠監聽四種不一樣類型的事件:
    1. Connect
    2. Accept
    3. Read
    4. Write
  通道觸發了一個事件意思是該事件已經就緒。因此,
    1. 某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。
    2. 一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。
    3. 一個有數據可讀的通道能夠說是「讀就緒」。
    4. 等待寫數據的通道能夠說是「寫就緒」。
  這四種事件用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;

 
    

 

7.4 SelectionKey

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

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的對象(可選)

下面我會描述這些屬性。

interest集合

 interest集合是你所選擇的感興趣的事件集合。能夠經過SelectionKey讀寫interest集合

int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

能夠看到,用「位與」操做interest 集合和給定的SelectionKey常量,能夠肯定某個肯定的事件是否在interest 集合中。

ready集合

ready 集合是通道已經準備就緒的操做的集合。在一次選擇(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();
//SelectionKey.channel()方法返回的通道須要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。 Selector selector = selectionKey.selector();

附加的對象

能夠將一個對象或者更多信息附着到SelectionKey上,這樣就能方便的識別某個給定的通道

//能夠附加與通道一塊兒使用的Buffer,或是包含彙集數據的某個對象。使用方法以下:
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment(); //還能夠在用register()方法向Selector註冊Channel的時候附加對象。如: SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

經過Selector選擇通道

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

下面是select()方法:

  • int select():select()阻塞到至少有一個通道在你註冊的事件上就緒了
  • int select(long timeout):select(long timeout)和select()同樣,除了最長會阻塞timeout毫秒(參數)。
  • int selectNow():selectNow()不會阻塞,無論什麼通道就緒都馬上返回譯者注:此方法執行非阻塞的選擇操做。若是自從前一次選擇操做後,沒有通道變成可選擇的,則此方法直接返回零。)。

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

selectedKeys()

一旦調用了select()方法,而且返回值代表有一個或更多個通道就緒了,而後能夠經過調用selector的selectedKeys()方法,訪問「已選擇鍵集(selected key set)」中的就緒通道。以下所示:

Set selectedKeys = selector.selectedKeys();

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

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

Set selectedKeys = selector.selectedKeys(); Iterator 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 }
   //Selector不會本身從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。 keyIterator.remove(); }

wakeUp()

  某個線程調用select()方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法便可。阻塞在select()方法上的線程會立馬返回。若是有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會當即「醒來(wake up)」。

close()

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

完整的示例

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

Selector selector = Selector.open();//建立一個Selector channel.configureBlocking(false);//非阻塞模式 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);//註冊通道 while(true) { int readyChannels = selector.select();//得到已經就緒的通道數 if(readyChannels == 0) continue; Set selectedKeys = selector.selectedKeys();//得到已選擇鍵集(selected key set) Iterator 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.  FileChannel

Java NIO中的FileChannel是一個鏈接到文件的通道。能夠經過文件通道讀寫文件。FileChannel沒法設置爲非阻塞模式,它老是運行在阻塞模式下

8.1 打開FileChannel

在使用FileChannel以前,必須先打開它。可是,咱們沒法直接打開一個FileChannel,須要經過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是經過RandomAccessFile打開FileChannel的示例:

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

8.2 從FileChannel讀取數據

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

ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buf);

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

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

8.3 向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中已經沒有還沒有寫入通道的字節。

8.4 關閉FileChannel

用完FileChannel後必須將其關閉。如:

channel.close();

8.5 FileChannel的position方法

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

也能夠經過調用position(long pos)方法設置FileChannel的當前位置。

這裏有兩個例子:

long pos = channel.position(); channel.position(pos +123);

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

若是將位置設置在文件結束符以後,而後向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能致使「文件空洞」,磁盤上物理文件中寫入的數據間有空隙。

8.6 FileChannel的size方法

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

long fileSize = channel.size();

8.7 FileChannel的truncate方法

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

channel.truncate(1024);

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

8.8 FileChannel的force方法

FileChannel.force()方法將通道里還沒有寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操做系統會將數據緩存在內存中,因此沒法保證寫入到FileChannel裏的數據必定會即時寫到磁盤上。要保證這一點,須要調用force()方法。force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。

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

channel.force(true);

9.  SocketChannel

Java NIO中的SocketChannel是一個鏈接到TCP網絡套接字的通道。能夠經過如下2種方式建立SocketChannel:

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

9.1 打開 SocketChannel

下面是SocketChannel的打開方式:

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

9.2 關閉 SocketChannel

當用完SocketChannel以後調用SocketChannel.close()關閉SocketChannel:

socketChannel.close();

9.3 從 SocketChannel 讀取數據

要從SocketChannel中讀取數據,調用一個read()的方法之一。如下是例子:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

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

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

9.4 寫入 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沒有要寫的字節爲止。

9.5 非阻塞模式

能夠設置 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返回值,它會告訴你讀取了多少字節。

9.6 非阻塞模式與選擇器

非阻塞模式與選擇器搭配會工做的更好,經過將一或多個SocketChannel註冊到Selector,能夠詢問選擇器哪一個通道已經準備好了讀取,寫入等。

10. ServerSocketChannel

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

//打開ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(9999)); while(true){
   //當 accept()方法返回的時候,它返回一個包含新進來的鏈接的 SocketChannel。所以, accept()方法會一直阻塞到有新鏈接到達。 SocketChannel socketChannel
= serverSocketChannel.accept();//監聽新進來的鏈接
   if(socketChannel != null){ //do something with socketChannel... }
 }
//關閉ServerSocketChannel
serverSocketChannel.close();

非阻塞模式

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. DatagramChannel

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

//打開 DatagramChannel,能夠在UDP端口9999上接收數據包。
DatagramChannel channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(9999)); //經過receive()方法從DatagramChannel接收數據 //receive()方法會將接收到的數據包內容複製到指定的Buffer. 若是Buffer容不下收到的數據,多出的數據將被丟棄。
ByteBuffer buf = ByteBuffer.allocate(48); buf.clear(); channel.receive(buf); String newData = "New String to write to file..." + System.currentTimeMillis(); buf.clear(); buf.put(newData.getBytes()); buf.flip(); //經過send()方法從DatagramChannel發送數據 //發送一串字符到」jenkov.com」服務器的UDP端口80。 由於服務端並無監控這個端口,因此什麼也不會發生。也不會通知你發出的數據包是否已收到,由於UDP在數據傳送方面沒有任何保證。 //能夠將DatagramChannel「鏈接」到網絡中的特定地址的。因爲UDP是無鏈接的,鏈接到特定地址並不會像TCP通道那樣建立一個真正的鏈接。而是鎖住DatagramChannel ,讓其只能從特定地址收發數據。
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80)); //當鏈接後,也可使用read()和write()方法,就像在用傳統的通道同樣。只是在數據傳送方面沒有任何保證。
int bytesRead = channel.read(buf); int bytesWritten = channel.write(but);

12. Pipe

Java NIO 管道是2個線程之間的單向數據鏈接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。

這裏是Pipe原理的圖示:

//建立管道,經過Pipe.open()方法打開管道。
Pipe pipe = Pipe.open(); //向管道寫數據,須要訪問sink通道。像這樣:
Pipe.SinkChannel sinkChannel = pipe.sink(); //經過調用SinkChannel的write()方法,將數據寫入SinkChannel
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); } //從讀取管道的數據,須要訪問source通道
Pipe.SourceChannel sourceChannel = pipe.source(); //調用source通道的read()方法來讀取數據
ByteBuffer buf = ByteBuffer.allocate(48); int bytesRead = sourceChannel.read(buf); //read()方法返回的int值會告訴咱們多少字節被讀進了緩衝區。

 

抄錄地址

  1. http://ifeve.com/overview/
  2. http://tutorials.jenkov.com/java-nio/index.html
  3. NIO - Buffer
  4. 【java基礎】Nio
  5. 深刻理解Java NIO
相關文章
相關標籤/搜索