一、 前言
最近在看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被提交到了執行緒池裡面處理
繼續跟進原始碼
在workQueue.offer(command)裡面可以看到提交到了任務佇列裡面,等待執行緒池的執行緒執行這個任務
看看執行processSocket()時,做了那些事情,這個執行緒排程最終會執行到Nio2EndPoint裡面的doRun()方法:
在doRun()方法裡面執行到這行
透過getHandler拿到了AbstactProtocol
再透過後續流程,拿到了Http11Processor來對當前這個socketWrapper進行處理,Http11Processor會呼叫Nio2SocketWrapper中的read()方法進行處理
注意:Nio2SocketWrapper有個回撥方法,這個回撥方法會被註冊,後續當資料準備好後會呼叫這個completed()方法來進行資料讀取,部分程式碼如下:
第一次是非回撥讀,主要是進行註冊操作,會經歷進入sockerwrapper裡面的read()方法再到fillReadBuffer(),並且會在fillReadBuffer()裡面進行註冊回撥操作
先看以下read方法(),這個地方是關鍵,第一次讀和回撥讀的區別就在下面這行程式碼,第一次讀因為應用層的buffer沒有資料,不會返回,會繼續執行
會繼續執行到fillReadBuffer()方法裡面,在這裡面進行回撥函式的註冊,並把資料的讀取交到作業系統核心,由核心將資料複製到應用層的buffer,再這個執行回撥
這是相關的呼叫棧
跟進原始碼,會呼叫到WindowsAsychronusSocketChannel的相關方法,由核心去複製資料
資料準備完成後,我這裡猜測是底層會呼叫我們的回撥方法,進行後續的讀取操作。
資料已經準備到了buffer裡面,這時另外啟動一個執行緒執行回撥方法,會執行到裡面最後一行,processSocket()
然後你會發現,回撥的流程和首次進行註冊的流程的呼叫棧基本一致
差別在,read()方法裡面,在回撥讀的時候,會因為nRead>0返回,並進行後續讀到資料的處理
最後再把整套邏輯捋一遍:在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》