1. 程式人生 > >你還記得 Tomcat 的工作原理麼

你還記得 Tomcat 的工作原理麼

SpringBoot 就像一條巨蟒,慢慢纏繞著我們,使我們麻痺。不得不承認,使用了 SpringBoot 確實提高了工作效率,但同時也讓我們遺忘了很多技能。剛入社會的時候,我還是通過 Tomcat 手動部署 JavaWeb 專案,還經常對 Tomcat 進行效能調優。除此之外,還需要自己理清楚各 Jar 之間的關係,以避免 Jar 丟失和各版本衝突導致服務啟動異常的問題。到如今,這些繁瑣而又重複的工作已經統統交給 SpringBoot 處理,我們可以把更多的精力放在業務邏輯上。但是,清楚 Tomcat 的工作原理和處理請求流程和分析 Spring 框架原始碼一樣的重要。至少面試官特別喜歡問這些底層原理和設計思路。希望這篇文章能給你一些幫助。 ## Tomcat 整體架構 Tomcat 是一個免費的、開源的、輕量級的 Web 應用伺服器。適合在併發量不是很高的中小企業專案中使用。 ### 檔案目錄結構 以下是 Tomcat 8 主要目錄結構 | 目錄 | 功能說明 | | ------- | ------------------------------------------------------------ | | bin | 存放可執行的檔案,如 startup 和 shutdown | | conf | 存放配置檔案,如核心配置檔案 server.xml 和應用預設的部署描述檔案 web.xml | | lib | 存放 Tomcat 執行需要的jar包 | | logs | 存放執行的日誌檔案 | | webapps | 存放預設的 web 應用部署目錄 | | work | 存放 web 應用程式碼生成和編譯檔案的臨時目錄 | ### 功能元件結構 Tomcat 的核心功能有兩個,分別是負責接收和反饋外部請求的聯結器 Connector,和負責處理請求的容器 Container。其中聯結器和容器相輔相成,一起構成了基本的 web 服務 Service。每個 Tomcat 伺服器可以管理多個 Service。 | 元件 | 功能 | | --------- | ------------------------------------------------------------ | | Connector | **負責對外接收反饋請求**。它是 Tomcat 與外界的交通樞紐,監聽埠接收外界請求,並將請求處理後傳遞給容器做業務處理,最後將容器處理後的結果反饋給外界。 | | Container | **負責對內處理業務邏輯**。其內部由Engine、Host、Context 和 Wrapper 四個容器組成,用於管理和呼叫 Servlet 相關邏輯。 | | Service | **對外提供的 Web 服務**。主要包含聯結器和容器兩個核心元件,以及其他功能元件。Tomcat 可以管理多個 Service,且各 Service 之間相互獨立。 | ![](https://img2020.cnblogs.com/blog/806956/202009/806956-20200909144009235-340003438.png) ## Tomcat 聯結器核心原理 Tomcat 聯結器框架——Coyote ### 聯結器核心功能 一、監聽網路埠,接收和響應網路請求。 二、網路位元組流處理。將收到的網路位元組流轉換成 Tomcat Request 再轉成標準的 ServletRequest 給容器,同時將容器傳來的 ServletResponse 轉成 Tomcat Response 再轉成網路位元組流。 ### 聯結器模組設計 為滿足聯結器的兩個核心功能,我們需要一個通訊端點來監聽埠;需要一個處理器來處理網路位元組流;最後還需要一個介面卡將處理後的結果轉成容器需要的結構。 | 元件 | 功能 | | --------------- | ------------------------------------------------------------ | | Endpoint | **端點**,用來處理 Socket 接收和傳送的邏輯。其內部由 Acceptor 監聽請求、Handler 處理資料、AsyncTimeout 檢查請求超時。具體的實現有 NioEndPoint、AprEndpoint 等。 | | Processor | **處理器**,負責構建 Tomcat Request 和 Response 物件。具體的實現有 Http11Processor、StreamProcessor 等。 | | Adapter | **介面卡**,實現 Tomcat Request、Response 與 ServletRequest、ServletResponse之間的相互轉換。這採用的是經典的介面卡設計模式。 | | ProtocolHandler | **協議處理器**,將不同的協議和通訊方式組合封裝成對應的協議處理器,如 Http11NioProtocol 封裝的是 HTTP + NIO。 | 對應的原始碼包路徑 `org.apache.coyote` 。對應的結構圖如下 ![](https://img2020.cnblogs.com/blog/806956/202009/806956-20200910152207307-659029230.png) ## Tomcat 容器核心原理 Tomcat 容器框架——Catalina ### 容器結構分析 每個 Service 會包含一個容器。容器由一個引擎可以管理多個虛擬主機。每個虛擬主機可以管理多個 Web 應用。每個 Web 應用會有多個 Servlet 包裝器。Engine、Host、Context 和 Wrapper,四個容器之間屬於父子關係。 | 容器 | 功能 | | ------- | ------------------------------------------------------------ | | Engine | **引擎**,管理多個虛擬主機。 | | Host | **虛擬主機**,負責 Web 應用的部署。 | | Context | **Web 應用**,包含多個 Servlet 封裝器。 | | Wrapper | **封裝器**,容器的最底層。對 Servlet 進行封裝,負責例項的建立、執行和銷燬功能。 | 對應的原始碼包路徑 `org.apache.coyote` 。對應的結構圖如下 ![](https://img2020.cnblogs.com/blog/806956/202009/806956-20200911115535193-1855186804.png) ### 容器請求處理 容器的請求處理過程就是在 Engine、Host、Context 和 Wrapper 這四個容器之間層層呼叫,最後在 Servlet 中執行對應的業務邏輯。各容器都會有一個通道 Pipeline,每個通道上都會有一個 Basic Valve(如StandardEngineValve), 類似一個閘門用來處理 Request 和 Response 。其流程圖如下。 ![](https://img2020.cnblogs.com/blog/806956/202009/806956-20200911144157086-177472555.png) ## Tomcat 請求處理流程 上面的知識點已經零零碎碎地介紹了一個 Tomcat 是如何處理一個請求。簡單理解就是聯結器的處理流程 + 容器的處理流程 = Tomcat 處理流程。哈!那麼問題來了,Tomcat 是如何通過請求路徑找到對應的虛擬站點?是如何找到對應的 Servlet 呢? ### 對映器功能介紹 這裡需要引入一個上面沒有介紹的元件 Mapper。顧名思義,其作用是提供請求路徑的路由對映。根據請求URL地址匹配是由哪個容器來處理。其中每個容器都會它自己對應的Mapper,如 MappedHost。不知道大家有沒有回憶起被 Mapper class not found 支配的恐懼。在以前,每寫一個完整的功能,都需要在 web.xml 配置對映規則,當檔案越來越龐大的時候,各個問題隨著也會出現 ### HTTP請求流程 開啟 tomcat/conf 目錄下的 server.xml 檔案來分析一個http://localhost:8080/docs/api 請求。 第一步:聯結器監聽的埠是8080。由於請求的埠和監聽的埠一致,聯結器接受了該請求。 第二步:因為引擎的預設虛擬主機是 localhost,並且虛擬主機的目錄是webapps。所以請求找到了 tomcat/webapps 目錄。 第三步:解析的 docs 是 web 程式的應用名,也就是 context。此時請求繼續從 webapps 目錄下找 docs 目錄。有的時候我們也會把應用名省略。 第四步:解析的 api 是具體的業務邏輯地址。此時需要從 docs/WEB-INF/web.xml 中找對映關係,最後呼叫具體的函式。 ```xml ``` ![](https://img2020.cnblogs.com/blog/806956/202009/806956-20200911171828952-321303971.png) ## SpringBoot 如何啟動內嵌的 Tomcat SpringBoot 一鍵啟動服務的功能,讓有很多剛入社會的朋友都忘記 Tomcat 是啥。隨著硬體的效能越來越高,普通中小專案都可以直接用內建 Tomcat 啟動。但是有些大一點的專案可能會用到 Tomcat 叢集和調優,內建的 Tomcat 就不一定能滿足需求了。 我們先從原始碼中分析 SpringBoot 是如何啟動 Tomcat,以下是 SpringBoot 2.x 的程式碼。 程式碼從 main 方法開始,執行 run 方法啟動專案。 ```java SpringApplication.run ``` 從 run 方法點進去,找到重新整理應用上下文的方法。 ```java this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); this.refreshContext(context); this.afterRefresh(context, applicationArguments); ``` 從 refreshContext 方法點進去,找 refresh 方法。並一層層往上找其父類的方法。 ```java this.refresh(context); ``` 在 AbstractApplicationContext 類的 refresh 方法中,有一行呼叫子容器重新整理的邏輯。 ```java this.postProcessBeanFactory(beanFactory); this.invokeBeanFactoryPostProcessors(beanFactory); this.registerBeanPostProcessors(beanFactory); this.initMessageSource(); this.initApplicationEventMulticaster(); this.onRefresh(); this.registerListeners(); this.finishBeanFactoryInitialization(beanFactory); this.finishRefresh(); ``` 從 onRefresh 方法點進去,找到 ServletWebServerApplicationContext 的實現方法。在這裡終於看到了希望。 ```java protected void onRefresh() { super.onRefresh(); try { this.createWebServer(); } catch (Throwable var2) { throw new ApplicationContextException("Unable to start web server", var2); } } ``` 從 createWebServer 方法點進去,找到從工廠類中獲取 WebServer的程式碼。 ```java if (webServer == null && servletContext == null) { ServletWebServerFactory factory = this.getWebServerFactory(); // 獲取 web server this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()}); } else if (servletContext != null) { try { // 啟動 web server this.getSelfInitializer().onStartup(servletContext); } catch (ServletException var4) { throw new ApplicationContextException("Cannot initialize servlet context", var4); } } ``` 從 getWebServer 方法點進去,找到 TomcatServletWebServerFactory 的實現方法,與之對應的還有 Jetty 和 Undertow。這裡配置了基本的聯結器、引擎、虛擬站點等配置。 ```java public WebServer getWebServer(ServletContextInitializer... initializers) { Tomcat tomcat = new Tomcat(); File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat"); tomcat.setBaseDir(baseDir.getAbsolutePath()); Connector connector = new Connector(this.protocol); tomcat.getService().addConnector(connector); this.customizeConnector(connector); tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false); this.configureEngine(tomcat.getEngine()); Iterator var5 = this.additionalTomcatConnectors.iterator(); while(var5.hasNext()) { Connector additionalConnector = (Connector)var5.next(); tomcat.getService().addConnector(additionalConnector); } this.prepareContext(tomcat.getHost(), initializers); return this.getTomcatWebServer(tomcat); } ``` ![](https://img2020.cnblogs.com/blog/806956/202009/806956-20200911222756876-593137606.gif) 服務啟動後會列印日誌 ``` o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8900 (http) o.apache.catalina.core.StandardService : Starting service [Tomcat] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.34 o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal ... o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 16858 ms ``` ## END 文章到這裡就結束了,實在是 hold 不住了,週末寫了一整天還沒有寫到原始碼部分,只能放在下一章了。再寫真的就要廢了,有什麼不對的地方請多多指出。最後完整版可以通過微信公眾號 **學英語會程式設計** 閱讀。完整版在這個基礎上添加了一些單詞解析,我們的口號是:英語學得好,原始碼看