網路程式設計之Selector & SelectionKey詳解(一)
NIO能通過單個執行緒管理多個I/O通道,主要就是通過選擇器Selector來實現的。
public abstract class Selector { protected Selector() { } public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); } public abstract boolean isOpen(); public abstract SelectorProvider provider(); public abstract Set<SelectionKey> keys(); public abstract Set<SelectionKey> selectedKeys(); public abstract int selectNow() throws IOException; public abstract int select(long timeout)throws IOException; public abstract int select() throws IOException; public abstract Selector wakeup(); public abstract void close() throws IOException; }
當我們呼叫
channel.register(selector, SelectionKey.OP_READ, buffer);
方法時,會將通道channel註冊到選擇器selector上,同時會返回一個SelectionKey選擇鍵物件,這個鍵物件標識了通道和選擇器之間的註冊關係。選擇鍵會記住您關心的通道。它們也會追蹤對應的通道是否已經就緒。當您呼叫一個選擇器物件的select( )方法時,相關的鍵建會被更新,用來檢查所有被註冊到該選擇器的通道。您可以獲取一個就緒鍵的集合,從而找到當時已經就緒的通道。通過遍歷這些鍵,您可以選擇出每個從上次您呼叫select( )開始直到現在,已經就緒的通道。
public abstract class SelectionKey { //以下四個常量是定義的通道的四種操作 public static final int OP_READ = 1; public static final int OP_WRITE = 4; public static final int OP_CONNECT = 8; public static final int OP_ACCEPT = 16; //可以針對指定的通道附加一個物件 private volatile Object attachment = null; // 獲得通道物件 public abstract SelectableChannel channel(); // 獲得通道所註冊的選擇器 public abstract Selector selector(); // 驗證通道和選擇器之間的註冊關係是否還生效 public abstract boolean isValid(); //將通道放入選擇器的已登出集合中 public abstract void cancel(); //獲得該通道所感興趣的操作 public abstract int interestOps(); //設定通道感興趣的操作 public abstract SelectionKey interestOps(int paramInt); //獲得通道已準備好的操作 public abstract int readyOps(); public final boolean isReadable() { return ((readyOps() & 0x1) != 0); } public final boolean isWritable() { return ((readyOps() & 0x4) != 0); } public final boolean isConnectable() { return ((readyOps() & 0x8) != 0); } public final boolean isAcceptable() { return ((readyOps() & 0x10) != 0); } public final Object attach(Object paramObject) { Object localObject = this.attachment; this.attachment = paramObject; return localObject; } public final Object attachment() { return this.attachment; } }
一個SelectionKey選擇鍵物件包含兩個以整數形式進行編碼的位元掩碼:一個用於指示那些通道/選擇器組合體所關心的操作(instrest集合),
另一個表示通道準備好要執行的操作(ready集合)。可以通過呼叫鍵物件的interestOps( )方法來獲取,這個interset集合永遠不會被選擇器改變,
但可以通過呼叫選擇鍵interestOps(int value )方法並傳入一個新的位元掩碼引數來改變它。
ready集合是interest集合的子集,可以通過呼叫鍵的readyOps( )方法來獲取相關的通道的已經就緒的操作並且表示了interest集合中從上次呼叫select( )以來已經就緒的那些操作。
每個選擇器selector中會包含三種鍵的集合,也可以認為是三種狀態通道的集合,比較通道和選擇鍵是一一對應的:
(1) 註冊鍵的集合,通過register函式進行註冊的通道這個集合可以通過Keys()方法返回
(2) 已選擇鍵的集合,這個是註冊鍵集合的子集,可以通過selectedKeys()方法返回,該集合中的通道都是準備好執行相應操作的通道
(3) 已取消鍵的集合,這個集合包含了cancel( )方法被呼叫過的鍵(這個鍵已經被無效化),但它們還沒有被登出。這個集合是選擇器物件的私有成員,因而無法直接訪問。
當我們呼叫選擇器的select()方法時,會影響上述三種鍵的集合,呼叫之後會發生如下過程:
1.已取消的鍵的集合將會被檢查。如果它是非空的,每個已取消的鍵的集合中的鍵將從另外兩個集合中移除,並且相關的通道將被登出。
這個步驟結束後,已取消的鍵的集合將是空的。
2.已註冊的鍵的集合中的鍵的interest集合將被檢查。在這個步驟中的檢查執行過後,對interest集合的改動不會影響剩餘的檢查過程。一旦就緒條件被定下來,底層作業系統將會進行查詢,以確定每個通道所關心的操作的真實就緒狀態。依賴於特定的select( )方法呼叫,如果沒有通道已經準備好,執行緒可能會在這時阻塞,通常會有一個超時值。直到系統呼叫完成為止,這個過程可能會使得呼叫執行緒睡眠一段時間,然後當前每個通道的就緒狀態將確定下來。對於那些還沒準備好的通道將不會執行任何的操作。對於那些作業系統指示至少已經準備好interest集合中的一種操作的通道,將執行以下兩種操作中的一種:
a.如果通道的鍵還沒有處於已選擇的鍵的集合中,那麼鍵的ready集合將被清空,然後表示作業系統發現的當前通道已經準備好的操作的位元掩碼將被設定。
b.否則,也就是鍵在已選擇的鍵的集合中。鍵的ready集合將被表示作業系統發現的當前已經準備好的操作的位元掩碼更新。所有之前的已經不再是就緒狀態的操作不會被清除。事實上,所有的位元位都不會被清理。由作業系統決定的ready集合是與之前的ready集合按位分離的,一旦鍵被放置於選擇器的已選擇的鍵的集合中,它的ready集合將是累積的。位元位只會被設定,不會被清理。
3.步驟2可能會花費很長時間,特別是所激發的執行緒處於休眠狀態時。與該選擇器相關的鍵可能會同時被取消。當步驟2結束時,步驟1將重新執行,以完成任意一個在選擇進行的過程中,鍵已經被取消的通道的登出。
4.select操作返回的值是ready集合在步驟2中被修改的鍵的數量,而不是已選擇的鍵的集合中的通道的總數。返回值不是已準備好的通道的總數,
而是從上一個select( )呼叫之後進入就緒狀態的通道的數量。之前的呼叫中就緒的,並且在本次呼叫中仍然就緒的通道不會被計入,而那些在前一次呼叫中已經就緒但已經不再處於就緒狀態的通道也不會被計入。這些通道可能仍然在已選擇的鍵的集合中,但不會被計入返回值中。返回值可能是0。