Java NIO 學習筆記(三)----Selector

目錄:
Java NIO 學習筆記(一)----概述,Channel/Buffer
Java NIO 學習筆記(二)----聚集和分散,通道到通道
Java NIO 學習筆記(三)----Selector

選擇器是一個 NIO 組件,它可以檢測一個或多個 NIO 通道,並確定哪些通道可以用於讀或寫了。 這樣,單個線程可以管理多個通道,從而管理多個網絡連接。

摘要:一個選擇器可對應多個通道,選擇器是通過 SelectionKey 這個關鍵對象完成對多個通道的選擇的。註冊選擇器的時候會返回此對象,調用選擇器的 selectedKeys() 方法也會返回此對象。每一個 SelectionKey 都包含了一些必要信息,比如關聯的通道和選擇器,獲取到 SelectionKey 後就可以從中取出對應通道進行操作。

爲什麼使用選擇器?

僅使用單個線程來處理多個通道的優點是,只需要更少的線程來處理通道。 實際上只需使用一個線程來處理所有通道。 對於操作系統而言,在線程之間切換是昂貴的,並且每個線程也佔用操作系統中的一些資源(存儲器)。 因此,使用的線程越少越好。

但請記住,現代操作系統和 CPU 在多任務處理中變得越來越好,因此隨着時間的推移,多線程的開銷會變得越來越小。 事實上,如果一個 CPU 有多個內核,你可能會因多任務而浪費 CPU 能力。 無論如何,這裏知道可以使用選擇器使用單個線程處理多個通道就可以。

以下是使用 1 個 Selector 處理 3 個 Channel 的線程圖示:

image

使用選擇器註冊通道

首先創建一個選擇器,它是通過這種方式創建的:

Selector selector = Selector.open();

要使用帶選擇器的通道,必須使用選擇器來註冊通道。 這是使用關聯 Channel 對象的 register() 方法完成的,如下所示:

channel.configureBlocking(false); //不阻塞
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 使用通道註冊一個選擇器

通道必須處於非阻塞模式才能與選擇器一起使用。 這意味着無法將 FileChannel 與 Selector一 起使用,因爲 FileChannel 無法切換到非阻塞模式。 套接字通道則支持。

注意 register() 方法的第二個參數。 這是一個“ interest 集合”,意味着通過 Selector 在 Channel 中監聽哪些事件。可以收聽四種不同的事件:

  • Connect 連接
  • Accept 接收
  • Read 讀
  • Write 寫

一個“發起事件”的通道也被稱爲“已就緒”事件。 因此,已成功連接到另一臺服務器的通道是“連接就緒”。 接受傳入連接的服務器套接字通道是“接收就緒”。 準備好要讀取的數據的通道“讀就緒”。 準備好寫入數據的通道稱爲“寫就緒”。

這四個事件由四個 SelectionKey 常量表示:

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

如果要監聽多個事件,那麼可以用“|”位或操作符將常量連接起來,如下所示:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;    

本文後面再進一步回顧 interest 集合。

register() 方法返回的 SelectionKey 對象

正如在上一節中看到的,當使用 Selector 註冊 Channel 時,register() 方法返回一個 SelectionKey 對象。 這個 SelectionKey 對象包含一些有趣的屬性:

  • interest 集合
  • ready 集合
  • 對應 Channel
  • 對應 Selector
  • 附加對象(可選)
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;    

可以使用給定的 SelectionKey 常量和 interest 集合進行“&”位與操作,以查明某個事件是否在 interest 集合中。

ready 集合

就緒集是通道準備好的一組操作。 將在 Selector 後訪問就緒集,可以像這樣訪問 ready set:

int readySet = selectionKey.readOps();

可以使用與上面 interest 集合相同的方式,使用位與操作進行檢測頻道已準備好的事件/操作。 但是,也可以使用下面這四種方法,它們都會返回一個布爾值:

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

對應 Channel + Selector

從 SelectionKey 訪問通道和選擇器非常簡單:

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

附加對象(可選)

可以將對象或者更多信息附加到 SelectionKey ,這是識別某個通道的便捷方式。 例如,可以將正在使用的緩衝區與通道或其他對象相關聯。 以下是使用方法:

// 將 theObject 對象附加到 SelectionKey 
selectionKey.attach(theObject);
// 從 SelectionKey 中取出附加的對象
Object attachedObj = selectionKey.attachment();

還可以在 register() 方法中添加參數,在使用 Selector 註冊 Channel 時就附加對象。如下:

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

通過選擇器選擇通道

使用 Selector 註冊一個或多個通道後,可以調用其中一個 select() 方法。 這些方法返回我們感興趣的,已就緒的事件(連接,接受,讀寫)的通道。 換句話說,如果對讀就緒通道感興趣,select() 方法會返回讀事件已經就緒的那些通道

以下是 select() 方法:

  • int select() : 將一直阻塞,直到至少有一個頻道爲註冊的事件做好準備。
  • int select(long timeout) :與 select() 相同,但它會最長阻塞 timeout 毫秒。
  • int selectNow() :完全沒有阻塞。 它會立即返回任何已準備好的通道。

select() 方法返回的 int 表示有多少通道準備好了。也就是說,自從你上次調用 select() 以來,有多少頻道已經準備好了。

如果調用 select() ,因爲一個頻道已準備就緒,它會返回 1 ,再次調用 select() ,因爲另外一個通道已準備就緒,它會再次返回 1 。如果沒有對第一個已準備就緒的通道做任何事情,那麼現在就有 2 個準備就緒的頻道,但是在每次 select() 調用之間,只有一個通道是準備就緒的。

選擇器的 selectedKeys() 方法返回的 SelectionKey 集合

一旦調用了其中一個 select() 方法並且其返回值表示有通道已準備就緒,就可以通過調用選擇器的 selectedKeys() 方法,因爲一個選擇器可以註冊多個通道,所以這裏返回集合。通過“已選擇鍵集(selected key set)”訪問就緒通道。 如下:

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

使用 Selector 註冊通道時,Channel 對象的 register() 方法返回 SelectionKey 對象。此對象代表了該選擇器註冊的通道。

可以迭代 selectedKeys() 方法返回的 Set 集合來訪問就緒通道。如下:

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

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        //  ServerSocketChannel接受了一個連接。
    } else if (key.isConnectable()) {
        //  與遠程服務器建立連接。
    } else if (key.isReadable()) {
        // 一個通道已讀就緒
    } else if (key.isWritable()) {
        // 一個通道已寫就緒
    }
    keyIterator.remove();
}

此循環迭代 Set,對於每個 key ,它測試 key 以確定 key 引用的通道已準備就緒的事件。

注意選擇器不會從 Set 本身中刪除 SelectionKey 對象。 完成通道處理後,必須在每次迭代結束時的調用 keyIterator.remove() 來刪除集合中已處理過的 SelectionKey 。 下一次通道變爲“就緒”時,選擇器會再次將其添加到選擇鍵集中。

這裏 Set 中的 SelectionKey 和當時使用 Selector 註冊 Channel 返回的 SelectionKey 是一樣的,請參考上述。

調用其對象方法 selectionKey.channel();就會返回 Channel 對象,這時候我們應該將其轉換爲具體需要使用的通道,例如 ServerSocketChannel 或 SocketChannel 等。

wakeUp() 喚醒被阻塞的線程

已調用 select() 方法的線程可能會被阻塞,這是可以通過調用 wakeUp() 方法離開 select() 方法,即使尚未準備好任何通道。其它線程來調用阻塞線程 Selector 對象的 select() 即可讓阻塞在 select() 方法上的線程立馬返回。

如果另一個線程調用 wakeup() 並且當前在 select() 中沒有阻塞線程,則調用 select() 的下一個線程將立即被“喚醒”。

close() 關閉選擇器

調用選擇器的 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;

      // 這裏的 SelectionKey 就和註冊時候返回的 key 一樣,
      // 因爲一個選擇器可以註冊多個通道,所以這裏返回集合
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while(keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {
            //  ServerSocketChannel接受了一個連接。
        } else if (key.isConnectable()) {
            //  與遠程服務器建立連接。
        } else if (key.isReadable()) {
            // 一個通道已讀就緒
        } else if (key.isWritable()) {
            // 一個通道已寫就緒
        }
        keyIterator.remove();
    }
}

再回顧一下:

  1. Selector.open() 打開選擇器,設置通道不阻塞,調用通道的 register() 方法註冊選擇器,此方法的第二個參數是一個“ interest 集合”(Connect 、Accept 、Read 、Write )
  2. register() 方法返回一個 SelectionKey 對象,此對象包含了一些註冊信息(interest 集合,ready 集合,對應 Channel,對應 Selector,附加對象(可選)),可以調用此對象的一些方法返回一些很有用的信息,例如Channel channel = selectionKey.channel();返回關聯的通道。
  3. 使用 Selector 註冊一個或多個通道後,可以調用其中一個 select() 方法來選擇通道,選擇什麼通道呢?選擇我們註冊時候, interest 集合裏面所關注的所有通道,然後返回被選擇的已準備就緒的通道數量,如果此方法返回值不爲 0 ,代表 selector 對象裏面有包含我們需要的通道了。
  4. 知道有就緒通道後,可以使用 selector.selectedKeys() 方法獲取 SelectionKey 集合,對於集合中每一個 SelectionKey 都包含了一些必要信息,比如關聯的通道和選擇器,注意一個選擇器可對應多個通道。獲取到 SelectionKey 後就可以從中取出對應通道進行操作,這也是選擇器的作用所在,一個選擇器,操作多個通道。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章