天天看點

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

本系列文章将整理到我在GitHub上的《Java面試指南》倉庫,更多精彩内容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章首發于我的個人部落格:

www.how2playlife.com

本文是微信公衆号【Java技術江湖】的《走進JavaWeb技術世界》其中一篇,本文部分内容來源于網絡,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格内容,引用其中了一些比較好的部落格文章,如有侵權,請聯系作者。

該系列博文會告訴你如何從入門到進階,從servlet到架構,從ssm再到SpringBoot,一步步地學習JavaWeb基礎知識,并上手進行實戰,接着了解JavaWeb項目中經常要使用的技術群組件,包括日志元件、Maven、Junit,等等内容,以便讓你更完整地了解整個Java Web技術體系,形成自己的知識架構。

為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公衆号【Java技術江湖】聯系作者,歡迎你參與本系列博文的創作和修訂。

很多東西在時序圖中展現的已經非常清楚了,沒有必要再一步一步的作介紹,是以本文以圖為主,然後對部分内容加以簡單解釋。

繪制圖形使用的工具是 PlantUML + Visual Studio Code + PlantUML Extension

本文對 Tomcat 的介紹以 Tomcat-9.0.0.M22 為标準。

Tomcat-9.0.0.M22 是 Tomcat 目前最新的版本,但尚未釋出,它實作了 Servlet4.0 及 JSP2.3 并提供了很多新特性,需要 1.8 及以上的 JDK 支援等等,詳情請查閱 Tomcat-9.0-doc。

https://tomcat.apache.org/tomcat-9.0-doc/index.html

Overview

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

Connector 啟動以後會啟動一組線程用于不同階段的請求處理過程。

  1. Acceptor 線程組。用于接受新連接配接,并将新連接配接封裝一下,選擇一個 Poller 将新連接配接添加到 Poller 的事件隊列中。
  2. Poller 線程組。用于監聽 Socket 事件,當 Socket 可讀或可寫等等時,将 Socket 封裝一下添加到 worker 線程池的任務隊列中。
  3. worker 線程組。用于對請求進行處理,包括分析請求封包并建立 Request 對象,調用容器的 pipeline 進行處理。

Acceptor、Poller、worker 所在的 ThreadPoolExecutor 都維護在 NioEndpoint 中。

Connector Init and Start

走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啟動部署過程
  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 對其進行定制。

Request Process

Acceptor

走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啟動部署過程
  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

走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啟動部署過程
  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

走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啟動部署過程
  • worker 線程被建立以後就執行 ThreadPoolExecutor 的 runWorker() 方法,試圖從 workQueue 中取待處理任務,但是一開始 workQueue 是空的,是以 worker 線程會阻塞在 workQueue.take() 方法。
  • 當新任務添加到 workQueue後,workQueue.take() 方法會傳回一個 Runnable,通常是 SocketProcessor,然後 worker 線程調用 SocketProcessor 的 run() 方法對 Socket 進行處理。
  • createProcessor() 會建立一個 Http11Processor, 它用來解析 Socket,将 Socket 中的内容封裝到 Request 中。注意這個 Request 是臨時使用的一個類,它的全類名是 org.apache.coyote.Request,
  • postParseRequest() 方法封裝一下 Request,并處理一下映射關系(從 URL 映射到相應的 Host、Context、Wrapper)。
  1. CoyoteAdapter 将 Rquest 送出給 Container 處理之前,并将 org.apache.coyote.Request 封裝到 org.apache.catalina.connector.Request,傳遞給 Container 處理的 Request 是 org.apache.catalina.connector.Request。
  2. connector.getService().getMapper().map(),用來在 Mapper 中查詢 URL 的映射關系。映射關系會保留到 org.apache.catalina.connector.Request 中,Container 處理階段 request.getHost() 是使用的就是這個階段查詢到的映射主機,以此類推 request.getContext()、request.getWrapper() 都是。
  • connector.getService().getContainer().getPipeline().getFirst().invoke() 會将請求傳遞到 Container 處理,當然了 Container 處理也是在 Worker 線程中執行的,但是這是一個相對獨立的子產品,是以單獨分出來一節。

Container

走進JavaWeb技術世界8:淺析Tomcat9請求處理流程與啟動部署過程
  • 需要注意的是,基本上每一個容器的 StandardPipeline 上都會有多個已注冊的 Valve,我們隻關注每個容器的 Basic Valve。其他 Valve 都是在 Basic Valve 前執行。
  • request.getHost().getPipeline().getFirst().invoke() 先擷取對應的 StandardHost,并執行其 pipeline。
  • request.getContext().getPipeline().getFirst().invoke() 先擷取對應的 StandardContext,并執行其 pipeline。
  • request.getWrapper().getPipeline().getFirst().invoke() 先擷取對應的 StandardWrapper,并執行其 pipeline。
  • 最值得說的就是 StandardWrapper 的 Basic Valve,StandardWrapperValve
  1. allocate() 用來加載并初始化 Servlet,值的一提的是 Servlet 并不都是單例的,當 Servlet 實作了 SingleThreadModel 接口後,StandardWrapper 會維護一組 Servlet 執行個體,這是享元模式。當然了 SingleThreadModel在 Servlet 2.4 以後就棄用了。
  2. createFilterChain() 方法會從 StandardContext 中擷取到所有的過濾器,然後将比對 Request URL 的所有過濾器挑選出來添加到 filterChain 中。
  3. doFilter() 執行過濾鍊,當所有的過濾器都執行完畢後調用 Servlet 的 service() 方法。

Reference

  1. 《How Tomcat works》
    https://www.amazon.com/How-Tomcat-Works-Budi-Kurniawan/dp/097521280X
               
  2. 《Tomcat 架構解析》– 劉光瑞
    http://product.dangdang.com/25084132.html
               
  3. Tomcat-9.0-doc
    https://tomcat.apache.org/tomcat-9.0-doc/index.html
               
  4. apache-tomcat-9.0.0.M22-src
    http://www-eu.apache.org/dist/tomcat/tomcat-9/v9.0.0.M22/src/
               
  5. tomcat架構分析 (connector NIO 實作)
    http://gearever.iteye.com/blog/1844203
               

微信公衆号

個人公衆号:程式員黃小斜

微信公衆号【程式員黃小斜】新生代青年聚集地,程式員成長充電站。作者黃小斜,職業是阿裡程式員,身份是斜杠青年,希望和更多的程式員交朋友,一起進步和成長!專注于分享技術、面試、職場等成長幹貨,這一次,我們一起出發。

關注公衆号後回複“2020”領取我這兩年整理的學習資料,涵蓋自學程式設計、求職面試、算法刷題、Java技術學習、計算機基礎和考研等8000G資料合集。

技術公衆号:Java技術江湖

微信公衆号【Java技術江湖】一位阿裡 Java 工程師的技術小站,專注于 Java 相關技術:SSM、SpringBoot、MySQL、分布式、中間件、叢集、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術幹貨和學習經驗,緻力于Java全棧開發!

關注公衆号後回複“PDF”即可領取200+頁的《Java工程師面試指南》強烈推薦,幾乎涵蓋所有Java工程師必知必會的知識點。