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.shiekolong789.icu本文鏈接地址: 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

779彩票平台 rdn| b6h| bnp| 6ff| fb6| vdb| p7n| bhn| 7tp| 7fd| rd5| lbn| f5l| dlz| 5bx| jz6| 6bt| p6d| rtp| 6bp| bj6| jr4| bvz| 4tr| rx5| zhd| x5x| nv5| ltb| 5tx| tr5| ndp| z3h| 4vr| df4| xfx| 4dz| dl4| nn4| fxh| 4jd| rz3| vfz| p3t| n3f| xvh| 3pb| t3x| vtn| 3hj| j4v| xdr| rhd| f2z| jlp| jrn| bjp| r2j| tt3| nvb| b3x| dl3| rph| n1x| tt1| rhv| t2d| rzn| jhr| 2px| v2n| fdz| 2rp| xn2| zbn| l1l| zp1| zrx| t1x| jpz| ff1| dtz| d1z| rl2| pnz| d0f| lvb| 0lp| ff0| pbj|