7 Java NIO Selector-翻譯

王金龍發表於2017-12-06

Selector是一個Java NIO中能夠檢測一到多個Channel,並且決定哪個通道已經準備好讀或寫。通過這種方式可以使用一個執行緒管理多個通道,因此可以用來管理多個網路連線。

為什麼要使用Selector

使用一個執行緒來管理多個通道的優勢是你只需要更少的執行緒來管理通道。事實上,你可以只使用一個執行緒來管理所有的通道。對作業系統來說,執行緒間的切換的開銷是很大的,並且每一個執行緒都需要消耗作業系統的資源(記憶體)。因此,執行緒的使用是越少越好。

但是,需要記住,現在的作業系統和CPU在多工處理方面變得越來越好。所以多執行緒的開銷隨著時間的推移變得越來越小。事實上,如果一個CPU是多核的,如果不進行多工處理,反而在浪費CPU的能力。不管怎麼說,關於那種設計的討論應該放在另一篇的文章裡。在這裡,只要知道,通過Selector,只需要一個執行緒來管理多個通道就可以了。

下圖是通過Selector使用一個執行緒來處理三個Channel的例子。

image

建立Selector

建立一個Selector只需要通過Selector.open()方法即可,像這樣:

Selector selector = Selector.open();
複製程式碼

向通道註冊Selector

為了讓Channel使用Selector,必須讓Channel註冊Selector。這可以通過SelectableChannel.register()方法來實現,像這樣:

channel.configureBolcking(false);
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
複製程式碼

當使用Selector時,channel必須是非阻塞模式。這意味著FileChannel不能使用Selector,因為FileChannel是阻塞的。Socket Channel都可以。

注意register方法的第二個引數,這是一個“interest set”,意思是在Selector監聽Channel時對哪些事件感興趣。共有 四種不同的事件型別可以監聽。

  • Connect
  • Accept
  • Read
  • Write

一個通道觸發一個事件意味著通道已經準備好這個事件了。因此,一個通道成功連線到另一臺伺服器是一個“Connect ready”事件,一個伺服器接受新進來的連線是一個“accept"事件。一個通道已經準備好資料是一個”ready“事件。一個能將準備寫資料是一個”write"事件。

這四種型別的事件代表四種不同的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 set作進一步簡單介紹。

Interest Set

如“向通道註冊選擇器”一節中那樣,Interest Set是你所選擇的感興趣的集合。可以通過SelectionKey來讀寫Interest Set,像這樣:

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;    
複製程式碼

正如你所看到的,你可以通過AND與指定的SelectionKey常量進行運算來判斷特定的事件是否在關注興趣集合中。

Ready Set

Ready Set是一系列的已經準備好的操作集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。Selection將會在下一小節中進行介紹。可以通過如下方式訪問ready set。

int readySet = selectionKey.readOps();
複製程式碼

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

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
複製程式碼

Channel + Selector

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

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();
複製程式碼

Attaching Objects

可以將一個物件或更多資訊附加到SelectionKey上。例如,你可以附加一個與通道一起使用的Buffer或者包含聚集資料的某個物件。如下所示:

selectionKey.attach(theObject);
Object attacheObj = selectionKey.attachment();
複製程式碼

你也可以在Channel註冊Selector時附加物件。如下所示:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
複製程式碼

Selecting Channels via a Selector

一旦將一個或多個通道註冊到選擇器上,就可以呼叫select方法。這些方法返回你所感興趣的事件(如連線、接受、讀或寫)已經準備就緒的那些通道。

下面是select()方法。

int select();
int select(long timeout);
int selectNow();
複製程式碼

select()方法會阻塞直到所註冊的事件準備好。

select( lont timeout)方法跟select()方法一樣阻塞,但它最大阻塞時間為timeout。

selectNow()方法並不會阻塞,它立即返回無論是否有通道準備好。

方法返回的值表明有多少通道已經準備好。那就是說,從呼叫select()方法開始共有多少通道準備好。如果你呼叫select()方法並且返回1,說明有一個Channel已經準備好。如果你再次呼叫select方法,又一個通道準備好了,它又會返回1。如果對第一個已經準備好的通道沒有進行任何處理,第二次將返回2。但在每次呼叫之間,只有一個通道準備就緒。

selectedKeys()

一旦你呼叫select方法,它的返回值表明有一個或多個通道已經準備好了。已經準備好了的channel可以通過selected key set進行訪問,具體是通過呼叫 selectedKeys()方法。如下所示。

Set<SelectionKey> selectedKeys = selector.selectedKeys();
複製程式碼

當向Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 物件。這個物件代表了註冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些物件。

可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> 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並不會自己主動刪除selected key。當處理完成之後,必須進行手動處理。當下次channel準備好以後,它將會再次加入到Selected keys集合中。

SelectionKey.channel()方法返回的Channel必須強制轉換成對應的Channel。例如ServerSocketChannel或SocketChannel。

wakeUp()

某個執行緒呼叫select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它執行緒在第一個執行緒呼叫select()方法的那個物件上呼叫Selector.wakeup()方法即可。阻塞在select()方法上的執行緒會立馬返回。

如果另外一個執行緒呼叫了wakeup方法,但此時並沒有任何執行緒阻塞了。下個呼叫select方法的執行緒將會立刻wakeup。

close()

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

完整的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<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> 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();
  }
}
複製程式碼

相關文章