Tomcat源碼分析(從啟動流程到請求處理)


 

Tomcat 8.5下載地址

https://tomcat.apache.org/download-80.cgi

Tomcat啟動流程

Tomcat源碼目錄

catalina目錄

catalina包含所有的Servlet容器實現,以及涉及到安全、會話、集群、部署管理Servlet容器的各個方面,同時,它還包含了啟動入口。

coyote目錄

coyoteTomcat鏈接器框架的名稱,是Tomcat服務器提供的客戶端訪問的外部接口,客戶端通過Coyote與服務器建立鏈接、發送請求並接收響應。

El目錄,提供java表達式語言

Jasper模塊提供JSP引擎

Naming模塊提供JNDI的服務

Juli提供服務器日志的服務

tomcat提供外部調用的接口api

Tomcat啟動流程分析

  1. 啟動流程解析:注意是標准的啟動,也就是從bin目錄下的啟動文件中啟動Tomcat

 

 

我們可以看到這個流程非常的清晰,同時注意到,Tomcat的啟動非常的標准,除去BoostrapCatalin,我們可以對照一下Server.xml的配置文件。Server,service等等這些組件都是一一對照,同時又有先后順序。

基本的順序是先init方法,然后再start方法。

  1. 加入調試信息():注意是標准的啟動,也就是從bin目錄下的啟動文件中啟動Tomcat

 

 

 

 

可以看到,在源碼中加入調試的信息和流程圖是一致的。

我們可以看到,除了Bootstrapcatalina類,其他的Server,service等等之類的都只是一個接口,實現類均為StandardXXX類。

我們來看下StandardServer類,

 

 

問題來了,我們發現StandardServer類中沒有init方法,只有一個類似於initinitInternal方法,這個是為什么?

帶着問題我們進入下面的內容。

分析Tomcat請求過程

解耦:網絡協議與容器的解耦。

Connector鏈接器封裝了底層的網絡請求(Socket請求及相應處理),提供了統一的接口,使Container容器與具體的請求協議以及I/O方式解耦。

ConnectorSocket輸入轉換成Request對象,交給Container容器進行處理,處理請求后,Container通過Connector提供的Response對象將結果寫入輸出流。

因為無論是Request對象還是Response對象都沒有實現Servlet規范對應的接口,Container會將它們進一步分裝成ServletRequestServletResponse.

問題來了,在Engine容器中,有四個級別的容器,他們的標准實現分別是StandardEngineStandardHostStandardContextStandardWrapper

 

組件的生命周期管理

各種組件如何統一管理

Tomcat的架構設計是清晰的、模塊化、它擁有很多組件,加入在啟動Tomcat時一個一個組件啟動,很容易遺漏組件,同時還會對后面的動態組件拓展帶來麻煩。如果采用我們傳統的方式的話,組件在啟動過程中如果發生異常,會很難管理,比如你的下一個組件調用了start方法,但是如果它的上級組件還沒有start甚至還沒有init的話,Tomcat的啟動會非常難管理,因此,Tomcat的設計者提出一個解決方案:用Lifecycle管理啟動,停止、關閉。

生命周期統一接口——Lifecycle

Tomcat內部架構中各個核心組件有包含與被包含關系,例如:Server包含了Service.Service又包含了ContainerConnector,這個結構有一點像數據結構中的樹,樹的根結點沒有父節點,其他節點有且僅有一個父節點,每一個父節點有0至多個子節點。所以,我們可以通過父容器啟動它的子容器,這樣只要啟動根容器,就可以把其他所有的容器都啟動,從而達到了統一的啟動,停止、關閉的效果。

所有所有組件有一個統一的接口——Lifecycle,把所有的啟動、停止、關閉、生命周期相關的方法都組織到一起,就可以很方便管理Tomcat各個容器組件的生命周期。

Lifecycle其實就是定義了一些狀態常量和幾個方法,主要方法是init,start,stop三個方法。

例如:TomcatServer組件的init負責遍歷調用其包含所有的Service組件的init方法。

注意:Server只是一個接口,實現類為StandardServer,有意思的是,StandardServer沒有init方法,init方法是在哪里,其實是在它的父類LifecycleBase中,這個類就是統一的生命周期管理。

 

 

 

 

 

所以StandardServer最終只會調用到initInternal方法,這個方法會初始化子容器Serviceinit方法

 

 

為什么LifecycleBase這么玩,其實很多架構源碼都是這么玩的,包括JDK的容器源碼都是這么玩的,一個類,有一個接口,同時抽象一個抽象骨架類,把通用的實現放在抽象骨架類中,這樣設計就方便組件的管理,使用LifecycleBase骨架抽象類,在抽象方法中就可以進行統一的處理,具體的內容見下面。

抽象類LifecycleBase統一管理組件生命周期

 

 

 

 

 

 

具體實現類StandardXXX類調用initInternal方法實現具體的業務處理。

 

 

分析Tomcat請求過程

Host設計的目的

Tomcat誕生時,服務器資源很貴,所以一般一台服務器其實可以有多個域名映射,滿了滿足這種需求,比如,我的這台電腦,有一個localhost域名,同時在我的hosts文件中配置兩個域名,一個www.a.com  一個localhost

Context設計的目的


container從上一個組件connector手上接過解析好的內部request,根據request來進行一系列的邏輯操作,直到調用到請求的servlet,然后組裝好response,返回給connecotr

先來看看container的分類吧:
Engine
Host
Context
Wrapper
它們各自的實現類分別是StandardEngine, StandardHost, StandardContext, and StandardWrapper,他們都在tomcatorg.apache.catalina.core包下。

它們之間的關系,可以查看tomcatserver.xml也能明白(根據節點父子關系),這么比喻吧:除了Wrapper最小,不能包含其他container外,Context內可以有零或多個WrapperHost可以擁有零或多個HostEngine可以有零到多個Host

Standard container都是直接繼承抽象類:org.apache.catalina.core.ContainerBase

 

 

 

Tomcat處理一個HTTP請求的過程

用戶點擊網頁內容,請求被發送到本機端口8080,被在那里監聽的Coyote HTTP/1.1 Connector獲得。

Connector把該請求交給它所在的ServiceEngine來處理,並等待Engine的回應。

Engine獲得請求localhost/test/index.jsp,匹配所有的虛擬主機Host

Engine匹配到名為localhostHost(即使匹配不到也把請求交給該Host處理,因為該Host被定義為該Engine的默認主機),名為localhostHost獲得請求/test/index.jsp,匹配它所擁有的所有的ContextHost匹配到路徑為/testContext(如果匹配不到就把該請求交給路徑名為“ ”Context去處理)。

path=“/test”Context獲得請求/index.jsp,在它的mapping table中尋找出對應的ServletContext匹配到URL PATTERN*.jspServlet,對應於JspServlet類。

構造HttpServletRequest對象和HttpServletResponse對象,作為參數調用JspServletdoGet()或doPost().執行業務邏輯、數據存儲等程序。

Context把執行完之后的HttpServletResponse對象返回給Host

HostHttpServletResponse對象返回給Engine

EngineHttpServletResponse對象返回Connector

ConnectorHttpServletResponse對象返回給客戶Browser

 

管道模式

管道與閥門

在一個比較復雜的大型系統中,如果一個對象或數據流需要進行繁雜的邏輯處理,我們可以選擇在一個大的組件中直接處理這些繁雜的邏輯處理,這個方式雖然達到目的,但是拓展性和可重用性差。因為牽一發而動全身。

管道是就像一條管道把多個對象連接起來,整體看起來就像若干個閥門嵌套在管道中,而處理邏輯放在閥門上。

它的結構和實現是非常值得我們學習和借鑒的。

首先要了解的是每一種container都有一個自己的StandardValve
上面四個container對應的四個是:
StandardEngineValve

StandardHostValve
StandardContextValve

StandardWrapperValve

Pipeline就像一個工廠中的生產線,負責調配工人(valve)的位置,valve則是生產線上負責不同操作的工人。
一個生產線的完成需要兩步:
1,把原料運到工人邊上
2,工人完成自己負責的部分

tomcatPipeline實現是這樣的:
1,在生產線上的第一個工人拿到生產原料后,二話不說就人給下一個工人,下一個工人模仿第一個工人那樣扔給下一個工人,直到最后一個工人,而最后一個工人被安排為上面提過的StandardValve,他要完成的工作居然是把生產資料運給自己包含的containerPipeline上去。
2,四個container就相當於有四個生產線(Pipeline),四個Pipeline都這么干,直到最后的StandardWrapperValve拿到資源開始調用servlet。完成后返回來,一步一步的valve按照剛才丟生產原料是的順序的倒序一次執行。如此才完成了tomcatPipeline的機制。

手寫管道模式實現

我們了解到了,在管道中連接一個或者多個閥門,每一個閥門負責一部分邏輯處理,數據按照規定的順序往下流。此種模式分解了邏輯處理任務,可方便對某個任務單元進行安裝、拆卸,提高流程的可拓展性,可重用性,機動性,靈活性。

源碼分析

CoyoteAdapterservice方法里,由下面這一句就進入Container的。
connector.getContainer().getPipeline().getFirst().invoke(request, response);  
是的,這就是進入container迷宮的大門,歡迎來到Container

 

 

一個StandardValve

來自org.apache.catalina.core.StandardEngineValveinvoke方法:

 

 

其他的類似StandardHostValve、StandardContextValve、StandardWrapperValve

Tomcat中定制閥門

管道機制給我們帶來了更好的拓展性,例如,你要添加一個額外的邏輯處理閥門是很容易的。

  1. 自定義個閥門PrintIPValve,只要繼承ValveBase並重寫invoke方法即可。注意在invoke方法中一定要執行調用下一個閥門的操作,否則會出現異常。

public class PrintIPValve extends ValveBase{

    @Override

    public void invoke(Request request, Response response) throws IOException, ServletException {

        System.out.println("------自定義閥門PrintIPValve:"+request.getRemoteAddr());

        getNext().invoke(request,response);

    }

}

  1. 配置Tomcat的核心配置文件server.xml,這里把閥門配置到Engine容器下,作用范圍就是整個引擎,也可以根據作用范圍配置在Host或者是Context

<Valve className="org.apache.catalina.valves.PrintIPValve"/>

  1. 源碼中是直接可以有效果,但是如果是運行版本,則可以將這個類導出成一個Jar包放入Tomcat/lib目錄下,也可以直接將.class文件打包進catalina.jar包中。

Tomcat中提供常用的閥門

AccessLogValve,請求訪問日志閥門,通過此閥門可以記錄所有客戶端的訪問日志,包括遠程主機IP,遠程主機名,請求方法,請求協議,會話ID,請求時間,處理時長,數據包大小等。它提供任意參數化的配置,可以通過任意組合來定制訪問日志的格式。

JDBCAccessLogValve,同樣是記錄訪問日志的閥門但是它有助於將訪問日志通過JDBC持久化到數據庫中。

ErrorReportValve,這是一個講錯誤以HTML格式輸出的閥門

PersistentValve,這是對每一個請求的會話實現持久化的閥門

RemoteAddrValve,訪問控制閥門可以通過配置決定哪些IP可以訪問WEB應用

RemoteHostValve,訪問控制閥門通過配置覺得哪些主機名可以訪問WEB應用

RemoteIpValve,針對代理或者負載均衡處理的一個閥門一般經過代理或者負載均衡轉發的請求都將自己的IP添加到請求頭”X-Forwarded-For”中,此時,通過閥門可以獲取訪問者真實的IP

SemaphoreValve,這個是一個控制容器並發訪問的閥門,可以作用在不同容器上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM