nio 原理

說明:nio理論及例子,瞭解nio的可以跳過本文看Hadoop的rpc實現,建議新手看看

一、阻塞式BIO的缺點:

前面自己實現了一個阻塞式BIO服務,採 用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,接收到客戶端連接之後爲客戶端連接創建一個新的線程處理請求消 息,處理完成之後,返回應答消息給客戶端,線程銷燬,這就是典型的一請求一應答模型。該架構最大的問題就是不具備彈性伸縮能力,當併發訪問量增加後,服務 端的線程個數和併發訪問數成線性正比,由於線程是Java虛擬機非常寶貴的系統資源,當線程數膨脹之後,系統的性能急劇下降,隨着併發量的繼續增加,可能 會發生句柄溢出、線程堆棧溢出等問題,並導致服務器最終宕機。(http://www.open-open.com/lib/view/open1403057331075.html)

根據阻塞I/O通信模型,它的兩缺點:
1. 當客戶端多時,會創建大量的處理線程。且每個線程都要佔用棧空間和一些CPU時間
2. 阻塞可能帶來頻繁的上下文切換,且大部分上下文切換可能是無意義的。


(本圖來自http://www.open-open.com/lib/view/open1403057331075.html)

二、異步非阻塞通信的引入

在 IO編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或者IO多路複用技術進行處理。IO多路複用技術通過把多個IO的阻塞複用到同一個 select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路複用的最大優勢是系統開銷 小,系統不需要創建新的額外進程或者線程,也不需要維護這些進程和線程的運行,降低了系統的維護工作量,節省了系統資源。JDK1.4提供了對非阻塞IO(NIO)的支持,JDK1.5_update10版本使用epoll替代了傳統的select/poll,極大的提升了NIO通信的性能。

關於NIO知識詳細見:點擊打開鏈接 http://blog.csdn.net/lzlchangqi/article/details/41209719
java NIO的工作原理:
1. 由一個專門的線程來處理所有的 IO 事件,並負責分發。 
2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。 
3. 線程通訊:線程之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的線程切換。

如下圖:一個線程Reactor用來處理所有io,並分發read、write等事件


                                             (本圖來自互聯網)

三:結合互聯網的例子進行分析NIO:

1、先看例子的源代碼,不妨debug調試下

  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.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.ServerSocketChannel;  
  8. import java.nio.channels.SocketChannel;  
  9. import java.util.Iterator;  
  10. import java.util.Set;  
  11.   
  12. public class NIOServer {  
  13.       
  14.     /*標識數字*/  
  15.     private  int flag = 0;  
  16.     /*緩衝區大小*/  
  17.     private  int BLOCK = 4096;  
  18.     /*接受數據緩衝區*/  
  19.     private  ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);  
  20.     /*發送數據緩衝區*/  
  21.     private  ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);  
  22.     private  Selector selector;  
  23.   
  24.     public NIOServer(int port) throws IOException {  
  25.         // 1、打開服務器套接字通道  
  26.         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
  27.         // 服務器配置爲非阻塞  
  28.         serverSocketChannel.configureBlocking(false);  
  29.         // 檢索與此通道關聯的服務器套接字  
  30.         ServerSocket serverSocket = serverSocketChannel.socket();  
  31.         //2、 進行服務的綁定  
  32.         serverSocket.bind(new InetSocketAddress(port));  
  33.         //3、 通過open()方法找到Selector  
  34.         selector = Selector.open();  
  35.         //4、註冊到selector,等待連接  
  36.         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  
  37.         System.out.println("Server Start----8888:");  
  38.     }  
  39.   
  40.   
  41.     // 監聽  
  42.     private void listen() throws IOException {  
  43.         while (true) {  
  44.             // 選擇一組鍵,並且相應的通道已經打開  
  45.             selector.select();  
  46.             // 返回此選擇器的已選擇鍵集。  
  47.             Set<SelectionKey> selectionKeys = selector.selectedKeys();  
  48.             Iterator<SelectionKey> iterator = selectionKeys.iterator();  
  49.             //5 輪詢就緒的key  
  50.             while (iterator.hasNext()) {          
  51.                 SelectionKey selectionKey = iterator.next();  
  52.                 iterator.remove();  
  53.                 handleKey(selectionKey);  
  54.             }  
  55.         }  
  56.     }  
  57.   
  58.     // 處理請求  
  59.     private void handleKey(SelectionKey selectionKey) throws IOException {  
  60.         // 接受請求  
  61.         ServerSocketChannel server = null;  
  62.         SocketChannel client = null;  
  63.         String receiveText;  
  64.         String sendText;  
  65.         int count=0;  
  66.         // 測試此鍵的通道是否已準備好接受新的套接字連接。  
  67.         //步驟6 handle connect 處理新的客戶接入  
  68.         if (selectionKey.isAcceptable()) {  
  69.             // 返回爲之創建此鍵的通道。  
  70.             //步驟7 設置新建連接的socket  
  71.             server = (ServerSocketChannel) selectionKey.channel();  
  72.             // 接受到此通道套接字的連接。  
  73.             // 此方法返回的套接字通道(如果有)將處於阻塞模式。  
  74.             client = server.accept();  
  75.             // 配置爲非阻塞  
  76.             client.configureBlocking(false);  
  77.             //步驟8   註冊到selector,等待連接  
  78.             client.register(selector, SelectionKey.OP_READ);  
  79.         } else if (selectionKey.isReadable()) {//步驟9:異步處理請求消息到ByteBuffer(),代碼中沒有步驟10  
  80.             // 返回爲之創建此鍵的通道。  
  81.             client = (SocketChannel) selectionKey.channel();  
  82.             //將緩衝區清空以備下次讀取  
  83.             receivebuffer.clear();  
  84.             //讀取服務器發送來的數據到緩衝區中  
  85.             count = client.read(receivebuffer);   
  86.             if (count > 0) {  
  87.                 receivebuffer.flip();  
  88.                 byte[] bytes = new byte[receivebuffer.remaining()];  
  89.                 receivebuffer.get(bytes);  
  90.                 receiveText = new String(bytes,"utf-8");  
  91.                 System.out.println("服務器端接受客戶端數據--:"+receiveText);  
  92.                 client.register(selector, SelectionKey.OP_WRITE);  
  93.             }  
  94.         } else if (selectionKey.isWritable()) {//步驟11 異步寫  
  95.             //將緩衝區清空以備下次寫入  
  96.             sendbuffer.clear();  
  97.             // 返回爲之創建此鍵的通道。  
  98.             client = (SocketChannel) selectionKey.channel();  
  99.             sendText="message from server--" + flag++;  
  100.             //向緩衝區中輸入數據  
  101.             sendbuffer.put(sendText.getBytes());  
  102.              //將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位  
  103.             sendbuffer.flip();  
  104.             //輸出到通道  
  105.             client.write(sendbuffer);  
  106.             System.out.println("服務器端向客戶端發送數據--:"+sendText);  
  107.             client.register(selector, SelectionKey.OP_READ);  
  108.         }  
  109.     }  
  110.   
  111.     /**  
  112.      * @param args  
  113.      * @throws IOException  
  114.      */  
  115.     public static void main(String[] args) throws IOException {  
  116.         // TODO Auto-generated method stub  
  117.         int port = 8888;  
  118.         NIOServer server = new NIOServer(port);  
  119.         server.listen();  
  120.     }  
  121. }  

  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. import java.util.Set;  
  9.   
  10. public class NIOClient {  
  11.   
  12.     /*標識數字*/  
  13.     private static int flag = 0;  
  14.     /*緩衝區大小*/  
  15.     private static int BLOCK = 4096;  
  16.     /*接受數據緩衝區*/  
  17.     private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);  
  18.     /*發送數據緩衝區*/  
  19.     private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);  
  20.     /*服務器端地址*/  
  21.     private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(  
  22.             "localhost", 8888);  
  23.   
  24.     public static void main(String[] args) throws IOException {  
  25.         // TODO Auto-generated method stub  
  26.         //步驟1  打開socket的通道socketChannel  
  27.         SocketChannel socketChannel = SocketChannel.open();  
  28.         //步驟2  設置爲非阻塞方式,同時設置tcp參數  
  29.         socketChannel.configureBlocking(false);  
  30.         Selector selector = null;  
  31.           
  32.           
  33.         // 異步連接服務器  
  34.         if (socketChannel.connect(SERVER_ADDRESS)) {  
  35.               
  36.         }  
  37.         else {  
  38.             //步驟5  註冊連接服務端socket動作  
  39.             selector = Selector.open();  
  40.             socketChannel.register(selector, SelectionKey.OP_CONNECT);  
  41.         }     
  42.         // 分配緩衝區大小內存  
  43.           
  44.           
  45.         Set<SelectionKey> selectionKeys;  
  46.         Iterator<SelectionKey> iterator;  
  47.         SelectionKey selectionKey;  
  48.         SocketChannel client;  
  49.         String receiveText;  
  50.         String sendText;  
  51.         int count=0;  
  52.                //步驟6 啓動線程  
  53.         while (true) {  
  54.             //選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。  
  55.             //此方法執行處於阻塞模式的選擇操作。  
  56.             int ret = selector.select();  
  57.             //System.out.println(ret);  
  58.               
  59.             //返回此選擇器的已選擇鍵集。  
  60.             selectionKeys = selector.selectedKeys();  
  61.             //System.out.println(selectionKeys.size());  
  62.             iterator = selectionKeys.iterator();  
  63.             while (iterator.hasNext()) {//7 輪詢就緒的key  
  64.                 selectionKey = iterator.next();  
  65.                 //4  判斷連接結果,如果連接成功,跳到步驟10,如果不成功,執行步驟5  
  66.                 if (selectionKey.isConnectable()) {  
  67.                     System.out.println("client connect");  
  68.                     client = (SocketChannel) selectionKey.channel();  
  69.                     // 判斷此通道上是否正在進行連接操作。  
  70.                     // 完成套接字通道的連接過程。8 handle connect()  
  71.                     if (client.isConnectionPending()) {  
  72.                         client.finishConnect();//9 判斷連接完成,完成連接  
  73.                         System.out.println("完成連接!");  
  74.                         sendbuffer.clear();  
  75.                         sendbuffer.put("Hello,Server".getBytes());  
  76.                         sendbuffer.flip();  
  77.                         client.write(sendbuffer);  
  78.                     }  
  79.                     //步驟10 向多路複用器註冊 OP_READ  
  80.                     client.register(selector, SelectionKey.OP_READ);  
  81.                 } else if (selectionKey.isReadable()) {//步驟11 handle read() 異步讀請求消息到ByteBuffer  
  82.                     client = (SocketChannel) selectionKey.channel();  
  83.                     //將緩衝區清空以備下次讀取  
  84.                     receivebuffer.clear();  
  85.                     //讀取服務器發送來的數據到緩衝區中  
  86.                     count=client.read(receivebuffer);  
  87.                     if(count>0){  
  88.                         receiveText = new String( receivebuffer.array(),0,count);  
  89.                         System.out.println("客戶端接受服務器端數據--:"+receiveText);  
  90.                         client.register(selector, SelectionKey.OP_WRITE);  
  91.                     }  
  92.   
  93.                 } else if (selectionKey.isWritable()) {//步驟13  異步寫ByteBuffer到SocketChannel  
  94.                     sendbuffer.clear();  
  95.                     client = (SocketChannel) selectionKey.channel();  
  96.                     sendText = "message from client--" + (flag++);  
  97.                     sendbuffer.put(sendText.getBytes());  
  98.                      //將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位  
  99.                     sendbuffer.flip();  
  100.                     client.write(sendbuffer);  
  101.                     System.out.println("客戶端向服務器端發送數據--:"+sendText);  
  102.                     client.register(selector, SelectionKey.OP_READ);  
  103.                 }  
  104.             }  
  105.             selectionKeys.clear();  
  106.         }  
  107.     }  
  108. }  

2、代碼可用如下圖說明(圖片來自:http://www.open-open.com/lib/view/open1403057331075.html)

       注意:通過debug可以發現Server端handle read後,就直接進行了步驟13,異步寫操作,這是因爲在步驟11進行了write的註冊,因此它不需要client的觸發,這就是selector輪詢的作用。

按照Reactor模式設計和實現,它的服務端通信序列圖如下:


客戶端通信序列圖如下:

客戶端步驟4-6的標註有些牽強,其實在大的程序中是這樣的,如hadoop的ipc代碼中就是這樣,稍後文章會講解hadoop如何使用nio進行rpc通信。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章