1. 程式人生 > >Java NIO 學習筆記(三)----Selector

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