Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,能夠替代標準的Java IO API。本系列教程將有助於你學習和理解Java NIO。javascript
Java NIO提供了與標準IO不一樣的IO工做方式:
html
- Channels and Buffers(通道和緩衝區):標準的IO基於字節流和字符流進行操做的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操做,數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。
- Asynchronous IO(異步IO):Java NIO可讓你異步的使用IO,例如:當線程從通道讀取數據到緩衝區時,線程仍是能夠進行其餘事情。當數據被寫入到緩衝區時,線程能夠繼續處理它。從緩衝區寫入通道也相似。
- Selectors(選擇器):Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(好比:鏈接打開,數據到達)。所以,單個的線程能夠監聽多個數據通道。
Java NIO 概述
(本部分
原文連接,做者:Jakob Jenkov, 譯者:airu,校對:丁一)
Java NIO 由如下幾個核心部分組成:
- 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實現:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer覆蓋了你能經過IO發送的基本數據類型:byte, short, int, long, float, double 和 char。
Java NIO 還有個 Mappedyteuffer,用於表示內存映射文件, 我也不打算在概述中說明。
Selector
Selector容許單線程處理多個 Channel。若是你的應用打開了多個鏈接(通道),但每一個鏈接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。
這是在一個單線程中使用一個Selector處理3個Channel的圖示:
要使用Selector,得向Selector註冊Channel,而後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就能夠處理這些事件,事件的例子有如新鏈接進來,數據接收等。
Java NIO vs. IO
(本部分
原文地址,做者:Jakob Jenkov,譯者:郭蕾,校對:方騰飛)
當學習了Java NIO和IO的API後,一個問題立刻涌入腦海:
引用
我應該什麼時候使用IO,什麼時候使用NIO呢?在本文中,我會盡可能清晰地解析Java NIO和IO的差別、它們的使用場景,以及它們如何影響您的代碼設計。
Java NIO和IO的主要區別
下表總結了Java NIO和IO之間的主要差異,我會更詳細地描述表中每部分的差別。
IO |
NIO |
Stream oriented |
Buffer oriented |
Blocking IO |
Non blocking IO |
|
Selectors |
面向流與面向緩衝
Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。 Java NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據。
阻塞與非阻塞IO
Java IO的各類流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。
選擇器(Selectors)
Java NIO的選擇器容許一個單獨的線程來監視多個輸入通道,你能夠註冊多個通道使用一個選擇器,而後使用一個單獨的線程來「選擇」通道:這些通道里已經有能夠處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
NIO和IO如何影響應用程序的設計
不管您選擇IO或NIO工具箱,可能會影響您應用程序設計的如下幾個方面:
- 對NIO或IO類的API調用。
- 數據處理。
- 用來處理數據的線程數。
API調用
固然,使用NIO的API調用時看起來與使用IO時有所不一樣,但這並不意外,由於並非僅從一個InputStream逐字節讀取,而是數據必須先讀入緩衝區再處理。
數據處理
使用純粹的NIO設計相較IO設計,數據處理也受到影響。
在IO設計中,咱們從InputStream或 Reader逐字節讀取數據。假設你正在處理一基於行的文本數據流,例如:
- Name: Anna
- Age: 25
- Email: anna@mailserver.com
- Phone: 1234567890
該文本行的流能夠這樣處理:
- InputStream input = … ;
- 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()調用返回的時候,你知道這行包含年齡等。 正如你能夠看到,該處理程序僅在有新數據讀入時運行,並知道每步的數據是什麼。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據(大多如此)。下圖也說明了這條原則:
從一個阻塞的流中讀數據
而一個NIO的實現會有所不一樣,下面是一個簡單的例子:
- 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()方法被調用以前狀態相同。若是沒有,下一個讀入緩衝區的數據可能沒法讀到正確的位置。這是不可能的,但倒是須要注意的又一問題。
若是緩衝區已滿,它能夠被處理。若是它不滿,而且在你的實際案例中有意義,你或許能處理其中的部分數據。可是許多狀況下並不是如此。下圖展現了「緩衝區數據循環就緒」:
從一個通道里讀數據,直到全部的數據都讀到緩衝區裏
總結
NIO可以讓您只使用一個(或幾個)單線程管理多個通道(網絡鏈接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
若是須要管理同時打開的成千上萬個鏈接,這些鏈接每次只是發送少許的數據,例如聊天服務器,實現NIO的服務器多是一個優點。一樣,若是你須要維持許多打開的鏈接到其餘計算機上,如P2P網絡中,使用一個單獨的線程來管理你全部出站鏈接,多是一個優點。一個線程多個鏈接的設計方案以下圖所示:
單線程管理多個鏈接
若是你有少許的鏈接使用很是高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能很是契合。下圖說明了一個典型的IO服務器設計:
一個典型的IO服務器設計:一個鏈接經過一個線程處理
通道(Channel)
(本部分
原文連接,做者:Jakob Jenkov,譯者:airu,校對:丁一)
Java NIO的通道相似流,但又有些不一樣:
- 既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向的。
- 通道能夠異步地讀寫。
- 通道中的數據老是要先讀到一個Buffer,或者老是要從一個Buffer中寫入。
正如上面所說,從通道讀取數據到緩衝區,從緩衝區寫入數據到通道。以下圖所示:
Channel的實現
這些是Java NIO中最重要的通道的實現:
- FileChannel:從文件中讀寫數據。
- DatagramChannel:能經過UDP讀寫網絡中的數據。
- SocketChannel:能經過TCP讀寫網絡中的數據。
- ServerSocketChannel:能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。
基本的 Channel 示例
下面是一個使用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中讀取數據。下一節會深刻講解Buffer的更多細節。
緩衝區(Buffer)
(本部分
原文連接,做者:Jakob Jenkov,譯者:airu,校對:丁一)
Java NIO中的Buffer用於和NIO通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。
緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。
Buffer的基本用法
使用Buffer讀寫數據通常遵循如下四個步驟:
- 寫入數據到Buffer
- 調用flip()方法
- 從Buffer中讀取數據
- 調用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();
-
-
- ByteBuffer buf = ByteBuffer.allocate(48);
-
- int bytesRead = inChannel.read(buf);
- while (bytesRead != -1) {
-
- buf.flip();
-
- while(buf.hasRemaining()){
- System.out.print((char) buf.get());
- }
-
- buf.clear();
- bytesRead = inChannel.read(buf);
- }
- aFile.close();
Buffer的capacity,position和limit
緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。
爲了理解Buffer的工做原理,須要熟悉它的三個屬性:
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
如你所見,這些Buffer類型表明了不一樣的數據類型。換句話說,就是能夠經過char,short,int,long,float 或 double類型來操做緩衝區中的字節。
MappedByteBuffer 有些特別,在涉及它的專門章節中再講。
Buffer的分配
要想得到一個Buffer對象首先要進行分配。 每個Buffer類都有一個allocate方法。下面是一個分配48字節capacity的ByteBuffer的例子。
- ByteBuffer buf = ByteBuffer.allocate(48);
這是分配一個可存儲1024個字符的CharBuffer:
- CharBuffer buf = CharBuffer.allocate(1024);
向Buffer中寫數據
寫數據到Buffer有兩種方式:
- 從Channel寫到Buffer。
- 經過Buffer的put()方法寫到Buffer裏。
從Channel寫到Buffer的例子
- int bytesRead = inChannel.read(buf);
經過put方法寫Buffer的例子:
put方法有不少版本,容許你以不一樣的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc。
flip()方法
flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成以前position的值。
換句話說,position如今用於標記讀的位置,limit表示以前寫進了多少個byte、char等 —— 如今能讀取多少個byte、char等。
從Buffer中讀取數據
從Buffer中讀取數據有兩種方式:
- 從Buffer讀取數據到Channel。
- 使用get()方法從Buffer中讀取數據。
從Buffer讀取數據到Channel的例子:
-
- int bytesWritten = inChannel.write(buf);
使用get()方法從Buffer中讀取數據的例子
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。例如:
- buffer.mark();
-
-
-
- buffer.reset();
equals()與compareTo()方法
可使用equals()和compareTo()方法兩個Buffer。
equals()
當知足下列條件時,表示兩個Buffer相等:
- 有相同的類型(byte、char、int等)。
- Buffer中剩餘的byte、char等的個數相等。
- Buffer中全部剩餘的byte、char等都相同。
如你所見,equals只是比較Buffer的一部分,不是每個在它裏面的元素都比較。實際上,它只比較Buffer中的剩餘元素。
compareTo()方法
compareTo()方法比較兩個Buffer的剩餘元素(byte、char等), 若是知足下列條件,則認爲一個Buffer「小於」另外一個Buffer:
- 第一個不相等的元素小於另外一個Buffer中對應的元素。
- 全部元素都相等,但第一個Buffer比另外一個先耗盡(第一個Buffer的元素個數比另外一個少)。
(譯註:剩餘元素是從 position到limit之間的元素)
分散(Scatter)/彙集(Gather)
(本部分
原文地址,做者: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中。以下圖描述:
代碼示例以下:
- 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
Gathering Writes是指數據從多個buffer寫入到同一個channel。以下圖描述:
代碼示例以下:
- ByteBuffer header = ByteBuffer.allocate(128);
- ByteBuffer body = ByteBuffer.allocate(1024);
-
-
-
- ByteBuffer[] bufferArray = { header, body };
-
- channel.write(bufferArray);
buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據纔會被寫入。所以,若是一個buffer的容量爲128byte,可是僅僅包含58byte的數據,那麼這58byte的數據將被寫入到channel中。所以與Scattering Reads相反,Gathering Writes能較好的處理動態消息。
通道之間的數據傳輸
(本部分
原文地址,做者:Jakob Jenkov,譯者:郭蕾,校對:周泰)
在Java NIO中,若是兩個通道中有一個是FileChannel,那你能夠直接將數據從一個channel(譯者注:channel中文常譯做通道)傳輸到另一個channel。
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中。
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);
是否是發現這個例子和前面那個例子特別類似?除了調用方法的FileChannel對象不同外,其餘的都同樣。
上面所說的關於SocketChannel的問題在transferTo()方法中一樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿。
選擇器(Selector)
(本部分
原文連接,做者:Jakob Jenkov,譯者:浪跡v,校對:丁一)
Selector(選擇器)是Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。
(1) 爲何使用Selector?
僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。
可是,須要記住,現代的操做系統和CPU在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個CPU有多個內核,不使用多任務多是在浪費CPU能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用Selector可以處理多個通道就足夠了。
下面是單線程使用一個Selector處理3個channel的示例圖:
(2) Selector的建立
經過調用Selector.open()方法建立一個Selector,以下:
- Selector selector = Selector.open();
(3) 向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集合」,意思是在經過Selector監聽Channel時對什麼事件感興趣。能夠監聽四種不一樣類型的事件:
- Connect
- Accept
- Read
- Write
通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。一個有數據可讀的通道能夠說是「讀就緒」。等待寫數據的通道能夠說是「寫就緒」。
這四種事件用SelectionKey的四個常量來表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來,以下:
- int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
在下面還會繼續提到interest集合。
(4) SelectionKey
在上一小節中,當向Selector註冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:
- interest集合
- ready集合
- Channel
- Selector
- 附加的對象(可選)
下面我會描述這些屬性。
interest集合
就像向Selector註冊通道一節中所描述的,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();
- 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);
(5) 經過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 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()) {
-
- } else if (key.isConnectable()) {
-
- } else if (key.isReadable()) {
-
- } else if (key.isWritable()) {
-
- }
- keyIterator.<tuihighlight class="tuihighlight"><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;" onclick="return false;">remove</a></tuihighlight>();
- }
這個循環遍歷已選擇鍵集中的每一個鍵,並檢測各個鍵所對應的通道的就緒事件。
注意每次迭代末尾的keyIterator.remove()調用。Selector不會本身從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
SelectionKey.channel()方法返回的通道須要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。
(6) wakeUp()
某個線程調用select()方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法便可。阻塞在select()方法上的線程會立馬返回。
若是有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會當即「醒來(wake up)」。
(7) close()
用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的全部SelectionKey實例無效。通道自己並不會關閉。
(8) 完整的示例
這裏有一個完整的示例,打開一個Selector,註冊一個通道註冊到這個Selector上(通道的初始化過程略去),而後持續監控這個Selector的四種事件(接受,鏈接,讀,寫)是否就緒。
- Selector selector = Selector.open();
- 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();
- Iterator keyIterator = selectedKeys.iterator();
- while(keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if(key.isAcceptable()) {
-
- } else if (key.isConnectable()) {
-
- } else if (key.isReadable()) {
-
- } else if (key.isWritable()) {
-
- }
- keyIterator.<tuihighlight class="tuihighlight"><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;" onclick="return false;">remove</a></tuihighlight>();
- }
- }
文件通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:周泰,校對:丁一)
Java NIO中的FileChannel是一個鏈接到文件的通道。能夠經過文件通道讀寫文件。
FileChannel沒法設置爲非阻塞模式,它老是運行在阻塞模式下。
打開FileChannel
在使用FileChannel以前,必須先打開它。可是,咱們沒法直接打開一個FileChannel,須要經過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是經過RandomAccessFile打開FileChannel的示例:
- RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
- FileChannel inChannel = aFile.getChannel();
從FileChannel讀取數據
調用多個read()方法之一從FileChannel中讀取數據。如:
- ByteBuffer buf = ByteBuffer.allocate(48);
- int bytesRead = inChannel.read(buf);
首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。
而後,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。若是返回-1,表示到了文件末尾。
向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中已經沒有還沒有寫入通道的字節。
關閉FileChannel
用完FileChannel後必須將其關閉。如:
FileChannel的position方法
有時可能須要在FileChannel的某個特定位置進行數據的讀/寫操做。能夠經過調用position()方法獲取FileChannel的當前位置。
也能夠經過調用position(long pos)方法設置FileChannel的當前位置。
這裏有兩個例子:
- long pos = channel.position();
- channel.position(pos +123);
若是將位置設置在文件結束符以後,而後試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標誌。
若是將位置設置在文件結束符以後,而後向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能致使「文件空洞」,磁盤上物理文件中寫入的數據間有空隙。
FileChannel的size方法
FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:
- long fileSize = channel.size();
FileChannel的truncate方法
可使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度後面的部分將被刪除。如:
這個例子截取文件的前1024個字節。
FileChannel的force方法
FileChannel.force()方法將通道里還沒有寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操做系統會將數據緩存在內存中,因此沒法保證寫入到FileChannel裏的數據必定會即時寫到磁盤上。要保證這一點,須要調用force()方法。
force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。
下面的例子同時將文件數據和元數據強制寫到磁盤上:
Socket 通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的SocketChannel是一個鏈接到TCP網絡套接字的通道。能夠經過如下2種方式建立SocketChannel:
- 打開一個SocketChannel並鏈接到互聯網上的某臺服務器。
- 一個新鏈接到達ServerSocketChannel時,會建立一個SocketChannel。
打開 SocketChannel
下面是SocketChannel的打開方式:
- SocketChannel socketChannel = SocketChannel.open();
- socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
關閉 SocketChannel
當用完SocketChannel以後調用SocketChannel.close()關閉SocketChannel:
從 SocketChannel 讀取數據
要從SocketChannel中讀取數據,調用一個read()的方法之一。如下是例子:
- ByteBuffer buf = ByteBuffer.allocate(48);
- int bytesRead = socketChannel.read(buf);
首先,分配一個Buffer。從SocketChannel讀取到的數據將會放到這個Buffer中。
而後,調用SocketChannel.read()。該方法將數據從SocketChannel 讀到Buffer中。read()方法返回的int值表示讀了多少字節進Buffer裏。若是返回的是-1,表示已經讀到了流的末尾(鏈接關閉了)。
寫入 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沒有要寫的字節爲止。
非阻塞模式
能夠設置 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() ){
-
- }
write()
非阻塞模式下,write()方法在還沒有寫出任何內容時可能就返回了。因此須要在循環中調用write()。前面已經有例子了,這裏就不贅述了。
read()
非阻塞模式下,read()方法在還沒有讀取到任何數據時可能就返回了。因此須要關注它的int返回值,它會告訴你讀取了多少字節。
非阻塞模式與選擇器
非阻塞模式與選擇器搭配會工做的更好,經過將一或多個SocketChannel註冊到Selector,能夠詢問選擇器哪一個通道已經準備好了讀取,寫入等。Selector與SocketChannel的搭配使用會在後面詳講。
ServerSocket 通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的 ServerSocketChannel 是一個能夠監聽新進來的TCP鏈接的通道,就像標準IO中的ServerSocket同樣。ServerSocketChannel類在 java.nio.channels包中。
這裏有個例子:
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
-
- serverSocketChannel.socket().bind(new InetSocketAddress(9999));
-
- while(true){
- SocketChannel socketChannel =
- serverSocketChannel.accept();
-
-
- }
打開 ServerSocketChannel
經過調用 ServerSocketChannel.open() 方法來打開ServerSocketChannel.如:
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
關閉 ServerSocketChannel
經過調用ServerSocketChannel.close() 方法來關閉ServerSocketChannel. 如:
- serverSocketChannel.close();
監聽新進來的鏈接
經過 ServerSocketChannel.accept() 方法監聽新進來的鏈接。當 accept()方法返回的時候,它返回一個包含新進來的鏈接的 SocketChannel。所以,accept()方法會一直阻塞到有新鏈接到達。
一般不會僅僅只監聽一個鏈接,在while循環中調用 accept()方法. 以下面的例子:
- while(true){
- SocketChannel socketChannel =
- serverSocketChannel.accept();
-
-
- }
固然,也能夠在while循環中使用除了true之外的其它退出準則。
非阻塞模式
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){
-
- }
- }
Datagram 通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的DatagramChannel是一個能收發UDP包的通道。由於UDP是無鏈接的網絡協議,因此不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。
打開 DatagramChannel
下面是 DatagramChannel 的打開方式:
- DatagramChannel channel = DatagramChannel.open();
- channel.socket().bind(new InetSocketAddress(9999));
這個例子打開的 DatagramChannel能夠在UDP端口9999上接收數據包。
接收數據
經過receive()方法從DatagramChannel接收數據,如:
- ByteBuffer buf = ByteBuffer.allocate(48);
- buf.clear();
- channel.receive(buf);
receive()方法會將接收到的數據包內容複製到指定的Buffer. 若是Buffer容不下收到的數據,多出的數據將被丟棄。
發送數據
經過send()方法從DatagramChannel發送數據,如:
- 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在數據傳送方面沒有任何保證。
鏈接到特定的地址
能夠將DatagramChannel「鏈接」到網絡中的特定地址的。因爲UDP是無鏈接的,鏈接到特定地址並不會像TCP通道那樣建立一個真正的鏈接。而是鎖住DatagramChannel ,讓其只能從特定地址收發數據。
這裏有個例子:
- channel.connect(new InetSocketAddress("jenkov.com", 80));
當鏈接後,也可使用read()和write()方法,就像在用傳統的通道同樣。只是在數據傳送方面沒有任何保證。這裏有幾個例子:
- int bytesRead = channel.read(buf);
- int bytesWritten = channel.write(but);
管道(Pipe)
(本部分
原文連接,做者:Jakob Jenkov,譯者:黃忠,校對:丁一)
Java NIO 管道是2個線程之間的單向數據鏈接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。
這裏是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()) {
- <b>sinkChannel.write(buf);</b>
- }
從管道讀取數據
從讀取管道的數據,須要訪問source通道,像這樣:
- Pipe.SourceChannel sourceChannel = pipe.source();
調用source通道的read()方法來讀取數據,像這樣:
- ByteBuffer buf = ByteBuffer.allocate(48);
-
- int bytesRead = inChannel.read(buf);
read()方法返回的int值會告訴咱們多少字節被讀進了緩衝區。
Java NIO 概述
(本部分
原文連接,做者:Jakob Jenkov, 譯者:airu,校對:丁一)
Java NIO 由如下幾個核心部分組成:
- 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實現:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer覆蓋了你能經過IO發送的基本數據類型:byte, short, int, long, float, double 和 char。
Java NIO 還有個 Mappedyteuffer,用於表示內存映射文件, 我也不打算在概述中說明。
Selector
Selector容許單線程處理多個 Channel。若是你的應用打開了多個鏈接(通道),但每一個鏈接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。
這是在一個單線程中使用一個Selector處理3個Channel的圖示:
要使用Selector,得向Selector註冊Channel,而後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就能夠處理這些事件,事件的例子有如新鏈接進來,數據接收等。
Java NIO vs. IO
(本部分
原文地址,做者:Jakob Jenkov,譯者:郭蕾,校對:方騰飛)
當學習了Java NIO和IO的API後,一個問題立刻涌入腦海:
引用
我應該什麼時候使用IO,什麼時候使用NIO呢?在本文中,我會盡可能清晰地解析Java NIO和IO的差別、它們的使用場景,以及它們如何影響您的代碼設計。
Java NIO和IO的主要區別
下表總結了Java NIO和IO之間的主要差異,我會更詳細地描述表中每部分的差別。
IO |
NIO |
Stream oriented |
Buffer oriented |
Blocking IO |
Non blocking IO |
|
Selectors |
面向流與面向緩衝
Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。 Java NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據。
阻塞與非阻塞IO
Java IO的各類流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。 Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,因此直至數據變的能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。 線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。
選擇器(Selectors)
Java NIO的選擇器容許一個單獨的線程來監視多個輸入通道,你能夠註冊多個通道使用一個選擇器,而後使用一個單獨的線程來「選擇」通道:這些通道里已經有能夠處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。
NIO和IO如何影響應用程序的設計
不管您選擇IO或NIO工具箱,可能會影響您應用程序設計的如下幾個方面:
- 對NIO或IO類的API調用。
- 數據處理。
- 用來處理數據的線程數。
API調用
固然,使用NIO的API調用時看起來與使用IO時有所不一樣,但這並不意外,由於並非僅從一個InputStream逐字節讀取,而是數據必須先讀入緩衝區再處理。
數據處理
使用純粹的NIO設計相較IO設計,數據處理也受到影響。
在IO設計中,咱們從InputStream或 Reader逐字節讀取數據。假設你正在處理一基於行的文本數據流,例如:
- Name: Anna
- Age: 25
- Email: anna@mailserver.com
- Phone: 1234567890
該文本行的流能夠這樣處理:
- InputStream input = … ;
- 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()調用返回的時候,你知道這行包含年齡等。 正如你能夠看到,該處理程序僅在有新數據讀入時運行,並知道每步的數據是什麼。一旦正在運行的線程已處理過讀入的某些數據,該線程不會再回退數據(大多如此)。下圖也說明了這條原則:
從一個阻塞的流中讀數據
而一個NIO的實現會有所不一樣,下面是一個簡單的例子:
- 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()方法被調用以前狀態相同。若是沒有,下一個讀入緩衝區的數據可能沒法讀到正確的位置。這是不可能的,但倒是須要注意的又一問題。
若是緩衝區已滿,它能夠被處理。若是它不滿,而且在你的實際案例中有意義,你或許能處理其中的部分數據。可是許多狀況下並不是如此。下圖展現了「緩衝區數據循環就緒」:
從一個通道里讀數據,直到全部的數據都讀到緩衝區裏
總結
NIO可以讓您只使用一個(或幾個)單線程管理多個通道(網絡鏈接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。
若是須要管理同時打開的成千上萬個鏈接,這些鏈接每次只是發送少許的數據,例如聊天服務器,實現NIO的服務器多是一個優點。一樣,若是你須要維持許多打開的鏈接到其餘計算機上,如P2P網絡中,使用一個單獨的線程來管理你全部出站鏈接,多是一個優點。一個線程多個鏈接的設計方案以下圖所示:
單線程管理多個鏈接
若是你有少許的鏈接使用很是高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能很是契合。下圖說明了一個典型的IO服務器設計:
一個典型的IO服務器設計:一個鏈接經過一個線程處理
通道(Channel)
(本部分
原文連接,做者:Jakob Jenkov,譯者:airu,校對:丁一)
Java NIO的通道相似流,但又有些不一樣:
- 既能夠從通道中讀取數據,又能夠寫數據到通道。但流的讀寫一般是單向的。
- 通道能夠異步地讀寫。
- 通道中的數據老是要先讀到一個Buffer,或者老是要從一個Buffer中寫入。
正如上面所說,從通道讀取數據到緩衝區,從緩衝區寫入數據到通道。以下圖所示:
Channel的實現
這些是Java NIO中最重要的通道的實現:
- FileChannel:從文件中讀寫數據。
- DatagramChannel:能經過UDP讀寫網絡中的數據。
- SocketChannel:能經過TCP讀寫網絡中的數據。
- ServerSocketChannel:能夠監聽新進來的TCP鏈接,像Web服務器那樣。對每個新進來的鏈接都會建立一個SocketChannel。
基本的 Channel 示例
下面是一個使用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中讀取數據。下一節會深刻講解Buffer的更多細節。
緩衝區(Buffer)
(本部分
原文連接,做者:Jakob Jenkov,譯者:airu,校對:丁一)
Java NIO中的Buffer用於和NIO通道進行交互。如你所知,數據是從通道讀入緩衝區,從緩衝區寫入到通道中的。
緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。
Buffer的基本用法
使用Buffer讀寫數據通常遵循如下四個步驟:
- 寫入數據到Buffer
- 調用flip()方法
- 從Buffer中讀取數據
- 調用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();
-
-
- ByteBuffer buf = ByteBuffer.allocate(48);
-
- int bytesRead = inChannel.read(buf);
- while (bytesRead != -1) {
-
- buf.flip();
-
- while(buf.hasRemaining()){
- System.out.print((char) buf.get());
- }
-
- buf.clear();
- bytesRead = inChannel.read(buf);
- }
- aFile.close();
Buffer的capacity,position和limit
緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的內存。這塊內存被包裝成NIO Buffer對象,並提供了一組方法,用來方便的訪問該塊內存。
爲了理解Buffer的工做原理,須要熟悉它的三個屬性:
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
如你所見,這些Buffer類型表明了不一樣的數據類型。換句話說,就是能夠經過char,short,int,long,float 或 double類型來操做緩衝區中的字節。
MappedByteBuffer 有些特別,在涉及它的專門章節中再講。
Buffer的分配
要想得到一個Buffer對象首先要進行分配。 每個Buffer類都有一個allocate方法。下面是一個分配48字節capacity的ByteBuffer的例子。
- ByteBuffer buf = ByteBuffer.allocate(48);
這是分配一個可存儲1024個字符的CharBuffer:
- CharBuffer buf = CharBuffer.allocate(1024);
向Buffer中寫數據
寫數據到Buffer有兩種方式:
- 從Channel寫到Buffer。
- 經過Buffer的put()方法寫到Buffer裏。
從Channel寫到Buffer的例子
- int bytesRead = inChannel.read(buf);
經過put方法寫Buffer的例子:
put方法有不少版本,容許你以不一樣的方式把數據寫入到Buffer中。例如, 寫到一個指定的位置,或者把一個字節數組寫入到Buffer。 更多Buffer實現的細節參考JavaDoc。
flip()方法
flip方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成以前position的值。
換句話說,position如今用於標記讀的位置,limit表示以前寫進了多少個byte、char等 —— 如今能讀取多少個byte、char等。
從Buffer中讀取數據
從Buffer中讀取數據有兩種方式:
- 從Buffer讀取數據到Channel。
- 使用get()方法從Buffer中讀取數據。
從Buffer讀取數據到Channel的例子:
-
- int bytesWritten = inChannel.write(buf);
使用get()方法從Buffer中讀取數據的例子
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。例如:
- buffer.mark();
-
-
-
- buffer.reset();
equals()與compareTo()方法
可使用equals()和compareTo()方法兩個Buffer。
equals()
當知足下列條件時,表示兩個Buffer相等:
- 有相同的類型(byte、char、int等)。
- Buffer中剩餘的byte、char等的個數相等。
- Buffer中全部剩餘的byte、char等都相同。
如你所見,equals只是比較Buffer的一部分,不是每個在它裏面的元素都比較。實際上,它只比較Buffer中的剩餘元素。
compareTo()方法
compareTo()方法比較兩個Buffer的剩餘元素(byte、char等), 若是知足下列條件,則認爲一個Buffer「小於」另外一個Buffer:
- 第一個不相等的元素小於另外一個Buffer中對應的元素。
- 全部元素都相等,但第一個Buffer比另外一個先耗盡(第一個Buffer的元素個數比另外一個少)。
(譯註:剩餘元素是從 position到limit之間的元素)
分散(Scatter)/彙集(Gather)
(本部分
原文地址,做者: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中。以下圖描述:
代碼示例以下:
- 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
Gathering Writes是指數據從多個buffer寫入到同一個channel。以下圖描述:
代碼示例以下:
- ByteBuffer header = ByteBuffer.allocate(128);
- ByteBuffer body = ByteBuffer.allocate(1024);
-
-
-
- ByteBuffer[] bufferArray = { header, body };
-
- channel.write(bufferArray);
buffers數組是write()方法的入參,write()方法會按照buffer在數組中的順序,將數據寫入到channel,注意只有position和limit之間的數據纔會被寫入。所以,若是一個buffer的容量爲128byte,可是僅僅包含58byte的數據,那麼這58byte的數據將被寫入到channel中。所以與Scattering Reads相反,Gathering Writes能較好的處理動態消息。
通道之間的數據傳輸
(本部分
原文地址,做者:Jakob Jenkov,譯者:郭蕾,校對:周泰)
在Java NIO中,若是兩個通道中有一個是FileChannel,那你能夠直接將數據從一個channel(譯者注:channel中文常譯做通道)傳輸到另一個channel。
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中。
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);
是否是發現這個例子和前面那個例子特別類似?除了調用方法的FileChannel對象不同外,其餘的都同樣。
上面所說的關於SocketChannel的問題在transferTo()方法中一樣存在。SocketChannel會一直傳輸數據直到目標buffer被填滿。
選擇器(Selector)
(本部分
原文連接,做者:Jakob Jenkov,譯者:浪跡v,校對:丁一)
Selector(選擇器)是Java NIO中可以檢測一到多個NIO通道,並可以知曉通道是否爲諸如讀寫事件作好準備的組件。這樣,一個單獨的線程能夠管理多個channel,從而管理多個網絡鏈接。
(1) 爲何使用Selector?
僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道。對於操做系統來講,線程之間上下文切換的開銷很大,並且每一個線程都要佔用系統的一些資源(如內存)。所以,使用的線程越少越好。
可是,須要記住,現代的操做系統和CPU在多任務方面表現的愈來愈好,因此多線程的開銷隨着時間的推移,變得愈來愈小了。實際上,若是一個CPU有多個內核,不使用多任務多是在浪費CPU能力。無論怎麼說,關於那種設計的討論應該放在另外一篇不一樣的文章中。在這裏,只要知道使用Selector可以處理多個通道就足夠了。
下面是單線程使用一個Selector處理3個channel的示例圖:
(2) Selector的建立
經過調用Selector.open()方法建立一個Selector,以下:
- Selector selector = Selector.open();
(3) 向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集合」,意思是在經過Selector監聽Channel時對什麼事件感興趣。能夠監聽四種不一樣類型的事件:
- Connect
- Accept
- Read
- Write
通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。一個有數據可讀的通道能夠說是「讀就緒」。等待寫數據的通道能夠說是「寫就緒」。
這四種事件用SelectionKey的四個常量來表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
若是你對不止一種事件感興趣,那麼能夠用「位或」操做符將常量鏈接起來,以下:
- int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
在下面還會繼續提到interest集合。
(4) SelectionKey
在上一小節中,當向Selector註冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:
- interest集合
- ready集合
- Channel
- Selector
- 附加的對象(可選)
下面我會描述這些屬性。
interest集合
就像向Selector註冊通道一節中所描述的,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();
- 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);
(5) 經過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 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()) {
-
- } else if (key.isConnectable()) {
-
- } else if (key.isReadable()) {
-
- } else if (key.isWritable()) {
-
- }
- keyIterator.<tuihighlight class="tuihighlight"><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;" onclick="return false;">remove</a></tuihighlight>();
- }
這個循環遍歷已選擇鍵集中的每一個鍵,並檢測各個鍵所對應的通道的就緒事件。
注意每次迭代末尾的keyIterator.remove()調用。Selector不會本身從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
SelectionKey.channel()方法返回的通道須要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。
(6) wakeUp()
某個線程調用select()方法後阻塞了,即便沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法便可。阻塞在select()方法上的線程會立馬返回。
若是有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會當即「醒來(wake up)」。
(7) close()
用完Selector後調用其close()方法會關閉該Selector,且使註冊到該Selector上的全部SelectionKey實例無效。通道自己並不會關閉。
(8) 完整的示例
這裏有一個完整的示例,打開一個Selector,註冊一個通道註冊到這個Selector上(通道的初始化過程略去),而後持續監控這個Selector的四種事件(接受,鏈接,讀,寫)是否就緒。
- Selector selector = Selector.open();
- 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();
- Iterator keyIterator = selectedKeys.iterator();
- while(keyIterator.hasNext()) {
- SelectionKey key = keyIterator.next();
- if(key.isAcceptable()) {
-
- } else if (key.isConnectable()) {
-
- } else if (key.isReadable()) {
-
- } else if (key.isWritable()) {
-
- }
- keyIterator.<tuihighlight class="tuihighlight"><a href="javascript:;" style="display:inline;float:none;position:inherit;cursor:pointer;color:#7962D5;text-decoration:underline;" onclick="return false;">remove</a></tuihighlight>();
- }
- }
文件通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:周泰,校對:丁一)
Java NIO中的FileChannel是一個鏈接到文件的通道。能夠經過文件通道讀寫文件。
FileChannel沒法設置爲非阻塞模式,它老是運行在阻塞模式下。
打開FileChannel
在使用FileChannel以前,必須先打開它。可是,咱們沒法直接打開一個FileChannel,須要經過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例。下面是經過RandomAccessFile打開FileChannel的示例:
- RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
- FileChannel inChannel = aFile.getChannel();
從FileChannel讀取數據
調用多個read()方法之一從FileChannel中讀取數據。如:
- ByteBuffer buf = ByteBuffer.allocate(48);
- int bytesRead = inChannel.read(buf);
首先,分配一個Buffer。從FileChannel中讀取的數據將被讀到Buffer中。
而後,調用FileChannel.read()方法。該方法將數據從FileChannel讀取到Buffer中。read()方法返回的int值表示了有多少字節被讀到了Buffer中。若是返回-1,表示到了文件末尾。
向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中已經沒有還沒有寫入通道的字節。
關閉FileChannel
用完FileChannel後必須將其關閉。如:
FileChannel的position方法
有時可能須要在FileChannel的某個特定位置進行數據的讀/寫操做。能夠經過調用position()方法獲取FileChannel的當前位置。
也能夠經過調用position(long pos)方法設置FileChannel的當前位置。
這裏有兩個例子:
- long pos = channel.position();
- channel.position(pos +123);
若是將位置設置在文件結束符以後,而後試圖從文件通道中讀取數據,讀方法將返回-1 —— 文件結束標誌。
若是將位置設置在文件結束符以後,而後向通道中寫數據,文件將撐大到當前位置並寫入數據。這可能致使「文件空洞」,磁盤上物理文件中寫入的數據間有空隙。
FileChannel的size方法
FileChannel實例的size()方法將返回該實例所關聯文件的大小。如:
- long fileSize = channel.size();
FileChannel的truncate方法
可使用FileChannel.truncate()方法截取一個文件。截取文件時,文件將中指定長度後面的部分將被刪除。如:
這個例子截取文件的前1024個字節。
FileChannel的force方法
FileChannel.force()方法將通道里還沒有寫入磁盤的數據強制寫到磁盤上。出於性能方面的考慮,操做系統會將數據緩存在內存中,因此沒法保證寫入到FileChannel裏的數據必定會即時寫到磁盤上。要保證這一點,須要調用force()方法。
force()方法有一個boolean類型的參數,指明是否同時將文件元數據(權限信息等)寫到磁盤上。
下面的例子同時將文件數據和元數據強制寫到磁盤上:
Socket 通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的SocketChannel是一個鏈接到TCP網絡套接字的通道。能夠經過如下2種方式建立SocketChannel:
- 打開一個SocketChannel並鏈接到互聯網上的某臺服務器。
- 一個新鏈接到達ServerSocketChannel時,會建立一個SocketChannel。
打開 SocketChannel
下面是SocketChannel的打開方式:
- SocketChannel socketChannel = SocketChannel.open();
- socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
關閉 SocketChannel
當用完SocketChannel以後調用SocketChannel.close()關閉SocketChannel:
從 SocketChannel 讀取數據
要從SocketChannel中讀取數據,調用一個read()的方法之一。如下是例子:
- ByteBuffer buf = ByteBuffer.allocate(48);
- int bytesRead = socketChannel.read(buf);
首先,分配一個Buffer。從SocketChannel讀取到的數據將會放到這個Buffer中。
而後,調用SocketChannel.read()。該方法將數據從SocketChannel 讀到Buffer中。read()方法返回的int值表示讀了多少字節進Buffer裏。若是返回的是-1,表示已經讀到了流的末尾(鏈接關閉了)。
寫入 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沒有要寫的字節爲止。
非阻塞模式
能夠設置 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() ){
-
- }
write()
非阻塞模式下,write()方法在還沒有寫出任何內容時可能就返回了。因此須要在循環中調用write()。前面已經有例子了,這裏就不贅述了。
read()
非阻塞模式下,read()方法在還沒有讀取到任何數據時可能就返回了。因此須要關注它的int返回值,它會告訴你讀取了多少字節。
非阻塞模式與選擇器
非阻塞模式與選擇器搭配會工做的更好,經過將一或多個SocketChannel註冊到Selector,能夠詢問選擇器哪一個通道已經準備好了讀取,寫入等。Selector與SocketChannel的搭配使用會在後面詳講。
ServerSocket 通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的 ServerSocketChannel 是一個能夠監聽新進來的TCP鏈接的通道,就像標準IO中的ServerSocket同樣。ServerSocketChannel類在 java.nio.channels包中。
這裏有個例子:
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
-
- serverSocketChannel.socket().bind(new InetSocketAddress(9999));
-
- while(true){
- SocketChannel socketChannel =
- serverSocketChannel.accept();
-
-
- }
打開 ServerSocketChannel
經過調用 ServerSocketChannel.open() 方法來打開ServerSocketChannel.如:
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
關閉 ServerSocketChannel
經過調用ServerSocketChannel.close() 方法來關閉ServerSocketChannel. 如:
- serverSocketChannel.close();
監聽新進來的鏈接
經過 ServerSocketChannel.accept() 方法監聽新進來的鏈接。當 accept()方法返回的時候,它返回一個包含新進來的鏈接的 SocketChannel。所以,accept()方法會一直阻塞到有新鏈接到達。
一般不會僅僅只監聽一個鏈接,在while循環中調用 accept()方法. 以下面的例子:
- while(true){
- SocketChannel socketChannel =
- serverSocketChannel.accept();
-
-
- }
固然,也能夠在while循環中使用除了true之外的其它退出準則。
非阻塞模式
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){
-
- }
- }
Datagram 通道
(本部分
原文連接,做者:Jakob Jenkov,譯者:鄭玉婷,校對:丁一)
Java NIO中的DatagramChannel是一個能收發UDP包的通道。由於UDP是無鏈接的網絡協議,因此不能像其它通道那樣讀取和寫入。它發送和接收的是數據包。
打開 DatagramChannel
下面是 DatagramChannel 的打開方式:
- DatagramChannel channel = DatagramChannel.open();
- channel.socket().bind(new InetSocketAddress(9999));
這個例子打開的 DatagramChannel能夠在UDP端口9999上接收數據包。
接收數據
經過receive()方法從DatagramChannel接收數據,如:
- ByteBuffer buf = ByteBuffer.allocate(48);
- buf.clear();
- channel.receive(buf);
receive()方法會將接收到的數據包內容複製到指定的Buffer. 若是Buffer容不下收到的數據,多出的數據將被丟棄。
發送數據
經過send()方法從DatagramChannel發送數據,如:
- 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在數據傳送方面沒有任何保證。
鏈接到特定的地址
能夠將DatagramChannel「鏈接」到網絡中的特定地址的。因爲UDP是無鏈接的,鏈接到特定地址並不會像TCP通道那樣建立一個真正的鏈接。而是鎖住DatagramChannel ,讓其只能從特定地址收發數據。
這裏有個例子:
- channel.connect(new InetSocketAddress("jenkov.com", 80));
當鏈接後,也可使用read()和write()方法,就像在用傳統的通道同樣。只是在數據傳送方面沒有任何保證。這裏有幾個例子:
- int bytesRead = channel.read(buf);
- int bytesWritten = channel.write(but);
管道(Pipe)
(本部分
原文連接,做者:Jakob Jenkov,譯者:黃忠,校對:丁一)
Java NIO 管道是2個線程之間的單向數據鏈接。Pipe有一個source通道和一個sink通道。數據會被寫到sink通道,從source通道讀取。
這裏是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()) {
- <b>sinkChannel.write(buf);</b>
- }
從管道讀取數據
從讀取管道的數據,須要訪問source通道,像這樣:
- Pipe.SourceChannel sourceChannel = pipe.source();
調用source通道的read()方法來讀取數據,像這樣:
- ByteBuffer buf = ByteBuffer.allocate(48);
-
- int bytesRead = inChannel.read(buf);
read()方法返回的int值會告訴咱們多少字節被讀進了緩衝區。