一、概述
從JDK1.4開始,Java提供了一系列改進的輸入/輸出處理的新特性,被統稱為NIO(即New I/O)。新增了許多用於處理輸入輸出的類,這些類都被放在java.nio包及子包下,並且對原java.io包中的很多類進行改寫,新增了滿足NIO的功能。NIO采用內存映射文件的方式來處理輸入輸出,NIO將文件或文件的一段區域映射到內存中,這樣就可以像訪問內存一樣訪問文件了。
NIO 與原來的 I/O 有同樣的作用和目的,但是它使用不同的方式? 塊I/O。塊 I/O 的效率可以比流 I/O 高許多。
流與塊的比較
原來的 I/O 庫(在 java.io.*中) 與 NIO 最重要的區別是數據打包和傳輸的方式。正如前面提到的,原來的 I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。
面向流 的 I/O 系統一次一個字節地處理數據。一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據。為流式數據創建過濾器非常容易。鏈接幾個過濾器,以便每個過濾器只負責單個復雜處理機制的一部分,這樣也是相對簡單的。不利的一面是,面向流的 I/O 通常相當慢。
一個 面向塊 的 I/O 系統以塊的形式處理數據。每一個操作都在一步中產生或者消費一個數據塊。按塊處理數據比按(流式的)字節處理數據要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優雅性和簡單性。
在NIO中有幾個核心對象需要掌握:緩沖區(Buffer)、通道(Channel)、選擇器(Selector)。
二、緩沖區Buffer
緩沖區實際上是一個容器對象,更直接的說,其實就是一個數組,在NIO庫中,所有數據都是用緩沖區處理的。在讀取數據時,它是直接讀到緩沖區中的; 在寫入數據時,它也是寫入到緩沖區中的;任何時候訪問 NIO 中的數據,都是將它放到緩沖區中。而在面向流I/O系統中,所有數據都是直接寫入或者直接將數據讀取到Stream對象中。具體看下面這張圖就理解了:
上面的圖描述了從一個客戶端向服務端發送數據,然后服務端接收數據的過程。客戶端發送數據時,必須先將數據存入Buffer中,然后將Buffer中的內容寫入通道。服務端這邊接收數據必須通過Channel將數據讀入到Buffer中,然后再從Buffer中取出數據來處理。
在NIO中,所有的緩沖區類型都繼承於抽象類Buffer,最常用的就是ByteBuffer,對於Java中的基本類型,基本都有一個具體Buffer類型與之相對應,它們之間的繼承關系如下圖所示:
下面是一個簡單使用IntBuffer的例子:
package com.demo.nio; import java.nio.IntBuffer; public class TestIntBuffer { public static void main(String[] args) { // 分配新的int緩沖區,參數為緩沖區容量 // 新緩沖區的當前位置將為零,其界限(限制位置)將為其容量。它將具有一個底層實現數組,其數組偏移量將為零。 IntBuffer buffer = IntBuffer.allocate(8); for (int i = 0; i < buffer.capacity(); ++i) { int j = 2 * (i + 1); // 將給定整數寫入此緩沖區的當前位置,當前位置遞增 buffer.put(j); } // 重設此緩沖區,將限制設置為當前位置,然后將當前位置設置為0 buffer.flip(); // 查看在當前位置和限制位置之間是否有元素 while (buffer.hasRemaining()) { // 讀取此緩沖區當前位置的整數,然后當前位置遞增 int j = buffer.get(); System.out.print(j + " "); } } }
運行后可以看到:
三、通道Channel
Channel和傳統IO中的Stream很相似。雖然很相似,但是有很大的區別,主要區別為:通道是雙向的,通過一個Channel既可以進行讀,也可以進行寫;而Stream只能進行單向操作,通過一個Stream只能進行讀或者寫,比如InputStream只能進行讀取操作,OutputStream只能進行寫操作;
通道是一個對象,通過它可以讀取和寫入數據,當然了所有數據都通過Buffer對象來處理。我們永遠不會將字節直接寫入通道中,相反是將數據寫入包含一個或者多個字節的緩沖區。同樣不會直接從通道中讀取字節,而是將數據從通道讀入緩沖區,再從緩沖區獲取這個字節。
在NIO中,提供了多種通道對象,而所有的通道對象都實現了Channel接口。它們之間的繼承關系如下圖所示:
Channel(通道)表示到實體如硬件設備、文件、網絡套接字或可以執行一個或多個不同I/O操作的程序組件的開放的連接。所有的Channel都不是通過構造器創建的,而是通過傳統的節點InputStream、OutputStream的getChannel方法來返回響應的Channel。
Channel中最常用的三個類方法就是map、read和write,其中map方法用於將Channel對應的部分或全部數據映射成ByteBuffer,而read或write方法有一系列的重載形式,這些方法用於從Buffer中讀取數據或向Buffer中寫入數據。
1、使用NIO讀取數據
在前面我們說過,任何時候讀取數據,都不是直接從通道讀取,而是從通道讀取到緩沖區。所以使用NIO讀取數據可以分為下面三個步驟:
(1). 從FileInputStream獲取Channel
(2). 創建Buffer
(3). 將數據從Channel讀取到Buffer中
下面是一個簡單的使用NIO從文件中讀取數據的例子:
import java.io.*; import java.nio.*; import java.nio.channels.*; public class Program { public static void main( String args[] ) throws Exception { FileInputStream fin = new FileInputStream("c:\\test.txt"); // 獲取通道 FileChannel fc = fin.getChannel(); // 創建緩沖區 ByteBuffer buffer = ByteBuffer.allocate(1024); // 讀取數據到緩沖區 fc.read(buffer); buffer.flip(); while (buffer.remaining()>0) { byte b = buffer.get(); System.out.print(((char)b)); } fin.close(); } }
2、使用NIO寫入數據
使用NIO寫入數據與讀取數據的過程類似,同樣數據不是直接寫入通道,而是寫入緩沖區,可以分為下面三個步驟:
(1). 從FileInputStream獲取Channel
(2). 創建Buffer
(3). 將數據從Channel寫入到Buffer中
下面是一個簡單的使用NIO向文件中寫入數據的例子:
package com.demo.nio; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class Program { private static final byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 }; public static void main( String args[] ) throws Exception { FileOutputStream fout = new FileOutputStream( "c:\\test.txt" ); FileChannel fc = fout.getChannel(); ByteBuffer buffer = ByteBuffer.allocate( 1024 ); for (int i=0; i<message.length; ++i) { buffer.put( message[i] ); } buffer.flip(); fc.write( buffer ); fout.close(); } }
四、選擇器Selector
Selector類是NIO的核心類,Selector能夠檢測多個注冊的通道上是否有事件發生,如果有事件發生,便獲取事件然后針對每個事件進行相應的響應處理。這樣一來,只是用一個單線程就可以管理多個通道,也就是管理多個連接。這樣使得只有在連接真正有讀寫事件發生時,才會調用函數來進行讀寫,就大大地減少了系統開銷,並且不必為每個連接都創建一個線程,不用去維護多個線程,並且避免了多線程之間的上下文切換導致的開銷。
與Selector有關的一個關鍵類是SelectionKey,一個SelectionKey表示一個到達的事件,這2個類構成了服務端處理業務的關鍵邏輯。