一、 前言 最近在看tomcat connector組件的相關源碼,對Nio2的非同步回調過程頗有興趣,平時讀源碼不讀,自己讀的時候很多流程都沒搞明白,去查網上相關解析講的給我感覺也不是特別清晰,於是就自己慢慢看源碼,以下是我自己的見解,因為開發經驗也不多,剛成為社畜不久,有些地方講錯如果有大佬看 ...
一、 前言
最近在看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》