06-NIO Selector

NIO Selector

  • Selector 是NIO中的核心組件,可以管理多個通道,將 Channel 註冊到 Selector 上,Selector 輪詢這些Channel,一旦某些Channel有事件發送就會返回對應的 SelectionKey,SelectionKey封裝了事件和事件的相關操作方法 ,程序對不同的事件進行不同的處理。
  • 因爲在通信過程中,往往真正的IO操作的時間佔比是很少的(不絕對),比如聊天系統,或者一些系統之間的調用,假設有1000個Channel通道,每個Channel通道每10秒內隨機通信一次,一次通信IO耗時是10ms,那麼10秒內總共就是耗時10秒,理論來說一個線程剛好能夠處理,當然這個例子很理想化,只是從這個角度來說由少量線程去管理大量連接是很划算的,不需要開闢大量線程,對於OS來說開闢線程的代價是很大的,在Netty中也並不是由一個線程去處理,而是由一個線程池去處理,由此能夠支持更多的連接。
  • 使用這種模式也有一定的缺點,會使得響應有一定的延遲,一個線程處理衆多連接也可能出現部分連接處理不及時的情況,使用線程池會更好。

一、Selector

  • Selector 是NIO 的核心組件,通過Selector 實現了少量線程管理大量 Channel,

1.1 Selector和Channel,SelectionKey

  • 三者的關係圖如下:Channel註冊到Selector,註冊成功後就會產生一個SelectionKey,SelectionKey內部持有Selector和Channel,SelectionKey 維護着一個Channel和Selector之間的關係,另外有一點圖中未標明,Selector 內部會有三個 SelectionKey 的集合,分別是全集(keys)、選擇集(select-keys)和取消集(cancle-keys),後面分析;

在這裏插入圖片描述

1.2 源碼註釋

  • 源碼註釋解讀:Selector 的源碼註釋很詳細,下面的解讀主要是按照源碼註釋大致翻譯過來的,快速閱讀可以直接看小結:
/**
 * SelectableChannel 的多路複用器,NIO 中大部分 Channel都繼承自 SelectableChannel 抽象類
 * A multiplexor of {@link SelectableChannel} objects.
 *
 * selector 可以調用 open方法打開,默認是通過SelectorProvider創建的,也可以直接調
 * 用SelectorProvider來創建selector,selector 會保持打開狀態直到調用了close方法
 * <p> A selector may be created by invoking the {@link #open open} method of
 * this class, which will use the system's default {@link
 * java.nio.channels.spi.SelectorProvider selector provider} to
 * create a new selector.  A selector may also be created by invoking the
 * {@link java.nio.channels.spi.SelectorProvider#openSelector openSelector}
 * method of a custom selector provider.  A selector remains open until it is
 * closed via its {@link #close close} method.
 *
 *
 *  SelectionKey 對象代表一個通道註冊到了一個Selctor,Selector 內部保存了三個 SelectionKey 集合
 * <p> A selectable channel's registration with a selector is represented by a
 * {@link SelectionKey} object.  A selector maintains three sets of selection
 * keys:
 *
 *   keys 方法返回一個 SelectionKey 集合,該集合的 SelectionKey 代表了Selector的註冊關係,簡單來說每註冊一
 *    個 Channel到Selector 就會產生一個 SelectionKey ,keys 方法會返回一個包含這些 SelectionKey 的集合
 *   <li><p> The <i>key set</i> contains the keys representing the current
 *   channel registrations of this selector.  This set is returned by the
 *   {@link #keys() keys} method. </p></li>
 *
 *   selectedKeys 方法返回一個 SelectionKey 集合,他是key set集合的子集,
 *   它們是selector在一次 select 操作過程中檢測到的興趣事件即中的通道就緒事件
 *   簡單舉例,假如Selector 註冊了100個Channel,對應100個 SelectionKey,並且每個Channel註冊時
 *   都指定了興趣事件,在一次select操作中,如果有10個通道都至少發生了它們對應的興趣事件中的一個,
 *   那麼就會把這十個 SelectionKey所組成的集合返回,因此容易理解它永遠都是 SelectionKey 全集(keys)的子集
 *
 *   <li><p> The <i>selected-key set</i> is the set of keys such that each
 *   key's channel was detected to be ready for at least one of the operations
 *   identified in the key's interest set during a prior selection operation.
 *   This set is returned by the {@link #selectedKeys() selectedKeys} method.
 *   The selected-key set is always a subset of the key set. </p></li>
 *
 *
 *   取消的 SelectionKey 集合,cancelled-key 不能直接被訪問,並且永遠全集(keys)的子集
 *   取消鍵的意思是,這些 SelectionKey 已經被取消了,(SelectionKey有cancel方法),但是
 *   它對應的Channel還未註銷,此時這個 SelectionKey就是取消鍵
 *   <li><p> The <i>cancelled-key</i> set is the set of keys that have been
 *   cancelled but whose channels have not yet been deregistered.  This set is
 *   not directly accessible.  The cancelled-key set is always a subset of the
 *   key set. </p></li>
 *
 * </ul>
 *
 * <p> All three sets are empty in a newly-created selector.
 * 新創建的 Selector 這三個集合都是空的
 *
 *
 * SelectableChannel#register(Selector,int) 方法被調用後,一個key就會被加入到
 * selector 的key集合(也就是全集會添加一個 SelectionKey ),
 * 在selection 操作階段取消鍵會被移除 
 * key集合不能直接修改,
 * 
 * <p> A key is added to a selector's key set as a side effect of registering a
 * channel via the channel's {@link SelectableChannel#register(Selector,int)
 * register} method.  Cancelled keys are removed from the key set during
 * selection operations.  The key set itself is not directly modifiable.
 *
 *
 * 當 SelectionKey 被取消,他就會被添加到取消鍵集合,關閉channel或者調用SelectionKey#cancel方法
 * 都會取消一個 SelectionKey ,取消一個 SelectionKey 會讓它對應的 Channel 在下一個 select  操作
 * 過程中被註銷,那時候這個 SelectionKey 會從全集中移除
 *
 * <p> A key is added to its selector's cancelled-key set when it is cancelled,
 * whether by closing its channel or by invoking its {@link SelectionKey#cancel
 * cancel} method.  Cancelling a key will cause its channel to be deregistered
 * during the next selection operation, at which time the key will removed from
 * all of the selector's key sets.
 *
 *
 * 通過 select 操作 ,SelectionKey 會被添加到selected-key集合,
 * 通過集合的迭代器的remove方法可以將key直接從selected-key 中移除,
 * 除此之外沒有其他方法將 SelectionKey 從選擇鍵中移除
 * 特殊情況在 select 操作階段會將 SelectionKey 移除
 * SelectionKey 不能直接的添加到選擇鍵集合
 *
 *
 * <a name="sks"></a><p> Keys are added to the selected-key set by selection
 * operations.  A key may be removed directly from the selected-key set by
 * invoking the set's {@link java.util.Set#remove(java.lang.Object) remove}
 * method or by invoking the {@link java.util.Iterator#remove() remove} method
 * of an {@link java.util.Iterator iterator} obtained from the
 * set.  Keys are never removed from the selected-key set in any other way;
 * they are not, in particular, removed as a side effect of selection
 * operations.  Keys may not be added directly to the selected-key set. </p>
 *
 *
 * <a name="selop"></a>
 * <h2>Selection</h2>
 *  下面是 Selection 操作階段的事情: 
 *
 *  調用select()或者select(long)方法後就處於selection 操作階段,這個階段 SelectionKey 可能被添加到
 *  選擇鍵集合,或者也可能從選擇鍵集合移除,或者可能從全集或者取消鍵集合移除
 *
 * <p> During each selection operation, keys may be added to and removed from a
 * selector's selected-key set and may be removed from its key and
 * cancelled-key sets.  Selection is performed by the {@link #select()}, {@link
 * #select(long)}, and {@link #selectNow()} methods, and involves three steps:
 * </p>
 *
 * <ol>
 *   每個取消鍵會從key集合中移除 (如果取消鍵是key集合的一個成員),這會讓取消鍵集合成爲空集合
 *   <li><p> Each key in the cancelled-key set is removed from each key set of
 *   which it is a member, and its channel is deregistered.  This step leaves
 *   the cancelled-key set empty. </p></li>
 *
 *
 *   下面的對系統的操作
 *   <li><p> The underlying operating system is queried for an update as to the
 *   readiness of each remaining channel to perform any of the operations
 *   identified by its key's interest set as of the moment that the selection
 *   operation began.  For a channel that is ready for at least one such
 *   operation, one of the following two actions is performed: </p>
 *
 *   <ol>
 *      
 *  如果Channel 的 SelectionKey 尚未在所選鍵集中,則會將其添加到該集合,並將其就緒集合
 *  準確地修改爲Channel 報告已經準備好的操作。 先前記錄在就緒中的任何準備信息
 *  集被丟棄。
 *     <li><p> If the channel's key is not already in the selected-key set then
 *     it is added to that set and its ready-operation set is modified to
 *     identify exactly those operations for which the channel is now reported
 *     to be ready.  Any readiness information previously recorded in the ready
 *     set is discarded.  </p></li>
 *
 *          
 *      除此以外,Channel通道的 SelectionKey 已經在選擇鍵集合,準備集合也修改得準確
 *      的包含Channel 彙報的那些準備好的事件,之前記錄的就緒集合保存完整,換句話說底
 *      層返回的就緒集合是按位分離的加入到就緒集合(新加入的不影響已有的)
 *
 *     <li><p> Otherwise the channel's key is already in the selected-key set,
 *     so its ready-operation set is modified to identify any new operations
 *     for which the channel is reported to be ready.  Any readiness
 *     information previously recorded in the ready set is preserved; in other
 *     words, the ready set returned by the underlying system is
 *     bitwise-disjoined into the key's current ready set. </p></li>
 *
 *   </ol>
 *  如果所有的鍵集合中的鍵在第一步開始的時候都是空的的興趣集合,那麼不管
 *   If all of the keys in the key set at the start of this step have empty
 *   interest sets then neither the selected-key set nor any of the keys'
 *   ready-operation sets will be updated.
 *
 *   <li><p> If any keys were added to the cancelled-key set while step (2) was
 *   in progress then they are processed as in step (1). </p></li>
 *
 * </ol>
 *
 * 三個select 操作的唯一的區別就是:是否阻塞等待通道事件就緒或者阻塞等待多久;
 * <p> Whether or not a selection operation blocks to wait for one or more
 * channels to become ready, and if so for how long, is the only essential
 * difference between the three selection methods. </p>
 *
 * 併發
 * <h2>Concurrency</h2>
 * 
 *  Selectors 是線程安全的,但是內部的 SelectionKey 集合不是線程安全的
 * <p> Selectors are themselves safe for use by multiple concurrent threads;
 * their key sets, however, are not.
 *
 * select 操作由 Selector 自己同步,在集合上同步
 * <p> The selection operations synchronize on the selector itself, on the key
 * set, and on the selected-key set, in that order.  They also synchronize on
 * the cancelled-key set during steps (1) and (3) above.
 *
 * select 操作期間對興趣集合的改變對本次select 操作沒有影響,這些改變在下一次 select 操作纔可見
 * <p> Changes made to the interest sets of a selector's keys while a
 * selection operation is in progress have no effect upon that operation; they
 * will be seen by the next selection operation.
 *
 *
 * SelectionKey 隨時都可能被取消,Channel 隨時都可能被關閉,
 * 因此一個 SelectionKey 在一個或者多個 Selector的SelectionKey集合中並不意味着
 * 這個 SelectionKey 是有效的也不意味着 Channel 是打開的,應用應該小心同步和檢查
 * 這些條件,就像有着另一個線程關閉Channel或者取消SelectionKey 一樣
 * <p> Keys may be cancelled and channels may be closed at any time.  Hence the
 * presence of a key in one or more of a selector's key sets does not imply
 * that the key is valid or that its channel is open.  Application code should
 * be careful to synchronize and check these conditions as necessary if there
 * is any possibility that another thread will cancel a key or close a channel.
 *
 * 一個線程阻塞在 select 方法有可能會被另一個線程用下面三種方式之一中斷
 * 1. 調用 Selector的 wakeup 方法
 * 2. 調用 Selector的 close 方法
 * 3. 調用阻塞線程的 interrupt 方法,此時阻塞線程的中斷狀態會被設置,Selector 的 wakeup會被調用
 * <p> A thread blocked in one of the {@link #select()} or {@link
 * #select(long)} methods may be interrupted by some other thread in one of
 * three ways:
 *
 * <ul>
 *
 *   <li><p> By invoking the selector's {@link #wakeup wakeup} method,
 *   </p></li>
 *
 *   <li><p> By invoking the selector's {@link #close close} method, or
 *   </p></li>
 *
 *   <li><p> By invoking the blocked thread's {@link
 *   java.lang.Thread#interrupt() interrupt} method, in which case its
 *   interrupt status will be set and the selector's {@link #wakeup wakeup}
 *   method will be invoked. </p></li>
 *
 * </ul>
 * close 方法在selector 上同步,按照順序同步三個集合,這個在 SelectorImpl#implCloseSelector 可以看到代碼
 * <p> The {@link #close close} method synchronizes on the selector and all
 * three key sets in the same order as in a selection operation.
 *
 * <a name="ksc"></a>
 *
 *
 * 一個 Selector 的鍵集合和選擇鍵集合通常不是線程安全的,如果一個線程要直接修改這些集合
 * 應該使用集合自身做同步控制,迭代器是 fail-fast 的,如果迭代器創建之後,那麼
 * 除了調用迭代器的remove方法之外,任何改變集合的方法都會導致併發修改異常的拋出
 * <p> A selector's key and selected-key sets are not, in general, safe for use
 * by multiple concurrent threads.  If such a thread might modify one of these
 * sets directly then access should be controlled by synchronizing on the set
 * itself.  The iterators returned by these sets' {@link
 * java.util.Set#iterator() iterator} methods are <i>fail-fast:</i> If the set
 * is modified after the iterator is created, in any way except by invoking the
 * iterator's own {@link java.util.Iterator#remove() remove} method, then a
 * {@link java.util.ConcurrentModificationException} will be thrown. </p>
 *
 */
  • 小結如下:

  • Selector 是可被選擇的 Channel 的多路複用器,可以由 open 方法打開,默認通過 SelectorProvider 創建,Selector 會一直保持打開狀態直到調用了它的 close 方法

  • SelectionKey 代表Channel 和 Selector之間的註冊關係,Selector 內部保存了三個 SelectionKey 集合,分別是keys(SelectionKey全集),selectedKeys(SelectionKey選擇集)和cancleKey(SelectionKey取消集),後兩個一定是全集的子集

  • keys:SelectionKey全集,每一個Channel 註冊到 Slecltor 就會產生一個代表二者註冊關係的 SelectionKey,這個SelectionKey會添加到keys集合,簡單說如果註冊了100個 Channel 在一個 Selector 上且都有效,那麼keys 集合裏面就有100個 SelectionKey 對象,每次register都會往裏面增加元素,對應的SelectorKey取消則會從集合移除,集合不能直接修改

  • selected-key:SelectionKey選擇集,selector通過select方法會返回有事件發生的 Channel 對應的SelectionKey,這些SelectionKey就組成了SelectionKey選擇集,比如一次select 操作中,100個Channel中有5個Channel發生了興趣事件,那麼select操作就會返回,Selector.selectedKeys() 就會返回選擇鍵集合,集合內包含這5個Channel對應的5個SelectionKey 對象。這個集合不能添加,只能remove ,處理完事件之後通過迭代器remove。

  • cancelled-key:SelectionKey取消集, 如果 SelectionKey 取消了,但是對應的Channel 還未註銷,那麼這些 SelectionKey 就會被加入到取消鍵集合,或者直接調用SelectionKey的cancle方法或者關閉一個Channel都會導致對應的 SelectionKey 加入取消鍵集合,取消鍵集合會在下一次select操作中被清空,同時註銷對應的 Channel

  • 下面是三個集合的對比

集合 作用 獲取 添加 移除
keys (全集) 保存全部Channel和Selector對應的SelectionKey keys()方法獲取 註冊 Channel時添加,不能直接添加 Selector 管理
select-key(選擇鍵集合) 有事件發送的Channel對應的SelectionKey selectedKeys()方法獲取 有事件時由Selector添加,不能直接添加 處理完事件通過迭代器移除
cancel-key(取消鍵集合) 取消的 SelectionKey 不能直接獲取 SelectorKey取消時,由Selector添加,不能直接添加 select 期間由Selector移除

二、源碼解讀

  • Selector 接口主要定義了一些操作方法,沒有屬性

2.1 open

  • 打開一個 Selector ,得到一個 Selector 對象,通過SelectorProvider 得到,不同平臺有所不同
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

2.2 select

  • select 包括三種模式,select 嘗試返回有事件發生的 Channel 對應的 SelectKeys,不同方法阻塞表現不一樣。
+ select 可能在三種情況下返回:有通道事件發生,返回非零數字,線程被中斷,或者wakeup()被調用;
+ select(long time)select()方法行爲一致,不過阻塞時間由參數決定,但阻塞時長不是精確的
+ selectNow:功能和前兩者一致,但是是非阻塞的

2.3 keys

  • keys(): 返回selector 的key set集合,簡稱 “key” 集合,其實就是一個SelectionKey 集合,比如三個Channel註冊到了一個 Channel,那麼該集合就有三個SelectionKey 對象;
  • selectedKeys():返回selector的 selected-key set集合,簡稱 “選擇key” 集合,

注意:key set集合(“key”), 不能直接修改,只有取消或者Channel 註銷的時候key 纔會從中移除,直接修改拋出 UnsupportedOperationException;
注意:selected-key集合 (“選擇key”), 可以從中移除,但是不能直接添加,直接修改拋出 UnsupportedOperationException;

public abstract Set<SelectionKey> keys();

public abstract Set<SelectionKey> selectedKeys();

2.4 wakeup和 close

  • wakeup: 可以讓阻塞的select 或者 select(long time) 方法返回;
  • 如果一個線程阻塞在 select或者 select(long time) 方法,另一個線程調用 wakeup ,那麼前一個線程會立刻返回;
  • 如果調用 wakeup 的時候,沒有線程調用select 和 select(long time ),那麼下一次調用這兩個方法就會立刻返回,除非下一次調用的selectNow。
+ public abstract Selector wakeup();
  • close:關閉 Selector,如果close調用的時候,一個線程正在調用select阻塞,那麼效果就和wakeup一樣,如果已經關閉,再次調用沒有任何作用,關閉之後除了close和wakeup方法可以調用之外,其他任何方法調用都會拋出 ClosedSelectorException
  • 關閉之後,和Selector 相關的資源會被釋放,關聯的 Channel會被註銷,關聯的未取消的鍵將無效(uncancelled keys are invalidated)
public abstract void close() throws IOException;

2.5 isOpen()

  • 判斷 Selector 是否處於打開狀態,true表示打開,false表示關閉

三、SelectorProvider

  • NIO中的核心類 Selector 和 兩個重要的Channel 實現類:ServerSocketChannel和 SocketChannel都是由 SelectorProvider 來創建的,這三個類都提供了一個open 方法來創建實例,底層都是調用SelectorProvider實現
//SocketChannel
public static SocketChannel open() throws IOException {
        return SelectorProvider.provider().openSocketChannel();
    }

//ServerSocketChannel
public static ServerSocketChannel open() throws IOException {
    return SelectorProvider.provider().openServerSocketChannel();
}

//Selector    
public static Selector open() throws IOException {
    return SelectorProvider.provider().openSelector();
}
  • 我們重點看看 SelectorProvider.provider() 獲取 provider 和 openSelector 方法

3.1 SelectorProvider.provider()

  • SelectorProvider 類內部持有一個靜態的SelectorProvider成員變量 provider,provider()方法會返回這個變量,如果首次則會初始化,可能看出是一個單例模式
    public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            //根據系統屬性加載
                            if (loadProviderFromProperty())
                                return provider;
                            //按照服務加載
                            if (loadProviderAsService())
                                return provider;
                            //默認加載
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

3.2 openSelector

  • windows 平臺下, provider使用的是 WindowsSelectorProvider,因此調用的 openSelector是走的WindowsSelectorProvider的openSelector()方法,返回的是windows 平臺下 Selector的具體實現類:WindowsSelectorImpl
public class WindowsSelectorProvider extends SelectorProviderImpl {

    public AbstractSelector openSelector() throws IOException {
        return new WindowsSelectorImpl(this);
    }
}

四、實現類AbstractSelector

  • AbstractSelector 是抽象實現類,AbstractSelector主要實現了 Selector 的打開關閉狀態的維護,cancelledKeys取消鍵,支持異步關閉和中斷的begin和end方法等。
public abstract class AbstractSelector extends Selector {

     // 是否打開的原子狀態變量
    private AtomicBoolean selectorOpen = new AtomicBoolean(true);

    // The provider that created this selector,創建Selector的provider
    private final SelectorProvider provider;

    // 構造方法
    protected AbstractSelector(SelectorProvider provider) {
        this.provider = provider;
    }

    //三大key集合之一:取消鍵集合 cancelledKeys 
    private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();

    //取消一個SelectionKey取消鍵集合加入到 
    void cancel(SelectionKey k) {                       // package-private
        synchronized (cancelledKeys) {
            cancelledKeys.add(k);
        }
    }

    //關閉 Selector,如果已經關閉則立刻返回,否則調用子類的 implCloseSelector方法
    public final void close() throws IOException {
        boolean open = selectorOpen.getAndSet(false);
        if (!open)
            return;
        implCloseSelector();
    }

    //子類實現
    protected abstract void implCloseSelector() throws IOException;

    //判斷是否打開
    public final boolean isOpen() {
        return selectorOpen.get();
    }

    //返回 provider
    public final SelectorProvider provider() {
        return provider;
    }

    //獲取取消鍵集合
    protected final Set<SelectionKey> cancelledKeys() {
        return cancelledKeys;
    }

    //註冊Channel 到 Selector,Channel的register註冊底層是調用這個方法
    protected abstract SelectionKey register(AbstractSelectableChannel ch, int ops, Object att);

    //註銷指定的 SelectionKey,removeKey由子類實現
    protected final void deregister(AbstractSelectionKey key) {
        ((AbstractSelectableChannel)key.channel()).removeKey(key);
    }

    private Interruptible interruptor = null;
 
    //支持異步關閉和中斷的begin和end方法
    protected final void begin() {
        if (interruptor == null) {
            interruptor = new Interruptible() {
                    public void interrupt(Thread ignore) {
                        AbstractSelector.this.wakeup();
                    }};
        }
        AbstractInterruptibleChannel.blockedOn(interruptor);
        Thread me = Thread.currentThread();
        if (me.isInterrupted())
            interruptor.interrupt(me);
    }
    
    protected final void end() {
        AbstractInterruptibleChannel.blockedOn(null);
    }
}
  • SelectorImpl也是一個抽象實現類,這塊不看了,後面的文章再看具體實現類 WindowsSelectorImpl

五、示例

5.1 註冊

  • 在前面的文章中有 NIO 的代碼示例,這裏給出核心代碼如下:
        Selector selector = Selector.open();

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  • 將非阻塞的 Channel 註冊到 Selector,
  • 配置感興趣的事件是:SelectionKey.OP_ACCEPT(連接事件),可選的事件包括:
    public static final int OP_ACCEPT = 1 << 4;     //接受事件,僅適用於服務端,準備好接受新的連接
    public static final int OP_CONNECT = 1 << 3;    //連接事件,僅適用與客戶端、連接成功
    public static final int OP_READ = 1 << 0;       //讀事件,有數據可讀
    public static final int OP_WRITE = 1 << 2;      //寫事件,有數據可寫
  • 注意多次調用 channel.register(selector, interest); 可以變更興趣事件,也可以使用邏輯與 | 來表示對多種事件感興趣

  • 回到 NIO 的代碼,SelectionKey.OP_ACCEPT表示已經有事件產生,並且這個事件表示準備好接受新的連接了,那麼產生這個事件的後一步就是程序自行決定到底是接受還是不接受這個連接,對應的代碼註釋如下:

        while (iterator.hasNext()) {
                //阻塞等待事件返回
                SelectionKey selectionKey = iterator.next();
        
                //有事件產生纔會到這一步,如果 selectionKey.isAcceptable() 爲true 表示產生的事件是 SelectionKey.OP_ACCEPT
                //說明已經準備好接受新的連接了,那麼if裏面的邏輯來決定接受or不接受
                if (selectionKey.isAcceptable()) {
    
                    //這裏面選擇接受,如何接受?先通過selectionKey拿到對應的Channel,因爲管理了衆多的 Channel,要處總的知道是哪一個Channel有事件吧
                    //拿到之後,因爲知道事件的含義是準備好接受新的連接,因此就進行下一步接受連接
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();

                    //通過 ServerSocketChannel 的 accept方法接受連接,與此同時客戶端應該會產生一個 OP_CONNECT 事件 
                    
                    SocketChannel socketChannel = channel.accept();
                    
                    //新建立的這個連接也是非阻塞的,並且也交給Selector管家來管理
                    socketChannel.configureBlocking(false);

                    //也把連接對象註冊到selector,連接對象關心的應該是讀寫事件
                    socketChannel.register(selector, SelectionKey.OP_READ);

                    //移除非常關鍵,因此這個連接事件已經處理了,不移除的話會多次處理
                    iterator.remove();
                    System.out.println("獲取到客戶端的連接: " + socketChannel);
}

5.2 關於remove SelectionKey

  • 在上面的代碼中,在處理完一個通道事件之後,我們需要通過迭代器將對應的SelectionKey從選擇鍵集合中移除,否則在while循環過程的下一次select之後,還會處理上次已經處理過的事件,如果不移除看會發生什麼,服務完整的代碼如下 (註釋了iterator.remove()方法):
public class NioServerTest {

    private static final int[] PORTS = new int[]{12345, 12346, 12347};

    public static void main(String[] args) throws Exception {

        //1.創建一個Selector
        Selector selector = Selector.open();

        for (int port : PORTS) {
            //2.創建 serverSocketChannel,註冊到 selector 選擇器 , 設置非阻塞模式
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            //3.端口綁定,通過 ServerSocketChannel 關聯的 ServerSocket 綁定
            ServerSocket serverSocket = serverSocketChannel.socket();
            serverSocket.bind(new InetSocketAddress(port));
        }

        while (true) {
            //4.select 是阻塞方法,有事件就返回
            int num = selector.select();
            //5.獲取事件,可能多個通道有事件,因此返回的是一個集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            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);

                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();

                    //得到 SocketChannel ,代表 TCP 連接對象
                    SocketChannel socketChannel = channel.accept();
                    System.out.println("服務端處理端口:" + socketChannel.socket().getPort());
                    //配置非阻塞,由此可以看到客戶端 socketChannel 也可以是非阻塞的,
                    // configureBlocking 方法實際上定義在父類,因此客戶端服務端都是非阻塞的
                    socketChannel.configureBlocking(false);

                    //也把連接對象註冊到selector,連接對象關心的應該是讀寫事件
                    socketChannel.register(selector, SelectionKey.OP_READ);

                    //移除非常關鍵,因此這個連接事件已經處理了,不移除的話會多次處理
                    //iterator.remove();
                    System.out.println("獲取到客戶端的連接: " + socketChannel);
                } else if (selectionKey.isReadable()) {
                    int interestOps = selectionKey.interestOps();
                    int readyOps = selectionKey.readyOps();
                    System.out.println("興趣事件值:" + interestOps);
                    System.out.println("就緒事件值:" + readyOps);

                    //拿到channel對象
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
                    int readBytes = socketChannel.read(byteBuffer);
                    if (readBytes > 0) {
                        byteBuffer.flip();
                        byte[] bytes = new byte[byteBuffer.remaining()];
                        byteBuffer.get(bytes);
                        String body = new String(bytes, "UTF-8");
                        System.out.println("服務端收到消息 : " + body);
                        //將消息寫回客戶端
                        byte[] resp = body.getBytes();
                        ByteBuffer write = ByteBuffer.allocate(body.getBytes().length);
                        write.put(resp).flip();
                        socketChannel.write(write);
                    }
                    //iterator.remove();
                }
            }
        }
    }
}
  • 客戶端代碼參考

  • 這裏服務端做的事情就是監聽12345,12346,12347三個端口,客戶端連接服務端第一次會觸發可連接事件因此會進入第一個處理邏輯,此時有一個就緒事件值爲16的 SelectionKey 在selectedKeys集合中,如果不移除,客戶端再來往服務端寫數據的時候,按理說第一次的移除之後,此時selectedKeys集合中應該就只有第二個isReadable事件,進入第二個邏輯在處理,不過遺憾的是因爲前面一個 isAcceptable的 SelectionKey沒有移除,因此接收到數據之後,selectedKeys集合裏面會有兩個 SelectedKeys,第一個代表可連接的事件,第二個代表可讀事件,因此收到數據之後還是會先進入第一個處理邏輯,此時又會調用accept,而此時連接早已經建立,應該讀取數據了,再次accept就會報錯,報錯打印如下:

興趣事件值:16
就緒事件值:16
服務端處理端口:64016
獲取到客戶端的連接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:64016]
興趣事件值:16
就緒事件值:16
Exception in thread "main" java.lang.NullPointerException
	at com.intellif.nio.server1.NioServerTest.main(NioServerTest.java:61)
  • 下面是調試過程,很容易幫助我們理解這三個集合的作用以及爲什麼要移除:
  • 首先服務端打上斷點,然後客戶端發起連接,我們能夠看到selector 對象裏的 selectedKeys 集合包含一個元素,它代表一個可連接事件

在這裏插入圖片描述

  • 記住 selectedKeys集合中第一個 SelectedKeys 對象地址是 939 (圖中SelectionKeyImpl對象後面的標示) ,方便後續證明確實對象沒有移除

  • 然後不移除,成功建立了連接,客戶端再發送一個消息,又會再次在上次的 selector.selectedKeys();處進入斷點,我們看到集合中有939的 SelectedKey對象,還有第二個943的對象,代表讀事件,也就是客戶端發送消息後觸發的,

在這裏插入圖片描述

  • 然後一步一步斷點,結果就會再次進入第一個應該處理連接事件的處理邏輯,在accept處拋出異常

在這裏插入圖片描述

  • 下面是一份正常的處理,第二次應該是讀事件,對應的事件值是1
興趣事件值:16
就緒事件值:16
服務端處理端口:64520
獲取到客戶端的連接: java.nio.channels.SocketChannel[connected local=/127.0.0.1:12345 remote=/127.0.0.1:64520]
興趣事件值:1
就緒事件值:1
服務端收到消息 : 1

六、小結

  • 主要分析了 Selector 在NIO中的作用,以及其內部維護的三個集合,通過示例看來移除選擇鍵集合的原因,
  • 後面的文章再看 Selector 的具體實現類的細節

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章