文章目錄
NIO SelectionKey
- 在前面的NIO部分尤其是 Selector 部分涉及到了很多關於 SelectionKey 的內容,Selector的select方法阻塞,返回之後得到的就是 SelectionKey集合(一個SelectionKey 對應一個Channel),可以認爲 SelectionKey 封裝了發送的事件。在代碼上 SelectionKey內部封裝了Selector 和 Channel,內部通過一個整數來表示興趣事件值,不同的bit 位表示一種事件,可以認爲SelectionKey 是 Selector 和 Cahnnel的紐帶,SelectionKey的代碼不多也比較簡單,本文來看看。
一、SelectionKey
-
SelectionKey 封裝了事件,包括興趣事件和就緒事件,內部通過兩個整數來表示這兩類事件,因爲一個整數有多個bit 位,特定的bit 位代表特定的時間,因此也可以理解爲兩個事件集合。
-
在SelectionKey對象的有效期間,Selector 會一直輪詢它管理的 Channel,有事件之後將事件添加到 與某個 Channel對應的 SelectionKey的集合中去,然後上層程序通過 SelectionKey 來處理對應的事件。
-
下面是源碼的註釋:
/**
* 一個代表通道和選擇器之間的註冊關係
* A token representing the registration of a {@link SelectableChannel} with a
* {@link Selector}.
*
* 通道註冊到Selector的時候會創建 SelectionKey,直到調用 SelectionKey的cancle,關閉通道或者
* 關閉選擇器之前 SelectionKey 都是有效的,可以通過 isValid 方法判斷 SelectionKey是否有效
* 取消一個SelectionKey不會馬上被移除,而是在下一次選擇操作的時候添加到cancelled-key集合(取消鍵集合)
*
* <p> A selection key is created each time a channel is registered with a
* selector. A key remains valid until it is <i>cancelled</i> by invoking its
* {@link #cancel cancel} method, by closing its channel, or by closing its
* selector. Cancelling a key does not immediately remove it from its
* selector; it is instead added to the selector's <a
* href="Selector.html#ks"><i>cancelled-key set</i></a> for removal during the
* next selection operation. The validity of a key may be tested by invoking
* its {@link #isValid isValid} method.
*
* <a name="opsets"></a>
*
SelectionKey 內部包含兩個操作集合,它們由兩個整數表示,它的每一bit表示一種SelectionKey對應的Channel支持的操作
* <p> A selection key contains two <i>operation sets</i> represented as
* integer values. Each bit of an operation set denotes a category of
* selectable operations that are supported by the key's channel.
*
*
* 興趣集合表示哪些操作在下一次select 操作中會被檢測是否被準備好,換言之只有興趣事
* 件,select纔會幫我們去檢測這個時間是否被準備好,興趣事件是SelectionKey 被創建的時
* 候在初始化的時候指定,其實就是 Channel註冊到 Selector的時候創建的,後們可以通過方法 interestOps(int) 修改興趣事件
*
* <li><p> The <i>interest set</i> determines which operation categories will
* be tested for readiness the next time one of the selector's selection
* methods is invoked. The interest set is initialized with the value given
* when the key is created; it may later be changed via the {@link
* #interestOps(int)} method. </p></li>
*
*
* 就緒事件是另一個集合,它代表與 SelectionKey對應的 Channel的哪些事件被 Selector 檢測到已經準備好了。
* 剛剛創建 SelectionKey 的時候就緒事件是0,也就對應着空集合
* 興趣事件在後續的 select 操作可能被改變,但是不能直接修改
* (這裏可以理解,興趣事件由外部觸發,而不能自己修改,自己只能改變興趣事件)
*
* <li><p> The <i>ready set</i> identifies the operation categories for which
* the key's channel has been detected to be ready by the key's selector.
* The ready set is initialized to zero when the key is created; it may later
* be updated by the selector during a selection operation, but it cannot be
* updated directly. </p></li>
*
* </ul>
*
* 就緒事件表示某一時刻 Channel 已經準備好可以執行的操作,但是不能保證,他可以由線程執行且不會引起線程阻塞;
* 就緒集合在selector操作之後很可能是準確的,對應的Channel 外部的IO操作被調用之後,很可能是不準確的
* 這裏感覺不好理解,我覺得大意就是就緒事件並不是總是準確的,而是某一時刻的,在select 操作之後它通常是準確的,但是
* Channel 觸發了一些IO操作之後他就不準確了,因此select 後處理完有需要循環繼續監聽
*
* <p> That a selection key's ready set indicates that its channel is ready for
* some operation category is a hint, but not a guarantee, that an operation in
* such a category may be performed by a thread without causing the thread to
* block. A ready set is most likely to be accurate immediately after the
* completion of a selection operation. It is likely to be made inaccurate by
* external events and by I/O operations that are invoked upon the
* corresponding channel.
*
*
*
* 這個類定義了所有已知的操作bit位, 但是精確的哪些bit位是Channel支持的,這取決於通道的類型。
* SelectableChannel的所以子類定義了validOps() 方法 返回Channel支持的操作,嘗試不支持的操作可能會引起運行期異常
* 這裏多說一句:SocketChannel支持 SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT
* ServerSocketChanne 只支持:SelectionKey.OP_ACCEPT
*
* <p> This class defines all known operation-set bits, but precisely which
* bits are supported by a given channel depends upon the type of the channel.
* Each subclass of {@link SelectableChannel} defines an {@link
* SelectableChannel#validOps() validOps()} method which returns a set
* identifying just those operations that are supported by the channel. An
* attempt to set or test an operation-set bit that is not supported by a key's
* channel will result in an appropriate run-time exception.
*
*
*
* 通常關聯一些應用具體的數據到 SelectionKey 上是必要的,比如一個代表高優先級協議狀態的數據對象。
* SelectionKey 因此支持添加一個單一任意對象,通過attach 方法添加對象, attachment獲取對象
*
* <p> It is often necessary to associate some application-specific data with a
* selection key, for example an object that represents the state of a
* higher-level protocol and handles readiness notifications in order to
* implement that protocol. Selection keys therefore support the
* <i>attachment</i> of a single arbitrary object to a key. An object can be
* attached via the {@link #attach attach} method and then later retrieved via
* the {@link #attachment() attachment} method.
*
*
* Selection keys 多線程使用是安全的,對興趣集合的操作通常會在具體的selector 操作的時候做現場安全同步,
* 究竟這些同步是如何執行的取決於具體的獨立實現,在native 實現中,如果 select 操作已經在執行,那麼對興趣集合的讀寫會無限期阻塞
* 在高性能實現中,讀寫興趣集合會短暫的阻塞,甚至不阻塞,
* 在任何情況下,select 操作使用的興趣事件的值是 select 操作開始的那一瞬間的值
*
* <p> Selection keys are safe for use by multiple concurrent threads. The
* operations of reading and writing the interest set will, in general, be
* synchronized with certain operations of the selector. Exactly how this
* synchronization is performed is implementation-dependent: In a naive
* implementation, reading or writing the interest set may block indefinitely
* if a selection operation is already in progress; in a high-performance
* implementation, reading or writing the interest set may block briefly, if at
* all. In any case, a selection operation will always use the interest-set
* value that was current at the moment that the operation began. </p>
*/
- 翻譯不一定精準,但是讀了之後會加深對NIO 中組件的理解,建議閱讀。
二、源碼解讀
2.1 獲取Channel和Selector
- SelectionKey 是Channel 和 Selector的紐帶,內部封裝了二者,因此內部持有這兩個對象,通過對應的方法獲取,
//返回創建 SelectionKey 的 Channel,即使key取消也會返回
public abstract SelectableChannel channel();
//返回創建 SelectionKey 的 Selector,即使key取消也會返回
public abstract Selector selector();
2.2 SelectionKey事件
- 事件類型:SelectionKey內部定義了事件,類型一共四種,通過不同的 bit位表示
//四種事件
public static final int OP_READ = 1 << 0; //接受事件,僅適用於服務端,準備好接受新的連接
public static final int OP_WRITE = 1 << 2; //連接事件,僅適用與客戶端、連接成功
public static final int OP_CONNECT = 1 << 3; //讀事件,有數據可讀
public static final int OP_ACCEPT = 1 << 4; //寫事件,有數據可寫
- 事件判斷:通過位與運算判斷是否爲某種事件,注意readyOps() 方法會返回一個int 代表事件,由子類實現
//判斷是否爲對應的事件類型
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
- 事件獲取和判斷:獲取事件,取消事件,判斷是否合法等,由子類實現
//判斷 SelectionKey 是否有效,三種請求會導致無效,cancle,Channel關閉,Selector關閉
public abstract boolean isValid();
//取消和key關聯的Channel和Selector之間的註冊關係
//方法返回後,key就會無效並被添加到取消鍵集合,在下一個selection操作期間鍵在Selector的所有的鍵集合中被移除
//一旦被調用,key永遠無效,取消後再次調用該方法沒有影響
//這個方法在取消鍵集合會同步,因此併發場景下調用會有一點點阻塞
public abstract void cancel();
//返回興趣事件集合
public abstract int interestOps();
//根據給定值返回鍵的興趣事件集合
public abstract SelectionKey interestOps(int ops);
//返回就緒事件集合
public abstract int readyOps();
- 事件觸發:OP_READ 事件不僅僅只有可讀時才觸發,以下情況都會觸發
1.channel 中數據讀完
2.連接管道的另一端被關閉
3.有一個錯誤的 pending
4.對方發送消息過來
2.3 attachment屬性
- SelectionKey 可以添加和獲取一個屬性
//屬性字段
private volatile Object attachment = null;
//屬性原子更新
private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
SelectionKey.class, Object.class, "attachment"
);
//添加屬性
public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}
//獲取屬性
public final Object attachment() {
return attachment;
}
}
2.4 關閉SelectionKey
- 在以下情況下,SelectionKey 對象會失效,意味着 Selector 再也不會監控與它相關的事件:
1.調用 SelectionKey 的 cancel() 方法
2.關閉與 SelectionKey 關聯的Channel
3.與 SelectionKey 關聯的 Selector 被關閉
- 從這些條件可以看出,SelectionKey 作爲 Channel 和 Selector的紐帶,三者任意一個都可能使 SelectionKey 無效
三、實現類
3.1 AbstractSelectionKey
- AbstractSelectionKey 實現了SelectionKey,主要實現了cancel 和 valid方法
public abstract class AbstractSelectionKey extends SelectionKey {
//合法標誌位
private volatile boolean valid = true;
public final boolean isValid() {
return valid;
}
//設置爲不合法
void invalidate() { // package-private
valid = false;
}
/**
* Cancels this key.
* <p> If this key has not yet been cancelled then it is added to its
* selector's cancelled-key set while synchronized on that set. </p>
*
* 取消key,如果鍵沒有被取消,就加到取消鍵集合,並且在集合操作的時候需要同步
* 同步是爲了避免一個鍵被多個線程取消多次,這可能會造成selector的select()和channel的close() 之間的競爭
*/
public final void cancel() {
// Synchronizing "this" to prevent this key from getting canceled
// multiple times by different threads, which might cause race
// condition between selector's select() and channel's close().
synchronized (this) {
if (valid) {
valid = false;
((AbstractSelector)selector()).cancel(this);
}
}
}
}
- valid:通過內部 volatile 變量實現;
- cancel:同步操作,通過Selector 的 cancel 方法實現;
3.2 SelectionKeyImpl
- sun.nio.ch.SelectionKeyImpl 是 SelectorKey的具體實現類,也是SelectionKey唯一的具體實現類
public class SelectionKeyImpl extends AbstractSelectionKey {
//封裝的channel和Selector對象,該屬性是包私有的,NIO的很多 Channel都實現了SelChImpl 接口
final SelChImpl channel;
public final SelectorImpl selector;
//index表示該 SelectionKey 對象存儲在與其關聯的 Selector 對象中所在的位置(一個Selector顯然可以有很多SelectionKey,一個Channel對應一個SelectionKey)
private int index;
//興趣事件變量和就緒事件變量
private volatile int interestOps;
private int readyOps;
//構造方法
SelectionKeyImpl(SelChImpl var1, SelectorImpl var2) {
this.channel = var1;
this.selector = var2;
}
//獲取Channel 、Selector 和 index讀寫
public SelectableChannel channel() {
return (SelectableChannel)this.channel;
}
public Selector selector() {
return this.selector;
}
int getIndex() {
return this.index;
}
void setIndex(int var1) {
this.index = var1;
}
//確認有效,由 AbstractSelectionKey 的isValid 方法實現
private void ensureValid() {
if (!this.isValid()) {
throw new CancelledKeyException();
}
}
//獲取興趣事件變量
public int interestOps() {
this.ensureValid();
return this.interestOps;
}
//獲取就緒事件變量
public int readyOps() {
this.ensureValid();
return this.readyOps;
}
//設置興趣事件和就緒事件
public SelectionKey interestOps(int var1) {
this.ensureValid();
return this.nioInterestOps(var1);
}
public void nioReadyOps(int var1) {
this.readyOps = var1;
}
//獲取興趣事件變量
public int nioInterestOps() {
return this.interestOps;
}
//獲取就緒事件變量
public int nioReadyOps() {
return this.readyOps;
}
//設置興趣事件
public SelectionKey nioInterestOps(int var1) {
//validOps()方法返回 Channel支持的事件,如果事件不支持,就拋出異常
if ((var1 & ~this.channel().validOps()) != 0) {
throw new IllegalArgumentException();
} else {
//設置興趣事件爲var1
this.channel.translateAndSetInterestOps(var1, this);
this.interestOps = var1;
return this;
}
}
}
四、示例
- 爲了更清晰的看到 SelectionKey 中表示事件的值,我把 BIO、NIO到Netty 中的NIO 示例添加了日誌,查看不同的事件對應的值,關鍵代碼如下:
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//可接受事件
if (selectionKey.isAcceptable()) {
//拿到channel對象
int interestOps = selectionKey.interestOps();
int readyOps = selectionKey.readyOps();
System.out.println("興趣事件值:" + interestOps);
System.out.println("就緒事件值:" + readyOps);
省略....
System.out.println("服務端處理端口:" + socketChannel.socket().getPort());
省略....
System.out.println("獲取到客戶端的連接: " + socketChannel);
} else if (selectionKey.isReadable()) {
int interestOps = selectionKey.interestOps();
int readyOps = selectionKey.readyOps();
System.out.println("興趣事件值:" + interestOps);
System.out.println("就緒事件值:" + readyOps);
省略....
System.out.println("服務端收到消息 : " + body);
省略....
}
}
- 服務端輸出:
興趣事件值:16
就緒事件值:16
服務端處理端口:60728
獲取到客戶端的連接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:60728]
興趣事件值:1
就緒事件值:1
服務端收到消息 : hellonio
-
從示例其實能夠看出,時間本身就是一個整型值,前文分析的,通過不同的bit 位表示不同的事件,通過與運算判斷事件是否屬於某個類型。Selector 管理着若干的 Channel,如果有些 Channel有事件發生,則 select方法返回,返回有事件發生的 Channel對應的SelectionKey 會返回,通過 SelectionKey 我們可以知道發生了什麼事件,然後進行後續的對應事件的處理。