天天看點

Java NIO 學習筆記(三)----Selector為什麼使用選擇器?使用選擇器注冊通道通過選擇器選擇通道Selector 選擇器總結

目錄:

Java NIO 學習筆記(一)----概述,Channel/Buffer

Java NIO 學習筆記(二)----聚集和分散,通道到通道

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

Java NIO 學習筆記(四)----檔案通道和網絡通道

Java NIO 學習筆記(五)----路徑、檔案和管道 Path/Files/Pipe

Java NIO 學習筆記(六)----異步檔案通道 AsynchronousFileChannel

Java NIO 學習筆記(七)----NIO/IO 的對比和總結

選擇器是一個 NIO 元件,它可以檢測一個或多個 NIO 通道,并确定哪些通道可以用于讀或寫了。 這樣,單個線程可以管理多個通道,進而管理多個網絡連接配接。

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

為什麼使用選擇器?

僅使用單個線程來處理多個通道的優點是,隻需要更少的線程來處理通道。 實際上隻需使用一個線程來處理所有通道。 對于作業系統而言,線上程之間切換是昂貴的,并且每個線程也占用作業系統中的一些資源(存儲器)。 是以,使用的線程越少越好。

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

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

Java NIO 學習筆記(三)----Selector為什麼使用選擇器?使用選擇器注冊通道通過選擇器選擇通道Selector 選擇器總結

使用選擇器注冊通道

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

要使用帶選擇器的通道,必須使用選擇器來注冊通道。 這是使用關聯 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

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

本文後面再進一步回顧 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:

可以使用與上面 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 時就附加對象。如下:

通過選擇器選擇通道

使用 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)”通路就緒通道。 如下:

使用 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 後就可以從中取出對應通道進行操作,這也是選擇器的作用所在,一個選擇器,操作多個通道。