Android NIO 系列教程(四) -- Selector

系列文章:
Android 網絡系列更新計劃
Android NIO 系列教程(一) NIO概述
Android NIO 系列教程(二) – Channel
Android NIO 系列教程(三) – Buffer
Android NIO 系列教程(四) – Selector

前面幾篇文章,我們已經認識了 selector ,它是一個可以檢測 一個 或 多個 channel ,並且能夠知道該 channel 的讀寫狀態的組件,通過這種方式,一個線程可以管理多個channel,從而管理這個網絡連接。

一、爲什麼使用 selector ?

一個好處是你可以使用一個線程去管理多個channel。對於操作系統來說,線程的切換開銷很大,且佔用資源(內存),因此,使用的線程越少越好。
但是,實際上,現在的 CPU 和 系統 性能越來越好,多線程的開銷,隨着時間的推移,也變得越來越少;實際上,多喝 cpu 不使用多線程去開發,是很浪費資源的。但是我們不深入討論這個問題,只需要知道 selector 是使用單個線程去處理多個 channel 的即可。
如下 一個 selector 監聽 三個 channel 的案例:
在這裏插入圖片描述

二、創建 selector 的實例

如何創建 selector 的實例呢,我們需要使用 open 方法:

Selector selector = Selector.open();

三、向 selector 註冊 channel

接着我們需要註冊 channel,但這裏的 channel 必須是非阻塞的,所以,我們將不能註冊 FileChannel,因爲它爲 非阻塞 IO, Socket 的channel是可以使用的,事實上,NIO 也是偏向網絡的IO.如:

channel.configureBlocking(false);

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

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

  • Connect
  • Accept
  • Read
  • Write

channel 的觸發事件也可以叫做準備事件,所以,當一個 channel 跟服務器連接成功叫做 Connected狀態,server socket 的channel 等待接收事件到來叫做 Accept狀態;一個可讀的 channel 則被稱爲 Read狀態;同理一個可寫的 chanenl 被稱爲Write狀態。

這四種事件,可以用 SelectionKey 裏的常量表示:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

如果對多個事件感興趣,可以使用以下方式:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

四、SelectionKey

在上面,當我們通過 register() 方法向 selector 註冊 channel 時,會返回一個 SelectionKey 的對象,該 SelectionKey 可以獲得以下數據:

  • Interest 集合
  • Ready 集合
  • Channel 通道
  • Selector
  • 附加對象

4.1 Interest 集合

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 集合中。

4.2 Ready 集合

ready 集合是 channel 準備就緒的集合,在選擇(Selection)之後,會優先選擇 ready 集合,selection 後面講,這裏我們就能拿到 ready 集合了:

int readySet = selectionKey.readyOps();

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

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

4.3 Channel + Selector

可以通過 SelctionKey 訪問到 channel 和 selector 的實例,如下:

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

4.4 附加對象

你可以將對象附加到 SelectionKey 上,這是給channel 或附加更多信息的一個比較簡便的方法。比如,你可以把channel 正在使用的 buffer ,或其他信息附加到 selectionkey 上,如:

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

你也可以通過 register 方法,在 channel 註冊給 selector 的時候,把對象附加進去:

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

五、通過 selector 選擇 channel

當你想 selector 註冊一個或多個 channel 時,你可以使用 select() 方法,來選擇你感興趣的事件 (如 connect, accept, read or write),已經準備就緒的事件,換句話,如果你對 channel 中的 read 事件感興趣,selector 就會返回已經就緒的那些通道。
selector 有以下方法:

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

select() 會一直阻塞,直到有註冊且準備就緒的channel到來
select(long timeout) 和select()一樣,除了最長會阻塞timeout毫秒
selectNow() 不會阻塞,無論什麼通道就緒立即返回。

select()方法返回的int值告訴我們有多少通道已經準備好了。也就是說,自上次調用select()以來,已經準備好了多少通道。如果你調用select(),它返回1,因爲一個通道已經就緒,再次調用select(),並且一個通道已經就緒,它將再次返回1。如果沒有對第一個就緒的通道執行任何操作,那麼現在就有了兩個就緒通道,但是在每個select()調用之間只有一個通道已經就緒。

六、selectedKeys()

一旦你調用了 select() 方法,且有返回一個或多個就緒的 channel,就可以使用 selector 的 selectionKey 集合了。像這樣:

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
    }

 // 記得要 remove 移除實例,不然下次事件過來就接收不到了
    keyIterator.remove();
}

這個循環遍歷已選擇鍵集中的每個鍵,並檢測各個鍵所對應的通道的就緒事件。

注意每次迭代末尾的keyIterator.remove()調用。Selector不會自己從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。

SelectionKey.channel()方法返回的通道需要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。

七、wakeUp()

當我們調用 select 方法,線程會一直阻塞;但幾遍沒有就緒的事件,我們也可以喚醒,只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。
如果有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會立即“醒來(wake up)

八、close()

當使用完 selector ,可以調用它的 close 方法,該方法會使 selector 關閉,並且讓 SelectionKey 註冊的事件失效。但 channel 本身不會關閉

完整實例

下面是一個實例,通過 open 創建 selector,通過register 向 selector 註冊 channel,並監聽自己感興趣的事件(accept, connect, read, write):

//獲取 selector 實例
Selector selector = Selector.open();
//設置非阻塞狀態
channel.configureBlocking(false);
//向 selector 註冊 channel 事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
//監聽感興趣的事件,當有就緒事件時立即返回
  int readyChannels = selector.selectNow();
//防止 cpu 空轉100%的問題
  if(readyChannels == 0) continue;

//拿到 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
    }
// 移除 selector 實例,方便下次接入
    keyIterator.remove();
  }
}

下一章,我們繼續學習 Channel 的一些實例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章