tomcat nio2原始碼分析

Scotyzh發表於2023-11-01

一、 前言

​ 最近在看tomcat connector元件的相關原始碼,對Nio2的非同步回撥過程頗有興趣,平時讀原始碼不讀,自己讀的時候很多流程都沒搞明白,去查網上相關解析講的給我感覺也不是特別清晰,於是就自己慢慢看原始碼,以下是我自己的見解,因為開發經驗也不多,剛成為社畜不久,有些地方講錯如果有大佬看到也希望能夠指正指導。

以下程式碼基於tomcat8.5版本

二、基本流程

​ 在tomcat的nio2流程下,會有多個Acceptor透過執行緒池進行管理執行,一個連線請求進來,會先被Acceptor監聽

   protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {
			....
		                // Configure the socket
                    if (running && !paused) {
                        // setSocketOptions() will hand the socket off to
                        // an appropriate processor if successful
                        if (!setSocketOptions(socket)) { // 監聽到socket請求後進入到這裡面
                            closeSocket(socket);
                       }
                    } else {
                        closeSocket(socket);
                    }
            ...

進入setSocketOptions()方法

    protected boolean setSocketOptions(AsynchronousSocketChannel socket) {
        try {
            socketProperties.setProperties(socket);
            Nio2Channel channel = nioChannels.pop();
   		   ...
            Nio2SocketWrapper socketWrapper = new Nio2SocketWrapper(channel, this);
            channel.reset(socket, socketWrapper);
            ...
            // 用另外一個執行緒處理這個socketWrapper(實現了runnable)
            return processSocket(socketWrapper, SocketEvent.OPEN_READ, true);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error("",t);
        }
        // Tell to close the socket
        return false;
    }

再進入processSocket()方法,sc被提交到了執行緒池裡面處理

image-20231021175643146

繼續跟進原始碼

image-20231021175748851

在workQueue.offer(command)裡面可以看到提交到了任務佇列裡面,等待執行緒池的執行緒執行這個任務

image-20231021175844968

看看執行processSocket()時,做了那些事情,這個執行緒排程最終會執行到Nio2EndPoint裡面的doRun()方法:

image-20231031194943380

在doRun()方法裡面執行到這行

image-20231031195038431

透過getHandler拿到了AbstactProtocol

image-20231031195408233

再透過後續流程,拿到了Http11Processor來對當前這個socketWrapper進行處理,Http11Processor會呼叫Nio2SocketWrapper中的read()方法進行處理

注意:Nio2SocketWrapper有個回撥方法,這個回撥方法會被註冊,後續當資料準備好後會呼叫這個completed()方法來進行資料讀取,部分程式碼如下:

image-20231031200137181

第一次是非回撥讀,主要是進行註冊操作,會經歷進入sockerwrapper裡面的read()方法再到fillReadBuffer(),並且會在fillReadBuffer()裡面進行註冊回撥操作

先看以下read方法(),這個地方是關鍵,第一次讀和回撥讀的區別就在下面這行程式碼,第一次讀因為應用層的buffer沒有資料,不會返回,會繼續執行

image-20231031200455714

會繼續執行到fillReadBuffer()方法裡面,在這裡面進行回撥函式的註冊,並把資料的讀取交到作業系統核心,由核心將資料複製到應用層的buffer,再這個執行回撥

image-20231031200617723

這是相關的呼叫棧

image-20231031200603284

跟進原始碼,會呼叫到WindowsAsychronusSocketChannel的相關方法,由核心去複製資料

image-20231031200703481

資料準備完成後,我這裡猜測是底層會呼叫我們的回撥方法,進行後續的讀取操作。

資料已經準備到了buffer裡面,這時另外啟動一個執行緒執行回撥方法,會執行到裡面最後一行,processSocket()

image-20231031201142899

然後你會發現,回撥的流程和首次進行註冊的流程的呼叫棧基本一致

image-20231031201224388

差別在,read()方法裡面,在回撥讀的時候,會因為nRead>0返回,並進行後續讀到資料的處理

image-20231031200455714

最後再把整套邏輯捋一遍:在tomcat的nio2下,會有多個acceptor,透過tommcat的執行緒池管理,當一個acceptor監聽到連線後,將socket包裝成一個socketWrapper,再建一個SocketProcessor,丟到執行緒池裡面,另外啟動一個執行緒執行SocketProcessor的run方法,這時候這個acceptor的監聽任務就結束,會返回繼續監聽其他請求。 後面執行run的時候拿到了Http11Processor來對當前這個socketWrapper進行處理,Http11Processor會呼叫Nio2SocketWrapper中的read()方法進行處理,在這裡會進行第一次讀資料,因為buffer裡面並沒有資料,會進行回撥函式的註冊,並把複製資料的任務交到核心去完成。核心完成後執行回撥函式,回撥函式再去進行第二次讀,將資料從buffer裡面讀出來,並執行後面的操作,至此實現了非阻塞非同步讀的流程。

核心思想:應用程式是無法直接訪問到核心空間的,核心空間涉及到的資料都需要核心將資料複製到使用者空間。為了解決這個問題,NIO2實際上讓應用程式呼叫讀資料操作的時候,告訴核心資料應該複製到哪個buffer,以及將回撥函式進行註冊,告訴核心呼叫哪個回撥函式。之後,核心會在網路卡資料到達,產生硬體中斷,核心在中斷程式裡面把資料從網路卡複製到核心空間,接著做TCP/IP協議層面的資料解包重組,把資料複製到應用程式指定的Buffer,最後執行回撥函式。

參考資料:《深入拆解Tomcat & Jetty》

相關文章