JDK IO多路復用基礎


本文所有源碼均基于JDK1.8版本

通過學習JDK提供的IO多路復用相關源碼,為后續的Netty源碼學習打下基礎

IO多路復用底層實現

JDK nio包多路復用基于底層操作系統,linux2.6版本使用epoll實現,低于2.6版本則使用select或poll實現多路復用


JDK NIO

Channel

該接口用于操作(讀,寫)數據源(文件,網絡socket等),并提供多種實現如FileChannel, ServerSocketChannel等,替換基于流的形式操作數據源

  • 通道是全雙工的,它可以比流更好地映射底層操作系統的API。特別是在UNIX網絡編程模型中,底層操作系統的通道都是全雙工的,同時支持讀寫操作

  • Channel用于在字節緩沖區和位于通道另一側的實體(通常是一個文件或套接字)之間有效地傳輸數據

SelectorProvider

一個抽象類,用于selector和可選擇的channel上,包括打開一個selector,打開一個 server-socket channel,或socket channel,同時該類持有自身類的一個屬性provider

該類提供了一個靜態方法provider(),用于獲取當前操作系統范圍下默認的支持的selector provider

public abstract class SelectorProvider {

    private static final Object lock = new Object();

    // 持有自身實例的引用
    private static SelectorProvider provider = null;

    // 獲取可用的selectorProvider
    public static SelectorProvider provider() {
        synchronized (lock) {
            // 如果已存在,直接返回
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            // 1. 從屬性配置中利用反射實例化selectorProvider
                            if (loadProviderFromProperty())
                                return provider;
                            // 2. 利用Java SPI加載(META-INF/services/)并反射實例化selectorProvider
                            if (loadProviderAsService())
                                return provider;
                            // 3. 以上都沒有的話,就采用默認jdk環境下默認的selectorProvider,windows下為WindowsSelectorProvider
                            // Linux內核2.6及以上版本,采用EPollSelectorProvider,
                            //低版本內核使用PollSelectorProvider
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

    // 從屬性配置中利用反射實例化selectorProvider
    private static boolean loadProviderFromProperty() {
        String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
        if (cn == null)
            return false;
        try {
            Class<?> c = Class.forName(cn, true,
                                    ClassLoader.getSystemClassLoader());
            provider = (SelectorProvider)c.newInstance();
            return true;
        } catch (ClassNotFoundException x) {
            throw new ServiceConfigurationError(null, x);
        } catch (IllegalAccessException x) {
            throw new ServiceConfigurationError(null, x);
        } catch (InstantiationException x) {
            throw new ServiceConfigurationError(null, x);
        } catch (SecurityException x) {
            throw new ServiceConfigurationError(null, x);
        }
    }

    // 利用ServiceLoader加載META-INF/services下全限定接口文件實例化文件內的實現類
    private static boolean loadProviderAsService() {

        ServiceLoader<SelectorProvider> sl =
            ServiceLoader.load(SelectorProvider.class,
                            ClassLoader.getSystemClassLoader());
        Iterator<SelectorProvider> i = sl.iterator();
        for (;;) {
            try {
                if (!i.hasNext())
                    return false;
                provider = i.next();
                return true;
            } catch (ServiceConfigurationError sce) {
                if (sce.getCause() instanceof SecurityException) {
                    // Ignore the security exception, try the next provider
                    continue;
                }
                throw sce;
            }
        }
    }

    // 打開一個selector(也是一個文件)
    public abstract AbstractSelector openSelector() throws IOException;

    // 打開一個 ServerSocketChannel
    public abstract ServerSocketChannel openServerSocketChannel() throws IOException;

    // 打開一個 SocketChannel
    public abstract SocketChannel openSocketChannel() throws IOException;

    // ...
}
WindowsSelectorProvider UML 類圖
WindowsSelectorProvider

抽象類 Selector

jdk.nio中的selector對應操作系統底層的select結構(select/poll/epoll),在linux下也是一個文件

windows下的具體的Selector實現類是WindowsSelectorImpl

WindowsSelectorImpl UML類圖
WindowsSelectorImpl
Selector抽象類

看下Selector中定義了哪些方法

public abstract class Selector implements Closeable {


    // 1. 通過open方法,內部還是調用了SelectorProvider的provider方法得到選擇器供應商,再調用其openSelector方法獲取selector
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

    // 2. 返回是否該selector是打開狀態
    public abstract boolean isOpen();

    // 3. 返回創建了該selector的selectorProvider
    public abstract SelectorProvider provider();

    // 返回該selector選擇器的注冊上來的key 集合 
    public abstract Set<SelectionKey> keys();

    // 返回該selector選擇器的注冊上來并且被選擇的key 集合 
    public abstract Set<SelectionKey> selectedKeys();

    // 選擇準備好IO操作的相應通道的keys集合,即發起系統調用,該方法會被阻塞,直到至少一個通道被選擇
    // 或該selector的wakeup方法被調用,或當前線程被打斷,或給定的事件timeout超時
    // 參數timeout若為正,則阻塞指定毫秒后返回0
    // 若為0,則無限阻塞
    // 不可為負數
    // 返回值代表了準備好IO操作的keys數量,可能返回0
    public abstract int select(long timeout)
        throws IOException;


    public abstract int select() throws IOException;

    // 調用該方法,會導致還未阻塞返回的selector立即返回
    public abstract Selector wakeup();

}

主要包括如下:

  • 獲取當前selector上注冊的所有keys

  • 獲取當前selector上所有已選擇的keys

  • 阻塞方法select()

  • 喚醒方法wakeup()

  • 其子類AbstractSelector定義的cancelledKeys(),register(),deregister()方法等

記住一點就是selector會維護三種key的集合

  • key set
    代表當前通道注冊到該selector上的集合

  • selected-key set
    代表注冊的通道發生了注冊感興趣的事件(ACCEPT/讀/寫),其selectionKey就會被加入到selected-key set中

  • canceled-key set
    當調用了cancel方法,相應的selectionKey會被加入cancelledKeys集合中

AbstractSelector 類

該類繼承了Selector類,主要定義了抽象方法register方法以及一個
cancelledKeys的集合和provider屬性持有

重點看register方法

protected abstract SelectionKey register(AbstractSelectableChannel ch,
                                         int ops, Object att);

該方法有三個參數

  1. 可被選擇的通道ch

  2. 感興趣的事件ops

  3. 附加屬性

返回值SelectionKey對象,代表了一個注冊到當前selector上的給定通道,并指定了感興趣的事件

注冊方法的具體實現由SelectorImpl類實現

SelectorImpl

protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
    if (!(var1 instanceof SelChImpl)) {
        throw new IllegalSelectorException();
    } else {
        SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
        var4.attach(var3);
        synchronized(this.publicKeys) {
            this.implRegister(var4);
        }

        var4.interestOps(var2);
        return var4;
    }
}

可以看到這里就是new了一個SelectionKeyImpl,并將其注冊到selector上(即加入到keys set集合中)同時設置感興趣的事件到該SelectionKey實例的interestOps屬性中

注冊操作:

  1. 添加SelectionKey至Java Selector對象的keys集合

  2. 記錄Java Selector對象的fdMap屬性,key為該SelectionKey注冊的channel的fd值,value為該SelectionKey

  3. 調用操作系統底層epoll_ctl系統調用中段注冊epollfd需要監聽的channel指定的事件

SelectionKeyImpl 選擇鍵

一個SelectionKey即代表了一個channel和其注冊的selector之間的關系,即通過這個SelectionKey就可以知道是哪個channel注冊到了哪個selector上,并且其希望Selector監聽該Channel的哪些事件

UML類圖
image.png

該類有幾個主要屬性

SelChImpl channel

注冊的通道

SelectorImpl selector

通道注冊到哪個選擇器

 int interestOps

注冊時感興趣的操作事件

readyOps

已準備好的操作事件

該類同時定義了一些列的操作常量如:OP_READ, OP_WRITE, OP_CONNECT, OP_ACCEPT

以及一些判斷是否該key的channel準備好了相應的事件如:isReadable(),isWritable(), isConnectable(), isAcceptable(),其判斷邏輯就是用key的內部屬性readyOps中是否含有指定的事件

select方法詳解

Java Selector有兩個重載的select方法以及一個selectNow()方法

public abstract int select() throws IOException;

public abstract int select(long timeout) throws IOException;

public abstract int selectNow() throws IOException;
  • selectNow()

該方法是非阻塞的,如果沒有channels變為可選擇的,會立即返回0

  • select()

該方法時阻塞的,返回的前提是至少有一個channel是可選擇的或該selector的wakeup方法被調用,或者運行select方法的當前線程被打斷

  • select(long timeout)

該方法基本上同select(),只是另外提供了timeout參數用于當沒有channel可選擇時,阻塞等待的毫秒數,注意該參數為0即代表一直阻塞,需要大于0才生效

以上三個方法最終的調用都會轉到doSelect方法

這里以windows環境為例,WindowsSelectorImpl

定義了最大可選擇的fd數量為1024

private static final int MAX_SELECTABLE_FDS = 1024;

doSelect偽代碼

protected int doSelect(long var1) throws IOException {

    // 1. 處理取消注冊channel
    this.processDeregisterQueue();

    // 2. 調用內部SubSelector的poll方法,SubSelector類為WindowsSelectorImpl的內部類,封裝了系統調用poll的相關native方法
    this.subSelector.poll();

    // 3. 再次處理取消注冊的channel
    this.processDeregisterQueue();

    // 4. 更新可選擇channel的keys到selector的selectedKeys屬性中
    int var3 = this.updateSelectedKeys();

    // 5. 返回可選擇key的數量
    return var3;
}

其中1. 中主要處理已取消注冊通道,即若cancelledKeys集合不為空,則遍歷依次取消該Key

主要包括

  1. 調用內核取消該fd的指定事件的監聽

  2. fdMap屬性的清除

  3. keys屬性清除

  4. selectedKeys屬性清除

  5. 失效該SelectionKey,設置valid屬性為false

而poll()的調用即是發起內核系統調用,阻塞獲取可選擇的fd

最后一個的updateSelectedKeys()方法就是根據內核返回的可選擇的fd去fdMap中找到對應的SelectionKey,再將SelectionKey加入到該selector的selectedKeys集合中,這樣我們應用程序的select方法返回后就可以從selectedKeys集合中獲取可選擇的key,進而進行我們自己的處理

總結

最后以一張圖表示整個IO多路復用流程

image.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容