1. 程式人生 > >java NIO selector全面深入理解

java NIO selector全面深入理解

                最近在學習java NIO,發現java nio selector 相對 channel ,buffer 這兩個概念是比較難理解的 ,把學習理解的東西以文字的東西記錄下來,就像從記憶體落地到硬碟,把記憶體中內容換成該知識點的索引。
    在介紹Selector之前先明確以下3個問題:
              1、selector的作用是什麼?
              2、selector如何做到網路通訊的?
              下面就針對上面的2個問題來展開介紹:
           1、Selector(選擇器)是Java NIO中能夠檢測一到多個NIO渠道,並能夠知曉渠道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。
           2、selector 操作的過程 
           2.1 首先建立Selector
                  Selector selector = Selector.open();
           2.2 向selector註冊channel,和感興趣的事件,
                  channel.configureBlocking(false);
                  SelectionKey key = channel.register(selector,Selectionkey.OP_READ); 
                  注意register()方法的第二個引數, 這是一個interest集合,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽以下4中不同型別的事件:
                     服務端接收客戶端連線事件   SelectionKey.OP_ACCEPT
                     客戶端連線服務端事件   SelectionKey.OP_CONNECT
                     讀事件   SelectionKey.OP_READ
                     寫事件   SelectionKey.OP_WRITE
                   如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量連線起來如下:
                   SelectionKey key = channel.register(selector,Selectionkey.OP_READ|SelectionKey.OP_WRITE); 
                   當向Selector註冊Channel時,registor()方法會返回一個SelectorKey物件。這個物件包含了一些你感興趣的屬性。
                            interest集合
                            ready集合
                            Channel
                            Selector
                            附加的物件(可選) 
            2.2.1 interest集合
       就像向Selector註冊通道一節中所描述的,interest集合是你所選擇的感興趣的事件集合。可以通過SelectionKey讀寫interest集合,像這樣:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;  
                                boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;  
                                boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;  
                                boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE; 
  可以看到,用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest集合中。
    2.2.2 ready集合
   ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:
            int readySet = selectionKey.readyOps();
可以用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布林型別:
selectionKey.isAcceptable();  
selectionKey.isConnectable();  
selectionKey.isReadable();  
selectionKey.isWritable(); 
            2.2.3 Channel + Selector
                從SelectionKey訪問Channel和Selector很簡單。如下:
Channel  channel  = selectionKey.channel(); 
Selector selector = selectionKey.selector();
    2.2.4 附加物件
   可以將一個物件或者更多資訊附著到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集資料的某個物件。使用方法如下:
selectionKey.attach(theObject);  
                Object attachedObj = selectionKey.attachment(); 
還可以在用register()方法向Selector註冊Channel的時候附加物件。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject); 
            2.3 通過Selector選擇通道
                一旦向Selector註冊了一或多個通道,就可以呼叫幾個過載的select()方法。這些方法返回你所感興趣的事件(如連線、接受、讀或寫)已經準備就緒的那些通道。
換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。


                下面是select()方法:(該方法是阻塞方法)
 
                int select()
                int select(long timeout)
                int selectNow()
                select()阻塞到至少有一個通道在你註冊的事件上就緒了。
                select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(引數)。
                selectNow()不會阻塞,不管什麼通道就緒都立刻返回(譯者注:此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。)。
                select()方法返回的int值表示有多少通道已經就緒。亦即,自上次呼叫select()方法後有多少通道變成就緒狀態。如果呼叫select()方法,因為有一個通道變成就緒狀態,返回了1,
若再次呼叫select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法呼叫之間,只有一個通道就緒了。
                
    2.3.1 selectedKeys()
                一旦呼叫了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過呼叫selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:
                Set selectedKeys = selector.selectedKeys(); 
                當像Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 物件。這個物件代表了註冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些物件。
                可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:


                Set selectedKeys = selector.selectedKeys();  
                Iterator keyIterator = selectedKeys.iterator();  
                while(keyIterator.hasNext()) {  
                      SelectionKey key = keyIterator.next();  
                      if(key.isAcceptable()) {  
                      // a connection was accepted by a ServerSocketChannel.  
                      } else if (key.isConnectable()) {  
                      // a connection was established with a remote server.  
                      } else if (key.isReadable()) {  
                      // a channel is ready for reading  
                      } else if (key.isWritable()) {  
                      // a channel is ready for writing  
                      }  
                      keyIterator.remove();  
                } 
                這個迴圈遍歷已選擇鍵集中的每個鍵,並檢測各個鍵所對應的通道的就緒事件。
 
                注意每次迭代末尾的keyIterator.remove()呼叫。Selector不會自己從已選擇鍵集中移除SelectionKey例項。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
 
                SelectionKey.channel()方法返回的通道需要轉型成你要處理的型別,如ServerSocketChannel或SocketChannel等。
 
                wakeUp()
 
                某個執行緒呼叫select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它執行緒在第一個執行緒呼叫select()方法的那個物件上呼叫Selector.wakeup()方法即可。阻塞在select()方法上的執行緒會立馬返回。如果有其它執行緒呼叫了wakeup()方法,但當前沒有執行緒阻塞在select()方法上,下個呼叫select()方法的執行緒會立即“醒來(wake up)”。
 
                close()
 

                用完Selector後呼叫其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey例項無效。通道本身並不會關閉。 

             總結:1、建立Selector,

                         2、建立Channel,()

                         3、向Selector中註冊Channel,及感興趣的事件,

                         4.1、等待註冊的事件到達,不然就一直等待 selector.select()

           4.2、獲取selector中選中項的迭代器,選中的項為註冊的事件 Iterator ite = this.selector.selectedKeys().iterator();

           4.3、針對選中的事件對感興趣的事件進行處理。 

           5、要想快速理解selector過程,結合程式碼來學習。

             2.3.2 server端例項程式碼

  1. package com.anders.selector;  
  2. import java.io.IOException;  
  3. import java.net.InetSocketAddress;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.SelectableChannel;  
  6. import java.nio.channels.SelectionKey;  
  7. import
     java.nio.channels.Selector;  
  8. import java.nio.channels.ServerSocketChannel;  
  9. import java.nio.channels.SocketChannel;  
  10. import java.util.Iterator;  
  11. import java.util.Set;  
  12. publicclass NIOServer {  
  13.     // 通道管理器
  14.     private Selector selector;  
  15.     publicvoid initServer(int port) throws Exception {  
  16.         // 獲得一個ServerSocket通道
  17.         ServerSocketChannel serverChannel = ServerSocketChannel.open();  
  18.         // 設定通道為 非阻塞
  19.         serverChannel.configureBlocking(false);  
  20.         // 將該通道對於的serverSocket繫結到port埠
  21.         serverChannel.socket().bind(new InetSocketAddress(port));  
  22.         // 獲得一耳光通道管理器
  23.         this.selector = Selector.open();  
  24.         // 將通道管理器和該通道繫結,併為該通道註冊selectionKey.OP_ACCEPT事件
  25.         // 註冊該事件後,當事件到達的時候,selector.select()會返回,
  26.         // 如果事件沒有到達selector.select()會一直阻塞
  27.         serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
  28.     }  
  29.     // 採用輪訓的方式監聽selector上是否有需要處理的事件,如果有,進行處理
  30.     publicvoid listen() throws Exception {  
  31.         System.out.println("start server");  
  32.         // 輪詢訪問selector
  33.         while (true) {  
  34.             // 當註冊事件到達時,方法返回,否則該方法會一直阻塞
  35.             selector.select();  
  36.             // 獲得selector中選中的相的迭代器,選中的相為註冊的事件
  37.             Iterator ite = this.selector.selectedKeys().iterator();  
  38.             while (ite.hasNext()) {  
  39.                 SelectionKey key = (SelectionKey) ite.next();  
  40.                 // 刪除已選的key 以防重負處理
  41.                 ite.remove();  
  42.                 // 客戶端請求連線事件
  43.                 if (key.isAcceptable()) {  
  44.                     ServerSocketChannel server = (ServerSocketChannel) key.channel();  
  45.                     // 獲得和客戶端連線的通道
  46.                     SocketChannel channel = server.accept();  
  47.                     // 設定成非阻塞
  48.                     channel.configureBlocking(false);  
  49.                     // 在這裡可以傳送訊息給客戶端
  50.                     channel.write(ByteBuffer.wrap(new String("hello client").getBytes()));  
  51.                     // 在客戶端 連線成功之後,為了可以接收到客戶端的資訊,需要給通道設定讀的許可權
  52.                     channel.register(this.selector, SelectionKey.OP_READ);  
  53.                     // 獲得了可讀的事件
  54.                 } elseif (key.isReadable()) {  
  55.                     read(key);  
  56.                 }  
  57.             }  
  58.         }  
  59.     }  
  60.     // 處理 讀取客戶端發來的資訊事件
  61.     privatevoid read(SelectionKey key) throws Exception {  
  62.         // 伺服器可讀訊息,得到事件發生的socket通道
  63.         SocketChannel channel = (SocketChannel) key.channel();  
  64.         // 穿件讀取的緩衝區
  65.         ByteBuffer buffer = ByteBuffer.allocate(10);  
  66.         channel.read(buffer);  
  67.         byte[] data = buffer.array();  
  68.         String msg = new String(data).trim();  
  69.         System.out.println("server receive from client: " + msg);  
  70.         ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
  71.         channel.write(outBuffer);  
  72.     }  
  73.     publicstaticvoid main(String[] args) throws Throwable {  
  74.         NIOServer server = new NIOServer();  
  75.         server.initServer(8989);  
  76.         server.listen();  
  77.     }  

            2.3.3 client 例項程式碼

  1. package com.anders.selector;  
  2. import java.io.IOException;  
  3. import java.net.InetSocketAddress;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.SocketChannel;  
  8. import java.util.Iterator;  
  9. publicclass NIOClient {  
  10.     // 通道管理器
  11.     private Selector selector;  
  12.     /** 
  13.      * * // 獲得一個Socket通道,並對該通道做一些初始化的工作 * @param ip 連線的伺服器的ip // * @param port 
  14.      * 連線的伺服器的埠號 * @throws IOException 
  15.      */
  16.     publicvoid initClient(String ip, int port) throws IOException { // 獲得一個Socket通道
  17.         SocketChannel channel = SocketChannel.open(); // 設定通道為非阻塞
  18.         channel.configureBlocking(false); // 獲得一個通道管理器
  19.         this.selector = Selector.open(); // 客戶端連線伺服器,其實方法執行並沒有實現連線,需要在listen()方法中調
  20.         // 用channel.finishConnect();才能完成連線
  21.         channel.connect(new InetSocketAddress(ip, port));  
  22.         // 將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_CONNECT事件。
  23.         channel.register(selector, SelectionKey.OP_CONNECT);  
  24.     }  
  25.     /** 
  26.      * * // 採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 * @throws // IOException 
  27.      * @throws Exception  
  28.      */
  29.     @SuppressWarnings("unchecked")  
  30.     publicvoid listen() throws Exception { // 輪詢訪問selector
  31.         while (true) {  
  32.             // 選擇一組可以進行I/O操作的事件,放在selector中,客戶端的該方法不會阻塞,
  33.             // 這裡和服務端的方法不一樣,檢視api註釋可以知道,當至少一個通道被選中時,
  34.             // selector的wakeup方法被呼叫,方法返回,而對於客戶端來說,通道一直是被選中的
  35.             selector.select(); // 獲得selector中選中的項的迭代器
  36.             Iterator ite = this.selector.selectedKeys().iterator();  
  37.             while (ite.hasNext()) {  
  38.                 SelectionKey key = (SelectionKey) ite.next(); // 刪除已選的key,以防重複處理
  39.                 ite.remove(); // 連線事件發生
  40.                 if (key.isConnectable()) {