NIO中SelectionKey和Channel、Selector的關係

dust1發表於2019-04-16
今天給自己的專案測出了個bug,NIOServer端在讀取一次資料後我一開始是選擇將獲取的迭代器remove掉,但是發現selectNow()中獲取的SelectionKey還存在,後來將其直接SelectionKey.cancel();後就只能傳遞一次資料了。這是由於我只是初步接觸NIO沒有深入瞭解每個模組的具體實現導致的,下面我通過閱讀原始碼來了解SelectionKey和Channel、Selector這三者的關係來梳理相關知識。
複製程式碼

--------------分割線--------------

由於Channel只是介面,因此繼續獲取該方法的實現,最終定位到AbstractSelectableChannel類,為了方便閱讀以及符合主題,我省略了部分無關程式碼,相關實現如下:

    public final SelectionKey register(Selector sel, int ops,
                                       Object att)
        throws ClosedChannelException
    {
        synchronized (regLock) {
            /* 異常狀態檢查程式碼,省略 */
            SelectionKey k = findKey(sel);
            if (k != null) {
                k.interestOps(ops);
                k.attach(att);
            }
            if (k == null) {
                synchronized (keyLock) {
                     /* 異常狀態檢查程式碼,省略 */
                    k = ((AbstractSelector)sel).register(this, ops, att);
                    addKey(k);
                }
            }
            return k;
        }
    }
複製程式碼

那麼這裡可以知道當Channel往Selector註冊的時候Channel會先呼叫本地方法findKey傳入要註冊的Selector獲取對應的SelectionKey,而引數只有Selector,沒有相關狀態,因此這裡就可以直接知道SelectionKey對應的就是一個Selector,我前面將其直接cancel掉的話實際上是將這個Channel從對應的Selector中取消註冊了,這就難免在同一個連線中只能傳遞一次,因為在等待下次IO的時候我已經將其從Selector中取消了讀狀態。

相應的,如果SelectionKey不為null,則表示該Channel已經在目標Selector上註冊,因此只要將目標狀態新增進SelectionKey中即可。

如果SelectionKey為null,則直接由Selector的註冊方法建立並新增進Channel中。

--------------分割線--------------

那麼繼續檢視這兩個方法,首先是addKey

    private void addKey(SelectionKey k) {
        assert Thread.holdsLock(keyLock);
        int i = 0;
        if ((keys != null) && (keyCount < keys.length)) {
            // Find empty element of key array
            for (i = 0; i < keys.length; i++)
                if (keys[i] == null)
                    break;
        } else if (keys == null) {
            keys =  new SelectionKey[3];
        } else {
            // Grow key array
            int n = keys.length * 2;
            SelectionKey[] ks =  new SelectionKey[n];
            for (i = 0; i < keys.length; i++)
                ks[i] = keys[i];
            keys = ks;
            i = keyCount;
        }
        keys[i] = k;
        keyCount++;
    }
複製程式碼

這段程式碼十分簡單,忽略相關安全檢查後可以看到這個操作主要是對keys的操作,這個keys通過檢視原始碼可以看到它是一個SelectionKey陣列用以儲存該Channel所繫結的Selector生成的SelectionKey物件。它預設長度為3,每次進行擴容將其長度*2.

那麼findKey就很好猜測它的實現了:應該是對這個keys陣列的遍歷,並將其中的SelectionKey與Selector對比即可.

    private SelectionKey findKey(Selector sel) {
        synchronized (keyLock) {
            if (keys == null)
                return null;
            for (int i = 0; i < keys.length; i++)
                if ((keys[i] != null) && (keys[i].selector() == sel))
                    return keys[i];
            return null;
        }
    }
複製程式碼

既然有find和and了那自然就會有remove

    void removeKey(SelectionKey k) {                    // package-private
        synchronized (keyLock) {
            for (int i = 0; i < keys.length; i++)
                if (keys[i] == k) {
                    keys[i] = null;
                    keyCount--;
                }
            ((AbstractSelectionKey)k).invalidate();
        }
    }
複製程式碼

結論

Channel、Selector通過一個唯一的SelectionKey實現註冊。

NIO中SelectionKey和Channel、Selector的關係

相關文章