Tomcat6源代碼分析-架構解析

1. Tomcat的整體框架結構
   Tomcat的基本框架, 分爲4個層次。
   Top Level Elements:
    Server
    Service   
   Connector
    HTTP
    AJP
   Container
   Engine
     Host
   Context
   Component  
    manager
   logger
   loader
   pipeline
   valve
         ...
站在框架的頂層的是Server和Service
   Server:  其 實就是BackGroud程序, 在Tomcat裏面的Server的用處是啓動和監聽服務端事件(諸如重啓、關閉等命令。 在tomcat的標準配置文 件:server.xml裏面, 我們可以看到 “”這裏的"SHUTDOWN"就 是server在監聽服務端事件的時候所使用的命令字)
   Service: 在tomcat裏面, service是指一類問題的解決方 案。 通常我們會默認使用tomcat提供的:Tomcat-Standalone 模式的service。 在這種方式下的service既給我們提 供解析jsp和servlet的服務, 同時也提供給我們解析靜態文本的服務。
   Connector: Tomcat都是在容器裏面處理問題的, 而容器又到哪裏去取得輸入信息呢?
Connector就是專幹這個的。 他會把從socket傳遞過來的數據, 封裝成Request, 傳遞給容器來處理。
通 常我們會用到兩種Connector,一種叫http connectoer, 用來傳遞http需求的。 另一種叫AJP, 在我們整合apache與 tomcat工作的時候, apache與tomcat之間就是通過這個協議來互動的。 (說到apache與tomcat的整合工作, 通常我們的目的 是爲了讓apache 獲取靜態資源, 而讓tomcat來解析動態的jsp或者servlet。)
   Container: 當http connector把需求傳遞給頂級的container: Engin的時候, 我們的視線就應該移動到Container這個層面來了。
在Container這個層, 我們包含了3種容器: Engin, Host, Context.
   Engin: 收到service傳遞過來的需求, 處理後, 將結果返回給service( service 是通過 connector 這個媒介來和Engin互動的 ).
   Host: Engin收到service傳遞過來的需求後,不會自己處理, 而是交給合適的Host來處理。
Host在這裏就是虛擬主機的意思, 通常我們都只會使用一個主機,既“localhost”本地機來處理。
   Context: Host接到了從Host傳過來的需求後, 也不會自己處理, 而是交給合適的Context來處理。
比如: <http://127.0.0.1:8080/foo/index.jsp>;
         <http://127.0.1:8080/bar/index.jsp>;
前者交給foo這個Context來處理, 後者交給bar這個Context來處理。
很明顯吧! context的意思其實就是一個web app的意思。
我們通常都會在server.xml裏面做這樣的配置
  
這個context容器,就是用來幹我們該乾的事兒的地方的。
   Compenent: 接下來, 我們繼續講講component是幹什麼用的。
我們得先理解一下容器和組件的關係。
需求被傳遞到了容器裏面, 在合適的時候, 會傳遞給下一個容器處理。
而容器裏面又盛裝着各種各樣的組件, 我們可以理解爲提供各種各樣的增值服務。
   manager: 當一個容器裏面裝了manager組件後,這個容器就支持session管理了, 事實上在tomcat裏面的session管理, 就是靠的在context裏面裝的manager component.
   logger: 當 一個容器裏面裝了logger組件後, 這個容器裏所發生的事情, 就被該組件記錄下來啦! 我們通常會在logs/ 這個目錄下看 見 catalina_log.time.txt 以及 localhost.time.txt 和 localhost_examples_log.time.txt。 這就是因爲我們分別爲:engin, host以及 context(examples)這三個容器安裝了logger組件, 這也是默認安裝, 又叫做標配 :)
   loader: loader這個組件通常只會給我們的context容器使用, loader是用來啓動context以及管理這個context的classloader用的。
    pipline: pipeline 是這樣一個東西, 當一個容器決定了要把從上級傳遞過來的需求交給子容器的時候, 他就把這個需求放進容器的管道(pipeline)裏面去。 而需求傻 呼呼得在管道里面流動的時候, 就會被管道里面的各個閥門攔截下來。 比如管道里面放了兩個閥門。 第一個閥門叫做 “access_allow_vavle”, 也就是說需求流過來的時候,它會看這個需求是哪個IP過來的, 如果這個IP已經在黑名單裏面 了, sure, 殺! 第二個閥門叫做“defaul_access_valve”它會做例行的檢查, 如果通過的話,OK, 把需求傳遞給當前容器的 子容器。 就是通過這種方式, 需求就在各個容器裏面傳遞,流動, 最後抵達目的地的了。
    valve: 就是上面所說的閥門啦。
   Tomcat裏面大概就是這麼些東西, 我們可以簡單地這麼理解tomcat的框架,它是一種自上而下, 容器裏又包含子容器的這樣一種結構。
2. Tomcat的啓動流程
這 篇文章是講tomcat怎麼啓動的,既然我們大體上了解了TOMCAT的框架結構了, 那麼我們可以望文生意地就猜到tomcat的啓動, 會先啓動父容 器,然後逐個啓動裏面的子容器。 啓動每一個容器的時候, 都會啓動安插在他身上的組件。 當所有的組件啓動完畢, 所有的容器啓動完畢的時 候, tomcat本身也就啓動完畢了。
順理成章地, 我們同樣可以猜到, tomcat的啓動會分成兩大部分, 第一步是裝配工作。 第二步是啓動工作。
裝配工作就是爲父容器裝上子容器, 爲各個容器安插進組件的工作。 這個地方我們會用到digester模式, 至於digester模式什麼, 有什麼用, 怎麼工作的. 請參考 <http://software.ccidnet.com/pub/article/c322_a31671_p2.html>;
啓 動工作是在裝配工作之後, 一旦裝配成功了, 我們就只需要點燃最上面的一根導線, 整個tomcat就會被激活起來。 這就好比我們要開一輛已經裝配好 了的汽車的時候一樣,我們只要把鑰匙插進鑰匙孔,一擰,汽車的引擎就會發動起來,空調就會開起來, 安全裝置就會生效, 如此一來,汽車整個就發動起來 了。(這個過程確實和TOMCAT的啓動過程不謀而和, 讓我們不得不懷疑 TOMCAT的設計者是在GE做JAVA開發的)。
2.1 一些有意思的名稱:
   Catalina
   Tomcat
   Bootstrap
   Engin
   Host
   Context
他們的意思很有意思:
   Catalina: 遠程轟炸機
   Tomcat: 熊貓轟炸機 -- 轟炸機的一種(這讓我想起了讓國人引以爲豪的熊貓手機,是不是英文可以叫做tomcat??? , 又讓我想起了另一則廣告: 波導-手機中的戰鬥機、波音-客機中的戰鬥機 )
   Bootstap: 引導
   Engin: 發動機
   Host: 主機,領土
   Context: 內容, 目標, 上下文
   ... 在許多許多年後, 現代人類已經滅絕。 後現代生物發現了這些單詞零落零落在一塊。 一個自以爲聰明的傢伙把這些東西翻譯出來了:
在 地勤人員的引導(bootstrap)下, 一架轟炸架(catalina)騰空躍起, 遠看是熊貓轟炸機(tomcat), 近看還是熊貓轟炸機! 憑 藉着優秀的發動機技術(engin), 這架熊貓轟炸機飛臨了敵國的領土上空(host), 對準目標(context)投下了毀天滅地的核彈頭, 波~ 現代生物就這麼隔屁了~
綜上所述, 這又不得不讓人聯想到GE是不是也參與了軍事設備的生產呢?
反對美帝國主義! 反對美霸權主義! 和平萬歲! 自由萬歲!
2.2  歷史就是那麼驚人的相似! tomcat的啓動就是從org.apache.catalina.startup.Bootstrap這個類悍然啓動的!
在Bootstrap裏做了兩件事:
   1. 指定了3種類型classloader:
      commonLoader: common/classes、common/lib、common/endorsed
      catalinaLoader: server/classes、server/lib、commonLoader
      sharedLoader:  shared/classes、shared/lib、commonLoader
   2. 引導Catalina的啓動。
用Reflection技術調用org.apache.catalina.startup.Catalina的process方法, 並傳遞參數過去。
2.3 Catalina.java
   Catalina完成了幾個重要的任務:
   1. 使用Digester技術裝配tomcat各個容器與組件。
      1.1 裝配工作的主要內容是安裝各個大件。 比如server下有什麼樣的servcie。 Host會容納多少個context。 Context都會使用到哪些組件等等。
      1.2 同時呢, 在裝配工作這一步, 還完成了mbeans的配置工作。 在這裏,我簡單地但不十分精確地描述一下mbean是什麼,幹什麼用的。
我 們自己生成的對象, 自己管理, 天經地義! 但是如果我們創建了對象了, 想讓別人來管, 怎麼辦呢? 我想至少得告訴別人我們都有什麼, 以及通過什 麼方法可以找到 吧! JMX技術給我們提供了一種手段。 JMX裏面主要有3種東西。Mbean, agent, connector.
       Mbean: 用來映射我們的對象。也許mbean就是我們創建的對象, 也許不是, 但有了它, 就可以引用到我們的對象了。
       Agent:  通過它, 就可以找到mbean了。
       Connector: 連接Agent的方式。 可以是http的, 也可以是rmi的,還可以直接通過socket。
發生在tomcat 裝配過程中的事情:  GlobalResourcesLifecycleListener 類的初始化會被觸發:
         protected static Registry registry = MBeanUtils.createRegistry();  會運行
         MBeanUtils.createRegistry()  會 依據/org/apache/catalina/mbeans/mbeans-descriptors.xml這個配置文件創 建 mbeans. Ok, 外界就有了條途徑訪問tomcat中的各個組件了。(有點像後門兒)
   2. 爲top level 的server 做初始化工作。 實際上就是做通常會配置給service的兩條connector.(http, ajp)
   3. 從server這個容器開始啓動, 點燃整個tomcat.
   4. 爲server做一個hook程序, 檢測當server shutdown的時候, 關閉tomcat的各個容器用。
   5. 監聽8005端口, 如果發送"SHUTDOWN"(默認培植下字符串)過來, 關閉8005serverSocket。
2.4 啓動各個容器
   1. Server
觸發Server容器啓動前(before_start), 啓動中(start), 啓動後(after_start)3個事件, 並運行相應的事件處理器。
啓動Server的子容器:Servcie. 
   2. Service
啓動Service的子容器:Engin
啓動Connector
   3. Engin
到了Engin這個層次,以及以下級別的容器, Tomcat就使用了比較一致的啓動方式了。
首先, 運行各個容器自己特有一些任務
隨後, 觸發啓動前事件
立即, 設置標籤,就表示該容器已經啓動
接着, 啓動容器中的各個組件: loader, logger, manager等等
再接着,啓動mapping組件。(注1)
緊跟着,啓動子容器。
接下來,啓動該容器的管道(pipline)
然後, 觸發啓動中事件
最後, 觸發啓動後事件。
      Engin 大致會這麼做, Host大致也會這麼做, Context大致還是會這麼做。 那麼很顯然地, 我們需要在這裏使用到代碼複用的技術。 tomcat在 處理這個問題的時候, 漂亮地使用了抽象類來處理。 ContainerBase. 最後使得這部分完成複雜功能的代碼顯得乾淨利落, 幹練爽快, 實在 是令人覺得歎爲觀止, 細細品來, 直覺如享佳珍, 另人齒頰留香, 留戀往返啊!
      Engin的觸發啓動前事件裏, 會激活綁定在Engin上的唯一一個Listener:EnginConfig。
這個EnginConfig類基本上沒有做什麼事情, 就是把EnginConfig的調試級別設置爲和Engin相當。 另外就是輸出幾行文本, 表示Engin已經配置完畢, 並沒有做什麼實質性的工作。
注1: mapping組件的用處是, 當一個需求將要從父容器傳遞到子容器的時候, 而父容器又有多個子容器的話, 那麼應該選擇哪個子容器來處理需求呢? 這個由mapping 組件來定奪。
   4. Host
同Engin一樣, 也是調用ContainerBase裏面的start()方法, 不過之前做了些自個兒的任務,就是往Host這個容器的通道(pipline)裏面, 安裝了一個叫做
“org.apache.catalina.valves.ErrorReportValve”的閥門。
這 個閥門的用處是這樣的: 需求在被Engin傳遞給Host後, 會繼續傳遞給Context做具體的處理。 這裏需求其實就是作爲參數傳遞的 Request, Response。 所以在context把需求處理完後, 通常會改動response。 而這個 org.apache.catalina.valves.ErrorReportValve的作用就是檢察response是否包含錯誤, 如果有就做相 應的處理。
   5. Context
到了這裏, 就終於輪到了tomcat啓動中真正的重頭戲,啓動Context了。
StandardContext.start() 這個啓動Context容器的方法被StandardHost調用.
5.1 webappResources 該context所指向的具體目錄
5.2 安 裝defaultContex, DefaultContext 就是默認Context。 如果我們在一個Host下面安裝了 DefaultContext,而且defaultContext裏面又安裝了一個數據庫連接池資源的話。 那麼其他所有的在該Host下的 Context, 都可以直接使用這個數據庫連接池, 而不用格外做配置了。
  5.3 指定Loader. 通常用默認的org.apache.catalina.loader.WebappLoader這個類。   Loader就是用來指定這個context會用到哪些類啊, 哪些jar包啊這些什麼的。
5.4 指定 Manager. 通常使用默認的org.apache.catalina.session. StandardManager 。 Manager是用來管理session的。
其 實session的管理也很好實現。 以一種簡單的session管理爲例。 當需求傳遞過來的時候, 在Request對象裏面有一個 sessionId 屬性。 OK, 得到這個sessionId後, 我們就可以把它作爲map的key,而value我們可以放置一個 HashMap. HashMap裏邊兒, 再放我們想放的東西。
5.5 postWorkDirectory (). Tomcat下面有一 個work目錄。 我們把臨時文件都扔在那兒去。 這個步驟就是在那裏創建一個目錄。 一般說來會在%CATALINA_HOME%/work /Standalone/localhost/ 這個地方生成一個目錄。
5.6  Binding thread。到了這裏, 就應該發 生 class Loader 互換了。 之前是看得見tomcat下面所有的class和lib. 接下來需要看得見當前context下的 class。 所以要設置contextClassLoader, 同時還要把舊的ClassLoader記錄下來,因爲以後還要用的。
5.7  啓動 Loader. 指定這個Context具體要使用哪些classes, 用到哪些jar文件。 如果reloadable設置成了true, 就會啓動一個線程來監視classes的變化, 如果有變化就重新啓動Context。
5.8  啓動logger
5.9  觸發安裝在它身上的一個監聽器。
lifecycle.fireLifecycleEvent(START_EVENT, null); 
作爲監聽器之一,ContextConfig會被啓動. ContextConfig就是用來配置web.xml的。 比如這個Context有多少Servlet, 又有多少Filter, 就是在這裏給Context裝上去的。
5.9.1 defaultConfig. 每個context都得配置 tomcat/conf/web.xml 這個文件。
5.9.2 applicationConfig 配置自己的 WEB-INF/web.xml 文件
5.9.3 validateSecurityRoles 權 限驗證。 通常我們在訪問/admin 或者/manager的時候,需要用戶要麼是admin的要麼是manager的, 才能訪問。 而且我們還可以 限制那些資源可以訪問, 而哪些不能。 都是在這裏實現的。
5.9.4 tldScan: 掃描一下, 需要用到哪些標籤(tag lab)
5.10 啓動 manager
5.11 postWelcomeFiles() 我們通常會用到的3個啓動文件的名稱:
index.html、index.htm、index.jsp 就被默認地綁在了這個context上
5.12 listenerStart 配置listener
5.13 filterStart 配置 filter
5.14 啓動帶有1的Servlet.
順序是從小到大: 1,2,3… 最後是0
默認情況下, 至少會啓動如下3個的Servlet: 
  org.apache.catalina.servlets.DefaultServlet   
處理靜態資源的Servlet. 什麼圖片啊, html啊, css啊, js啊都找他
  org.apache.catalina.servlets.InvokerServlet
處理沒有做Servlet Mapping的那些Servlet.
  org.apache.jasper.servlet.JspServlet 
處理JSP文件的.
       5.15  標識context已經啓動完畢。
走了多少個步驟啊, Context總算是啓動完畢嘍。
    OK! 走到了這裏, 每個容器以及組件都啓動完畢。 Tomcat終於不辭辛勞地爲人民服務了!
3. 參考文獻:
    <http://jakarta.apache.org/tomcat/>;
    <http://www.onjava.com/pub/a/onjava/2003/05/14/java_webserver.html>;

Tomcat的架構總的來說是分層次的、可插拔的組件架構。分層次是指構成Tomcat的組件不是同一級別的,上層組件可以包含子組件,各個組件有其功能範圍,當一個組件停止服務時,不會影響上層組件的服務。可插拔是指對於組件的添加和刪除並不影響服務器的運行。那麼爲了達到可插拔的組件架構,分層次的組件架構必成爲基礎。

對於任何服務器,即使最簡單的實現,從面向對象設計(OOD)的角度來說,我們都有必要將“服務器”這個概念抽象出來,爲什麼呢?因爲只有有了這個概念,才能談服務器的實例,服務器的功能等等其它概念,此之謂“皮之不存,毛將焉附”。趕巧(其實是我的想法恰好撞上人家的想法),Tomcat也將“服務器”抽象爲java接口org.apache.catalina.Server,顯然Server應該就是最最頂層的組件了。

有了Server這個抽象,很自然的,我們希望它能夠提供對servlet和jsp支 持的功能。但是我們發現這個概念太大了,我們還需再細化。所以別急,我們還有一些事情要解決。服務器要提供服務就必須能夠啓動,當然也應該能夠停止吧,也就是說服務器應該是有生命的,在啓動時初始化必要的資源,而在停止時將其其銷燬掉。好吧,我們把這個也抽象出來,叫做生命週期接口,tomcat 實現爲org.apache.catalina.Lifecycle.如上所述我們知道Lifecycle需要完成的工作了。

public void start() throws LifecycleException;

public void stop() throws LifecycleException;

接下來我們分析服務器如何來處理客戶端的請求,一般的我們會在瀏覽器中輸入如下格式的請求,http://192.168.8.221:8080/explorer/loginInit.do。對於服務器來說,要想處理這個請求,就必須監聽指定的端口8080,當有TCP的請求包來時,建立Socket連接,分析並解析之,然後給客戶端返回響應。在這個過程中,我們發現,其實包含了倆個功能點,即監聽並接受請求和處理請求。那麼我們能否將這倆個功能給抽象出來呢?Tomcat告訴我們,可以。是的,Tomcat將“監聽並接收請求”抽象爲org.apache.catalina.connector.Connector類,負責接受請求;將“處理請求”抽象爲“容器” org.apache.catalina.Container,負責處理Connector傳遞過來的請求。

Ok,到此,我們分析構建的簡單服務器模型出來了,Server由Connector組件和Container組件結合提供web服務。

clip_image001

圖2

有了這個模型後,要實現一個簡單的Server已經很簡單了,但是在實現Container時,我們還是要做很多事情,如當來請求,我們怎麼知道該請求對應得虛擬主機,以及請求的那個應用,應該交給那個servlet對象來處理?這樣看來,Container還是太大了,需要細化。根據Servlet規範,我們知道,servlet屬於某個應用,且有上下文環境,Container要根據應用上下文環境初始化servlet,然後根據servlet映射調用servlet的service方法。在這裏“應用上下文環境”的概念很重要,Tomcat將其抽象爲org.apache.catalina.Context,Context繼承了Container接口。對於虛擬主機,Tomcat將其抽象爲org.apache.catalina.Host,Host繼承了Container接口。

好了,有了這些概念,我們再回顧一下請求的處理過程:瀏覽器發出請求,Connector接受請求,將請求交由Container處理,Container查找請求對應的Host並將請求傳遞給它,Host拿到請求後查找相應的應用上下文環境,準備servlet環境並調用service方法。

現在,我們的服務器模型變成了如圖3所示了。

clip_image002

圖3

但是在Tomcat的實現體系中還有一個Engine的接口,Engine也繼承了Container接口,那麼這個接口什麼用呢?設計Engine的目的有倆個目的,一,當希望使用攔截器查看(過濾或預處理)每個請求時,Engine是個很好的攔截點。二,當希望多個虛擬Host共享一個Http的Connector時,Engine是個很好的門面。所以,Engine接口是作爲頂級Container組件來設計的,其作用相當於一個Container的門面。有了Engine,請求的處理過程變爲:瀏覽器發出請求,Connector接受請求,將請求交由Container(這裏是Engine)處理,Container(Engine來擔當)查找請求對應的Host並將請求傳遞給它,Host拿到請求後查找相應的應用上下文環境,準備servlet環境並調用service方法。再看看服務器的模型,如圖4.

clip_image003

圖4

到目前,我們碰到的組件類型有Connector和Container,其實,這也就是Tomcat的核心組件。如圖4,一組Connector和一個Container有機的組合在一起構成Server,就可以提供服務了,對於Tomcat來說,主要是提供Servlet服務,那麼也就是說Tomcat服務器也可以提供其它服務了?是的,Tomcat將“一組Connector和一個Container有機的組合”抽象爲“服務”接口org.apache.catalina.Service,然而,這些服務實例彼此獨立,僅僅共享JVM的基礎設施,如系統類路徑。

進一步的,我們得到了服務器的框架模型,如圖5.

clip_image004

圖5

由圖5,我們知道,對於Tomcat服務器來說,除了Server代表它自己以外,其它組件都是功能組件,都有其職責範圍。Service爲最頂層的組件,可以添加Connector和Container組件。Engine是Container的最頂層組件,可以添加Host組件,但不能添加父組件。Host組件的父組件是Engine,Host下面包含有Context組件。

接下來看看標準的Tomcat體系結構,如圖6.

clip_image005

圖6

比較圖5和圖6.我們發現,還有很多輔助組件沒有抽象出來。當然,隨着需求的一步步加深,我的分析也會一步步深入,這些個組件也會慢慢浮出水面。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章