07. Java NIO Selector選擇器

Selector是Java NIO中的一個組件,用於檢查一個或多個NIO Channel的狀態是否處於可讀、可寫。如此能夠實現單線程管理多個channels,也就是能夠管理多個網絡連接。網絡

爲何使用Selector(Why Use a Selector?)

用單線程處理多個channels的好處是我須要更少的線程來處理channel。實際上,你甚至能夠用一個線程來處理全部的channels。從操做系統的角度來看,切換線程開銷是比較昂貴的,而且每一個線程都須要佔用系統資源,所以暫用線程越少越好。多線程

須要留意的是,現代操做系統和CPU在多任務處理上已經變得愈來愈好,因此多線程帶來的影響也愈來愈小。若是一個CPU是多核的,若是不執行多任務反而是浪費了機器的性能。不過這些設計討論是另外的話題了。簡而言之,經過Selector咱們能夠實現單線程操做多個channel。性能

這有一幅示意圖,描述了單線程處理三個channel的狀況:spa

overview-selectors.png

Java NIO: A Thread uses a Selector to handle 3 Channel's操作系統

建立Selector(Creating a Selector)

建立一個Selector能夠經過Selector.open()方法:線程

Selector selector = Selector.open();

註冊Channel到Selector上(Registering Channels with the Selector)

爲了同Selector掛了Channel,咱們必須先把Channel註冊到Selector上,這個操做使用SelectableChannel。register():設計

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

Channel必須是非阻塞的。因此FileChannel不適用Selector,由於FileChannel不能切換爲非阻塞模式。Socket channel能夠正常使用。rest

注意register的第二個參數,這個參數是一個「關注集合」,表明咱們關注的channel狀態,有四種基礎類型可供監聽:code

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

一個channel觸發了一個事件也可視做該事件處於就緒狀態。所以當channel與server鏈接成功後,那麼就是「鏈接就緒」狀態。server channel接收請求鏈接時處於「可鏈接就緒」狀態。channel有數據可讀時處於「讀就緒」狀態。channel能夠進行數據寫入時處於「寫就緒」狀態。server

上述的四種就緒狀態用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;

SelectionKey's

在上一小節中,咱們利用register方法把Channel註冊到了Selectors上,這個方法的返回值是SelectionKeys,這個返回的對象包含了一些比較有價值的屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

這5個屬性都表明什麼含義呢?下面會一一介紹。

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;

Ready Set

"就緒集合"中的值是當前channel處於就緒的值,通常來講在調用了select方法後都會須要用到就緒狀態,select的介紹在鬍鬚文章中繼續展開。

int readySet = selectionKey.readyOps();

從「就緒集合」中取值的操做相似月「關注集合」的操做,固然還有更簡單的方法,SelectionKey提供了一系列返回值爲boolean的的方法:

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

Channel + Selector

從SelectionKey操做Channel和Selector很是簡單:

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

Attaching Objects

咱們能夠給一個SelectionKey附加一個Object,這樣作一方面能夠方便咱們識別某個特定的channel,同時也增長了channel相關的附加信息。例如,能夠把用於channel的buffer附加到SelectionKey上:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

附加對象的操做也能夠在register的時候就執行:

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

從Selector中選擇channel(Selecting Channels via a Selector)

一旦咱們向Selector註冊了一個或多個channel後,就能夠調用select來獲取channel。select方法會返回全部處於就緒狀態的channel。 select方法具體以下:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()方法在返回channel以前處於阻塞狀態。 select(long timeout)和select作的事同樣,不過他的阻塞有一個超時限制。

selectNow()不會阻塞,根據當前狀態馬上返回合適的channel。

select()方法的返回值是一個int整形,表明有多少channel處於就緒了。也就是自上一次select後有多少channel進入就緒。舉例來講,假設第一次調用select時正好有一個channel就緒,那麼返回值是1,而且對這個channel作任何處理,接着再次調用select,此時剛好又有一個新的channel就緒,那麼返回值仍是1,如今咱們一共有兩個channel處於就緒,可是在每次調用select時只有一個channel是就緒的。

selectedKeys()

在調用select並返回了有channel就緒以後,能夠經過選中的key集合來獲取channel,這個操做經過調用selectedKeys()方法:

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

還記得在register時的操做吧,咱們register後的返回值就是SelectionKey實例,也就是咱們如今經過selectedKeys()方法所返回的SelectionKey。

遍歷這些SelectionKey能夠經過以下方法:

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();
}

上述循環會迭代key集合,針對每一個key咱們單獨判斷他是處於何種就緒狀態。

注意keyIterater.remove()方法的調用,Selector自己並不會移除SelectionKey對象,這個操做須要咱們收到執行。當下次channel處於就緒是,Selector任然會吧這些key再次加入進來。

SelectionKey.channel返回的channel實例須要強轉爲咱們實際使用的具體的channel類型,例如ServerSocketChannel或SocketChannel.

wakeUp()

y因爲調用select而被阻塞的線程,能夠經過調用Selector.wakeup()來喚醒即使此時已然沒有channel處於就緒狀態。具體操做是,在另一個線程調用wakeup,被阻塞與select方法的線程就會馬上返回。

close()

當操做Selector完畢後,須要調用close方法。close的調用會關閉Selector並使相關的SelectionKey都無效。channel自己無論被關閉。

完整的Selector案例(Full Selector Example)

這有一個完整的案例,首先打開一個Selector,而後註冊channel,最後錦亭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();
  }
}
相關文章
相關標籤/搜索