java nio

原文連接     做者:Jakob Jenkov     譯者:airu     校對:丁一html

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

  • Channels
  • Buffers
  • Selectors

雖然Java NIO 中除此以外還有不少類和組件,但在我看來,Channel,Buffer 和 Selector 構成了核心的API。其它組件,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類。所以,在概述中我將集中在這三個組件上。其它組件會在單獨的章節中講到。數組

Channel 和 Buffer

基本上,全部的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據能夠從Channel讀到Buffer中,也能夠從Buffer 寫到Channel中。這裏有個圖示:緩存

Channel和Buffer有好幾種類型。下面是JAVA NIO中的一些主要Channel的實現:服務器

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

正如你所看到的,這些通道涵蓋了UDP 和 TCP 網絡IO,以及文件IO。網絡

與這些類一塊兒的有一些有趣的接口,但爲簡單起見,我儘可能在概述中不提到它們。本教程其它章節與它們相關的地方我會進行解釋。多線程

如下是Java NIO裏關鍵的Buffer實現:app

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

這些Buffer覆蓋了你能經過IO發送的基本數據類型:byte, short, int, long, float, double 和 char。dom

Java NIO 還有個 Mappedyteuffer,用於表示內存映射文件, 我也不打算在概述中說明。異步

Selector

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

這是在一個單線程中使用一個Selector處理3個Channel的圖示:

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

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

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

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

Channel的實現

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

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 從文件中讀寫數據。

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

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

ServerSocketChannel能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。

基本的 Channel 示例

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

01 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
02 FileChannel inChannel = aFile.getChannel();
03  
04 ByteBuffer buf = ByteBuffer.allocate(48);
05  
06 int bytesRead = inChannel.read(buf);
07 while (bytesRead != -1) {
08  
09 System.out.println("Read " + bytesRead);
10 buf.flip();
11  
12 while(buf.hasRemaining()){
13 System.out.print((char) buf.get());
14 }
15  
16 buf.clear();
17 bytesRead = inChannel.read(buf);
18 }
19 aFile.close();

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

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

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

下面是NIO Buffer相關的話題列表:

  1. Buffer的基本用法
  2. Buffer的capacity,position和limit
  3. Buffer的類型
  4. Buffer的分配
  5. 向Buffer中寫數據
  6. flip()方法
  7. 從Buffer中讀取數據
  8. clear()與compact()方法
  9. mark()與reset()方法
  10. equals()與compareTo()方法

Buffer的基本用法

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

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

當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,須要經過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,能夠讀取以前寫入到buffer的全部數據。

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

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

01 RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
02 FileChannel inChannel = aFile.getChannel();
03  
04 //create buffer with capacity of 48 bytes
05 ByteBuffer buf = ByteBuffer.allocate(48);
06  
07 int bytesRead = inChannel.read(buf); //read into buffer.
08 while (bytesRead != -1) {
09  
10   buf.flip();  //make buffer ready for read
11  
12   while(buf.hasRemaining()){
13       System.out.print((char) buf.get()); // read 1 byte at a time
14   }
15  
16   buf.clear(); //make buffer ready for writing
17   bytesRead = inChannel.read(buf);
18 }
19 aFile.close();

Buffer的capacity,position和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的類型

Java NIO 有如下Buffer類型

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

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

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

Buffer的分配

要想得到一個Buffer對象首先要進行分配。 每個Buffer類都有一個allocate方法。下面是一個分配48字節capacity的ByteBuffer的例子。

1 ByteBuffer buf = ByteBuffer.allocate(48);

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

1 CharBuffer buf = CharBuffer.allocate(1024);

向Buffer中寫數據

寫數據到Buffer有兩種方式:

  • 從Channel寫到Buffer。
  • 經過Buffer的put()方法寫到Buffer裏。

從Channel寫到Buffer的例子

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

經過put方法寫Buffer的例子:

1 buf.put(127);

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

flip()方法

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

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

從Buffer中讀取數據

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

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

從Buffer讀取數據到Channel的例子:

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

使用get()方法從Buffer中讀取數據的例子

1 byte aByte = buf.get();

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

rewind()方法

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

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準備好寫數據了,可是不會覆蓋未讀的數據。

mark()與reset()方法

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

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

equals()與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的元素個數比另外一個少)。

做者:Jakob Jenkov   譯者:郭蕾   

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
Scattering Reads是指數據從一個channel讀取到多個buffer中。以下圖描述:

Java NIO: Scattering Read

Java NIO: Scattering Read

代碼示例以下:

1 ByteBuffer header = ByteBuffer.allocate(128);
2 ByteBuffer body   = ByteBuffer.allocate(1024);
3  
4 ByteBuffer[] bufferArray = { header, body };
5  
6 channel.read(bufferArray);

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

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

Gathering Writes

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

Java NIO: Gathering Write

Java NIO: Gathering Write

代碼示例以下:

1 ByteBuffer header = ByteBuffer.allocate(128);
2 ByteBuffer body   = ByteBuffer.allocate(1024);
3  
4 //write data into buffers
5  
6 ByteBuffer[] bufferArray = { header, body };
7  
8 channel.write(bufferArray);

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

在Java NIO中,若是兩個通道中有一個是FileChannel,那你能夠直接將數據從一個channel(譯者注:channel中文常譯做通道)傳輸到另一個channel。

transferFrom()

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

01 RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
02 FileChannel      fromChannel = fromFile.getChannel();
03  
04 RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
05 FileChannel      toChannel = toFile.getChannel();
06  
07 long position = 0;
08 long count = fromChannel.size();
09  
10 toChannel.transferFrom(position, count, fromChannel);

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

transferTo()

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

01 RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
02 FileChannel      fromChannel = fromFile.getChannel();
03  
04 RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
05 FileChannel      toChannel = toFile.getChannel();
06  
07 long position = 0;
08 long count = fromChannel.size();
09  
10 fromChannel.transferTo(position, count, toChannel);

是否是發現這個例子和前面那個例子特別類似?除了調用方法的FileChannel對象不同外,其餘的都同樣。
上面所說的關於SocketChannel的問題在transferTo()方法中一樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿。

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

下面是本文所涉及到的主題列表:

  1. 爲何使用Selector?
  2. Selector的建立
  3. 向Selector註冊通道
  4. SelectionKey
  5. 經過Selector選擇通道
  6. wakeUp()
  7. close()
  8. 完整的示例

爲何使用Selector?

僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。

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

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

Selector的建立

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

1 Selector selector = Selector.open();

向Selector註冊通道

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

1 channel.configureBlocking(false);
2 SelectionKey key = channel.register(selector,
3     Selectionkey.OP_READ);

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

注意register()方法的第二個參數。這是一個「interest集合」,意思是在經過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

若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來,以下:

1 int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

在下面還會繼續提到interest集合。

SelectionKey

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

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

下面我會描述這些屬性。

interest集合

就像向Selector註冊通道一節中所描述的,interest集合是你所選擇的感興趣的事件集合。能夠經過SelectionKey讀寫interest集合,像這樣:

1 int interestSet = selectionKey.interestOps();
2  
3 boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
4 boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
5 boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
6 boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;

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

ready集合

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

1 int readySet = selectionKey.readyOps();

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

1 selectionKey.isAcceptable();
2 selectionKey.isConnectable();
3 selectionKey.isReadable();
4 selectionKey.isWritable();

Channel + Selector

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

1 Channel  channel  = selectionKey.channel();
2 Selector selector = selectionKey.selector();

附加的對象

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

1 selectionKey.attach(theObject);
2 Object attachedObj = selectionKey.attachment();

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

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

經過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)」中的就緒通道。以下所示:

1 Set selectedKeys = selector.selectedKeys();

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

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

01 Set selectedKeys = selector.selectedKeys();
02 Iterator keyIterator = selectedKeys.iterator();
03 while(keyIterator.hasNext()) {
04     SelectionKey key = keyIterator.next();
05     if(key.isAcceptable()) {
06         // a connection was accepted by a ServerSocketChannel.
07     } else if (key.isConnectable()) {
08         // a connection was established with a remote server.
09     } else if (key.isReadable()) {
10         // a channel is ready for reading
11     } else if (key.isWritable()) {
12         // a channel is ready for writing
13     }
14     keyIterator.remove();
15 }

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

注意每次迭代末尾的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實例無效。通道自己並不會關閉。

完整的示例

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

01 Selector selector = Selector.open();
02 channel.configureBlocking(false);
03 SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
04 while(true) {
05   int readyChannels = selector.select();
06   if(readyChannels == 0) continue;
07   Set selectedKeys = selector.selectedKeys();
08   Iterator keyIterator = selectedKeys.iterator();
09   while(keyIterator.hasNext()) {
10     SelectionKey key = keyIterator.next();
11     if(key.isAcceptable()) {
12         // a connection was accepted by a ServerSocketChannel.
13     } else if (key.isConnectable()) {
14         // a connection was established with a remote server.
15     } else if (key.isReadable()) {
16         // a channel is ready for reading
17     } else if (key.isWritable()) {
18         // a channel is ready for writing
19     }
20     keyIterator.remove();
21   }
22 }

Java NIO中的FileChannel是一個鏈接到文件的通道。能夠經過文件通道讀寫文件。

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

打開FileChannel

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

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

從FileChannel讀取數據

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

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

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

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

向FileChannel寫數據

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

01 String newData = "New String to write to file..." + System.currentTimeMillis();
02  
03 ByteBuffer buf = ByteBuffer.allocate(48);
04 buf.clear();
05 buf.put(newData.getBytes());
06  
07 buf.flip();
08  
09 while(buf.hasRemaining()) {
10     channel.write(buf);
11 }

注意FileChannel.write()是在while循環中調用的。由於沒法保證write()方法一次能向FileChannel寫入多少字節,所以須要重複調用write()方法,直到Buffer中已經沒有還沒有寫入通道的字節。

關閉FileChannel

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

1 channel.close();

FileChannel的position方法

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

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

這裏有兩個例子:

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

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

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

FileChannel的size方法

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

1 long fileSize = channel.size();

FileChannel的truncate方法

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

1 channel.truncate(1024);

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

FileChannel的force方法

FileChannel.force()方法將通道里還沒有寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操做系統會將數據緩存在內存中,因此沒法保證寫入到FileChannel裏的數據必定會即時寫到磁盤上。要保證這一點,須要調用force()方法。

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

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

1 channel.force(true);

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

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

打開 SocketChannel

下面是SocketChannel的打開方式:

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

關閉 SocketChannel

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

1 socketChannel.close();

從 SocketChannel 讀取數據

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

相關文章
相關標籤/搜索