一.簡介
NIO(Non-blocking I/O,在Java領域,也稱為New I/O),在jdk1.4 里提供的新api 。Sun 官方標榜的特性如下: 為所有的原始類型提供(Buffer)緩存支持,字符集編碼解碼解決方案。
Channel :一個新的原始I/O 抽象。 支持鎖和內存映射文件的文件訪問接口。 提供多路(non-bloking) 非阻塞式的高伸縮性網絡I/O 。NIO也是I/O多路復用的基礎,已經被越來越多地應用到大型應用服務器,
成為解決高並發與大量連接、I/O處理問題的有效方式。
二.NIO的原理
2.1 傳統的IO的原理和弊端
使用傳統的I/O程序讀取文件內容, 並寫入到另一個文件(或Socket), 如下程序:
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
會有較大的性能開銷, 主要表現在一下兩方面:
1. 上下文切換(context switch), 此處有4次用戶態和內核態的切換
2. Buffer內存開銷, 一個是應用程序buffer, 另一個是系統讀取buffer以及socket buffer
其運行示意圖如下
1) 先將文件內容從磁盤中拷貝到操作系統buffer
2) 再從操作系統buffer拷貝到程序應用buffer
3) 從程序buffer拷貝到socket buffer
4) 從socket buffer拷貝到協議引擎.
傳統BIO代碼舉例:
TraditionalClient.java
1 import java.io.DataOutputStream; 2 import java.io.FileInputStream; 3 import java.net.Socket; 4 5 public class TraditionalClient { 6 7 public static void main(String[] args) throws Exception { 8 long start = System.currentTimeMillis(); 9 // 創建socket鏈接 10 Socket socket = new Socket("localhost", 2000); 11 System.out.println("Connected with server " + socket.getInetAddress() + ":" + socket.getPort()); 12 // 讀取文件 13 FileInputStream inputStream = new FileInputStream("C:/sss.txt"); 14 // 輸出文件 15 DataOutputStream output = new DataOutputStream(socket.getOutputStream()); 16 // 緩沖區4096K 17 byte[] b = new byte[4096]; 18 // 傳輸長度 19 long read = 0; 20 long total = 0; 21 // 讀取文件,寫到socketio中 22 while ((read = inputStream.read(b)) >= 0) { 23 total = total + read; 24 output.write(b); 25 } 26 // 關閉 27 output.close(); 28 socket.close(); 29 inputStream.close(); 30 // 打印時間 31 System.out.println("bytes send--" + total + " and totaltime--" + (System.currentTimeMillis() - start)); 32 } 33 }
TraditionalServer.java
1 import java.io.DataInputStream; 2 import java.net.ServerSocket; 3 import java.net.Socket; 4 5 public class TraditionalServer { 6 7 public static void main(String args[]) throws Exception { 8 // 監聽端口 9 ServerSocket server_socket = new ServerSocket(2000); 10 System.out.println("等待,端口為:" + server_socket.getLocalPort()); 11 12 while (true) { 13 // 阻塞接受消息 14 Socket socket = server_socket.accept(); 15 // 打印鏈接信息 16 System.out.println("新連接: " + socket.getInetAddress() + ":" + socket.getPort()); 17 // 從socket中獲取流 18 DataInputStream input = new DataInputStream(socket.getInputStream()); 19 // 接收數據 20 byte[] byteArray = new byte[4096]; 21 while (true) { 22 int nread = input.read(byteArray, 0, 4096); 23 System.out.println(new String(byteArray, "UTF-8")); 24 if (-1 == nread) { 25 break; 26 } 27 } 28 socket.close(); 29 server_socket.close(); 30 System.out.println("Connection closed by client"); 31 32 } 33 } 34 }
2.2 為什么需要NIO
兩個字,效率,NIO能夠處理的所有場景,原IO基本都能做到,NIO因效率而生,效率包括處理速度和吞吐量(Througthout, Scalability)。Java原IO都是流式的(Stream Oriented),一個Byte一個Byte的讀取,且需要在JVM(用戶空間)和操作系統內核空間之間復制數據(Bytes),速度較慢。IO主要分兩塊,文件系統IO和網絡IO。文件系統方面,通過批量處理(Buffer),操作直接委托給操作系統(Direct),充分的利用操作系統的IO能力,提高訪問性能,不過NIO還不支持文件系統的異步調用。網絡IO方面,提供非阻塞操作,減少處理網絡IO的線程數,增強可伸縮性(Scalability)。額外提一下,線程切換(Context Swith)是繁重的,多了會嚴重影響性能(可伸縮性, Scalability),線程越多越糟糕,n核的機器參考的線程數是n或n+1。
2.3 NIO的原理和優勢
NIO技術省去了將操作系統的read buffer拷貝到程序的buffer, 以及從程序buffer拷貝到socket buffer的步驟, 直接將 read buffer 拷貝到 socket buffer.
java 的 FileChannel.transferTo() 方法就是這樣的實現, 這個實現是依賴於操作系統底層的sendFile()實現的.
publicvoid transferTo(long position, long count, WritableByteChannel target);
它的底層調用的是系統調用sendFile()方法
sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
如下圖
2.4 IO和NIO的區別:
1. 面向流與面向緩沖
Java NIO和IO之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。
即IO是基於字節流和字符流進行操作的,而NIO是基於通道(Channel)和緩沖區(Buffer)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。
2. 阻塞與非阻塞IO
IO的各種流是阻塞的,即:當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被完全讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。
而NIO采用的是非阻塞模式,即:一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。
2.5 NIO的三個重要抽象:
Java NIO引入的最重要三個抽象是Buffer,Channel,Selector,因為Java NIO中文件系統部分是阻塞的的,故不會用到Selector。下面分別來介紹。
Buffer
讀或寫數據的暫存容器,與Channel匹配使用。提供批量的向Channel寫入或者讀取數據功能。有四個int屬性需要理解:
mark 標記的位置,default -1。
position 游標當前位置
limit 存活(可用)Byte最后一個的位置
capacity Buffer的最大容量
很多Buffer的操作都與上面四個屬性有關,比如flip(),rewind(), clear()等。最重要的一個Buffer實現是ByteBuffer,主要實現對Byte的存取操作,常用的還有CharBuffer。
Channel
Client與IO設備之間讀寫的交互通道,數據先放到Buffer里面,再經Channel寫入設備,或者從Channel里面讀取數據。常用的有FileChannel,ServerSocketChannel和SocketChannel。
Selector
針對非阻塞(No-Blocking)的Channel,提供事件通知的機制。很多教程強調Selector提供了多路復用(單個線程或少數線程負責多個Channel的事件通知)的機制,非阻塞式編程是基於事件的,Selector就是事件的通訊機制。非阻塞式才是關鍵,Event Bus構建非阻塞式編程的環境。
NIO代碼舉例一:
TransferToClient.java
1 import java.io.FileInputStream; 2 import java.io.IOException; 3 import java.net.InetSocketAddress; 4 import java.nio.channels.FileChannel; 5 import java.nio.channels.SocketChannel; 6 7 public class TransferToClient { 8 9 public static void main(String[] args) throws IOException { 10 long start = System.currentTimeMillis(); 11 // 打開socket的nio管道 12 SocketChannel socketChannel = SocketChannel.open(); 13 socketChannel.connect(new InetSocketAddress("localhost", 9026));// 綁定相應的ip和端口 14 socketChannel.configureBlocking(true);// 設置阻塞 15 // 將文件放到channel中 16 FileChannel fileChannel = new FileInputStream("C:/sss.txt").getChannel();// 打開文件管道 17 //做好標記量 18 long size = fileChannel.size(); 19 int pos = 0; 20 int offset = 4096; 21 long curnset = 0; 22 long counts = 0; 23 //循環寫 24 while (pos<size) { 25 curnset = fileChannel.transferTo(pos, 4096, socketChannel);// 把文件直接讀取到socket chanel中,返回文件大小 26 pos+=offset; 27 counts+=curnset; 28 } 29 //關閉 30 fileChannel.close(); 31 socketChannel.close(); 32 //打印傳輸字節數 33 System.out.println(counts); 34 // 打印時間 35 System.out.println("bytes send--" + counts + " and totaltime--" + (System.currentTimeMillis() - start)); 36 } 37 }
TransferToServer.java
1 import java.io.IOException; 2 import java.net.InetSocketAddress; 3 import java.net.ServerSocket; 4 import java.nio.ByteBuffer; 5 import java.nio.channels.ServerSocketChannel; 6 import java.nio.channels.SocketChannel; 7 8 public class TransferToServer { 9 10 public static void main(String[] args) throws IOException { 11 // 創建socket channel 12 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 13 ServerSocket serverSocket = serverSocketChannel.socket(); 14 serverSocket.setReuseAddress(true);// 地址重用 15 serverSocket.bind(new InetSocketAddress("localhost", 9026));// 綁定地址 16 System.out.println("監聽端口 : " + new InetSocketAddress("localhost", 9026).toString()); 17 18 // 分配一個新的字節緩沖區 19 ByteBuffer dst = ByteBuffer.allocate(4096); 20 // 讀取數據 21 while (true) { 22 SocketChannel channle = serverSocketChannel.accept();// 接收數據 23 System.out.println("Accepted : " + channle); 24 channle.configureBlocking(true);// 設置阻塞,接不到就停 25 int nread = 0; 26 while (nread != -1) { 27 try { 28 nread = channle.read(dst);// 往緩沖區里讀 29 byte[] array = dst.array();//將數據轉換為array 30 //打印 31 String string = new String(array, 0, dst.position()); 32 System.out.print(string); 33 dst.clear(); 34 } catch (IOException e) { 35 e.printStackTrace(); 36 nread = -1; 37 } 38 } 39 } 40 } 41 }
NIO代碼舉例二:
NIOClient.java
1 import java.io.IOException; 2 import java.net.InetSocketAddress; 3 import java.nio.ByteBuffer; 4 import java.nio.channels.SelectionKey; 5 import java.nio.channels.Selector; 6 import java.nio.channels.SocketChannel; 7 import java.util.Iterator; 8 9 /** 10 * NIO客戶端 11 */ 12 public class NIOClient { 13 //通道管理器 14 private Selector selector; 15 16 /** 17 * 獲得一個Socket通道,並對該通道做一些初始化的工作 18 * @param ip 連接的服務器的ip 19 * @param port 連接的服務器的端口號 20 * @throws IOException 21 */ 22 public void initClient(String ip,int port) throws IOException { 23 // 獲得一個Socket通道 24 SocketChannel channel = SocketChannel.open(); 25 // 設置通道為非阻塞 26 channel.configureBlocking(false); 27 // 獲得一個通道管理器 28 this.selector = Selector.open(); 29 30 // 客戶端連接服務器,其實方法執行並沒有實現連接,需要在listen()方法中調用channel.finishConnect();才能完成連接 31 channel.connect(new InetSocketAddress(ip,port)); 32 //將通道管理器和該通道綁定,並為該通道注冊SelectionKey.OP_CONNECT事件。 33 channel.register(selector, SelectionKey.OP_CONNECT); 34 } 35 36 /** 37 * 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 38 * @throws IOException 39 */ 40 public void listen() throws IOException { 41 // 輪詢訪問selector 42 while (true) { 43 selector.select(); 44 // 獲得selector中選中的項的迭代器 45 Iterator ite = this.selector.selectedKeys().iterator(); 46 while (ite.hasNext()) { 47 SelectionKey key = (SelectionKey) ite.next(); 48 // 刪除已選的key,以防重復處理 49 ite.remove(); 50 // 連接事件發生 51 if (key.isConnectable()) { 52 SocketChannel channel = (SocketChannel) key.channel(); 53 // 如果正在連接,則完成連接 54 if(channel.isConnectionPending()){ 55 channel.finishConnect(); 56 } 57 // 設置成非阻塞 58 channel.configureBlocking(false); 59 //在這里可以給服務端發送信息哦 60 channel.write(ByteBuffer.wrap((new String("向服務端發送了一條信息").getBytes("UTF-8") ))); 61 //在和服務端連接成功之后,為了可以接收到服務端的信息,需要給通道設置讀的權限。 62 channel.register(this.selector, SelectionKey.OP_READ); 63 64 // 獲得了可讀的事件 65 } else if (key.isReadable()) { 66 read(key); 67 } 68 69 } 70 } 71 } 72 /** 73 * 處理讀取服務端發來的信息 的事件 74 * @param key 75 * @throws IOException 76 */ 77 public void read(SelectionKey key) throws IOException{ 78 //和服務端的read方法一樣 79 } 80 81 /** 82 * 啟動客戶端測試 83 * @throws IOException 84 */ 85 public static void main(String[] args) throws IOException { 86 NIOClient client = new NIOClient(); 87 client.initClient("localhost",8001); 88 client.listen(); 89 } 90 91 }
NIOServer.java
1 import java.io.IOException; 2 import java.net.InetSocketAddress; 3 import java.nio.ByteBuffer; 4 import java.nio.channels.SelectionKey; 5 import java.nio.channels.Selector; 6 import java.nio.channels.ServerSocketChannel; 7 import java.nio.channels.SocketChannel; 8 import java.nio.charset.Charset; 9 import java.util.Iterator; 10 11 /** 12 * NIO服務端 13 */ 14 public class NIOServer { 15 //通道管理器 16 private Selector selector; 17 18 /** 19 * 獲得一個ServerSocket通道,並對該通道做一些初始化的工作 20 * @param port 綁定的端口號 21 * @throws IOException 22 */ 23 public void initServer(int port) throws IOException { 24 // 獲得一個ServerSocket通道 25 ServerSocketChannel serverChannel = ServerSocketChannel.open(); 26 // 設置通道為非阻塞 27 serverChannel.configureBlocking(false); 28 // 將該通道對應的ServerSocket綁定到port端口 29 serverChannel.socket().bind(new InetSocketAddress(port)); 30 // 獲得一個通道管理器 31 this.selector = Selector.open(); 32 //將通道管理器和該通道綁定,並為該通道注冊SelectionKey.OP_ACCEPT事件,注冊該事件后, 33 //當該事件到達時,selector.select()會返回,如果該事件沒到達selector.select()會一直阻塞。 34 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 35 } 36 37 /** 38 * 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 39 * @throws IOException 40 */ 41 public void listen() throws IOException { 42 System.out.println("服務端啟動成功!"); 43 // 輪詢訪問selector 44 while (true) { 45 //當注冊的事件到達時,方法返回;否則,該方法會一直阻塞 46 selector.select(); 47 // 獲得selector中選中的項的迭代器,選中的項為注冊的事件 48 Iterator ite = this.selector.selectedKeys().iterator(); 49 while (ite.hasNext()) { 50 SelectionKey key = (SelectionKey) ite.next(); 51 // 刪除已選的key,以防重復處理 52 ite.remove(); 53 // 客戶端請求連接事件 54 if (key.isAcceptable()) { 55 ServerSocketChannel server = (ServerSocketChannel) key.channel(); 56 // 獲得和客戶端連接的通道 57 SocketChannel channel = server.accept(); 58 // 設置成非阻塞 59 channel.configureBlocking(false); 60 61 //在這里可以給客戶端發送信息哦 62 channel.write(ByteBuffer.wrap(new String("向客戶端發送了一條信息").getBytes("UTF-8"))); 63 //在和客戶端連接成功之后,為了可以接收到客戶端的信息,需要給通道設置讀的權限。 64 channel.register(this.selector, SelectionKey.OP_READ); 65 // 獲得了可讀的事件 66 } else if (key.isReadable()) { 67 read(key); 68 } 69 70 } 71 72 } 73 } 74 /** 75 * 處理讀取客戶端發來的信息 的事件 76 * @param key 77 * @throws IOException 78 */ 79 public void read(SelectionKey key) throws IOException{ 80 // 服務器可讀取消息:得到事件發生的Socket通道 81 SocketChannel channel = (SocketChannel) key.channel(); 82 // 創建讀取的緩沖區 83 ByteBuffer buffer = ByteBuffer.allocate(10); 84 channel.read(buffer); 85 byte[] data = buffer.array(); 86 String msg = new String(new String(data).trim().getBytes(),"UTF-8"); 87 System.out.println("服務端收到信息:"+msg); 88 ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes()); 89 channel.write(outBuffer);// 將消息回送給客戶端 90 } 91 92 /** 93 * 啟動服務端測試 94 * @throws IOException 95 */ 96 public static void main(String[] args) throws IOException { 97 NIOServer server = new NIOServer(); 98 server.initServer(8001); 99 server.listen(); 100 } 101 102 }
三.BIO、NIO、AIO適用場景分析:
BIO方式適用於連接數目比較小並且一次發送大量數據的場景,這種方式對服務器資源要求比較高,並發局限於應用中,JDK1.4以前的唯一選擇,但程序直觀簡單易理解。
NIO方式適用於連接數目多,每次只是發送少量的數據,服務器需要支持超大量的長時間連接。比如10000個連接以上,並且每個客戶端並不會頻繁地發送太多數據。例如總公司的一個中心服務器需要收集全國便利店各個收銀機的交易信息,只需要少量線程按需處理維護的大量長期連接,或者聊天服務器.Jetty、Mina、Netty、ZooKeeper等都是基於NIO方式實現。
對於BIO、NIO、AIO的區別和應用場景,知乎上有位同學是這樣回答的:
BIO:Apache,Tomcat。主要是並發量要求不高的場景.如果你有少量的連接使用非常高的帶寬,一次發送大量的數據,也許典型的IO服務器實現可能非常契合。
NIO:Nginx,Netty。主要是高並發量要求的場景,如果需要管理同時打開的成千上萬個連接,這些連接,例如聊天服務器,實現NIO的服務器可能是一個優勢。
用形象的例子來理解一下概念,以銀行取款為例:
同步 : 自己親自出馬持銀行卡到銀行取錢(使用同步IO時,Java自己處理IO讀寫)。
異步 : 委托一小弟拿銀行卡到銀行取錢,然后給你(使用異步IO時,Java將IO讀寫委托給OS處理,需要將數據緩沖區地址和大小傳給OS(銀行卡和密碼),OS需要支持異步IO操作API)。
阻塞 : ATM排隊取款,你只能等待(使用阻塞IO時,Java調用會一直阻塞到讀寫完成才返回)。
非阻塞 : 櫃台取款,取個號,然后坐在椅子上做其它事,等號廣播會通知你辦理,沒到號你就不能去,
你可以不斷問大堂經理排到了沒有,大堂經理如果說還沒到你就不能去(使用非阻塞IO時,如果不能讀寫Java調用會馬上返回,當IO事件分發器會通知可讀寫時再繼續進行讀寫,不斷循環直到讀寫完成)。