走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啓動部署過程

微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)html

談談 Tomcat 請求處理流程java

轉自:https://github.com/c-rainstor...git

《談談 Tomcat 架構及啓動過程[含部署]》已從新修訂!(與本文在 GitHub 同一目錄下)包括架構和 Tomcat Start 過程當中的 MapperListener 相關描述。Connector 啓動相關的內容與請求處理關係比較緊密,因此就獨立出來放在本文中了。程序員

建議結合《談談 Tomcat 架構及啓動過程[含部署]》一塊兒看!github

不少東西在時序圖中體現的已經很是清楚了,沒有必要再一步一步的做介紹,因此本文以圖爲主,而後對部份內容加以簡單解釋。web

本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 爲標準。面試

Tomcat-9.0.0.M22 是 Tomcat 目前最新的版本,但還沒有發佈,它實現了 Servlet4.0 及 JSP2.3 並提供了不少新特性,須要 1.8 及以上的 JDK 支持等等,詳情請查閱 Tomcat-9.0-doc數據庫

<figure data-block="true" data-editor="b4p0g" data-offset-key="6esng-0-0" contenteditable="false">apache

    • *

</figure>segmentfault

Overview

<figure data-block="true" data-editor="b4p0g" data-offset-key="eik80-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. Connector 啓動之後會啓動一組線程用於不一樣階段的請求處理過程。
  2. Acceptor 線程組。用於接受新鏈接,並將新鏈接封裝一下,選擇一個 Poller 將新鏈接添加到 Poller 的事件隊列中。
  3. Poller 線程組。用於監聽 Socket 事件,當 Socket 可讀或可寫等等時,將 Socket 封裝一下添加到 worker 線程池的任務隊列中。
  4. worker 線程組。用於對請求進行處理,包括分析請求報文並建立 Request 對象,調用容器的 pipeline 進行處理。
  • Acceptor、Poller、worker 所在的 ThreadPoolExecutor 都維護在 NioEndpoint 中。

Connector Init and Start

<figure data-block="true" data-editor="b4p0g" data-offset-key="c7n5r-0-0" contenteditable="false"></figure>

  1. initServerSocket(),經過 ServerSocketChannel.open() 打開一個 ServerSocket,默認綁定到 8080 端口,默認的鏈接等待隊列長度是 100, 當超過 100 個時會拒絕服務。咱們能夠經過配置 conf/server.xml 中 Connector 的 acceptCount 屬性對其進行定製。
  2. createExecutor() 用於建立 Worker 線程池。默認會啓動 10 個 Worker 線程,Tomcat 處理請求過程當中,Woker 最多不超過 200 個。咱們能夠經過配置 conf/server.xml 中 Connector 的 minSpareThreads 和 maxThreads 對這兩個屬性進行定製。
  3. Pollor 用於檢測已就緒的 Socket。 默認最多不超過 2 個,Math.min(2,Runtime.getRuntime().availableProcessors());。咱們能夠經過配置 pollerThreadCount 來定製。
  4. Acceptor 用於接受新鏈接。默認是 1 個。咱們能夠經過配置 acceptorThreadCount 對其進行定製。

Requtst Process

Acceptor

<figure data-block="true" data-editor="b4p0g" data-offset-key="6ucio-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. Acceptor 在啓動後會阻塞在 ServerSocketChannel.accept(); 方法處,當有新鏈接到達時,該方法返回一個 SocketChannel。
  2. 配置完 Socket 之後將 Socket 封裝到 NioChannel 中,並註冊到 Poller,值的一提的是,咱們一開始就啓動了多個 Poller 線程,註冊的時候,鏈接是公平的分配到每一個 Poller 的。NioEndpoint 維護了一個 Poller 數組,當一個鏈接分配給 pollers[index] 時,下一個鏈接就會分配給 pollers[(index+1)%pollers.length].
  3. addEvent() 方法會將 Socket 添加到該 Poller 的 PollerEvent 隊列中。到此 Acceptor 的任務就完成了。

Poller

<figure data-block="true" data-editor="b4p0g" data-offset-key="dpl-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. selector.select(1000)。當 Poller 啓動後由於 selector 中並無已註冊的 Channel,因此當執行到該方法時只能阻塞。全部的 Poller 共用一個 Selector,其實現類是 sun.nio.ch.EPollSelectorImpl
  2. events() 方法會將經過 addEvent() 方法添加到事件隊列中的 Socket 註冊到 EPollSelectorImpl,當 Socket 可讀時,Poller 纔對其進行處理
  3. createSocketProcessor() 方法將 Socket 封裝到 SocketProcessor 中,SocketProcessor 實現了 Runnable 接口。worker 線程經過調用其 run() 方法來對 Socket 進行處理。
  4. execute(SocketProcessor) 方法將 SocketProcessor 提交到線程池,放入線程池的 workQueue 中。workQueue 是 BlockingQueue 的實例。到此 Poller 的任務就完成了。

Worker

<figure data-block="true" data-editor="b4p0g" data-offset-key="7dour-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. worker 線程被建立之後就執行 ThreadPoolExecutor 的 runWorker() 方法,試圖從 workQueue 中取待處理任務,可是一開始 workQueue 是空的,因此 worker 線程會阻塞在 workQueue.take() 方法。
  2. 當新任務添加到 workQueue後,workQueue.take() 方法會返回一個 Runnable,一般是 SocketProcessor,而後 worker 線程調用 SocketProcessor 的 run() 方法對 Socket 進行處理。
  3. createProcessor() 會建立一個 Http11Processor, 它用來解析 Socket,將 Socket 中的內容封裝到 Request 中。注意這個 Request 是臨時使用的一個類,它的全類名是 org.apache.coyote.Request,
  4. postParseRequest() 方法封裝一下 Request,並處理一下映射關係(從 URL 映射到相應的 Host、Context、Wrapper)。
  5. CoyoteAdapter 將 Rquest 提交給 Container 處理以前,並將 org.apache.coyote.Request 封裝到 org.apache.catalina.connector.Request,傳遞給 Container 處理的 Request 是 org.apache.catalina.connector.Request。
  6. connector.getService().getMapper().map(),用來在 Mapper 中查詢 URL 的映射關係。映射關係會保留到 org.apache.catalina.connector.Request 中,Container 處理階段 request.getHost() 是使用的就是這個階段查詢到的映射主機,以此類推 request.getContext()、request.getWrapper() 都是。
  7. connector.getService().getContainer().getPipeline().getFirst().invoke() 會將請求傳遞到 Container 處理,固然了 Container 處理也是在 Worker 線程中執行的,可是這是一個相對獨立的模塊,因此單獨分出來一節。

Container

<figure data-block="true" data-editor="b4p0g" data-offset-key="9g5hs-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. 須要注意的是,基本上每個容器的 StandardPipeline 上都會有多個已註冊的 Valve,咱們只關注每一個容器的 Basic Valve。其餘 Valve 都是在 Basic Valve 前執行。
  2. request.getHost().getPipeline().getFirst().invoke() 先獲取對應的 StandardHost,並執行其 pipeline。
  3. request.getContext().getPipeline().getFirst().invoke() 先獲取對應的 StandardContext,並執行其 pipeline。
  4. request.getWrapper().getPipeline().getFirst().invoke() 先獲取對應的 StandardWrapper,並執行其 pipeline。
  5. 最值得說的就是 StandardWrapper 的 Basic Valve,StandardWrapperValve
  6. allocate() 用來加載並初始化 Servlet,值的一提的是 Servlet 並不都是單例的,當 Servlet 實現了 SingleThreadModel 接口後,StandardWrapper 會維護一組 Servlet 實例,這是享元模式。固然了 SingleThreadModel在 Servlet 2.4 之後就棄用了。
  7. createFilterChain() 方法會從 StandardContext 中獲取到全部的過濾器,而後將匹配 Request URL 的全部過濾器挑選出來添加到 filterChain 中。
  8. doFilter() 執行過濾鏈,當全部的過濾器都執行完畢後調用 Servlet 的 service() 方法。

談談 Tomcat 架構及啓動過程[含部署]

這個題目命的實際上是很大的,寫的時候仍是很忐忑的,但我儘量把這個過程描述清楚。由於這是讀過源碼之後寫的總結,在寫的過程當中可能會忽略一些前提條件,若是有哪些比較突兀就出現,或很差理解的地方能夠給我提 Issue,我會盡快補充修訂相關內容。

不少東西在時序圖中體現的已經很是清楚了,沒有必要再一步一步的做介紹,因此本文以圖爲主,而後對部份內容加以簡單解釋。

  1. tomcat-architecture.pu
  2. tomcat-init.pu
  3. tomcat-start.pu
  4. tomcat-context-start.pu
  5. tomcat-background-thread.pu

本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 爲標準。

Tomcat-9.0.0.M22 是 Tomcat 目前最新的版本,但還沒有發佈,它實現了 Servlet4.0 及 JSP2.3 並提供了不少新特性,須要 1.8 及以上的 JDK 支持等等,詳情請查閱 Tomcat-9.0-doc

<figure data-block="true" data-editor="b4p0g" data-offset-key="23pl-0-0" contenteditable="false">

    • *

</figure>

Overview

<figure data-block="true" data-editor="b4p0g" data-offset-key="8efth-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. Bootstrap 做爲 Tomcat 對外界的啓動類,在 $CATALINA_BASE/bin 目錄下,它經過反射建立 Catalina 的實例並對其進行初始化及啓動。
  2. Catalina 解析 $CATALINA_BASE/conf/server.xml 文件並建立 StandardServer、StandardService、StandardEngine、StandardHost 等
  3. StandardServer 表明的是整個 Servlet 容器,他包含一個或多個 StandardService
  4. StandardService 包含一個或多個 Connector,和一個 Engine,Connector 和 Engine 都是在解析 conf/server.xml 文件時建立的,Engine 在 Tomcat 的標準實現是 StandardEngine
  5. MapperListener 實現了 LifecycleListener 和 ContainerListener 接口用於監聽容器事件和生命週期事件。該監聽器實例監聽全部的容器,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,當容器有變更時,註冊容器到 Mapper。
  6. Mapper 維護了 URL 到容器的映射關係。當請求到來時會根據 Mapper 中的映射信息決定將請求映射到哪個 Host、Context、Wrapper。
  7. Http11NioProtocol 用於處理 HTTP/1.1 的請求
  8. NioEndpoint 是鏈接的端點,在請求處理流程中該類是核心類,會重點介紹。
  9. CoyoteAdapter 用於將請求從 Connctor 交給 Container 處理。使 Connctor 和 Container 解耦。
  10. StandardEngine 表明的是 Servlet 引擎,用於處理 Connector 接受的 Request。包含一個或多個 Host(虛擬主機), Host 的標準實現是 StandardHost。
  11. StandardHost 表明的是虛擬主機,用於部署該虛擬主機上的應用程序。一般包含多個 Context (Context 在 Tomcat 中表明應用程序)。Context 在 Tomcat 中的標準實現是 StandardContext。
  12. StandardContext 表明一個獨立的應用程序,一般包含多個 Wrapper,一個 Wrapper 容器封裝了一個 Servlet,Wrapper的標準實現是 StandardWrapper。
  13. StandardPipeline 組件表明一個流水線,與 Valve(閥)結合,用於處理請求。 StandardPipeline 中含有多個 Valve, 當須要處理請求時,會逐一調用 Valve 的 invoke 方法對 Request 和 Response 進行處理。特別的,其中有一個特殊的 Valve 叫 basicValve,每個標準容器都有一個指定的 BasicValve,他們作的是最核心的工做。
  • StandardEngine 的是 StandardEngineValve,他用來將 Request 映射到指定的 Host;
  • StandardHost 的是 StandardHostValve, 他用來將 Request 映射到指定的 Context;
  • StandardContext 的是 StandardContextValve,它用來將 Request 映射到指定的 Wrapper;
  • StandardWrapper 的是 StandardWrapperValve,他用來加載 Rquest 所指定的 Servlet,並調用 Servlet 的 Service 方法。

Tomcat init

<figure data-block="true" data-editor="b4p0g" data-offset-key="8196v-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  • 當經過 ./startup.sh 腳本或直接經過 java 命令來啓動 Bootstrap 時,Tomcat 的啓動過程就正式開始了,啓動的入口點就是 Bootstrap 類的 main 方法。
  • 啓動的過程分爲兩步,分別是 init 和 start,本節主要介紹 init;
  1. 初始化類加載器。關於 Tomcat 類加載機制,能夠參考我以前寫的一片文章:[談談Java類加載機制]
  2. 經過從 CatalinaProperties 類中獲取 common.loader 等屬性,得到類加載器的掃描倉庫。CatalinaProperties 類在的靜態塊中調用了 loadProperties() 方法,從 conf/catalina.properties 文件中加載了屬性.(即在類建立的時候屬性就已經加載好了)。
  3. 經過 ClassLoaderFactory 建立 URLClassLoader 的實例
  4. 經過反射建立 Catalina 的實例並設置 parentClassLoader
  5. setAwait(true)。設置 Catalina 的 await 屬性爲 true。在 Start 階段尾部,若該屬性爲 true,Tomcat 會在 main 線程中監聽 SHUTDOWN 命令,默認端口是 8005.當收到該命令後執行 Catalina 的 stop() 方法關閉 Tomcat 服務器。
  6. createStartDigester()。Catalina 的該方法用於建立一個 Digester 實例,並添加解析 conf/server.xml 的 RuleSet。Digester 本來是 Apache 的一個開源項目,專門解析 XML 文件的,但我看 Tomcat-9.0.0.M22 中直接將這些類整合到 Tomcat 內部了,而不是引入 jar 文件。Digester 工具的原理不在本文的介紹範圍,有興趣的話能夠參考 The Digester Component - Apache 或 [《How Tomcat works》- Digester [推薦]](https://www.amazon.com/How-To... "按住 Ctrl 點擊訪問 https://www.amazon.com/How-To...X") 一章
  7. parse() 方法就是 Digester 處理 conf/server.xml 建立各個組件的過程。值的一提的是這些組件都是使用反射的方式來建立的。特別的,在建立 Digester 的時候,添加了一些特別的 rule Set,用於建立一些十分核心的組件,這些組件在 conf/server.xml 中沒有可是其做用都比較大,這裏作下簡單介紹,當 Start 時用到了再詳細說明:
  8. EngineConfig。LifecycleListener 的實現類,觸發 Engine 的生命週期事件後調用,這個監聽器沒有特別大的做用,就是打印一下日誌
  9. HostConfig。LifecycleListener 的實現類,觸發 Host 的生命週期事件後調用。這個監聽器的做用就是部署應用程序,這包括 conf/<Engine>/<Host>/ 目錄下全部的 Context xml 文件 和 webapps 目錄下的應用程序,不論是 war 文件仍是已解壓的目錄。 另外後臺進程對應用程序的熱部署也是由該監聽器負責的。
  10. ContextConfig。LifecycleListener 的實現類,觸發 Context 的生命週期事件時調用。這個監聽器的做用是配置應用程序,它會讀取併合並 conf/web.xml 和 應用程序的 web.xml,分析 /WEB-INF/classes/ 和 /WEB-INF/lib/*.jar中的 Class 文件的註解,將其中全部的 Servlet、ServletMapping、Filter、FilterMapping、Listener 都配置到 StandardContext 中,以備後期使用。固然了 web.xml 中還有一些其餘的應用程序參數,最後都會一併配置到 StandardContext 中。
  11. reconfigureStartStopExecutor() 用於從新配置啓動和中止子容器的 Executor。默認是 1 個線程。咱們能夠配置 conf/server.xml 中 Engine 的 startStopThreads,來指定用於啓動和中止子容器的線程數量,若是配置 0 的話會使用 Runtime.getRuntime().availableProcessors() 做爲線程數,若配置爲負數的話會使用 Runtime.getRuntime().availableProcessors() + 配置值,若和小與 1 的話,使用 1 做爲線程數。當線程數是 1 時,使用 InlineExecutorService 它直接使用當前線程來執行啓動中止操做,不然使用 ThreadPoolExecutor 來執行,其最大線程數爲咱們配置的值。
  12. 須要注意的是 Host 的 init 操做是在 Start 階段來作的, StardardHost 建立好後其 state 屬性的默認值是 LifecycleState.NEW,因此在其調用 startInternal() 以前會進行一次初始化。

Tomcat Start[Deployment]

<figure data-block="true" data-editor="b4p0g" data-offset-key="5j33i-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. 圖中從 StandardHost Start StandardContext 的這步其實在真正的執行流程中會直接跳過,由於 conf/server.xml 文件中並無配置任何的 Context,因此在 findChildren() 查找子容器時會返回空數組,因此以後遍歷子容器來啓動子容器的 for 循環就直接跳過了。
  2. 觸發 Host 的 BEFORE_START_EVENT 生命週期事件,HostConfig 調用其 beforeStart() 方法建立 $CATALINA_BASE/webapps& $CATALINA_BASE/conf/<Engine>/<Host>/ 目錄。
  3. 觸發 Host 的 START_EVENT 生命週期事件,HostConfig 調用其 start() 方法開始部署已在 $CATALINA_BASE/webapps & $CATALINA_BASE/conf/<Engine>/<Host>/ 目錄下的應用程序。
  4. 解析 $CATALINA_BASE/conf/<Engine>/<Host>/ 目錄下全部定義 Context 的 XML 文件,並添加到 StandardHost。這些 XML 文件稱爲應用程序描述符。正由於如此,咱們能夠配置一個虛擬路徑來保存應用程序中用到的圖片,詳細的配置過程請參考 開發環境配置指南 - 6.3. 配置圖片存放目錄
  5. 部署 $CATALINA_BASE/webapps 下全部的 WAR 文件,並添加到 StandardHost。
  6. 部署 $CATALINA_BASE/webapps 下全部已解壓的目錄,並添加到 StandardHost。
  • 特別的,添加到 StandardHost 時,會直接調用 StandardContext 的 start() 方法來啓動應用程序。啓動應用程序步驟請看 Context Start 一節。
  1. 在 StandardEngine 和 StandardContext 啓動時都會調用各自的 threadStart() 方法,該方法會建立一個新的後臺線程來處理該該容器和子容器及容器內各組件的後臺事件。StandardEngine 會直接建立一個後臺線程,StandardContext 默認是不建立的,和 StandardEngine 共用同一個。後臺線程處理機制是週期調用組件的 backgroundProcess() 方法。詳情請看 Background process 一節。
  2. MapperListener
  • addListeners(engine) 方法會將該監聽器添加到 StandardEngine 和它的全部子容器中
  • registerHost() 會註冊全部的 Host 和他們的子容器到 Mapper 中,方便後期請求處理時使用。
  • 當有新的應用(StandardContext)添加進來後,會觸發 Host 的容器事件,而後經過 MapperListener 將新應用的映射註冊到 Mapper 中。
  1. Start 工做都作完之後 Catalina 會建立一個 CatalinaShutdownHook 並註冊到 JVM。CatalinaShutdownHook 繼承了 Thread,是 Catalina 的內部類。其 run 方法中直接調用了 Catalina 的 stop() 方法來關閉整個服務器。註冊該 Thread 到 JVM 的緣由是防止用戶非正常終止 Tomcat,好比直接關閉命令窗口之類的。當直接關閉命令窗口時,操做系統會向 JVM 發送一個終止信號,而後 JVM 在退出前會逐一啓動已註冊的 ShutdownHook 來關閉相應資源。

Context Start

<figure data-block="true" data-editor="b4p0g" data-offset-key="1lemk-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. StandRoot 類實現了 WebResourceRoot 接口,它容納了一個應用程序的全部資源,通俗的來講就是部署到 webapps 目錄下對應 Context 的目錄裏的全部資源。由於我對 Tomcat 的資源管理部分暫時不是很感興趣,因此資源管理相關類只是作了簡單瞭解,並無深刻研究源代碼。
  2. resourceStart() 方法會對 StandardRoot 進行初始配置
  3. postWorkDirectory() 用於建立對應的工做目錄 $CATALINA_BASE/work/<Engine>/<Host>/<Context>, 該目錄用於存放臨時文件。
  4. StardardContext 只是一個容器,而 ApplicationContext 則是一個應用程序真正的運行環境,相關類及操做會在請求處理流程看完之後進行補充。
  5. StardardContext 觸發 CONFIGURE_START_EVENT 生命週期事件,ContextConfig 開始調用 configureStart() 對應用程序進行配置。
  6. 這個過程會解析併合並 conf/web.xml & conf/<Engine>/<Host>/web.xml.default & webapps/<Context>/WEB-INF/web.xml 中的配置。
  7. 配置配置文件中的參數到 StandardContext, 其中主要的包括 Servlet、Filter、Listener。
  8. 由於從 Servlet3.0 之後是直接支持註解的,因此服務器必須可以處理加了註解的類。Tomcat 經過分析 WEB-INF/classes/ 中的 Class 文件和 WEB-INF/lib/ 下的 jar 包將掃描到的 Servlet、Filter、Listerner 註冊到 StandardContext。
  9. setConfigured(true),是很是關鍵的一個操做,它標識了 Context 的成功配置,若未設置該值爲 true 的話,Context 會啓動失敗。

Background process

<figure data-block="true" data-editor="b4p0g" data-offset-key="1kbr9-0-0" contenteditable="false">

<figcaption>添加描述</figcaption>

</figure>

  1. 後臺進程的做用就是處理一下 Servlet 引擎中的週期性事件,處理週期默認是 10s。
  2. 特別的 StandardHost 的 backgroundProcess() 方法會觸發 Host 的 PERIODIC_EVENT 生命週期事件。而後 HostConfig 會調用其 check() 方法對已加載並進行太重新部署的應用程序進行 reload 或對新部署的應用程序進行熱部署。熱部署跟以前介紹的部署步驟一致, reload() 過程只是簡單的順序調用 setPause(true)、stop()、start()、setPause(false),其中 setPause(true) 的做用是暫時中止接受請求。
相關文章
相關標籤/搜索