深入理解NIO - Selector、ServerSocketChannel、SocketChannel底層原理

IO多路複用模型

IO多路複用需要OS的支持,IO多路複用模型中,引入了一種新的系統調用,查詢IO的就緒狀態。

在Linux系統中,JavaNIO的Selector#select() 方法對應的系統調用爲select/epoll系統調用。

在這裏插入圖片描述

通過該系統調用,一個進程可以監視多個文件描述符,一旦某個描述符就緒(一般是內核緩衝區可讀/可寫),內核能夠將就緒的狀態返回給應用程序。隨後,應用程序根據就緒的狀態,進行相應的IO系統調用。

在這裏插入圖片描述

目前支持IO多路複用的系統調用,有select、epoll等等。select系統調用,幾乎在所有的操作系統上都有支持,具有良好的跨平臺特性。epoll是在Linux 2.6內核中提出的,是select系統調用的Linux增強版本。

在IO多路複用模型中通過select/epoll系統調用單個應用程序的線程,可以不斷地輪詢成百上千的socket連接,當某個或者某些socket網絡連接有IO就緒的狀態,就返回對應的可以執行的讀寫操作。

在這裏插入圖片描述

IO多路複用模型的特點:

IO多路複用模型的IO涉及兩種系統調用(System Call),一種是IO操作,另一種是select/epoll(就緒查詢)。

IO多路複用模型建立在操作系統的基礎設施之上,即操作系統的內核必須能夠提供多路分離的系統調用select/epoll。

多路複用IO也需要輪詢。負責select/epoll狀態查詢調用的線程,需要不斷地進行select/epoll輪詢,查找出達到IO操作就緒的socket連接。IO多路複用模型與同步非阻塞IO模型是有密切關係的。

對於註冊在選擇器上的每一個可以查詢的socket連接,一般都設置成爲同步非阻塞模型。僅是這一點,對於用戶程序而言是無感知的。

IO多路複用模型的優點:

與一個線程維護一個連接的阻塞IO模式相比,使用select/epoll的最大優勢在於,一個選擇器查詢線程可以同時處理成千上萬個連接(Connection)。系統不必創建大量的線程,也不必維護這些線程,從而大大減小了系統的開銷。Java語言的NIO(New IO)技術,使用的就是IO多路複用模型。在Linux系統上,使用的是epoll系統調用。

IO多路複用模型的缺點:

本質上,select/epoll系統調用是阻塞式的,屬於同步IO。都需要在讀寫事件就緒後,由系統調用本身負責進行讀寫,也就是說這個讀寫過程是阻塞的。

⭐️注意:輪詢的效率比較低,在硬件和操作系統層有個概念叫做“中斷”, 這種機制可以在設備準備好的情況下,主動通知等待的程序。

Selector


Selector selector = Selector.open();

調用棧如下,在我的筆記本上open() 方法就是返回一個WindowsSelectorImpl 實例,在其構造方法中調用了一些native 方法。

<init>:126, WindowsSelectorImpl (sun.nio.ch)
openSelector:44, WindowsSelectorProvider (sun.nio.ch)
open:227, Selector (java.nio.channels)
startServer:63, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
在這裏插入圖片描述

ServerSocketChannel

 // 2、獲取通道
 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 // 3.設置爲非阻塞
 serverSocketChannel.configureBlocking(false);
 // 4、綁定連接
 ServerSocket serverSocket = serverSocketChannel.socket();
 InetSocketAddress address
         = new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT);
 serverSocket.bind(address);
 // 5、將通道註冊到選擇器上,並註冊的IO事件爲:“接收新連接”
 Print.tcfo("serverSocketChannel is linstening...");
 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

ServerSocketChannel.open() 創建一個ServerSocketChannel 對象(實際類型是 ServerSocketChannelImpl)對象,如下圖

# new ServerSocketChannelImpl(SelectorProviderImpl)
openServerSocketChannel:56, SelectorProviderImpl (sun.nio.ch)
open:108, ServerSocketChannel (java.nio.channels)
run:120, PipeImpl$Initializer$LoopbackConnector (sun.nio.ch)
run:76, PipeImpl$Initializer (sun.nio.ch)
run:61, PipeImpl$Initializer (sun.nio.ch)
doPrivileged:-1, AccessController (java.security)
<init>:171, PipeImpl (sun.nio.ch)
openPipe:50, SelectorProviderImpl (sun.nio.ch)
open:155, Pipe (java.nio.channels)
<init>:127, WindowsSelectorImpl (sun.nio.ch)
openSelector:44, WindowsSelectorProvider (sun.nio.ch)
open:227, Selector (java.nio.channels)
startServer:63, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
在這裏插入圖片描述

serverSocketChannel.socket() 方法,在ServerSocketChannel 中創建一個SocketChannel 對象 (實際類型是ServerSocketAdaptor)

socket:110, ServerSocketChannelImpl (sun.nio.ch)
createServerSocketChannel:110, NioReceiveServer (com.yh.stu.nio.socket)
startServer:65, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
在這裏插入圖片描述

serverSocket.bind(address) 方法最終調用的是 ServerSocketChannelbind(..) 方法,調用棧和示例圖如下:

bind:220, ServerSocketChannelImpl (sun.nio.ch)
bind:74, ServerSocketAdaptor (sun.nio.ch)
bind:67, ServerSocketAdaptor (sun.nio.ch)
createServerSocketChannel:113, NioReceiveServer (com.yh.stu.nio.socket)
startServer:65, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)

| 在這裏插入圖片描述 ||
|–|

調用 WindowsSelectorImpl 父類 SelectorImplregister 方法將進行註冊,源碼如下:

	//sun.nio.ch.SelectorImpl#register
    protected final SelectionKey register(AbstractSelectableChannel ch,
                                          int ops,
                                          Object attachment)
    {
        if (!(ch instanceof SelChImpl))
            throw new IllegalSelectorException();
        //將`Selector` 和`ServerSocketChannel` 包裝成了 `SelectionKeyImpl `對象
        SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
        k.attach(attachment);
        synchronized (publicKeys) {
            implRegister(k);
        }
        k.interestOps(ops);
        return k;
    }
    
	// sun.nio.ch.WindowsSelectorImpl#implRegister
    protected void implRegister(SelectionKeyImpl ski) {
        synchronized (closeLock) {
            if (pollWrapper == null)
                throw new ClosedSelectorException();
            growIfNeeded();
            channelArray[totalChannels] = ski;
            ski.setIndex(totalChannels);
            fdMap.put(ski);
            keys.add(ski);// 將 `SelectionKeyImpl ski` 放入`HashSet` 中
            pollWrapper.addEntry(totalChannels, ski);
            totalChannels++;
        }
    }
在這裏插入圖片描述

從源碼中不難看出,將SelectorServerSocketChannel 包裝成了 SelectionKeyImpl對象,並將 SelectionKeyImpl 放入Set 中,同時SelectionKey.OP_ACCEPT

在這裏插入圖片描述

其實在ServerSocketChannel 中也有一個數組,用來放 SelectionKey 的引用,方法調用棧和圖示如下

addKey:115, AbstractSelectableChannel (java.nio.channels.spi)
register:213, AbstractSelectableChannel (java.nio.channels.spi)
register:280, SelectableChannel (java.nio.channels)
startServer:66, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)

···

在這裏插入圖片描述

到這裏一切準備就緒了,等待客戶端來連接

創建一個連接

調用棧如下

<init>:129, SocketChannelImpl (sun.nio.ch)
accept:266, ServerSocketChannelImpl (sun.nio.ch)
createSocketChannel:97, NioReceiveServer (com.yh.stu.nio.socket)
startServer:78, NioReceiveServer (com.yh.stu.nio.socket)
main:177, NioReceiveServer (com.yh.stu.nio.socket)
SocketChannelImpl(SelectorProvider sp,
                      FileDescriptor fd, InetSocketAddress remote)
        throws IOException
    {
        super(sp);
        this.fd = fd;
        this.fdVal = IOUtil.fdVal(fd);
        this.state = ST_CONNECTED;
        this.localAddress = Net.localAddress(fd);
        this.remoteAddress = remote;
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章