首先瞭解下所謂的java nio是個什麼東西。
IO是靠字元或位元組進行傳輸,比較慢。 而NIO是靠塊, 也就相當於一個Buffer, 一塊一塊
的傳輸, 速度較快
。 同時加入了多線程
的控制, 一個NIO流可以同時傳輸多個塊等, 也就是所謂的非同步傳輸
;
傳統
的並髮型
伺服器設計是利用阻塞型網路I/O
以多線程的模式(一個SOCKET連結,服務端就啟動一個線程接受服務
)來實現的,然而由
於系統常常在進行網路讀寫時處於阻塞狀態,會大大影響系統的效能
;自Java1. 4 開始引入
了NIO(新I/O) API,通過使用非阻塞型
I/O
,實現流暢的網路讀寫操作,為開發高效能並發
型伺服器程式提供了一個很好的解決方案。這就是java nio
首先來看下傳統的阻塞型網路 I/O的不足 Java 平台傳統的I/O 系統都是基於Byte(位元組)和Stream(資料流)的,相應的I/O 操作都是阻塞型的
,
所以伺服器程式也採用阻塞型I/O
進行資料的讀、寫操作。本文以TCP長串連模式來討論並髮型伺服器的相關設計,為了實現伺服器程式的並發性要求,系統由一個單獨的主線程來監聽使用者發起的
串連請求,一直處於阻塞狀態;當有使用者串連請求到來時,程式都會啟一個新的線程來統一處理使用者資料的讀、寫操作。
這種模式的優點是簡單、實用、易管理;然而缺點也是顯而易見的:由於是為每一個用戶端分配一個線程來處理輸入、輸出資料,其線程與客戶機的比例近似為1:1
,隨著線程數量的不斷增加,伺服器啟動了大量的並發線程,會大大加大系統對線程的管理開銷
,這將成為輸送量瓶頸的主要原因
;其次由於底層的I/O 操作採用的同步模式,I/O 操作的阻塞管理粒度是以服務於請求的線程為單位的,有可能大量的線程會閑置,處於盲等狀態,造成I/O資源使用率不高,影響整個系統的效能。
對於並髮型伺服器,系統用在阻塞型I/O 等待和線程間切換的時間遠遠多於
CPU 在內
存中處理資料的時間,因此傳統的阻塞型I/O 已經成為制約系統效能的瓶頸。Java1.4 版本
後推出的NIO 工具包,提供了非阻塞型I/O 的非同步輸入輸出
機制
,為提高系統的效能提供
了可實現的基礎機制。
NIO 包及工作原理
針對傳統I/O 工作模式的不足,NIO 工具包提出了基於Buffer(緩衝區)、Channel(通
道)、Selector(選取器)的新模式;Selector(選取器)、可選擇的Channel(通道)和
SelectionKey(選擇鍵)配合起來使用,可以實現並發的非阻塞型I/O 能力。
NIO 工具包的成員
Buffer(緩衝器)
1. 基本 概念
IO 是主存和外部裝置 ( 硬碟、終端和網路等 ) 拷貝資料的過程。 IO 是作業系統的底層功能實現,底層通過 I/O 指令進行完成。
所有語言運行時系統提供執行 I/O 較進階別的工具。 (c 的 printf scanf,Java 的物件導向封裝 )
2. Java 標準 io 回顧
Java 標準 IO 類庫是 io 物件導向的一種抽象。基於本地方法的底層實現,我們無須關注底層實現。 InputStream\OutputStream( 位元組流 ):一次傳送一個位元組。 Reader\Writer( 字元流 ) :一次一個字元。
3. nio 簡介
nio 是 Java New IO 的簡稱,在 jdk1.4 裡提供的新 api 。 Sun 官方標榜的特性如下:
– 為所有的原始類型提供 (Buffer) 緩衝支援。
– 字元集編碼解碼解決方案。
– Channel :一個新的原始 I/O 抽象。
– 支援鎖和記憶體對應檔的檔案提供者。
– 提供多路 (non-bloking) 非阻塞式的高伸縮性網路 I/O 。
本文將圍繞這幾個特性進行學習和介紹。
4. Buffer&Chanel
Channel 和 buffer 是 NIO 是兩個最基本的資料類型抽象。
Buffer:
– 是一塊連續的記憶體塊。
– 是 NIO 資料讀或寫的中轉地。
Channel:
– 資料的源頭或者資料的目的地
– 用於向 buffer 提供資料或者讀取 buffer 資料 ,buffer 對象的唯一介面。
– 非同步 I/O 支援
圖1:channel和buffer關係
例子 1:CopyFile.java:
Java代碼 package sample; import java.io.FileInputStream; import java.io.FileOutputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class CopyFile { public static void main(String[] args) throws Exception { String infile = "C:\\copy.sql"; String outfile = "C:\\copy.txt"; // 擷取源檔案和目標檔案的輸入輸出資料流 FileInputStream fin = new FileInputStream(infile); FileOutputStream fout = new FileOutputStream(outfile); // 擷取輸入輸出通道 FileChannel fcin = fin.getChannel(); FileChannel fcout = fout.getChannel(); // 建立緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); while (true) { // clear方法重設緩衝區,使它可以接受讀入的資料 buffer.clear(); // 從輸入通道中將資料讀到緩衝區 int r = fcin.read(buffer); // read方法返回讀取的位元組數,可能為零,如果該通道已到達流的末尾,則返回-1 if (r == -1) { break; } // flip方法讓緩衝區可以將新讀入的資料寫入另一個通道 buffer.flip(); // 從輸出通道中將資料寫入緩衝區 fcout.write(buffer); } } }
其中 buffer 內部結構如下 ( 下圖拷貝自資料 ):
圖2:buffer內部結構
一個 buffer 主要由 position,limit,capacity 三個變數來控制讀寫的過程。此三個變數的含義見如下表格:
| 參數 |
寫入模式 |
讀模式 |
| position |
當前寫入的單位元據數量。 |
當前讀取的單位元據位置。 |
| limit |
代表最多能寫多少單位元據和容量是一樣的。 |
代表最多能讀多少單位元據,和之前寫入的單位元據量一致。 |
| capacity |
buffer 容量 |
buffer 容量 |
Buffer 常見方法:
flip(): 寫入模式轉換成讀模式
rewind() :將 position 重設為 0 ,一般用於重複讀。
clear() :清空 buffer ,準備再次被寫入 (position 變成 0 , limit 變成 capacity) 。
compact(): 將未讀取的資料拷貝到 buffer 的頭部位。
mark() 、 reset():mark 可以標記一個位置, reset 可以重設到該位置。
Buffer 常見類型: ByteBuffer 、 MappedByteBuffer 、 CharBuffer 、 DoubleBuffer 、 FloatBuffer 、 IntBuffer 、 LongBuffer 、ShortBuffer 。
channel 常見類型 :FileChannel 、 DatagramChannel(UDP) 、 SocketChannel(TCP) 、 ServerSocketChannel(TCP)
在本機上面做了個簡單的效能測試。我的筆記本效能一般。 ( 具體代碼可以見附件。見 nio.sample.filecopy 包下面的例子 ) 以下是參考資料:
– 情境 1 : Copy 一個 370M 的檔案
– 情境 2: 三個線程同時拷貝,每個線程拷貝一個 370M 檔案
| 情境 |
FileInputStream+ FileOutputStream |
FileInputStream+ BufferedInputStream+ FileOutputStream |
ByteBuffer+ FileChannel |
MappedByteBuffer +FileChannel |
| 情境一時間( 毫秒) |
25155 |
17500 |
19000 |
16500 |
| 情境二時間( 毫秒 ) |
69000 |
67031 |
74031 |
71016 |
5. nio.charset
字元編碼解碼 : 位元組碼本身只是一些數字,放到正確的上下文中被正確被解析。向 ByteBuffer 中存放資料時需要考慮字元集的編碼方式,讀取展示 ByteBuffer 資料時涉及對字元集解碼。
Java.nio.charset 提供了編碼解碼一套解決方案。
以我們最常見的 http 請求為例,在請求的時候必須對請求進行正確的編碼。在得到響應時必須對響應進行正確的解碼。
以下代碼向 baidu 發一次請求,並擷取結果進行顯示。例子示範到了 charset 的使用。
例子 2BaiduReader.java Java代碼 package nio.readpage; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.net.InetSocketAddress; import java.io.IOException; public class BaiduReader { private Charset charset = Charset.forName("GBK");// 建立GBK字元集 private SocketChannel channel; public void readHTMLContent() { try { InetSocketAddress socketAddress = new InetSocketAddress( "www.baidu.com", 80); //step1:開啟串連 channel = SocketChannel.open(socketAddress); //step2:發送請求,使用GBK編碼 channel.write(charset.encode("GET " + "/ HTTP/1.1" + "\r\n\r\n")); //step3:讀取資料 ByteBuffer buffer = ByteBuffer.allocate(1024);// 建立1024位元組的緩衝 while (channel.read(buffer) != -1) { buffer.flip();// flip方法在讀緩衝區位元組操作之前調用。 System.out.println(charset.decode(buffer)); // 使用Charset.decode方法將位元組轉換為字串 buffer.clear();// 清空緩衝