Tomcat 系統架構原理學習筆記
本文以 Tomcat 5 為基礎,也兼顧最新的 Tomcat 6 和 Tomcat 4。Tomcat 的基本設計思路和架構是具有一定連續性的。
Tomcat 總體結構
Tomcat 的結構很複雜,但是 Tomcat 也非常的模組化,找到了 Tomcat 最核心的模組,您就抓住了 Tomcat 的“七寸”。下面是 Tomcat 的總體結構圖:
圖 1.Tomcat 的總體結構
從上圖中可以看出 Tomcat 的心臟是兩個元件:Connector 和 Container,關於這兩個元件將在後面詳細介紹。Connector 元件是可以被替換,這樣可以提供給伺服器設計者更多的選擇,因為這個元件是如此重要,不僅跟伺服器的設計的本身,而且和不同的應用場景也十分相關,所以一個 Container 可以選擇對應多個 Connector。多個 Connector 和一個 Container 就形成了一個 Service,Service 的概念大家都很熟悉了,有了 Service 就可以對外提供服務了,但是 Service 還要一個生存的環境,必須要有人能夠給她生命、掌握其生死大權,那就非 Server 莫屬了。所以整個 Tomcat 的生命週期由 Server 控制。
以 Service 作為“婚姻”
我們將 Tomcat 中 Connector、Container 作為一個整體比作一對情侶的話,Connector 主要負責對外交流,可以比作為 Boy,Container 主要處理 Connector 接受的請求,主要是處理內部事務,可以比作為 Girl。那麼這個 Service 就是連線這對男女的結婚證了。是 Service 將它們連線在一起,共同組成一個家庭。當然要組成一個家庭還要很多其它的元素。
說白了,Service 只是在 Connector 和 Container 外面多包一層,把它們組裝在一起,向外面提供服務,一個 Service 可以設定多個 Connector,但是隻能有一個 Container 容器。這個 Service 介面的方法列表如下:
圖 2. Service 介面
從 Service 介面中定義的方法中可以看出,它主要是為了關聯 Connector 和 Container,同時會初始化它下面的其它元件,注意介面中它並沒有規定一定要控制它下面的元件的生命週期。所有元件的生命週期在一個 Lifecycle 的介面中控制,這裡用到了一個重要的設計模式,關於這個介面將在後面介紹。
Tomcat 中 Service 介面的標準實現類是 StandardService 它不僅實現了 Service 藉口同時還實現了 Lifecycle 介面,這樣它就可以控制它下面的元件的生命週期了。StandardService 類結構圖如下:
圖 3. StandardService 的類結構圖
從上圖中可以看出除了 Service 介面的方法的實現以及控制組件生命週期的 Lifecycle 介面的實現,還有幾個方法是用於在事件監聽的方法的實現,不僅是這個 Service 元件,Tomcat 中其它元件也同樣有這幾個方法,這也是一個典型的設計模式,將在後面介紹。
下面看一下 StandardService 中主要的幾個方法實現的程式碼,下面是 setContainer 和 addConnector 方法的原始碼:
清單 1. StandardService. SetContainer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
這段程式碼很簡單,其實就是先判斷當前的這個 Service 有沒有已經關聯了 Container,如果已經關聯了,那麼去掉這個關聯關係—— oldContainer.setService(null)。如果這個 oldContainer 已經被啟動了,結束它的生命週期。然後再替換新的關聯、再初始化並開始這個新的 Container 的生命週期。最後將這個過程通知感興趣的事件監聽程式。這裡值得注意的地方就是,修改 Container 時要將新的 Container 關聯到每個 Connector,還好 Container 和 Connector 沒有雙向關聯,不然這個關聯關係將會很難維護。
清單 2. StandardService. addConnector
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
上面是 addConnector 方法,這個方法也很簡單,首先是設定關聯關係,然後是初始化工作,開始新的生命週期。這裡值得一提的是,注意 Connector 用的是陣列而不是 List 集合,這個從效能角度考慮可以理解,有趣的是這裡用了陣列但是並沒有向我們平常那樣,一開始就分配一個固定大小的陣列,它這裡的實現機制是:重新建立一個當前大小的陣列物件,然後將原來的陣列物件 copy 到新的陣列中,這種方式實現了類似的動態陣列的功能,這種實現方式,值得我們以後拿來借鑑。
最新的 Tomcat6 中 StandardService 也基本沒有變化,但是從 Tomcat5 開始 Service、Server 和容器類都繼承了 MBeanRegistration 介面,Mbeans 的管理更加合理。
以 Server 為“居”
前面說一對情侶因為 Service 而成為一對夫妻,有了能夠組成一個家庭的基本條件,但是它們還要有個實體的家,這是它們在社會上生存之本,有了家它們就可以安心的為人民服務了,一起為社會創造財富。
Server 要完成的任務很簡單,就是要能夠提供一個介面讓其它程式能夠訪問到這個 Service 集合、同時要維護它所包含的所有 Service 的生命週期,包括如何初始化、如何結束服務、如何找到別人要訪問的 Service。還有其它的一些次要的任務,如您住在這個地方要向當地政府去登記啊、可能還有要配合當地公安機關日常的安全檢查什麼的。
Server 的類結構圖如下:
圖 4. Server 的類結構圖
它的標準實現類 StandardServer 實現了上面這些方法,同時也實現了 Lifecycle、MbeanRegistration 兩個介面的所有方法,下面主要看一下 StandardServer 重要的一個方法 addService 的實現:
清單 3. StandardServer.addService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
從上面第一句就知道了 Service 和 Server 是相互關聯的,Server 也是和 Service 管理 Connector 一樣管理它,也是將 Service 放在一個數組中,後面部分的程式碼也是管理這個新加進來的 Service 的生命週期。Tomcat6 中也是沒有什麼變化的。
元件的生命線“Lifecycle”
前面一直在說 Service 和 Server 管理它下面元件的生命週期,那它們是如何管理的呢?
Tomcat 中元件的生命週期是通過 Lifecycle 介面來控制的,元件只要繼承這個介面並實現其中的方法就可以統一被擁有它的元件控制了,這樣一層一層的直到一個最高階的元件就可以控制 Tomcat 中所有元件的生命週期,這個最高的元件就是 Server,而控制 Server 的是 Startup,也就是您啟動和關閉 Tomcat。
下面是 Lifecycle 介面的類結構圖:
圖 5. Lifecycle 類結構圖
除了控制生命週期的 Start 和 Stop 方法外還有一個監聽機制,在生命週期開始和結束的時候做一些額外的操作。這個機制在其它的框架中也被使用,如在 Spring 中。關於這個設計模式會在後面介紹。
Lifecycle 介面的方法的實現都在其它元件中,就像前面中說的,元件的生命週期由包含它的父元件控制,所以它的 Start 方法自然就是呼叫它下面的元件的 Start 方法,Stop 方法也是一樣。如在 Server 中 Start 方法就會呼叫 Service 元件的 Start 方法,Server 的 Start 方法程式碼如下:
清單 4. StandardServer.Start
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
監聽的程式碼會包圍 Service 元件的啟動過程,就是簡單的迴圈啟動所有 Service 元件的 Start 方法,但是所有 Service 必須要實現 Lifecycle 介面,這樣做會更加靈活。
Server 的 Stop 方法程式碼如下:
清單 5. StandardServer.Stop
1 2 3 4 5 6 7 8 9 10 11 12 |
|
它所要做的事情也和 Start 方法差不多。
Connector 元件
Connector 元件是 Tomcat 中兩個核心元件之一,它的主要任務是負責接收瀏覽器的發過來的 tcp 連線請求,建立一個 Request 和 Response 物件分別用於和請求端交換資料,然後會產生一個執行緒來處理這個請求並把產生的 Request 和 Response 物件傳給處理這個請求的執行緒,處理這個請求的執行緒就是 Container 元件要做的事了。
由於這個過程比較複雜,大體的流程可以用下面的順序圖來解釋:
圖 6. Connector 處理一次請求順序圖
Tomcat5 中預設的 Connector 是 Coyote,這個 Connector 是可以選擇替換的。Connector 最重要的功能就是接收連線請求然後分配執行緒讓 Container 來處理這個請求,所以這必然是多執行緒的,多執行緒的處理是 Connector 設計的核心。Tomcat5 將這個過程更加細化,它將 Connector 劃分成 Connector、Processor、Protocol, 另外 Coyote 也定義自己的 Request 和 Response 物件。
下面主要看一下 Tomcat 中如何處理多執行緒的連線請求,先看一下 Connector 的主要類圖:
圖 7. Connector 的主要類圖
看一下 HttpConnector 的 Start 方法:
清單 6. HttpConnector.Start
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
threadStart() 執行就會進入等待請求的狀態,直到一個新的請求到來才會啟用它繼續執行,這個啟用是在 HttpProcessor 的 assign 方法中,這個方法是程式碼如下 :
清單 7. HttpProcessor.assign
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
建立 HttpProcessor 物件是會把 available 設為 false,所以當請求到來時不會進入 while 迴圈,將請求的 socket 賦給當期處理的 socket,並將 available 設為 true,當 available 設為 true 是 HttpProcessor 的 run 方法將被啟用,接下去將會處理這次請求。
Run 方法程式碼如下:
清單 8. HttpProcessor.Run
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
解析 socket 的過程在 process 方法中,process 方法的程式碼片段如下:
清單 9. HttpProcessor.process
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
當 Connector 將 socket 連線封裝成 request 和 response 物件後接下來的事情就交給 Container 來處理了。
Servlet 容器“Container”
Container 是容器的父介面,所有子容器都必須實現這個介面,Container 容器的設計用的是典型的責任鏈的設計模式,它有四個子容器元件構成,分別是:Engine、Host、Context、Wrapper,這四個元件不是平行的,而是父子關係,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。通常一個 Servlet class 對應一個 Wrapper,如果有多個 Servlet 就可以定義多個 Wrapper,如果有多個 Wrapper 就要定義一個更高的 Container 了,如 Context,Context 通常就是對應下面這個配置:
清單 10. Server.xml
1 2 3 4 5 |
|
容器的總體設計
Context 還可以定義在父容器 Host 中,Host 不是必須的,但是要執行 war 程式,就必須要 Host,因為 war 中必有 web.xml 檔案,這個檔案的解析就需要 Host 了,如果要有多個 Host 就要定義一個 top 容器 Engine 了。而 Engine 沒有父容器了,一個 Engine 代表一個完整的 Servlet 引擎。
那麼這些容器是如何協同工作的呢?先看一下它們之間的關係圖:
圖 8. 四個容器的關係圖
當 Connector 接受到一個連線請求時,將請求交給 Container,Container 是如何處理這個請求的?這四個元件是怎麼分工的,怎麼把請求傳給特定的子容器的呢?又是如何將最終的請求交給 Servlet 處理。下面是這個過程的時序圖:
圖 9. Engine 和 Host 處理請求的時序圖
這裡看到了 Valve 是不是很熟悉,沒錯 Valve 的設計在其他框架中也有用的,同樣 Pipeline 的原理也基本是相似的,它是一個管道,Engine 和 Host 都會執行這個 Pipeline,您可以在這個管道上增加任意的 Valve,Tomcat 會挨個執行這些 Valve,而且四個元件都會有自己的一套 Valve 集合。您怎麼才能定義自己的 Valve 呢?在 server.xml 檔案中可以新增,如給 Engine 和 Host 增加一個 Valve 如下:
清單 11. Server.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
StandardEngineValve 和 StandardHostValve 是 Engine 和 Host 的預設的 Valve,它們是最後一個 Valve 負責將請求傳給它們的子容器,以繼續往下執行。
前面是 Engine 和 Host 容器的請求過程,下面看 Context 和 Wrapper 容器時如何處理請求的。下面是處理請求的時序圖:
圖 10. Context 和 wrapper 的處理請求時序圖
從 Tomcat5 開始,子容器的路由放在了 request 中,request 中儲存了當前請求正在處理的 Host、Context 和 wrapper。
Engine 容器
Engine 容器比較簡單,它只定義了一些基本的關聯關係,介面類圖如下:
圖 11. Engine 介面的類結構
它的標準實現類是 StandardEngine,這個類注意一點就是 Engine 沒有父容器了,如果呼叫 setParent 方法時將會報錯。新增子容器也只能是 Host 型別的,程式碼如下:
清單 12. StandardEngine. addChild
1 2 3 4 5 6 7 8 9 10 11 |
|
它的初始化方法也就是初始化和它相關聯的元件,以及一些事件的監聽。
Host 容器
Host 是 Engine 的字容器,一個 Host 在 Engine 中代表一個虛擬主機,這個虛擬主機的作用就是執行多個應用,它負責安裝和展開這些應用,並且標識這個應用以便能夠區分它們。它的子容器通常是 Context,它除了關聯子容器外,還有就是儲存一個主機應該有的資訊。
下面是和 Host 相關的類關聯圖:
圖 12. Host 相關的類圖
從上圖中可以看出除了所有容器都繼承的 ContainerBase 外,StandardHost 還實現了 Deployer 介面,上圖清楚的列出了這個介面的主要方法,這些方法都是安裝、展開、啟動和結束每個 web application。
Deployer 介面的實現是 StandardHostDeployer,這個類實現了的最要的幾個方法,Host 可以呼叫這些方法完成應用的部署等。
Context 容器
Context 代表 Servlet 的 Context,它具備了 Servlet 執行的基本環境,理論上只要有 Context 就能執行 Servlet 了。簡單的 Tomcat 可以沒有 Engine 和 Host。
Context 最重要的功能就是管理它裡面的 Servlet 例項,Servlet 例項在 Context 中是以 Wrapper 出現的,還有一點就是 Context 如何才能找到正確的 Servlet 來執行它呢? Tomcat5 以前是通過一個 Mapper 類來管理的,Tomcat5 以後這個功能被移到了 request 中,在前面的時序圖中就可以發現獲取子容器都是通過 request 來分配的。
Context 準備 Servlet 的執行環境是在 Start 方法開始的,這個方法的程式碼片段如下:
清單 13. StandardContext.start
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
它主要是設定各種資源屬性和管理元件,還有非常重要的就是啟動子容器和 Pipeline。
我們知道 Context 的配置檔案中有個 reloadable 屬性,如下面配置:
清單 14. Server.xml
1 2 3 4 5 |
|
當這個 reloadable 設為 true 時,war 被修改後 Tomcat 會自動的重新載入這個應用。如何做到這點的呢 ? 這個功能是在 StandardContext 的 backgroundProcess 方法中實現的,這個方法的程式碼如下:
清單 15. StandardContext. backgroundProcess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
它會呼叫 reload 方法,而 reload 方法會先呼叫 stop 方法然後再呼叫 Start 方法,完成 Context 的一次重新載入。可以看出執行 reload 方法的條件是 reloadable 為 true 和應用被修改,那麼這個 backgroundProcess 方法是怎麼被呼叫的呢?
這個方法是在 ContainerBase 類中定義的內部類 ContainerBackgroundProcessor 被週期呼叫的,這個類是執行在一個後臺執行緒中,它會週期的執行 run 方法,它的 run 方法會週期呼叫所有容器的 backgroundProcess 方法,因為所有容器都會繼承 ContainerBase 類,所以所有容器都能夠在 backgroundProcess 方法中定義週期執行的事件。
Wrapper 容器
Wrapper 代表一個 Servlet,它負責管理一個 Servlet,包括的 Servlet 的裝載、初始化、執行以及資源回收。Wrapper 是最底層的容器,它沒有子容器了,所以呼叫它的 addChild 將會報錯。
Wrapper 的實現類是 StandardWrapper,StandardWrapper 還實現了擁有一個 Servlet 初始化資訊的 ServletConfig,由此看出 StandardWrapper 將直接和 Servlet 的各種資訊打交道。
下面看一下非常重要的一個方法 loadServlet,程式碼片段如下:
清單 16. StandardWrapper.loadServlet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|