Tomcat處理HTTP請求源碼分析

很多開源應用服務器都是集成tomcat作爲web container的,而且對於tomcat的servlet container這部分代碼很少改動。這樣,這些應用服務器的性能基本上就取決於Tomcat處理HTTP請求的connector模塊的性能。本文首先從應用層次分析了tomcat所有的connector種類及用法,接着從架構上分析了connector模塊在整個tomcat中所處的位置,最後對connector做了詳細的源代碼分析。並且我們以Http11NioProtocol爲例詳細說明了tomcat是如何通過實現ProtocolHandler接口而構建connector的。

通過本文的學習,應該可以輕鬆做到將tomcat做爲web container集成到第三方系統,並且自定義任何你想要的高性能的HTTP連接器。

1 Connector介紹

1.1 Connector的種類

Tomcat源碼中與connector相關的類位於org.apache.coyote包中,Connector分爲以下幾類:

  • Http Connector, 基於HTTP協議,負責建立HTTP連接。它又分爲BIO Http Connector與NIO Http Connector兩種,後者提供非阻塞IO與長連接Comet支持。
  • AJP Connector, 基於AJP協議,AJP是專門設計用來爲tomcat與http服務器之間通信專門定製的協議,能提供較高的通信速度和效率。如與Apache服務器集成時,採用這個協議。
  • APR HTTP Connector, 用C實現,通過JNI調用的。主要提升對靜態資源(如HTML、圖片、CSS、JS等)的訪問性能。現在這個庫已獨立出來可用在任何項目中。Tomcat在配置APR之後性能非常強勁。

1.2 Connector的配置

對Connector的配置位於conf/server.xml文件中。

1.2.1 BIO HTTP/1.1 Connector配置

一個典型的配置如下:

<Connector port=”8080” protocol=”HTTP/1.1” maxThreads=”150” 
connectionTimeout=”20000” redirectPort=”8443”

其它一些重要屬性如下:

  • acceptCount : 接受連接request的最大連接數目,默認值是10
  • address : 綁定IP地址,如果不綁定,默認將綁定任何IP地址
  • allowTrace : 如果是true,將允許TRACE HTTP方法
  • compressibleMimeTypes : 各個mimeType, 以逗號分隔,如text/html,text/xml
  • compression : 如果帶寬有限的話,可以用GZIP壓縮
  • connectionTimeout : 超時時間,默認爲60000ms (60s)
  • maxKeepAliveRequest : 默認值是100
  • maxThreads : 處理請求的Connector的線程數目,默認值爲200

如果是SSL配置,如下:

<Connector port="8181" protocol="HTTP/1.1" SSLEnabled="true" 
    maxThreads="150" scheme="https" secure="true" 
    clientAuth="false" sslProtocol = "TLS" 
    address="0.0.0.0" 
    keystoreFile="E:/java/jonas-full-5.1.0-RC3/conf/keystore.jks" 
    keystorePass="changeit" /> 

其中,keystoreFile爲證書位置,keystorePass爲證書密碼

1.2.2 NIO HTTP/1.1 Connector配置

<Connector port=”8080” protocol=”org.apache.coyote.http11.Http11NioProtocol” 
    maxThreads=”150” connectionTimeout=”20000” redirectPort=”8443” 

1.2.3 Native APR Connector配置

  1. ARP是用C/C++寫的,對靜態資源(HTML,圖片等)進行了優化。所以要下載本地庫

    tcnative-1.dll與openssl.exe,將其放在%tomcat%\bin目錄下。

    下載地址是:http://tomcat.heanet.ie/native/1.1.10/binaries/win32/

  2. 在server.xml中要配置一個Listener,如下圖。這個配置tomcat是默認配好的。
    <!--APR library loader. Documentation at /docs/apr.html --> 
    <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> 
  3. 配置使用APR connector
    <Connector port=”8080” protocol=”org.apache.coyote.http11.Http11AprProtocol

    maxThreads=”150” connectionTimeout=”20000” redirectPort=”8443”

  4. 如果配置成功,啓動tomcat,會看到如下信息:
    org.apache.coyote.http11.Http11AprProtocol init 

2 Connector在Tomcat中所處的位置

2.1 Tomcat架構

圖2-1 Tomcat架構

  • Server(服務器)是Tomcat構成的頂級構成元素,所有一切均包含在Server中,Server的實現類StandardServer可以包含一個到多個Services;
  • 次頂級元素Service的實現類爲StandardService調用了容器(Container)接口,其實是調用了Servlet Engine(引擎),而且StandardService類中也指明瞭該Service歸屬的Server;
  • 接下來次級的構成元素就是容器(Container),主機(Host)、上下文(Context)和引擎(Engine)均繼承自Container接口,所以它們都是容器。但是,它們是有父子關係的,在主機(Host)、上下文(Context)和引擎(Engine)這三類容器中,引擎是頂級容器,直接包含是主機容器,而主機容器又包含上下文容器,所以引擎、主機和上下文從大小上來說又構成父子關係,雖然它們都繼承自Container接口。
  • 連接器(Connector)將Service和Container連接起來,首先它需要註冊到一個Service,它的作用就是把來自客戶端的請求轉發到Container(容器),這就是它爲什麼稱作連接器的原因。

故我們從功能的角度將Tomcat源代碼分成5個子模塊,它們分別是:

  1. Jsper子模塊:這個子模塊負責jsp頁面的解析、jsp屬性的驗證,同時也負責將jsp頁面動態轉換爲java代碼並編譯成class文件。在Tomcat源代碼中,凡是屬於org.apache.jasper包及其子包中的源代碼都屬於這個子模塊;
  2. Servlet和Jsp規範的實現模塊:這個子模塊的源代碼屬於javax.servlet包及其子包,如我們非常熟悉的javax.servlet.Servlet接口、javax.servet.http.HttpServlet類及javax.servlet.jsp.HttpJspPage就位於這個子模塊中;
  3. Catalina子模塊:這個子模塊包含了所有以org.apache.catalina開頭的java源代碼。該子模塊的任務是規範了Tomcat的總體架構,定義了Server、Service、Host、Connector、Context、Session及Cluster等關鍵組件及這些組件的實現,這個子模塊大量運用了Composite設計模式。同時也規範了Catalina的啓動及停止等事件的執行流程。從代碼閱讀的角度看,這個子模塊應該是我們閱讀和學習的重點。
  4. Connectors子模塊:如果說上面三個子模塊實現了Tomcat應用服務器的話,那麼這個子模塊就是Web服務器的實現。所謂連接器(Connector)就是一個連接客戶和應用服務器的橋樑,它接收用戶的請求,並把用戶請求包裝成標準的Http請求(包含協議名稱,請求頭Head,請求方法是Get還是Post等等)。同時,這個子模塊還按照標準的Http協議,負責給客戶端發送響應頁面,比如在請求頁面未發現時,connector就會給客戶端瀏覽器發送標準的Http 404錯誤響應頁面。
  5. Resource子模塊:這個子模塊包含一些資源文件,如Server.xml及Web.xml配置文件。嚴格說來,這個子模塊不包含java源代碼,但是它還是Tomcat編譯運行所必需的。

2.2 Tomcat運行流程

圖2-2 tomcat運行流程

假設來自客戶的請求爲:http://localhost:8080/test/index.jsp

  1. 請求被髮送到本機端口8080,被在那裏偵聽的Coyote HTTP/1.1 Connector獲得
  2. Connector把該請求交給它所在的Service的Engine來處理,並等待Engine的迴應
  3. Engine獲得請求localhost:8080/test/index.jsp,匹配它所有虛擬主機Host
  4. Engine匹配到名爲localhost的Host(即使匹配不到也把請求交給該Host處理,因爲該Host被定義爲該Engine的默認主機)
  5. localhost Host獲得請求/test/index.jsp,匹配它所擁有的所有Context
  6. Host匹配到路徑爲/test的Context(如果匹配不到就把該請求交給路徑名爲""的Context去處理)
  7. path="/test"的Context獲得請求/index.jsp,在它的mapping table中尋找對應的servlet
  8. Context匹配到URL PATTERN爲*.jsp的servlet,對應於JspServlet類
  9. 構造HttpServletRequest對象和HttpServletResponse對象,作爲參數調用JspServlet的doGet或doPost方法
  10. Context把執行完了之後的HttpServletResponse對象返回給Host
  11. Host把HttpServletResponse對象返回給Engine
  12. Engine把HttpServletResponse對象返回給Connector
  13. Connector把HttpServletResponse對象返回給客戶browser

3 Connector源碼分析

3.1 Tomcat的啓動分析與集成設想

我們知道,啓動tomcat有兩種方式:

  • 雙擊bin/startup.bat
  • 運行bin/catalina.bat run

它們對應於Bootstrap與Catalina兩個類,我們現在只關心Catalina這個類,這個類使用Apache Digester解析conf/server.xml文件生成tomcat組件,然後再調用Embedded類的start方法啓動tomcat。

所以,集成Tomcat的方式就有以下兩種了:

  • 沿用tomcat自身的server.xml
  • 自己定義一個xml格式來配置tocmat的各參數,自己再寫解析這段xml,然後使用tomcat提供的API根據這些xml來生成Tomcat組件,最後調用Embedded類的start方法啓動tomcat

個人覺得第一種方式要優越,給開發者比較好的用戶體驗,如果使用這種,直接模仿Catalina類的方法即可實現集成。

目前,JOnAS就使用了這種集成方式,JBoss、GlassFish使用的第二種自定義XML的方式。

3.2 Connector類圖與順序圖

圖3-1 Connector相關類圖

圖3-2 Connector工作流程順序圖

從上面二圖中我們可以得到如下信息:

  1. Tomcat中有四種容器(Context、Engine、Host、Wrapper),前三者常見,第四個不常見但它也是實現了Container接口的容器
  2. 如果要自定義一個Connector的話,只需要實現ProtocolHander接口,該接口定義如下:

圖3-3 自定義connector時需實現的ProtocolHandler接口

Tomcat以HTTP(包括BIO與NIO)、AJP、APR、內存四種協議實現了該接口(它們分別是:AjpAprProtocol、AjpProtocol、Http11AprProtocol、Http11NioProtocol、Http11Protocal、JkCoyoteHandler、MemoryProtocolHandler),要使用哪種Connector就在conf/server.xml中配置,在Connector的構造函數中會通過反射實例化所配置的實現類:

<Connector port="8181" 
   protocol="org.apache.coyote.http11.Http11AprProtocol " /> 

3.3 Connector的工作流程

下面我們以Http11AprProtocol爲例說明Connector的工作流程。

  1. 它將工作委託給NioEndpoint類。在NioEndpoint類的init方法中構建一個SocketServer(當然,不同的實現類會有一些微小的變化,例如如果是NIO,它構建的就是SocketServerChannel)
  2. 在NioEndpoint.Acceptor類中會接收一個客戶端新的連接請求,如下圖:

  3. 在NioEndpoint類中,有一個內部接口Handle,該接口定義如下:

  4. 在Http11NioProtocol類中實現了Handle這個內部接口,並調用Http11NioProcessor類(該類實現了ActionHook回調接口)。在Response類中會調用ActionHook實現類的相關方法的,Response類的action方法如下:

  5. Http11NioProcessor的process實現方法中,會通過Adapter來調用Servler容器生成響應結果。

4 如何實現Connector

由上面的介紹我們可以知道,實現Connector就是實現ProtocolHander接口的過程。

AjpAprProtocol、AjpProtocol、Http11AprProtocol、Http11Protocol、JkCoyoteHandler、MemoryProtocolHandler這些實現類的實現流程與Http11NioProtocol相同,下面我們以Http11NioProtocol爲類重點說明tomcat中如何實現ProtocolHander接口的。

Http11NioProtocol實現了ProtocolHander接口,它將所有的操作委託給NioEndpoint類去做,如下圖:


NioEndpoint類中的init方法中首先以普通阻塞方式啓動了SocketServer:

NioEndpoint類的start方法是關鍵,如下:

可以看出,在start方法中啓動了兩個線程和一個線程池:

  • Acceptor線程,該線程以普通阻塞方式接收客戶端請求(socket.accep()),將客戶Socket交由線程池是處理,線程池要將該Socket配置成非阻塞模式(socket.configureBlocking(false)),並且向Selector註冊READ事件。該線程數目可配置,默認爲1個。
  • Poller線程,由於Acceptor委託線程爲客戶端Socket註冊了READ事件,當READ準備好時,就會進入Poller線程的循環,Poller線程也是委託線程池去做,線程池將NioChannel加入到ConcurrentLinkedQueue<NioChannel>隊列中。該線程數目可配置,默認爲1個。
  • 線程池,就是上面說的做Acceptor與Poller線程委託要做的事情。

4.1 Init接口實現方法中阻塞方式啓動ServerSocketChannel

在Init接口實現方法中阻塞方式啓動ServerSocketChannel。

4.2 Start接口實現方法中啓動所有線程

Start方法中啓動了線程池,acceptor線程與Poller線程。其中acceptor與poller線程一般數目爲1,當然,數目也可配置。

可以看出,線程池有兩種實現方式:

  • 普通queue + wait + notify方式,默認使用的方式,據說實際測試這種比下種效率高
  • JDK1.5自帶的線程池方式

4.3 Acceptor線程接收客戶請求、註冊READ事件

在Acceptor線程中接收了客戶請求,同時委託線程池註冊READ事件。

  1. 在Acceptior線程中接收了客戶請求(serverSock.accept())

  2. 委託線程池處理

  3. 在線程池的Worker線程的run方法中有這麼幾句:

在setSocketOptions方法中,首先將socket配置成非阻塞模式:

在setSocketOptions方法中,最後調用getPoller0().register(channel);一句爲SocketChannel註冊READ事件,register方法代碼如下(注意:這是Poller線程的方法):

其中attachment的結構如下,它可以看做是一個共享的數據結構:

4.4 Poller線程讀請求、生成響應數據、註冊WRITE事件

  1. 在上面說的setSocketOptions方法中調用Poller線程的register方法註冊讀事件之後,當READ準備就緒之後,就開始讀了。下面代碼位於Poller線程的run方法之中:

  2. 可以看到,可讀之後調用processSocket方法,該方法將讀處理操作委拖給線程池處理(注意此時加入到線程池的是NioChannel,不是SocketChannel):

  3. 線程池的Worker線程中的run方法中的部分代碼如下(請注意handler.process(socket)這一句):

    注意:

    • 調用了hanler.process(socket)來生成響應數據)
    • 數據生成完之後,註冊WRITE事件的,代碼如下:

4.5 Handle接口實現類通過Adpater調用Servlet容器生成響應數據

NioEndpoint類中的Handler接口定義如下:

其中process方法通過Adapter來調用Servlet Container生成返回結果。Adapter接口定義如下:

4.6 小結

實現一個tomcat連接器Connector就是實現ProtocolHander接口的過程。Connector用來接收Socket Client端的請求,通過內置的線程池去調用Servlet Container生成響應結果,並將響應結果同步或異步的返回給Socket Client。在第三方應用集成tomcat作爲Web容器時,一般不會動Servlet Container端的代碼,那麼connector的性能將是整個Web容器性能的關鍵。

發佈了9 篇原創文章 · 獲贊 17 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章