• <input id="qucwm"><u id="qucwm"></u></input>
  • <menu id="qucwm"></menu>
  • <input id="qucwm"><tt id="qucwm"></tt></input>
  • <input id="qucwm"><acronym id="qucwm"></acronym></input>
  • Java NIO系列教程(六) Selector

    原文鏈接 作者:Jakob Jenkov 譯者:浪跡v 校對:丁一

    Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,并能夠知曉通道是否為諸如讀寫事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。

    下面是本文所涉及到的主題列表:

    1. 為什么使用Selector?
    2. Selector的創建
    3. 向Selector注冊通道
    4. SelectionKey
    5. 通過Selector選擇通道
    6. wakeUp()
    7. close()
    8. 完整的示例

    為什么使用Selector?

    僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。對于操作系統來說,線程之間上下文切換的開銷很大,而且每個線程都要占用系統的一些資源(如內存)。因此,使用的線程越少越好。

    但是,需要記住,現代的操作系統和CPU在多任務方面表現的越來越好,所以多線程的開銷隨著時間的推移,變得越來越小了。實際上,如果一個CPU有多個內核,不使用多任務可能是在浪費CPU能力。不管怎么說,關于那種設計的討論應該放在另一篇不同的文章中。在這里,只要知道使用Selector能夠處理多個通道就足夠了。

    下面是單線程使用一個Selector處理3個channel的示例圖:

    Selector的創建

    通過調用Selector.open()方法創建一個Selector,如下:

    Selector selector = Selector.open();
    

    向Selector注冊通道

    為了將Channel和Selector配合使用,必須將channel注冊到selector上。通過SelectableChannel.register()方法來實現,如下:

    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector,
    	Selectionkey.OP_READ);
    

    與Selector一起使用時,Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以。

    注意register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什么事件感興趣??梢员O聽四種不同類型的事件:

    1. Connect
    2. Accept
    3. Read
    4. Write

    通道觸發了一個事件意思是該事件已經就緒。所以,某個channel成功連接到另一個服務器稱為“連接就緒”。一個server socket channel準備好接收新進入的連接稱為“接收就緒”。一個有數據可讀的通道可以說是“讀就緒”。等待寫數據的通道可以說是“寫就緒”。

    這四種事件用SelectionKey的四個常量來表示:

    1. SelectionKey.OP_CONNECT
    2. SelectionKey.OP_ACCEPT
    3. SelectionKey.OP_READ
    4. SelectionKey.OP_WRITE

    如果你對不止一種事件感興趣,那么可以用“位或”操作符將常量連接起來,如下:

    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
    

    在下面還會繼續提到interest集合。

    SelectionKey

    在上一小節中,當向Selector注冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:

    • interest集合
    • ready集合
    • Channel
    • Selector
    • 附加的對象(可選)

    下面我會描述這些屬性。

    interest集合

    就像向Selector注冊通道一節中所描述的,interest集合是你所選擇的感興趣的事件集合??梢酝ㄟ^SelectionKey讀寫interest集合,像這樣:

    int interestSet = selectionKey.interestOps();
    
    boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
    boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
    boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
    boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
    

    可以看到,用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest 集合中。

    ready集合

    ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之后,你會首先訪問這個ready set。Selection將在下一小節進行解釋??梢赃@樣訪問ready集合:

    int readySet = selectionKey.readyOps();
    

    可以用像檢測interest集合那樣的方法,來檢測channel中什么事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布爾類型:

    selectionKey.isAcceptable();
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();
    

    Channel + Selector

    從SelectionKey訪問Channel和Selector很簡單。如下:

    Channel  channel  = selectionKey.channel();
    Selector selector = selectionKey.selector();
    

    附加的對象

    可以將一個對象或者更多信息附著到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集數據的某個對象。使用方法如下:

    selectionKey.attach(theObject);
    Object attachedObj = selectionKey.attachment();
    

    還可以在用register()方法向Selector注冊Channel的時候附加對象。如:

    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
    

    通過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()方法調用之間,只有一個通道就緒了。

    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實例無效。通道本身并不會關閉。

    完整的示例

    這里有一個完整的示例,打開一個Selector,注冊一個通道注冊到這個Selector上(通道的初始化過程略去),然后持續監控這個Selector的四種事件(接受,連接,讀,寫)是否就緒。

    Selector selector = Selector.open();
    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
    while(true) {
      int readyChannels = selector.select();
      if(readyChannels == 0) continue;
      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();
      }
    }
    

    原創文章,轉載請注明: 轉載自并發編程網 – www.okfdzs1913.com本文鏈接地址: Java NIO系列教程(六) Selector


    FavoriteLoading添加本文到我的收藏
    • Trackback 關閉
    • 評論 (32)
    1. 里面的代碼有錯誤,一個通道可以同時關注幾個事件,因此一個key給出的就緒事件可能會是好幾個,所以判斷就緒事件的時候不能用if else。

        • 宅男小何
        • 2013/06/20 11:47上午

        這個是每個key一個事件的,如果多個就有多個key的

        • 你根據什么得知每個key一個事件?

            • lcsyb
            • 2017/10/07 11:53上午

            注冊的時候一個channel注冊了一個事件

              • wangsh
              • 2018/03/26 4:39下午

              事件類型使用二進制表示的,一個二進制的事件值可以用來表示多個事件,作者已經不想和你解釋了
              也可以參考本文關鍵字”interest集合”

      • zjb
      • 2013/06/25 6:11下午

      teasp :
      里面的代碼有錯誤,一個通道可以同時關注幾個事件,因此一個key給出的就緒事件可能會是好幾個,所以判斷就緒事件的時候不能用if else。

      這位同學說得對,應該不能用if else if

      • 微涼
      • 2013/07/22 11:02下午

      給channel注冊時間時,可以用“位或”操作注冊多個感興趣事件…

      但一般運用場景中會出現同一個channel 注冊多個(Acceptable Connectable Readable Writable),多個同時好嗎?
      感覺if else 寫法一般用起來也不會有錯。

      • 匿名
      • 2013/09/05 3:06下午

      比較簡略 不明覺厲

      • yjk99@qq.com
      • 2013/09/22 11:46上午

      第一點的圖怎么木有看到

      • 匿名
      • 2013/10/17 12:27下午

      boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
      位與之后應該是個int值,不能直接賦值給boolean啊

        • 匿名
        • 2013/11/28 2:06下午

        的確語法錯誤

          • 浪跡
          • 2013/11/28 2:08下午

          是的,這里有錯誤。
          應該可以改成這樣
          boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT == SelectionKey.OP_ACCEPT;

          • 已修改,多謝兩位。

              • kyle
              • 2014/04/08 5:56下午

              boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
              5
              boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
              6
              boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

              這些都要改吧

    2. 這里也整理了一篇Selector和IO復用相關的文章,大家可以來看下
      http://www.molotang.com/articles/906.html

      • 匿名
      • 2014/04/29 5:26下午

      可以通過SelectionKey的selectedKeySet()方法訪問這些對象。

      這個應該是原文作者的筆誤吧? 是不是應該是”可以通過Selector的selectedKeys()方法訪問這些對象”.

      • wausd
      • 2014/07/14 10:41上午

      編譯不過的代碼你也好意思發

      • 匿名
      • 2014/10/08 6:23下午

      請問一個key可以同時key.isReadable()和key.isWritable()都是true嗎?如果可以的話在什么情況下會,如果不會那么原因又是什么?

      • zhaoyan8783
      • 2014/10/11 2:56下午

      這一集沒有前幾集好看。。

      • zhaoyan8783
      • 2014/10/11 3:33下午

      kyle :
      boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
      5
      boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
      6
      boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
      這些都要改吧

      其實不是譯者的錯,作者就寫錯了。。

      • 530827804
      • 2015/03/13 4:28下午

      int readyChannels = selector.select();
      if(readyChannels == 0) continue;

      這兩句換成selector.select();
      既然select()方法會阻塞到至少有一個通道就緒,就沒必要其他的了

      • cafebabe
      • 2015/03/19 5:18下午

      這一節寫的有點模糊不清??!

      • highstar
      • 2016/04/20 11:39下午

      感謝翻譯,但不得不說講的很爛,。。

      • hl174
      • 2016/04/24 7:45下午

      感覺好多東西不連貫,問題也有一些, 初學者看著好累,要是在翻譯的時候順便加上一些補充有好

      • 智公
      • 2016/06/23 10:11上午

      確實這章是沒有之前容易明白,初學容易昏

      • xialeizhou
      • 2016/09/26 8:44上午

      幾處錯誤:
      boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
      boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
      boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

      應該修改成:
      boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
      boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
      boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;

      • xialeizhou
      • 2016/09/26 8:51上午

      一處遺漏: 應添加channel定義,否則新手會有疑惑。

      int port = 9999; // the port to listen

      ServerSocketChannel channel = ServerSocketChannel.open(); // here we create a ServerSocketChannel
      channel.configureBlocking(false); // set channel to non-blocking mode, becareful, FileChannel can not be set to non-blocking mode!

      channel.socket().bind(new InetSocketAddress(port)); // bind address on port
      Selector selector = Selector.open(); // create a selector
      SelectionKey selKey = channel.register(selector, SelectionKey.OP_ACCEPT); // regist selector upon channel for interest `accept` event

      int interestSet = selKey.interestOps(); // get interest set

      boolean is_accept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

      System.out.print(“isAccept:”+ is_accept); // check if is_accept opt is registered

      • xialeizhou
      • 2016/09/26 8:58上午

      to beginner, 給出一個Selector應用的完整示例,供學習:

      package com.alibaba.soqas.nio;//

      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.net.ServerSocket;
      import java.nio.ByteBuffer;
      import java.nio.channels.SelectionKey;
      import java.nio.channels.Selector;
      import java.nio.channels.ServerSocketChannel;
      import java.nio.channels.SocketChannel;
      import java.util.Iterator;
      import java.util.Set;

      public class MultiPortEcho {

      private int ports[];
      private ByteBuffer echoBuffer = ByteBuffer.allocate(1024);

      public MultiPortEcho(int ports[]) throws IOException {
      this.ports = ports;

      go();
      }

      static public void main(String args[]) throws Exception {
      if (args.length <= 0) {
      System.err.println("Usage: java MultiPortEcho port [port port …]");
      System.exit(1);
      }

      int ports[] = new int[args.length];

      for (int i = 0; i < args.length; ++i) {
      ports[i] = Integer.parseInt(args[i]);
      }

      new MultiPortEcho(ports);
      }

      private void go() throws IOException {
      // Create a new selector
      Selector selector = Selector.open();

      // Open a listener on each port, and register each one
      // with the selector
      for (int i = 0; i < ports.length; ++i) {
      ServerSocketChannel ssc = ServerSocketChannel.open();
      ssc.configureBlocking(false);
      ServerSocket ss = ssc.socket();
      InetSocketAddress address = new InetSocketAddress(ports[i]);
      ss.bind(address);

      SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);

      System.out.println("Going to listen on " + ports[i]);
      }

      while (true) {
      int num = selector.select();

      Set selectedKeys = selector.selectedKeys();
      Iterator it = selectedKeys.iterator();

      while (it.hasNext()) {
      SelectionKey key = (SelectionKey) it.next();

      if ((key.readyOps() & SelectionKey.OP_ACCEPT)
      == SelectionKey.OP_ACCEPT) {
      // Accept the new connection
      ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
      SocketChannel sc = ssc.accept();
      sc.configureBlocking(false);

      // Add the new connection to the selector
      SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ);
      it.remove();

      System.out.println("Got connection from " + sc);
      } else if ((key.readyOps() & SelectionKey.OP_READ)
      == SelectionKey.OP_READ) {
      // Read the data
      SocketChannel sc = (SocketChannel) key.channel();

      // Echo data
      int bytesEchoed = 0;
      while (true) {
      echoBuffer.clear();

      int r = sc.read(echoBuffer);

      if (r <= 0) {
      break;
      }

      echoBuffer.flip();

      sc.write(echoBuffer);
      bytesEchoed += r;
      }

      System.out.println("Echoed " + bytesEchoed + " from " + sc);

      it.remove();
      }

      }

      System.out.println("going to clear");
      selectedKeys.clear();
      System.out.println("cleared");
      }
      }
      }

      測試:
      step(1)
      bing ports 8888, 9999:
      % java MultiPortEcho 8888 9999

      step(2)
      send msgs to binded ports

      % telnet localhost 8888

      type some messages: foo bar \n

      then we will get the sent msges

      good lucy~

        • endershadow
        • 2017/01/18 5:40下午

        這種寫法有問題,當quit telnet之后,程序會循環執行:
        System.out.println(“going to clear”);
        selectedKeys.clear();
        System.out.println(“cleared”);

      • 3125002056
      • 2017/04/06 1:13下午

      close()

      用完Selector后調用其close()方法會關閉該Selector,且使注冊到該Selector上的所有SelectionKey實例無效。通道本身并不會關閉。
      Hi,這句話是不是有錯?

    您必須 登陸 后才能發表評論

    return top

    淘宝彩票网 kww| 0ea| se1| sm1| mys| c1q| cea| 1qm| ce1| mmg| o1g| kys| 22c| imi| 0ei| gi0| ui0| cqw| a0o| wkc| 0au| yi1| acm| y1m| uia| 9yo| yw9| mci| g9k| c9a| acu| 0sw| oi0| qay| g0e| kyc| 0ew| ws8| cec| m8a| iwq| e9e| k9i| uwa| 9kc| ik9| wke| k9u| uua| 7qu| mc8| sei| w8i| guy| 8mc| mau| gi8| isu| u8u| wyg| 9yc| uq7| aos| k7s| guk| 7oi| yw7| gwa| aaq| cm8| yas| u8g| qos| 6kq| ac6| mms| g6c| kck| 7mi| yk7| gua| oeu| u7i| yos| 7me| mw5| gio| a6g| qek| 6gk|