Netty 源碼解析 ——— 基于 NIO 網絡傳輸模式的 OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE 事件處理流程

本文是Netty文集中“Netty 源碼解析”系列的文章。主要對Netty的重要流程以及類進行源碼解析,以使得我們更好的去使用Netty。Netty是一個非常優秀的網絡框架,對其源碼解讀的過程也是不斷學習的過程。

預備知識

首先,我們知道JDK NIO的Selector實現了I/O多路復用。可以通過一個線程來管理多個Socket。我們可以將多個Channel(一個Channel代表了一個Socket)注冊到一個Selector上,并且設置其感興趣的事件。這樣一來,在Selector.select操作時,若發現Channel有我們所感興趣的事件發生時Selector就會將其記錄下來(即,SelectedKeys),然后我們就可以對事件進行相應的處理了。更多關于JDK NIO的知識請參閱關于 NIO 你不得不知道的一些“地雷”

ServerSocketChannel的有效事件為OP_ACCEPT。
SocketChannel的有效事件為OP_CONNECT、OP_READ、OP_WRITE

SelectionKey.OP_ACCEPT 事件處理流程

當服務端收到客戶端的一個連接請求時,‘SelectionKey.OP_ACCEPT’將會觸發。在NioEventLoop的事件循環中會對該事件進行處理:

if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
    unsafe.read();
}

我們來看看unsafe.read()的實現,在NioServerSocketChannel中unsafe是一個NioMessageUnsafe實例:

首先,需要說明的是在處理ACCEPT事件時,雖然整個方法的實現好像是在處理讀取數據的操作,但實際上對于ACCEPT事件來說,其中消息的讀取指的就是接收一個客戶端的請求「serverSocket.accpet()」操作。
再者,這里我們主要針對接收連接的邏輯進行分析,關于allocHandle相關的分析不會進行展開,可以參閱Netty 源碼解析 ——— AdaptiveRecvByteBufAllocator,這篇文章已經對AdaptiveRecvByteBufAllocator做了詳細的分析了。所以此處,我們主要關注的是真實的接收客戶端連接請求所涉及的流程。

① 對于ACCEPT事件,每次讀循環執行一次讀操作(但并沒有讀取任何字節數據,totalBytesRead > 0 為false)這也是符合NIO規范的,因為每次ACCEPT事件被觸發時,僅表示有一個客戶端向服務器端發起了連接請求。
② doReadMessages(readBuf):這步主要是通過serverSocket.accpet()來接受生成和客戶端通信的socket,并將其放入到readBuf集合中。

接收當前客戶端的連接請求,以得到一個SocketChannel。
創建一個NioSocketChannel實例,NioSocketChannel表示一個使用了NIO Selector的TCP/IP socket的實現。因此我們在構造NioSocketChannel是將上面accept()返回的SocketChannel作為構造函數的參數傳入,也就是說NioSocketChannel中持有SocketChannel的引用。同時,我們也將NioServerSocketChannel作為構造函數的參數傳入,作為當前NioSocketChannel的parent[一個Channel可以擁有一個父親(parent),這取決于這個channel是如何被創建的。比如說,一個SocketChannel,它是被ServerSocketChannel所接受的,SocketChannel將ServerSocketChannel作為parent()方法的結果返回]。
而NioSocketChannel的構建和NioServerSocketChannel的構建是非常類似的,關于NioServerSocketChannel的構造我們已經在Netty 源碼解析 ——— 服務端啟動流程 (下)中進行了詳細分析。這里我們提及重要的幾點,NioSocketChannel實例的構造主要完成了:
a) 構建Unsafe實例賦值給成員變量unsafe,這里是NioByteUnsafe對象,該類用于完成Channel真實的I/O操作和傳輸。
b) 創建了該Channel的ChannelPipeline實例(即,DefaultChannelPipeline)賦值給成員變量pipeline。每一個Channel都有它自己的pipeline。
這里ChannelPipeline的構建是很重要的一步。每一個新的Channel被創建時都會分配一個新的ChannelPipeline。ChannelPipeline本質上就是一系列的ChannelHandlers。ChannelPipeline還提供了方法用于傳播事件通過ChannelPipeline本身。
ChannelPipeline通過兩個AbstractChannelHandlerContext對象本身(head、tail)來維護一個雙向鏈表。
c) SelectionKey.OP_READ賦值給成員變量readInterestOp
設置SocketChannel.configureBlocking(false);配置SocketChannel為非阻塞模式,這步很重要。因為只有非阻塞模式Channel才能使用NIO的Selector來實現非阻塞的I/O操作。
d) 構建NioSocketChannelConfig實例賦值給成員變量config,即,這是負責維護NioSocketChannel相關配置的對象。
③ pipeline.fireChannelRead(readBuf.get(i)):這里實際上就是將第②步構建的NioSocketChannel作為ChannelRead事件的傳播數據進行傳播。ChannelRead是一個入站事件,它會從ChannelPipeline中的head開始傳播,依次順序回調ChannelInboundHandler的channelRead方法。
這里我們重點來看看ServerBootstrapAcceptor對channelRead事件的處理,ServerBootstrapAcceptor是服務端啟動流程中Netty底層加入到NioServerSocketChannel所關聯的ChannelPipeline中的一個入站處理器。

        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);

            for (Entry<AttributeKey<?>, Object> e: childAttrs) {
                child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }

            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
        }

a) 將我們在啟動流程中設置的childHandler(如,「serverBootstrap.childHandler(new MyServerInitializer())」)以及childOptions(如,「serverBootstrap.childOption(ChannelOption.ALLOW_HALF_CLOSURE, true)」)、childAttrs(如,「serverBootstrap.childAttr(AttributeKey.valueOf("userID"), UUID.randomUUID().toString())」)設置到這個NioSocketChannel中。
b) 將配置好的NioSocketChannel注冊到childGroup中,并注冊了一個監聽器,因為注冊是一個異步操作,該監聽器會得以調用在注冊操作完成時(完成包括,成功的完成、失敗的完成、取消的完成)。該監聽器實現:如果發現該注冊操作失敗了,則會強制關閉當前這個NioSocketChannel。
而將NioSocketChannel注冊到childGroup的操作和將NioServerSocketChannel注冊到parentGroup的流程也是極其類似的。詳細的說明請參閱Netty 源碼解析 ——— 服務端啟動流程 (下)。這里做一個簡單的概述。
整個注冊流程:
a) 首先會通過輪詢的方式從childGroup中獲取一個NioEventLoop,將當前的NioSocketChannel注冊到這個NioEventLoop上。
b) 將當前的SocketChannel注冊到NioEventLoop中的Selector上,并將NioSocketChannel作為附加屬性設置到SelectionKey中。
c) 回調我們自定義的ChannelInitializer的initChannel方法,將我們定義的一個個ChannelHandler添加到當前NioSocketChannel所關聯的ChannelPipeline上,然后將ChannelInitializer本身從ChannelPipeline中移除。
d) 標記注冊這個異步操作標志為成功完成。
e) 觸發ChannelRegistered事件,該事件會在ChannelPipeline中得以傳播。
f) 觸發ChannelActive事件,該事件也是一個入站事件,它會從ChannelPipeline中的head開始傳播,而head的channelActive方法除了將ChannelActive事件傳播給下一個ChannelInboundHandler之外,還調用一個readIfIsAutoRead()方法。
而readIfIsAutoRead()最終會調用到「AbstractNioChannel.doBeginRead()」方法:

    protected void doBeginRead() throws Exception {
        // Channel.read() or ChannelHandlerContext.read() was called
        final SelectionKey selectionKey = this.selectionKey;
        if (!selectionKey.isValid()) {
            return;
        }

        readPending = true;

        final int interestOps = selectionKey.interestOps();
        if ((interestOps & readInterestOp) == 0) {
            selectionKey.interestOps(interestOps | readInterestOp);
        }
    }

而這個方法中就完成了將readInterestOp(即,上面在說明NioSocketChannel的構建過程中已經提及,該成員變量值為SelectionKey.OP_READ)設置為感興趣的事件。這樣一來,Selector就會監聽該SocketChannel的讀事件了。

到目前為止,NioServerSocketChannel通過accept接受了一個客戶端的連接請求的整個流程就完成了。這里NioSocketChannel的創建是由NioServerSocketChannel所在的NioEventLoop( 實際上是NioEventLoop所在的線程上 )完成的。然后將創建好的NioSocketChannel注冊到childGroup中,也就是通過輪詢的方式的方式從childGroup中獲取一個NioEventLoop,然后將NioSocketChannel注冊的其上。在注冊的過程中也完成了將SocketChannel注冊到Selector,并設置SelectionKey.OP_READ為感興趣的事件,這樣Selector就會開始監聽這個SocketChannel的讀事件了。

SelectionKey.OP_CONNECT 事件處理流程

當SelectionKey.OP_CONNECT(連接事件)準備就緒時,我們執行如下操作:

            // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
            // the NIO JDK channel implementation may throw a NotYetConnectedException.
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
                // See https://github.com/netty/netty/issues/924
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);

                unsafe.finishConnect();
            }

① 將SelectionKey.OP_CONNECT事件從SelectionKey所感興趣的事件中移除,這樣Selector就不會再去監聽該連接的SelectionKey.OP_CONNECT事件了。而SelectionKey.OP_CONNECT連接事件是只需要處理一次的事件,一旦連接建立完成,就可以進行讀、寫操作了。

② unsafe.finishConnect():
注意,當連接操作既沒有被取消也沒有超時的情況下,該方法才會被事件循環所調用。

a) boolean wasActive = isActive():

    public boolean isActive() {
        SocketChannel ch = javaChannel();
        return ch.isOpen() && ch.isConnected();
    }

因為此時「SocketChannel.finishConnect()」還沒調用,所以「ch.isConnected()」將返回false,因此isActive()的結果為false。
b) doFinishConnect():

    protected void doFinishConnect() throws Exception {
        if (!javaChannel().finishConnect()) {
            throw new Error();
        }
    }

該方法會調用SocketChannel.finishConnect()來標識連接的完成,如果我們不調用該方法,就去調用read/write方法,則會拋出一個NotYetConnectedException異常。
注意,無論如何在connect后finishConnect()方法都是需要被調用的。調用finishConnect()的三種返回:

  1. 如果你在connect()后直接調用了finishConnect()( 并非在CONNECT事件中調用 ),則若finishConnect()返回了true,則表示channel連接已經建立,而且CONNECT事件也不會被觸發了。
  2. 如果finishConnect()方法返回false,則表示連接還未建立好。那么就可以通過CONNECT事件來監聽連接的完成。
  3. 如果finishConnect()方法拋出了一個IOException異常,則表示連接操作失敗。

    c) fulfillConnectPromise(connectPromise, wasActive):
    I. 當異步的“連接嘗試”操作通過取消來關閉了,那么則直接返回。因為當“連接嘗試”操作被取消時,connectPromise會被置為null。

    II. isActive():獲取當前SocketChannel的狀態,因為此時「SocketChannel.finishConnect()」已經被調用過了,因此該方法會返回true。
    III. boolean promiseSet = promise.trySuccess():
    將當前的異步的“連接嘗試”操作嘗試標記為成功。如果用戶取消了“連接嘗試(即,調用connect操作后,用戶調用了cancel來取消該操作)”的操作的話,該方法將返回false;否則返回true,并且如果有ChannelFutureListener注冊到了這個ChannelFuture(即,ChannelPromise)上,那么監聽器的operationComplete方法將得以回調。
    IV. 無論當前的“連接嘗試”操作是否被取消,channelActive()事件都將被觸發,因為此時channel確實已經處于active狀態了。
    channelActive是一個入站事件,該事件會從ChannelPipeline的head開始傳播至tail間的所有ChannelInboundHandler,并回調它們的channelActive方法。

    我們來深入看看head對channelActive事件的處理:
    它除了將channelActive事件傳播給下一個ChannelInboundHandler外,還會進行一個「readIfIsAutoRead()」操作:
        private void readIfIsAutoRead() {
            if (channel.config().isAutoRead()) {
                channel.read();
            }
        }

因為NioSocketChannel的NioSocketChannelConfig對象的autoRead屬性默認就為1,因此isAutoRead()為true。那么就會調用channel.read()操作,這將觸發一個read事件在ChannelPipeline中,而read是一個出站操作。它會從ChannelPipeline的尾部開始傳播至head間的每個ChannelOutboundHandler。
我們接著來看head對read事件的處理:

        // DefaultChannelPipeline#HeadContext#read()
        public void read(ChannelHandlerContext ctx) {
            unsafe.beginRead();
        }

        // AbstractChannel#AbstractUnsafe#beginRead()
        public final void beginRead() {
            assertEventLoop();

            if (!isActive()) {
                return;
            }

            try {
                doBeginRead();
            } catch (final Exception e) {
                invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.fireExceptionCaught(e);
                    }
                });
                close(voidPromise());
            }
        }

        // AbstractNioChannel#doBeginRead()
        protected void doBeginRead() throws Exception {
            // Channel.read() or ChannelHandlerContext.read() was called
            final SelectionKey selectionKey = this.selectionKey;
            if (!selectionKey.isValid()) {
                return;
            }

            readPending = true;

            final int interestOps = selectionKey.interestOps();
            if ((interestOps & readInterestOp) == 0) {
                selectionKey.interestOps(interestOps | readInterestOp);
            }
        }

這里的readInterestOp是SelectionKey.OP_READ,是在構建NioSocketChannel對象時傳進來的。因此,我們可以知道,此時的read事件主要是完成了,在Selector中對已經注冊到其上的NioSocketChannel的OP_READ標識為感興趣的事件。這樣Selector就監控該SocketChannel的讀操作了。
V. 如果用戶執行了取消“連接嘗試”的操作,那么就關閉channel,并觸發channelInactive事件。
d) 如果成員變量connectTimeoutFuture非空,則說明該“連接嘗試”操作設置了一個連接超時時間。那么,此時連接已經完成了,我們就可以取消這個連接超時檢測的定時任務了。超時任務會記錄本次“連接嘗試”操作為失敗狀態,并且會將connectTimeoutFuture成員變量置為null。
比如,可能存在這樣一種情況:也就是當程序執行完fulfillConnectPromise方法中的「promise.trySuccess()」之后,以及在執行finally代碼塊之前,“連接嘗試”的已經完成,并且ChannelPromise已標記為了true。但是此時設置的連接超時時間已到并且連接超時任務被得以執行,此時超時任務發現ChannelPromise的狀態已經被標識過了也就不會進行關閉channel的操作。
因此如上流程我們知道,『connectTimeoutFuture == null』有兩種情況:1,如果沒有設置連接超時;2,設置了連接超時,并且只因為超時或其他原因已經執行了 NioSocketChannel 的 close() 操作。『connectTimeoutFuture#operationComplete』也有兩種情況:1,在超時時間內連接還沒建立,則執行相應的close操作;2,連接已經建立了,但是還未執行到connectTimeoutFuture#cancel(false)操作,connectTimeoutFuture 就觸發了,此時因為 promise的狀態已經被表示過了,也不會進行close操作。

總的來說,OP_CONNECT事件的觸發時,表示當前的socket處于了可連接的狀態了,需要調用SocketChannel.finishConnect()來完成連接的后續事件。同時會觸發ChannelActive事件,該事件為一個入站事件,它會在NioSocketChannel所關聯的ChannelPipeline管道得以傳播,即,回調head到tail之間所有的ChannelInboundHandler的channelActive方法。而head的channelActive方法中又會觸發一個channel的read操作,該操作最終會在NioSocketChannel所注冊的Selector中標識OP_READ為感興趣的事件,這使得Selector會監聽NioSocketChannel上是否有可讀的數據準備好被讀取了。

SelectionKey.OP_READ 事件處理流程

當有可讀數據準備被讀取時,‘SelectionKey.OP_READ’將會觸發。在NioEventLoop的事件循環中會對該事件進行處理:

if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
    unsafe.read();
}

我們來看看unsafe.read()的實現,在NioSocketChannel中unsafe是一個NioByteUnsafe實例:

這里,我們主要針對讀數據涉及的邏輯進行分析,關于allocHandle如何計算出一個最優緩沖區的大小用于創建緩沖區,以及判斷讀循環是否應該繼續,已經在Netty 源碼解析 ——— AdaptiveRecvByteBufAllocator做了詳細的分析了。所以此處,我們主要關注的是真實的讀數據操作所涉及的流程。
① 首先,在循環前將分配器處理器中累加的消息/字節計數重置。接著,進行一個讀循環操作:
I. 根據給定的分配器以及預測的最優緩沖區容量大小創建一個緩沖區用于準備接受可讀取的數據。在Netty中,分配器(allocator)默認為PooledByteBufAllocator實例,而PooledByteBufAllocator實現通過使用jemalloc算法來實現高效的內存分配。
II. allocHandle.lastBytesRead(doReadBytes(byteBuf)):
使用已經分配好的ByteBuf來讀取數據。并在分配器處理器中記錄本次讀取的字節數。
這里會將SocketChannel中的內容寫到byteBuf中,從byteBuf的writerIndex開始寫入數據,writerIndex會增加所寫入的字節數。并且設置了最大可從SocketChannel讀取的數據大小為“allocHandle.attemptedBytesRead()”,這也是byteBuf的容量大小。
III. 判斷本次讀操作所讀取到的字節數:
a) 若‘讀取到的字節數 < 0’,即為’-1’時,說明對端已經關閉了。close變量會被標記為true。因為沒有讀取到數據,因此調用‘byteBuf.release()’來釋放bytebuf(因為,此時 bytebuf 不會在通過 channelPipeline進行傳輸了,也就是,這個 bytebuf 最后使用的地方就是當前方法,因此應該調用 release() 方法來聲明對其的釋放),然后退出讀循環操作(break)。需要說明的是,因為byteBuf是通過PooledByteBufAllocator來分配的緩沖區,是一個池中的ByteBuf,因此是要通過release()方法來減小bytebud的引用計數,當bytebuf的引用計數為0時,則說明此時已經沒有引用指向這個bytebuf了,那么它就會被“回收”;
b) 若‘讀取到的字節數 == 0’,僅僅說明本次讀操作沒有讀取到數據,那么就會執行同上面一樣的釋放bytebuf操作,即,‘byteBuf.release()’,然后退出讀循環操作(break);
c) 若‘讀取到的字節數 > 0’,說明本次讀操作已經到達有效的數據了。那么執行:
???[1]. 「allocHandle.incMessagesRead(1)」對讀消息的次數進行累加。
???[2]. 然后標識readPending為false,表示本次讀操作已經讀取到了有效數據,無需等待再一次的讀操作。
???[3]. 接著觸發ChannelRead事件,它會在ChannelPipeline中傳播,「pipeline.fireChannelRead(byteBuf)」。這是一個入站事件,它會從ChannelPipeline的head開始傳播,依次順序回調ChannelInboundHandler的channelRead()方法。這里可以看到,是讀循環中每一次有效的讀操作都對觸發一次ChannelRead事件,并不是在所有數據都讀取到之后才觸發一次ChannelRead事件。因此,我們需要提供一系列的編解碼器來將收到的數據分割成我們一個個的邏輯數據包,對此Netty也提供了一系列拆箱即有的編解碼器為我們解決相關的問題。
IV. 根據當前的NioSocketChannel是否是自動讀取的配置,以及已經讀取的數據字節數,以及已經進行的讀操作次數,以及最近一次讀取的字節數來判斷是否需要繼續進行讀循環操作。若需要則繼續讀循環操作;否則退出讀循環,繼續后面的流程。
② allocHandle.readComplete():在本次讀循環結束后調用一次「allocHandle.readComplete()」來記錄本次讀循環的數據信息以用于預測下一次讀事件觸發時,應該分配多大的ByteBuf容量更加合理些。
③ pipeline.fireChannelReadComplete():觸發ChannelReadComplete事件,用于表示當前讀操作的最后一個消息已經被ChannelRead所消費。ChannelReadComplete是一個入站消息,它會從ChannelPipeline的head開始傳播,依次順序回調ChannelInboundHandler的channelReadComplete方法。
ChannelRead vs ChannelReadComplete
當Channel檢測到對端有數據可以讀取的時候,channelRead方法會被調用。
channelRead方法可能會被調用多次,當channelReadComplete方法被回調的時候,標識著數據已經都讀取完了。也就是說,channelRead方法會被調用多次,當所有消息都讀取完后channelReadComplete方法會得到一次調用。
④ 如果close被標識為了true,則說明對端已經關閉了連接。(即,讀操作中讀取的字節數量為-1,則表示遠端已經關閉了),則執行「closeOnRead(pipeline)」
首先需要補充一點,在NIO傳輸模式下,當SocketChannel的read操作返回’-1’時,有兩種情況:a) 對端已經關閉了連接,即SocketChannel被關閉了;b) 當前端執行了「socketChannel.shutdownInput()」。
I. 若“isInputShutdown0()”返回false,則說明是遠端連接已經關閉了。那么此時,如果我們的程序配置了“ChannelOption.ALLOW_HALF_CLOSURE”屬性(即,可以在啟動引導類時通過option(ChannelOption.ALLOW_HALF_CLOSURE, true)來啟用配置),那么就會進行shutdownInput()操作,并觸發一個用戶自定義的ChannelInputShutdownEvent.INSTANCE事件,在ChannelPipeline中傳播。該事件是一個入站事件,它會從ChannelPipeline中的head開始傳播,異常順序調用ChannelInboundHandler的userEventTriggered方法。
II. 若“isInputShutdown0()”返回false,則說明是遠端連接已經關閉了。并且此時我們的程序并沒有配置啟動“ChannelOption.ALLOW_HALF_CLOSURE”。那么此時就會進行相應SocketChannel的關閉等相關操作。
III. 若“isInputShutdown0()”返回true,則說明是當前的NioSocketChannel自動調用了shutdownInput()方法來關閉了輸入流。那么此時就會觸發一個用戶自定義的ChannelInputShutdownEvent.INSTANCE事件,在ChannelPipeline中傳播。該事件是一個入站事件,它會從ChannelPipeline中的head開始傳播,異常順序調用ChannelINboundHandler的userEventTriggered方法。
關于「SocketChannel.shutdownInput()」:關閉一個連接的讀,但不關閉這個通道。一旦關閉了讀,那么在這之后調用channel的read都將返回’-1’,來表示’流的結尾’。如果往已經關閉的輸入流中發送數據,都會默認被丟棄。
⑤ 如果本次讀操作已經讀取到有效數據(即,最近一次讀操作返回的讀取字節數>0),并且當前的NioSocketChannel的配置為非自動讀取(disable autoRead,說明此時用戶不希望Selector去監聽當前SocketChannel的讀事件,用戶可以根據業務邏輯的需要,在希望讀取數據時再去添加OP_READ事件到Selector中。并且在每次讀取到數據后就將OP_READ事件從所感興趣的事件中移除),那么此時需要將OP_READ事件從所感興趣的事件中移除,這樣Selector就不會繼續監聽該SocketChannel的讀事件了。
removeReadOp()操作就是將OP_READ從SelectionKey的interestOps集合中移除:
在NioSocketChannel中成員變量readInterestOp就是SelectionKey.OP_READ。

PS:注意,如果在當前端主動調用「channel.shutdownInput()」方法時,需要在處理’ChannelInputShutdownReadComplete’這個用戶自定義的事件時調用「channel().config().setAutoRead(false);」來將autoRead置為false。不然,OP_READ事件會一直被觸發,而上的步驟’III’會一直被調用,這會導致一些問題,比如不必要的CPU消耗。
調用方式類似:

public class MyServerHandler extends SimpleChannelInboundHandler<String> {
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        Channel serverChannel = ctx.channel();
        if(serverChannel instanceof NioSocketChannel) {
            System.out.println("server shutdownInput...");
            ((NioSocketChannel) serverChannel).shutdownInput();
        }
    }

    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof ChannelInputShutdownReadComplete) {
            System.out.println("detail ChannelInputShutdownEvent event......");
            ctx.channel().config().setAutoRead(false);
        }
        super.userEventTriggered(ctx, evt);
    }
}


SelectionKey.OP_WRITE 事件處理流程

在NioEventLoop的事件循環中’SelectionKey.OP_WRITE’事件的處理流程如下:

    // Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
     if ((readyOps & SelectionKey.OP_WRITE) != 0) {
        // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
        ch.unsafe().forceFlush();
    }

關于OP_WRITE事件:
OP_WRITE事件的就緒條件并不是發生在調用channel的write方法之后,而是在當底層緩沖區有空閑空間的情況下。因為寫緩沖區在絕大部分時候都是有空閑空間的,所以如果你注冊了寫事件,這會使得寫事件一直處于就就緒,選擇處理線程就會一直占用著CPU資源。所以,只有當你確實有數據要寫時再注冊寫操作,并在寫完以后馬上取消注冊。
SocketChannel會在寫數據時,若發現當buffer還有數據,但寫緩沖區已經滿的情況下,socketChannel.write(buffer)會返回已經寫出去的字節數,此時為0。那么這個時候我們就需要注冊OP_WRITE事件,這樣當寫緩沖區又有空閑空間的時候就會觸發OP_WRITE事件,這樣我們就可以繼續將沒寫完的數據繼續寫出了。而且在寫完后,一定要記得將OP_WRITE事件注銷。
比如,來看看NioSocketChannel的doWrite()操作(「 ch.unsafe().forceFlush()」方法最終也就是會調用到這里):

就像上面所說的那樣,當發現socketChannel.write(buffer)返回的已經寫出去的字節數為0時,則說明此時寫緩沖區已經滿了無法寫入,因此就需要注冊一個OP_WRITE事件,這樣當寫緩存有空間來繼續接受數據時,該寫事件就會被觸發,這樣我們就可以繼續將沒寫完的數據繼續寫出了。而且在寫完后,一定要記得將OP_WRITE事件注銷。


關于寫操作的具體流程分析請參見Netty 源碼解析 ——— writeAndFlush流程分析

后記

本文主要對NioEventLoop中涉及到的四種NIO事件的處理流程進行了分析。四個看似簡單的處理流程,深入探索后發現其實并不簡單,其實可以展開的點還有很多,特別是關于寫事件涉及到ChannelOutboundBuffer以及Netty默認使用的PooledByteBufAllocator實現了jemalloc算法來完成高效的內存分配等等,希望在后面的文章中能繼續和大家分享我的分析以及想法。
若文章有任何錯誤,望大家不吝指教:)

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

推薦閱讀更多精彩內容