1.Tomcat總體架構
Tomcat有Connector和Container兩大核心組件,Connector組件負責網絡請求接入,Connector目前支持BIO、NIO、APR三種模式,后續文章會再重點對比下NIO和APR,Tomcat5以后的版本開始支持NIO了;Container組件實現了對servlet的容器管理功能;service服務將Connector和Container又包了一層,包裝成外部可以獲取的服務;多有service都運行在Tomcat這個大Server服務上,Server有所有service的實例,并實現了LifeCycle接口可以控制所有service的生命周期。
2.Tomcat NIO相關類
Tomcat的NIO實現主要是在Connector組件內,Connector 組件是 Tomcat 中兩個核心組件之一,它的主要任務是負責接收瀏覽器的發過來的 tcp 連接請求,創建一個 Request 和 Response 對象分別用于和請求端交換數據,然后會產生一個線程來處理這個請求并把產生的 Request 和 Response 對象傳給處理這個請求的線程,處理這個請求的線程就是 Container 組件要做的事了。
整個Connector組件包含三部分:Http11NioProtocol、Mapper、CoyoteAdapter。Http11NioProtocol包含NioEndpoint和Http11ConnectionHandler,NioEndpoint是Http11NioProtocol中負責接收處理socket的主要模塊;Http11ConnectionHandler是連接處理器。NioEndpoint主要是實現了socket請求監聽線程Acceptor、socket NIO poller線程、以及請求處理線程池。
NioEndpoint的內部處理流程為:
Acceptor 接收socket線程,這里雖然是基于NIO的connector,但是在接收socket方面還是傳統的serverSocket.accept()方式,獲得SocketChannel對象,然后封裝在一個tomcat的實現類org.apache.tomcat.util.net.NioChannel對象中。然后將NioChannel對象封裝在一個PollerEvent對象中,并將PollerEvent對象壓入events queue里。這里是個典型的生產者-消費者模式,Acceptor與Poller線程之間通過queue通信,Acceptor是events queue的生產者,Poller是events queue的消費者。
Poller Poller線程中維護了一個Selector對象,NIO就是基于Selector來完成邏輯的。在connector中并不止一個Selector,在socket的讀寫數據時,為了控制timeout也有一個Selector,在后面的BlockSelector中介紹。可以先把Poller線程中維護的這個Selector標為主Selector。 Poller是NIO實現的主要線程。首先作為events queue的消費者,從queue中取出PollerEvent對象,然后將此對象中的channel以OP_READ事件注冊到主Selector中,然后主Selector執行select操作,遍歷出可以讀數據的socket,并從Worker線程池中拿到可用的Worker線程,然后將socket傳遞給Worker。整個過程是典型的NIO實現。
Worker Worker線程拿到Poller傳過來的socket后,將socket封裝在SocketProcessor對象中。然后從Http11ConnectionHandler中取出Http11NioProcessor對象,從Http11NioProcessor中調用CoyoteAdapter的邏輯,跟BIO實現一樣。在Worker線程中,會完成從socket中讀取http request,解析成HttpServletRequest對象,分派到相應的servlet并完成邏輯,然后將response通過socket發回client。在從socket中讀數據和往socket中寫數據的過程,并沒有像典型的非阻塞的NIO的那樣,注冊OP_READ或OP_WRITE事件到主Selector,而是直接通過socket完成讀寫,這時是阻塞完成的,但是在timeout控制上,使用了NIO的Selector機制,但是這個Selector并不是Poller線程維護的主Selector,而是BlockPoller線程中維護的Selector,稱之為輔Selector。
NioSelectorPool NioEndpoint對象中維護了一個NioSelecPool對象,這個NioSelectorPool中又維護了一個BlockPoller線程,這個線程就是基于輔Selector進行NIO的邏輯。以執行servlet后,得到response,往socket中寫數據為例,最終寫的過程調用NioBlockingSelector的write方法。
3.請求處理流程
上面介紹了Tomcat的總體架構和涉及到NIO的相關工作類,下面從一個網絡請求到Tomcat處理的過程直到業務servlet處理的過程,整體上說下一個網絡請求的處理流程,下面借用網上的一張流程圖,如果圖片作者看到覺得侵權請下面留言,馬上刪掉:)
對于Acceptor監聽到的Socket請求,經過NioEndpoint內部的NIO 線程模型處理后,會轉變為SocketProcessor在Executor中運行,其在Run過程中會交給Http11ConnectionHandler處理,Http11ConnectionHandler會從ConcurrentHashMap<NioChannel,Http11NioProcessor>緩存中獲取相應的Http11NioProcessor來繼續處理,Http11NioProcessor主要是負責解析socket請求Header,解析完成后,會將Request、Response(這里的請求、響應在tomcat中看成是coyote的請求、響應,意思是還需要CoyoteAdaper處理)交給CoyoteAdaper繼續處理,CoyoteAdaper這里的工作主要將socket解析的Request、Response轉化為HttpServletRequest、HttpServletResponse,而這里的請求響應就是最后交給Container去處理。
同時我們可以看到Acceptor線程會將接受到的SocketChannel(一個socket請求)封裝為PollerEvent放到Poller線程中的ConcurrentLinkedQueue<PollerEvent>緩存中,注意到這里的緩存是ConcurrentLinkedQueue是支持并發的,那么在Poller線程的內部,它只需要從這個緩存中不停地獲取PollerEvent然后處理就可以了。最后Poller線程處理完成后會封裝成SocketProcessor交給NioEndpoint內的線程池Executor去處理。線程池中的Work thread線程在處理SocketProcessor過程中,會調用Http11ConnectionHandler處理,而Http11ConnectionHandler則從ConcurrentHashMap<NioChannel,Http11NioProcessor>緩存中獲取相應的Http11NioProcessor來繼續處理,這里要注意的ConcurrentHashMap也是支持并發的。
4.NIO相關參數
一個或多個Acceptor線程,每個線程都有自己的Selector,Acceptor只負責accept新的連接,一旦連接建立之后就將連接注冊到其他Worker線程中
多個Worker線程,有時候也叫IO線程,就是專門負責IO讀寫的。一種實現方式就是像Netty一樣,每個Worker線程都有自己的Selector,可以負責多個連接的IO讀寫事件,每個連接歸屬于某個線程。另一種方式實現方式就是有專門的線程負責IO事件監聽,這些線程有自己的Selector,一旦監聽到有IO讀寫事件,并不是像第一種實現方式那樣(自己去執行IO操作),而是將IO操作封裝成一個Runnable交給Worker線程池來執行,這種情況每個連接可能會被多個線程同時操作,相比第一種并發性提高了,但是也可能引來多線程問題,在處理上要更加謹慎些。tomcat的NIO模型就是第二種。
所以一般參數就是Acceptor線程個數,Worker線程個數。
參考官方文檔https://tomcat.apache.org/tomcat-8.5-doc/config/http.html?spm=5176.100239.blogcont39093.5.Vomyf0
參數主要有以下幾個:
1)acceptCount
連接在被ServerSocketChannel accept之前就暫存在這個隊列中,acceptCount就是這個隊列的最大長度。ServerSocketChannel accept就是從這個隊列中不斷取出已經建立連接的的請求。所以當ServerSocketChannel accept取出不及時就有可能造成該隊列積壓,一旦滿了連接就被拒絕了;
2)acceptorThreadCount
Acceptor線程只負責從上述隊列中取出已經建立連接的請求。在啟動的時候使用一個ServerSocketChannel監聽一個連接端口如8080,可以有多個Acceptor線程并發不斷調用上述ServerSocketChannel的accept方法來獲取新的連接。參數acceptorThreadCount其實使用的Acceptor線程的個數;
- maxConnections
這里就是tomcat對于連接數的一個控制,即最大連接數限制。一旦發現當前連接數已經超過了一定的數量(NIO默認是10000),上述的Acceptor線程就被阻塞了,即不再執行ServerSocketChannel的accept方法從隊列中獲取已經建立的連接。但是它并不阻止新的連接的建立,新的連接的建立過程不是Acceptor控制的,Acceptor僅僅是從隊列中獲取新建立的連接。所以當連接數已經超過maxConnections后,仍然是可以建立新的連接的,存放在上述acceptCount大小的隊列中,這個隊列里面的連接沒有被Acceptor獲取,就處于連接建立了但是不被處理的狀態。當連接數低于maxConnections之后,Acceptor線程就不再阻塞,繼續調用ServerSocketChannel的accept方法從acceptCount大小的隊列中繼續獲取新的連接,之后就開始處理這些新的連接的IO事件了; - maxThread
專門處理IO的Worker數,默認是200;
這篇文章從tomcat的整體架構入手,分別介紹了tomcat中的NIO相關類,也介紹了一個網絡請求在tomcat中的處理流程,最后介紹了一下tomcat中關鍵的幾個參數對NIO線程模式的作用和影響,相信會對希望了解tomcat nio線程模型的同學會有所幫助。