Java NIO Selector詳解

Selector選擇器

Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠發現通道是否爲讀寫等事件做好準備的組件。這樣,一個單獨的線程可以管理多個channel,從而管理多個網絡連接。

Selector的實現根據JVM運行的操作系統不同會有相應的不同的實現,上層API對底層做了抽象,這樣上層API無需關心底層操作系統的變化,可以在不同操作系統上實現相同的功能。

實現了SelectableChannel接口的通道可以被註冊到Selector上,配合Selector工作,實現IO多路複用。

SelectionKey選擇鍵

選擇鍵封裝了某一個的通道與某一個的選擇器的註冊關係。通道和選擇器往往是配合工作,選擇鍵對象被SelectableChannel.register()返回並提供一個表示這種註冊關係的鍵。選擇鍵包含了兩個比特集(以整數的形式進行編碼),指示了該註冊關係所關心的通道操作,以及通道已經準備好的操作。

public abstract class SelectionKey {

    protected SelectionKey() { }

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

    public abstract int readyOps();

     //關心的操作的比特掩碼
    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;

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

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

}

一個鍵表示了一個特定的通道對象和一個特定的選擇器對象之間的註冊關係。

channel()方法返回與該鍵相關的SelectableChannel對象。

selector()則返回相關的Selector對象。

調用SelectionKey對象的cancel()方法可以取消通道和選擇器的關聯。可以通過調用isValid()方法來檢查它是否仍然是有效的關聯關係。當鍵被取消時,它將被放在相關的選擇器的已取消的鍵的集合裏。註冊不會立即被取消,但鍵會立即失效。當再次調用select()方法時(或者一個正在進行的select()調用結束時),已取消的鍵的集合中的被取消的鍵將被清理掉,並且相應的註銷也將完成。當通道關閉時,所有相關的鍵會自動取消。當選擇器關閉時,所有被註冊到該選擇器的通道都將被註銷,並且相關的鍵將立即被無效化。一旦鍵被無效化,調用它的與選擇相關的方法就將拋出CancelledKeyException。

    public static final int OP_READ = 1 << 0;//1

    public static final int OP_WRITE = 1 << 2;//4

    public static final int OP_CONNECT = 1 << 3;//8

    public static final int OP_ACCEPT = 1 << 4;//16

上面的常量表示通道相關的操作的比特掩碼。

一個SelectionKey對象包含兩個以整數形式進行編碼的比特掩碼:一個用於指示那些通道選擇器組合體所關心的操作(instrest集合),另一個表示通道準備好要執行的操作(ready集合)。

  • instrest集合:當前的interest集合可以通過調用鍵對象的interestOps()方法來獲取。可以通過調用interestOps()方法並傳入一個新的比特掩碼參數來改變它。當相關的Selector上的select()操作正在進行時改變鍵的interest集合,不會影響那個正在進行的選擇操作。所有更改將會在select()的下一個調用中體現出來。

  • ready集合:可以通過調用鍵的readyOps()方法來獲取相關的通道的已經就緒的操作,不能直接改變鍵的ready集合。ready集合是interest集合的子集,並且表示了interest集合中從上次調用select()以來已經就緒的那些操作。

SelectionKey類定義了四個便於使用的布爾方法來爲您測試這些比特值,用來檢測channel中什麼事件或操作已經就緒:

  • isReadable()
  • isWritable()
  • isConnectable()
  • isAcceptable()

attach()方法將在鍵對象中保存所提供的對象的引用。SelectionKey類除了保存它之外,不 會將它用於任何其他用途。任何一個之前保存在鍵中的附件引用都會被替換。可以使用null值來清除附件。可以通過調用attachment()方法來獲取與鍵關聯的附件句柄。

Selector API

創建

Selector selector = Selector.open();
public abstract class Selector implements Closeable {

    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

}

通過Selector的open()方法可以獲取一個選擇器實例,底層通過SelectorProvider創建一個相應的通道實例。SelectorProvider實例根據JVM運行的操作系統不同會有相應的不同的實現,上層API無需關心底層操作系統的變化。

註冊

    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
    public final SelectionKey register(Selector sel, int ops,
                                       Object att)
        throws ClosedChannelException
    {
        synchronized (regLock) {
            if (!isOpen())
                throw new ClosedChannelException();
            if ((ops & ~validOps()) != 0) //validOps是通道支持的所有操作的比特碼
                throw new IllegalArgumentException();
            if (blocking)
                throw new IllegalBlockingModeException();
            SelectionKey k = findKey(sel);//查看通道是否已經註冊到該選擇器
            if (k != null) {//已經註冊上則只更新其關心的IO操作和附件attach
                k.interestOps(ops);
                k.attach(att);
            }
            if (k == null) {
                // New registration
                synchronized (keyLock) {
                    if (!isOpen())
                        throw new ClosedChannelException();
                    k = ((AbstractSelector)sel).register(this, ops, att);//最終要掉用Selector的register方法將自己註冊到選擇器上,並將SelectionKey添加到Selector維護的集合中
                    addKey(k);//添加到當前通道維護的SelectionKey集合
                }
            }
            return k;
        }
    }

與Selector一起使用時,Channel必須處於非阻塞模式下。

register()方法定義在SelectableChannel接口上,接受一個Selector對象作爲參數,以及一個名爲ops的整數參數。第二個參數表示所關心的通道操作。這是一個表示選擇器在檢查通道就緒狀態時需要關心的操作的比特掩碼。特定的操作比特值在SelectonKey類中被定義爲public static字段。

register()方法首先檢測通道是open的且要關心的操作是當前通道支持的,且是非阻塞狀態的通道。通過校驗之後,會在通道所維護的鍵集合中查看是否已經有當前通道和當前選擇器相關聯SelectionKey,有意味着已經註冊過了,則修改關心的操作和附件;沒有則需要調用Selector的註冊方法,將當前通道註冊到選擇器上,註冊操作會將該SelectionKey添加Selector維護的已註冊的集合中,並添加到當前通道維護的SelectionKey集合中。

選擇器內部三個集合

public abstract class Selector implements Closeable {

    public abstract Set<SelectionKey> keys();

    public abstract Set<SelectionKey> selectedKeys();

}

選擇器維護着已註冊的鍵的集合,已選擇的鍵的集合,已取消的鍵的集合:

  • 已註冊的鍵的集合

    keys()方法返回與選擇器關聯的已經註冊的鍵的集合。並不是所有註冊過的鍵都仍然有效。這個集合通過keys()方法返回,並且可能是空的。這個已註冊的鍵的集合是不可以直接修改的,只能通過註冊和註銷選擇器的行爲添加或者移除。

  • 已選擇的鍵的集合

    selectedKeys()方法返回已選擇的鍵的集合。他是已註冊的鍵的集合的子集。這個集合的每個成員都是相關的通道被選擇器上一次選擇過程中被識別爲已經準備好的IO操作的,並且是包含於鍵的interest集合中的IO操作。這個集合通過selectedKeys()方法返回,並有可能是空的。集合中每個鍵都關聯一個已經準備好至少一種操作的通道。每個鍵都有一個內嵌的ready集合,指示了所關聯的通道已經準備好何種操作。
    鍵可以直接從這個集合中移除,但不能添加。

  • 已取消的鍵的集合

    已註冊的鍵的集合的子集,這個集合包含了鍵的cancel()方法被調用過的鍵,但它們還沒有被註銷,它們會在每次選擇操作前後完成註銷。這個集合是選擇器對象的私有成員,因而無法直接訪問。

在一個剛初始化的 Selector 對象中,這三個集合都是空的。

選擇

    public int select(long var1) throws IOException;

    public int select() throws IOException;

    public int selectNow() throws IOException;

select方法返回的int值表示有多少通道已經就緒。有可能是0。這三種select的形式,僅在阻塞和超時設置上有所不同。

  • select()阻塞到至少有一個通道在你註冊的事件上就緒了。

  • select(long timeout)和select()一樣,但是最長會阻塞timeout毫秒(參數)。

  • selectNow()不會阻塞,不管什麼通道就緒都立刻返回。

選擇方法是選擇器的核心,選擇方法是對select()、poll()、epoll()等本地調用或者類似的操作系統特定的系統調用的一個包裝,依賴底層操作系統的支持。

當三種形式的select()中的任意一種被調用時,下面步驟將被執行:

  1. 已取消的鍵的集合將會被檢查。如果它是非空的,每個已取消的鍵的集合中的鍵將從另外兩 個集合中移除,並且相關的通道將被註銷。這個步驟結束後,已取消的鍵的集合將是空的。

  2. 執行底層操作系統相關的select,poll或者epoll之類的底層操作系統調用,底層操作系統將會進行檢查,以確定每個通道所關心的操作的真實就緒狀態。依賴於特定的select()方法調用,如果沒有通道已經準備好,線程可能會在這時阻塞,通常會有一個超時值。

  3. 步驟2可能會花費很長時間,特別是線程處於休眠狀態時。與該選擇器相關的鍵可能會同時被取消。當步驟2結束時,步驟1將重新執行,以完成任意一個在選擇進行的過程中,鍵已經被取消的通道的註銷。

  4. 當前每個通道的就緒狀態將確定下來。對於那些還沒準備好的通道將不會執行任何的操作。對於那些操作系統指示至少已經準備好interest集合中的一種操作的通道,將執行以下兩種操作中的一種:

    • a. 如果通道的鍵還沒有處於已選擇的鍵的集合中,那麼鍵的ready集合將先被清空,然後表示操作系統發現的當前通道已經準備好的操作的比特掩碼將被設置。

    • b.否則,也就是鍵在已選擇的鍵的集合中。鍵的ready集合將被表示操作系統發現的當前已經準備好的操作的比特掩碼更新。所有之前的已經不再是就緒狀態的操作不會被清除。由操作系統決定的ready集合是與之前的ready集合按位分離的,一旦鍵被放置於選擇器的已選擇的鍵的集合中,它的ready集合將是累積的。比特位只會被添加,不會被清理,所以一般在select之後操作時會將已選擇的鍵從已選擇的鍵的集合中移除。

    select操作返回的值是在步驟4中被修改的鍵的數量,返回值不是已準備好的通道的總數,而是從上一個select()調用之後進入就緒狀態的通道的數量。之前的調用中就緒的,並且在本次調用中仍然就緒的通道不會被計入,而那些在前一次調用中已經就緒但已經不再處於就緒狀態的通道也不會被計入。這些通道可能仍然在已選擇的鍵的集合中,但不會被計入返回值中。

使用內部的已取消的鍵的集合來延遲註銷,是一種防止線程在取消鍵時阻塞,並防止與正在進行的選擇操作衝突的優化。註銷通道是一個潛在的代價很高的操作,這可能需要重新分配資源。清理已取消的鍵,並在選擇操作之前和之後立即註銷通道,可以消除它們可能正好在選擇的過程中執行的潛在棘手問題。這是另一個兼顧健壯性的折中方案。

停止選擇

public abstract class Selector implements Closeable {

    //方法1
    public abstract void wakeup()

    //方法2
    public abstract void close() throws IOException;

}
public class Thread implements Runnable {

    //方法3
    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

}

使線程從被阻塞的select()方法中退出的三種方法:

  1. wakeup()

    調用Selector對象的wakeup()方法將使得選擇器上的第一個還沒有返回的選擇操作立即返回。如果當前沒有在進行中的選擇,那麼下一次對select()方法的一種形式的調用將立即返回。在選擇操作之間多次調用wakeup()方法與調用它一次沒有什麼不同。wakeup()提供了使線程從被阻塞的select()方法中優雅地退出的能力。

  2. close()

    調用Selector對象的close()方法,那麼任何一個在選擇操作中阻塞的線程都將被喚醒,因爲內部會調用wakeup()方法。與選擇器相關的通道將被註銷,而鍵將被取消。

  3. interrupt()

    調用選擇過程中的線程的interrupt()方法,Selector對象將捕捉InterruptedException異常並調用wakeup()方法。如果被喚醒的線程之後試圖在通道上執行I/O操作,通道將立即關閉,需要清理中斷狀態。

代碼示例

通過一個服務端應用的代碼示例展示選擇器如何與通道和緩衝區共同使用,具體選擇器和選擇鍵還有通道如何相互配合以及底層如何調用還是自己看一下關鍵方法的源代碼瞭解一下吧。^_^

public class SocketServer3 {

    private static final Logger LOGGER = LoggerFactory.getLogger(SocketServer3.class);


    private static final int PORT_NUMBER = 10003;
    private static ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

    public static void main(String[] args) throws Exception {
        int port = PORT_NUMBER;
        LOGGER.info("Listening on port " + port);

        Selector selector = Selector.open();

        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(port));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {

            //查看是否有註冊的IO事件發生
            int n = selector.select();
            if (n == 0) {
                continue; // nothing to do
            }

            //獲取已準備好IO事件的通道集合
            Iterator it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();

                //通道有accept事件發生
                if (key.isValid() && key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    registerChannel(selector, channel, SelectionKey.OP_READ);
                }

                //通道有read事件發生
                if (key.isValid() && key.isReadable()) {
                    readDataFromSocket(key);
                }

                //通道有write事件發生
                if (key.isValid() && key.isWritable()) {
                    LOGGER.info("isWritable = true");
                }

                //通道有connect事件發生
                if (key.isValid() && key.isConnectable()) {
                    LOGGER.info("isConnectable = true");
                }

                //移除已處理IO事件的通道
                it.remove();
            }
        }
    }

    /**
     * 處理監聽IO操作,將接收到通道註冊到Selector上
     * @param selector
     * @param channel
     * @param ops
     * @throws Exception
     */
    private static void registerChannel(Selector selector, SelectableChannel channel, int ops) throws Exception {
        if (channel == null) {
            return;
        }
        channel.configureBlocking(false);
        channel.register(selector, ops);
    }

    /**
     * 處理讀取IO操作,將服務端接收到的數據發送回客戶端
     * @param key
     * @throws Exception
     */
    private static void readDataFromSocket(SelectionKey key) throws Exception {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int count;
        buffer.clear();

        while ((count = socketChannel.read(buffer)) > 0) {
            buffer.flip();
            while (buffer.hasRemaining()) {
                socketChannel.write(buffer);
            }
            buffer.clear();
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        }

        if (count < 0) {
            // Close channel on EOF, invalidates the key
            socketChannel.close();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章