文章概覽
相信很多接觸java的人都對Tom貓有着多少的熟悉,就個人而言,本來只知道Tom簡單的操作與配置,就像裹上一層紗,迷迷糊糊的.
Tomcat的書籍本來就不多,高分的還是很久之前的版本,直到最近看到下面這本書,解答了我的很多疑問,同時這篇文章將總結讀書收獲.
如果覺得文章寫的內容是你感興趣的或者我的貓使你感興趣,建議你讀讀這本書.


該文會介紹Tom的架構,服務器如何從一層層抽象設計到完整的架構
Tomcat介紹
Tom是一款全世界著名的輕量級應用服務器,基於java,服務於java.主要作為應用服務器來處理客戶端發來的動態資源響應.
目前版本是9.x,很多人都在使用6.x,但新版其實提供了很多新的功能,比如WebSocket的支持,點擊了解WebSocket.

Tomcat啟動參數
windows修改$CATALINA_HOME/bin/catalina.bat文件
Set JAVA_OPTS=-server -Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m
linux修改$CATALINA_HOME/bin/catalina.sh文件
JAVA_OPTS="-server -Xms1024m -Xmx2048m -XX:PermSize=256m -XX:MaxPermSize=512m"
-server Server端啟動Tomcat,Client啟動Tomcat兩者的初始化參數會有所不同
-Xms1024m 初始化堆內存大小
-Xmx2048m 允許的最大堆內存大小
-XX:PermSize=256m 初始化非堆內存大小
-XX:MaxPermSize=512m 允許的最大非堆內存大小
Debug方式
依賴於JDK提供的JPDA(Java Platform Debugger Architecture,Java平台調試體系)
catalina jpda start
目錄結構

該文使用的版本是apache-tomcat-9.0.1,目錄大致都很容易理解.
conf放置的Tomcat的核心配置文件,下文會介紹.
webapps是默認的web app的應用目錄,只要把項目目錄放置進去就可以運行
work是Tomcat運行時產生的jsp編譯文件所存放的位置
總體架構
Tomcat是一款應用服務器,我們從最根本的類一層一層演變,直至Tomcat當前版本.
1.應用服務器是接收其他計算機(客戶端)發來的請求數據並對其解析,完成相關業務處理,然后把處理結果作為響應返回給計算機.

2.一個問題擺在眼前,前面Server請求監聽和請求處理放在一起,應用服務器通常會與web服務器進行集群部署和負載均衡,但是這兩者的協議並不是HTTP.
也就是說,服務器連接的另一端需要適配不同的協議來對請求作出不同的處理.前面的模型擴展性太差,應該分離請求監聽和請求處理.
Connector模塊管理請求監聽,Container模塊負責請求處理,兩個組件都擁有start()和stop()來加載和釋放自己維護的資源.
這樣子,Server下可以有多個Connector來傳送請求至不同的Container中.

3.上面的設計有個缺陷,既然server可以有多個Connector和Container,那么如何知道哪個Connector將請求發至哪個Container呢?
考慮一下下面這個設計圖,

4.我們接觸過的Tomcat應該是放置web app的容器,在哪放置web app?這將決定哪個app來處理Engine所獲取的請求信息.

再想一下,我們瀏覽器是個app,對吧?然后服務器其實也是app對吧?網絡就是兩者的通信.我們來看一下網絡是怎么進行通信的.

沒錯,web app需要端點信息(IP地址,端口號),我們需要提供這一層的抽象.一個Host下可以對應有多個app(Context).
5.現在設計已經可以滿足兩個應用的連接了,現在設想一下,應用該怎么進行表示?畢竟Tomcat作為一款Servlet容器而存在.首先Apache組織按照Servlet官方的標准,加入了Servlet的包裝類Wrapper.

6.就目前為止,我們使用"容器"這一概念來形容處理接收客戶端的請求並且返回響應數據的組件,依此,使用一個類Container來統一表示這一想法,讓Engine,Host,Context,Wrapper這類組件來繼承Container.

Container類能夠添加子組件addChild方法,有時需要執行一些異步處理,所以加入backgroundProcess方法.
由於Engine,Host,Context,Wrapper這類的引用變成了父類Container,所以之前的強組合關系變成了弱組合關系.強弱關系指的是兩個類直接關聯或者是間接關聯.
7.為了從抽象和復用層面上再審視一下當前設計,使概念更加清晰,提供通用性定義.由於所有容器都有着自身的生命周期管理方法,那么我們可以將其進行抽象成一個接口Lifecycle,在方法定義上加入初始化方法init,銷毀方法destroy,事件監聽方法addLifecycleListener和removeLifecycleListener.

8.上面這個設計Container部分具有伸縮性和擴展性,這很棒.接下來Tomcat的開發人員為了提高每個組件的靈活性,使其更易擴展,加入了Pipeline和Valve這兩個接口.這兩個接口的設計運用了職責鏈模式.

簡單介紹以下職責鏈模式,關於設計模式可以查看博客里的文章《軟件設計 : 聚焦設計模式》
職責鏈模式使用一個抽象類來統一定義處理器,然后將處理器構造成一條鏈,當Client端發來請求時,第一個Handler判斷是否處理,不處理則往下個Handler傳遞,直至被處理或則處理鏈結束.


回頭看Tomcat怎么運用這個設計模式,Pipeline接口用於構建職責鏈,Valve接口代表職責鏈上的每個處理器.
Pipeline中維護一個基礎的Valve,它始終位於Pipeline執行鏈的末端,封裝了具體的請求處理和輸出響應過程.

這樣就可以構造一條職責鏈,可是為什么要這么做?記得之前Container不就是可以自包含的容器嗎?為什么要弄出多兩個接口?
的確,Container可以自包含,但是它是作為容器抽象類而存在,而閥(Valve)作為接口而存在,我們可以在實現這個接口的類中添加屬於我們自己的Valve實現類,你想做什么都行.
就像水管一樣,你如果是超級馬里奧,你可以隨時給它加個閥,做任何事.


9.前面的設計基本落在Container部分,來看看Connector的設計方案,Connector必須完成下面的功能項.
①監聽服務器端口,讀取客戶端的請求
②將請求數據按指定協議進行解析
③根據請求地址匹配正確的容器進行處理
④將請求返回客戶端
Tomcat支持多協議(HTTP/AJP)和多種IO方式(BIO,NIO,NIO2,APR,HTTP/2)

ProtocolHandler表示協議處理器,針對不同的協議和IO方式,提供不同的實現,ProtocolHandler包含一個Endpoint用來啟動socket監聽,該接口按照IO方式進行分類實現,還包含一個Process用於按照指定協議讀取數據,並交由容器處理.
處理邏輯如下:
1.在Connector啟動時,Endpoint會啟動線程來監聽服務器端口,並在接收到請求后調用Process進行數據讀取.
2.當Process讀取客戶端請求之后,需要按照地址映射到具體的容器進行處理,即請求映射.
3.由於Tomcat各個組件采用通用的生命周期進行管理,而且通過管理工具進行狀態變更,因此請求映射除了考慮映射規則的實現外,還要考慮容器組件的注冊和銷毀.

Tomcat采用Mapper來維護容器映射信息,按照映射規則(Servlet規范定義)查找容器;
MapperListener實現LifecycleListener和ContainerListener,用於在容器組件狀態變更時,注冊或者取消對應的容器映射信息;
MapperListener實現了Lifecycle接口,當Service啟動時,會自動作為監聽器注冊到各個容器組件之上,同時將已創建的容器注冊到Mapper;
Tomcat通過適配器模式實現了Connector與Mapper,Container的解耦,默認實現為CoyotoAdapter;
10.到這里,服務器可以正常接入請求和完成響應,可是我們還沒考慮到一個關鍵的問題——並發
Tomcat使用組件式的設計理念,那么也會有並發組件.
Tomcat組織為此提供了一個Executor接口表示一個可以在組件間共享的線程池,該接口同樣繼承自Lifecycle接口,按照通用組件進行管理.
Executor由Service進行維護,因此同一個Service中的組件共享一個線程池.值得注意的是如果沒有定義線程池,相關組件會自動創建線程池,此時線程池不再共享.
在Tomcat中,Endpoint會啟動一組線程來監聽Socket端口,當接收到客戶請求會創建請求處理對象,並交由線程池處理,由此支持並發處理客戶端請求.

11.現在Tomcat基礎的核心組件已經完整了,但是架構其實還有很多組件沒有顯示出來.Tomcat開發人員為了讓使用者很好地使用Tomcat,提供了一套配置環境來支持系統的可配置性——Catalina.
Catalina代表了整個Servlet容器架構,包含了上面所有組件,還有還沒談及的安全,會話,集群,部署,管理等Servlet容器組件.它通過松耦合的方式集成了Coyoto,以完成按照請求協議進行數據讀寫.同時,還包括啟動入口、Shell程序等.
Bootstrap是Catalina的啟動入口.
為什么Tomcat不通過Catalina啟動,而又提供了Bootstrap?
查看一下Tomcat發布包目錄,Bootstrap並不存放於Catalina的lib目錄下,而是置於bin目錄中.Bootstrap通過反射調用Catalina實例,與Tomcat服務器完全松耦合,它可以直接依賴JRE運行並為Tomcat應用服務器創建共享類加載器,用於構建Catalina實例以及整個Tomcat服務器.

至此,Tomcat的基礎核心組件介紹結束,我們回顧一下組件的概念
Server 表示整個Servlet容器,一個Tomcat運行環境只存在一個Server,可存在多個Service.
Service 表示鏈接器和處理器的集合,同一個Service下的鏈接器將請求傳至該Service下的處理器
Connector 表示鏈接器,用於監聽並轉化Socket請求,支持不同協議與IO方式
Container 表示容器組件,能執行客戶端請求並返回響應的組件
Engine 表示頂級容器,是獲取目標容器的入口
Host 表示Servlet引擎中的虛擬機,提供Host之類的域名信息
Context 表示一個web app應用上下文環境
Wrapper 具體的Servlet包裝類
Executor 組件間共享的線程池
Tomcat啟動與請求響應


Tomcat類加載器
應用服務器通常會自行創建類加載器以實現更加靈活的控制,這是對規范的實現(Servlet規范要求每個Web應用都有獨立的類加載器實例),也是架構層面的考慮.
書中p46對類加載器進行了詳細說明
JVM默認提供了三個類加載器來進行類加載,Tomcat在加載器上進行擴展,用來加載應用自身的類.

Bootstrap JVM提供,加載JVM運行的基礎運行類,即位於%JAVA_HOME%/jre/lib目錄下的核心類庫
Extension JVM提供,加載%JAVA_HOME%/jre/lib/ext目錄下的擴展類庫
System JVM提供,加載CLASSPATH指定目錄下或者-classpath運行參數指定的jar包
Tomcat的Bootstrap類即由這個加載器載入
Common 以System為父類加載器,是Tomcat應用服務器頂層的公用類加載器,
其路徑common.loader,默認指向$Catalina_Home/lib目錄.
Catalina 用於加載Tomcat應用服務器的類加載器,路徑為server.loader,
默認為空,此時Tomcat使用Common類加載器加載應用服務器.
Shared 所有Web應用的類加載器,路徑為shared.loader,默認為空.
此時使用Common類加載器作為Web應用的父加載器.
Web App 加載WEB-INF/classes目錄下未壓縮的Class和資源文件以及/WEB-INF/lib目錄下的jar包.
該類加載器對當前web應用可見,對其他web應用不可見.
