NIO-Selector源碼分析




NIO-Selector源碼分析

目錄

NIO-概覽
NIO-Buffer
NIO-Channel
NIO-Channel接口分析
NIO-SocketChannel源碼分析
NIO-FileChannel源碼分析
NIO-Selector源碼分析
NIO-WindowsSelectorImpl源碼分析
NIO-EPollSelectorIpml源碼分析

前言

本來是想學習Netty的,但是Netty是一個NIO框架,因此在學習netty之前,還是先梳理一下NIO的知識。通過剖析源碼理解NIO的設計原理。

本系列文章針對的是JDK1.8.0.161的源碼。

前幾篇文章對Buffer和Channel的源碼的常用功能進行了研究,本篇將對Selector源碼進行解析。

什么是Selector

在網絡傳輸時,客戶端不定時的會與服務端進行連接,而在高並發場景中,大多數連接實際上是空閑的。因此為了提高網絡傳輸高並發的性能,就出現各種I/O模型從而優化CPU處理效率。不同選擇器實現了不同的I/O模型算法。同步I/O在linux上有EPoll模型,mac上有KQueue模型,windows上則為select模型。

關於I/O模型相關知識可以查看《高性能網絡通訊原理》

為了能知道哪些連接已就緒,在一開始我們需要定時輪詢Socket是否有接收到新的連接,同時我們還要監控是否接收到已建立連接的數據,由於大多數情況下大多數網絡連接實際是空閑的,因此每次都遍歷所有的客戶端,那么隨着並發量的增加,性能開銷也是呈線性增長。

有了Selector,我們可以讓它幫我們做"監控"的動作,而當它監控到連接接收到數據時,我們只要去將數據讀取出來即可,這樣就大大提高了性能。要Selector幫我們做“監控”動作,那么我們需要告知它需要監控哪些Channel

注意,只有網絡通訊的時候才需要通過Selector監控通道。從代碼而言,Channel必須繼承AbstractSelectableChannel

創建Selector

首先我們需要通過靜態方法Selector.open()從創建一個Selector

Selector selector = Selector.open();

需要注意的是,Channel必須是非阻塞的,我們需要手動將Channel設置為非阻塞。調用Channel的實例方法SelectableChannel.configureBlocking(boolean block)

注冊通道

需要告訴Selector監控哪些Channel,通過channel.register將需要監控的通道注冊到Selector

注冊是在AbstractSelectableChannel中實現的,當新的通道向Selector注冊時會創建一個SelectionKey,並將其保存到 SelectionKey[] keys緩存中。


public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException
{
    synchronized (regLock) {
        if (!isOpen())
            throw new ClosedChannelException();
            //當前Channel是否支持操作
        if ((ops & ~validOps()) != 0)
            throw new IllegalArgumentException();
            //阻塞不支持
        if (blocking)
            throw new IllegalBlockingModeException();
        SelectionKey k = findKey(sel);
        if (k != null) {
            //已經存在,則將其注冊支持的操作
            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);
                //添加到緩存
                addKey(k);
            }
        }
        return k;
    }
}

新的SelectionKey會調用到AbstractSelector.register,首先會先創建一個SelectionKeyImpl,然后調用方法implRegister執行實際注冊,該功能是在各個平台的SelectorImpl的實現類中做具體實現。

k = ((AbstractSelector)sel).register(this, ops, att);
protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment)
{
    if (!(ch instanceof SelChImpl))
        throw new IllegalSelectorException();
        //創建SelectionKey
    SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
    k.attach(attachment);
    synchronized (publicKeys) {
        //注冊
        implRegister(k);
    }
    //設置事件
    k.interestOps(ops);
    return k;
}

創建了SelectionKey后就將他加入到keys的緩存中,當keys緩存不足時,擴容兩倍大小。

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 {
        // 擴容兩倍大小
        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++;
}

SelectorProvider

在討論Selector如何工作之前,我們先看一下Selector是如何創建的。我們通過Selector.open()靜態方法創建了一個Selector。內部實際是通過SelectorProvider.openSelector()方法創建Selector

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

創建SelectorProvider

通過SelectorProvider.provider()靜態方法,獲取到SelectorProvider,首次獲取時會通過配置等方式注入,若沒有配置,則使用DefaultSelectorProvider生成。

public static SelectorProvider provider() {
    synchronized (lock) {
        if (provider != null)
            return provider;
        return AccessController.doPrivileged(
            new PrivilegedAction<SelectorProvider>() {
                public SelectorProvider run() {
                        //通過配置的java.nio.channels.spi.SelectorProvider值注入自定義的SelectorProvider
                        if (loadProviderFromProperty())
                            return provider;
                        //通過ServiceLoad注入,然后獲取配置的第一個服務
                        if (loadProviderAsService())
                            return provider;
                        provider = sun.nio.ch.DefaultSelectorProvider.create();
                        return provider;
                    }
                });
    }
}

若我們沒有做特殊配置,則會使用默認的DefaultSelectorProvider創建SelectorProvider
不同平台的DefaultSelectorProvider實現不一樣。可以在jdk\src\[macosx|windows|solaris]\classes\sun\nio\ch找到實現DefaultSelectorProvider.java。下面是SelectorProvider的實現。

//windows
public class DefaultSelectorProvider {
    private DefaultSelectorProvider() { }
    public static SelectorProvider create() {
        return new sun.nio.ch.WindowsSelectorProvider();
    }
}
//linux
public class DefaultSelectorProvider {

    private DefaultSelectorProvider() { }

    @SuppressWarnings("unchecked")
    private static SelectorProvider createProvider(String cn) {
        Class<SelectorProvider> c;
        try {
            c = (Class<SelectorProvider>)Class.forName(cn);
        } catch (ClassNotFoundException x) {
            throw new AssertionError(x);
        }
        try {
            return c.newInstance();
        } catch (IllegalAccessException | InstantiationException x) {
            throw new AssertionError(x);
        }
    }

    public static SelectorProvider create() {
        String osname = AccessController
            .doPrivileged(new GetPropertyAction("os.name"));
        if (osname.equals("SunOS"))
            return createProvider("sun.nio.ch.DevPollSelectorProvider");
        if (osname.equals("Linux"))
            return createProvider("sun.nio.ch.EPollSelectorProvider");
        return new sun.nio.ch.PollSelectorProvider();
    }

}

創建Selector

獲取到SelectorProvider后,創建Selector了。通過SelectorProvider.openSelector()實例方法創建一個Selector

//windows
public class WindowsSelectorProvider extends SelectorProviderImpl {

    public AbstractSelector openSelector() throws IOException {
        return new WindowsSelectorImpl(this);
    }
}
//linux
public class EPollSelectorProvider
    extends SelectorProviderImpl
{
    public AbstractSelector openSelector() throws IOException {
        return new EPollSelectorImpl(this);
    }
    ...
}

windows下創建了WindowsSelectorImpl,linux下創建了EPollSelectorImpl

所有的XXXSelectorImpl都繼承自SelectorImpl,可以在jdk\src\[macosx|windows|solaris|share]\classes\sun\nio\ch找到實現XXXSelectorImpl.java。繼承關系如下圖所示。
20200102205431.png

接下里我們討論一下Selector提供的主要功能,后面在分析Windows和Linux下Selector的具體實現。

SelectorImpl

在創建SelectorImpl首先會初始化2個HashSet,publicKeys存放用於一個存放所有注冊的SelectionKey,selectedKeys用於存放已就緒的SelectionKey。

protected SelectorImpl(SelectorProvider sp) {
    super(sp);
    keys = new HashSet<SelectionKey>();
    selectedKeys = new HashSet<SelectionKey>();
    if (Util.atBugLevel("1.4")) {
        publicKeys = keys;
        publicSelectedKeys = selectedKeys;
    } else {
        //創建一個不可修改的集合
        publicKeys = Collections.unmodifiableSet(keys);
        //創建一個只能刪除不能添加的集合
        publicSelectedKeys = Util.ungrowableSet(selectedKeys);
    }
}

關於Util.atBugLevel找到一篇文章有提到該方法。似乎是和EPoll的一個空指針異常相關。這個bug在nio bugLevel=1.4版本引入,這個bug在jdk1.5中存在,直到jdk1.7才修復。

前面我們已經向Selector注冊了通道,現在我們需要調用Selector.select()實例方法從系統內存中加載已就緒的文件描述符。


public int select() throws IOException {
    return select(0);
}
public int select(long timeout)
    throws IOException
{
    if (timeout < 0)
        throw new IllegalArgumentException("Negative timeout");
    return lockAndDoSelect((timeout == 0) ? -1 : timeout);
}

private int lockAndDoSelect(long timeout) throws IOException {
    synchronized (this) {
        if (!isOpen())
            throw new ClosedSelectorException();
        synchronized (publicKeys) {
            synchronized (publicSelectedKeys) {
                return doSelect(timeout);
            }
        }
    }
}
protected abstract int doSelect(long timeout) throws IOException;

最終會調用具體SelectorImpldoSelect,具體內部主要執行2件事

  1. 調用native方法獲取已就緒的文件描述符。
  2. 調用updateSelectedKeys更新已就緒事件的SelectorKey

當獲取到已就緒的SelectionKey后,我們就可以遍歷他們。根據SelectionKey的事件類型決定需要執行的具體邏輯。

//獲取到已就緒的Key進行遍歷
Set<SelectionKey> selectKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectKeys.iterator();
while (it.hasNext()) {
    SelectionKey key = it.next();
    //處理事件。
    if(key.isAcceptable()){
        doAccept(key);
    }
    else if(key.isReadable())
    {
        doRead(key);
    }
    ...
    it.remove();
}

總結

本文對SelectorSelectorProvider的創建進行分析,總的流程可以參考下圖

對於后面步驟的EpollArrayWarpper()會在SelectorImpl個平台具體實現進行講解。后面會分2對WindowsSelectorImplEpollSelectorImpl進行分析。

相關文獻

  1. ServiceLoader詳解
  2. SelectorImpl分析
  3. NIO源碼分析(一)

20191127212134.png
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:https://www.cnblogs.com/Jack-Blog/p/12367953.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及鏈接。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM