你還記得 Tomcat 的工作原理麼

ITDragon龍發表於2020-09-14

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 之間相互獨立。

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 。對應的結構圖如下

Tomcat 容器核心原理

Tomcat 容器框架——Catalina

容器結構分析

每個 Service 會包含一個容器。容器由一個引擎可以管理多個虛擬主機。每個虛擬主機可以管理多個 Web 應用。每個 Web 應用會有多個 Servlet 包裝器。Engine、Host、Context 和 Wrapper,四個容器之間屬於父子關係。

容器 功能
Engine 引擎,管理多個虛擬主機。
Host 虛擬主機,負責 Web 應用的部署。
Context Web 應用,包含多個 Servlet 封裝器。
Wrapper 封裝器,容器的最底層。對 Servlet 進行封裝,負責例項的建立、執行和銷燬功能。

對應的原始碼包路徑 org.apache.coyote 。對應的結構圖如下

容器請求處理

容器的請求處理過程就是在 Engine、Host、Context 和 Wrapper 這四個容器之間層層呼叫,最後在 Servlet 中執行對應的業務邏輯。各容器都會有一個通道 Pipeline,每個通道上都會有一個 Basic Valve(如StandardEngineValve), 類似一個閘門用來處理 Request 和 Response 。其流程圖如下。

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 version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">

  <Service name="Catalina">

	<!-- 聯結器監聽埠是 8080,預設通訊協議是 HTTP/1.1 -->
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
			   
	<!-- 名字為 Catalina 的引擎,其預設的虛擬主機是 localhost -->
    <Engine name="Catalina" defaultHost="localhost">

	  <!-- 名字為 localhost 的虛擬主機,其目錄是 webapps-->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

      </Host>
    </Engine>
  </Service>
</Server>

SpringBoot 如何啟動內嵌的 Tomcat

SpringBoot 一鍵啟動服務的功能,讓有很多剛入社會的朋友都忘記 Tomcat 是啥。隨著硬體的效能越來越高,普通中小專案都可以直接用內建 Tomcat 啟動。但是有些大一點的專案可能會用到 Tomcat 叢集和調優,內建的 Tomcat 就不一定能滿足需求了。

我們先從原始碼中分析 SpringBoot 是如何啟動 Tomcat,以下是 SpringBoot 2.x 的程式碼。

程式碼從 main 方法開始,執行 run 方法啟動專案。

SpringApplication.run

從 run 方法點進去,找到重新整理應用上下文的方法。

this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);

從 refreshContext 方法點進去,找 refresh 方法。並一層層往上找其父類的方法。

this.refresh(context);

在 AbstractApplicationContext 類的 refresh 方法中,有一行呼叫子容器重新整理的邏輯。

this.postProcessBeanFactory(beanFactory);
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();

從 onRefresh 方法點進去,找到 ServletWebServerApplicationContext 的實現方法。在這裡終於看到了希望。

protected void onRefresh() {
    super.onRefresh();

    try {
        this.createWebServer();
    } catch (Throwable var2) {
        throw new ApplicationContextException("Unable to start web server", var2);
    }
}

從 createWebServer 方法點進去,找到從工廠類中獲取 WebServer的程式碼。

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。這裡配置了基本的聯結器、引擎、虛擬站點等配置。

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);
}

服務啟動後會列印日誌

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 不住了,週末寫了一整天還沒有寫到原始碼部分,只能放在下一章了。再寫真的就要廢了,有什麼不對的地方請多多指出。最後完整版可以通過微信公眾號 學英語會程式設計 閱讀。完整版在這個基礎上新增了一些單詞解析,我們的口號是:英語學得好,原始碼看的爽。

相關文章