IO 與 NIO之網路通訊
一、阻塞IO / 非阻塞NIO
阻塞IO:當一條執行緒執行 read() 或者 write() 方法時,這條執行緒會一直阻塞直到讀取到了一些資料或者要寫出去的資料已經全部寫出,在這期間這條執行緒不能做任何其他的事情。
非阻塞NIO:NIO 與原有的 IO 有同樣的作用和目的,但是使用的方式完全不同,NIO 支援面向緩衝區的、基於通道的操作。NIO 將以更加高效的方式進行檔案讀寫操作。JAVA NIO的核心在於:通道(Channel)和緩衝區(Buffer)。通道表示開啟 IO 裝置(例如:檔案、套接字)的連線。若需要使用 NIO 系統,需要獲取用於連線 IO 裝置的通道以及用於容納資料的緩衝區。對資料進行處理。
二、傳統IO測試程式碼如下
當出現 accept() 、read() 等方法是就會阻塞。
1 /** 2 * 傳統socket服務端 3 * @author -zhengzx- 4 */ 5 public class OioServer { 6 7 @SuppressWarnings("resource") 8 public static void main(String[] args) throws Exception { 9 10 //建立socket服務,監聽10101埠 11 ServerSocket server=newServerSocket(10101); 12 System.out.println("伺服器啟動!"); 13 while(true){ 14 //獲取一個套接字(阻塞) 15 final Socket socket = server.accept();//(測試時可以通過:telnet 127.0.0.1 10101。進行測試) 16 System.out.println("來個一個新客戶端!"); 17 //業務處理 18 handler(socket);19 } 20 } 21 22 /** 23 * 讀取資料 24 * @param socket 25 * @throws Exception 26 */ 27 public static void handler(Socket socket){ 28 try { 29 byte[] bytes = new byte[1024]; 30 InputStream inputStream = socket.getInputStream(); 31 32 while(true){ 33 //讀取資料(阻塞) 34 int read = inputStream.read(bytes); 35 if(read != -1){ 36 System.out.println(new String(bytes, 0, read)); 37 }else{ 38 break; 39 } 40 } 41 } catch (Exception e) { 42 e.printStackTrace(); 43 }finally{ 44 try { 45 System.out.println("socket關閉"); 46 socket.close(); 47 } catch (IOException e) { 48 e.printStackTrace(); 49 } 50 } 51 } 52 }
三、阻塞 IO解決辦法
可以通過執行緒池建立多執行緒,為每一次連線建立一個新的執行緒來執行。問題是對於長連線而言,執行緒過多時會嚴重消耗系統資源導致效能下降。比較適合短連線的應用。
1 public static void main(String[] args) throws Exception { 2 3 //建立執行緒池(可以通過執行緒解決阻塞問題、問題:每次連線都會建立一個執行緒,特別是長連線時特別消耗系統資源) 4 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); 5 //建立socket服務,監聽10101埠 6 ServerSocket server=new ServerSocket(10101); 7 System.out.println("伺服器啟動!"); 8 while(true){ 9 //獲取一個套接字(阻塞) 10 final Socket socket = server.accept();//(測試時可以通過:telnet 127.0.0.1 10101。進行測試) 11 System.out.println("來個一個新客戶端!"); 12 newCachedThreadPool.execute(new Runnable() { 13 14 @Override 15 public void run() { 16 //業務處理 17 handler(socket); 18 } 19 }); 20 21 } 22 }
四、NIO 的非阻塞模式
Java NIO 有阻塞模式和非阻塞模式,阻塞模式的 NIO除了使用 Buffer儲存資料外和 IO基本沒有區別,允許一條執行緒從 Channel 中讀取資料,通過返回值來判斷 buffer中是否有資料,如果沒有資料,NIO不會阻塞,因為不阻塞這條執行緒就可以去做其他的事情,過一段時間再回來判斷一下有沒有資料。
*Selectors:Java NIO的 selectors 允許一條執行緒去監控多個 channels的輸入,你可以向一個 selector上註冊多個 channel,然後呼叫 selector 的select() 方法判斷是否有新的連線進來或者已經在 selector 上註冊時 channel 是否有資料進入。selector 的機制讓一個執行緒管理多個 channel 變得簡單。
五、NIO示例程式碼如下
客戶端使用 SocketChannel,服務端使用 ServerSocketChannel 獲取通道
1 public class NIOServerSocket { 2 //定義一個socket入口 3 private ServerSocketChannel serverSocket; 4 //定義一個監聽器 5 Selector selector; 6 public static void main(String[] args) throws IOException { 7 NIOServerSocket nio =new NIOServerSocket(); 8 nio.initServer(8000); 9 nio.listen(); 10 } 11 public void initServer(int port) throws IOException { 12 //獲取一個serverSocket通道 13 serverSocket = ServerSocketChannel.open(); 14 //設定為非阻塞狀態(分為阻塞和非阻塞兩種情況) 15 serverSocket.configureBlocking(false); 16 //將通道對應的serverSocketChannel繫結到埠上 17 serverSocket.socket().bind(new InetSocketAddress(port)); 18 //獲取一個通道管理器 19 this.selector = Selector.open(); 20 //將通道管理器與通道進行繫結,並賦值SelectionKey.OP_ACCEPT事件,當事件 21 //註冊後,當事件到達後,select.select()會返回,如果沒有返回,就一直阻塞。 22 serverSocket.register(selector, SelectionKey.OP_ACCEPT); 23 } 24 public void listen() throws IOException { 25 System.out.println("伺服器啟動"); 26 //輪詢訪問select.select() 27 while(true) { 28 //當事件到達時返回,否則一直阻塞 29 selector.select(); 30 //獲取selector中選中項的迭代器,相中的項為註冊事件。 31 Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator(); 32 while(iterator.hasNext()) { 33 SelectionKey selectionKey = iterator.next(); 34 //刪除已選的key,防止重複處理 35 iterator.remove(); 36 handler(selectionKey); 37 } 38 } 39 } 40 public void handler(SelectionKey key) throws IOException { 41 if(key.isAcceptable()) { 42 handlerAccept(key); 43 }else if(key.isReadable()) { 44 handlerRead(key); 45 } 46 } 47 public void handlerAccept(SelectionKey key) throws IOException { 48 //獲取以有的通道 49 ServerSocketChannel channel = (ServerSocketChannel) key.channel(); 50 //獲取和客戶端連線的通道 51 SocketChannel accept = channel.accept(); 52 //設定為非阻塞 53 accept.configureBlocking(false); 54 // 在這裡可以給客戶端傳送資訊哦 55 System.out.println("新的客戶端連線"); 56 //連線成功之後,為了讀取客戶端傳送的訊息,需要設定讀許可權 57 accept.register(selector, SelectionKey.OP_READ); 58 } 59 public void handlerRead(SelectionKey key) throws IOException { 60 //伺服器可讀取訊息,獲取事件發生的Socket通道 61 SocketChannel channel = (SocketChannel) key.channel(); 62 //建立讀取內容的快取區buffer 63 ByteBuffer buffer = ByteBuffer.allocate(1024); 64 int read = channel.read(buffer); 65 if(read > 0) { 66 byte[] array = buffer.array(); 67 String msg = new String(array).trim(); 68 System.out.println("服務端收到資訊:" + msg); 69 70 //會寫 71 ByteBuffer byteBuffer = ByteBuffer.wrap("success".getBytes()); 72 channel.write(byteBuffer); 73 }else { 74 System.out.println("客戶端關閉"); 75 key.cancel(); 76 } 77 } 78 }
六 、selector.select()
selector.select() 雖阻塞,但可以通過 selector.wakeup()喚醒 selector 執行,也可以通過 selector.select(int timeout) 設定時間限制,timeout 時間後喚醒 selector。
七、NIO提高效能
新增多執行緒,一個執行緒對應一個 selector,埠的監聽可以單獨建立一個 selector。(既Netty的工作原理)
總結:NIO允許你用一個單獨的執行緒或幾個執行緒管理很多個 channels(網路的或者檔案的),代價是程式的處理和處理 IO相比更加複雜。如果你需要同時管理成千上萬的連線,但是每個連線只發送少量資料,例如一個聊天伺服器,用 NIO實現會更好一些,相似的,如果你需要保持很多個到其他電腦的連線,例如P2P網路,用一個單獨的執行緒來管理所有出口連線是比較合適的。
IO:如果你只有少量的連線但是每個連線都佔有很高的頻寬,同時傳送很多資料,傳統的 IO會更適合