聊聊Tomcat的架構設計

微信公衆號「後端進階」,專一後端技術分享:Java、Golang、WEB框架、分佈式中間件、服務治理等等。
老司機傾囊相授,帶你一路進階,來不及解釋了快上車!

Tomcat 是 Java WEB 開發接觸最多的 Servlet 容器,但它不只僅是一個 Servlet 容器,它仍是一個 WEB 應用服務器,在微服務架構體系下,爲了下降部署成本,減小資源的開銷,追求的是輕量化與穩定,而 Tomcat 是一個輕量級應用服務器,天然被不少開發人員所接受。java

Tomcat 裏面藏着不少值得咱們每一個 Java WEB 開發者學習的知識,能夠這麼說,當你弄懂了 Tomcat 的設計原理,Java WEB 開發對你來講已經沒有什麼祕密可言了。本篇文章主要是跟你們聊聊 Tomcat 的內部架構體系,讓你們對 Tomcat 有個總體的認知。web

前面我也說了,Tomcat 的本質其實就是一個 WEB 服務器 + 一個 Servlet 容器,那麼它必然須要處理網絡的鏈接與 Servlet 的管理,所以,Tomcat 設計了兩個核心組件來實現這兩個功能,分別是鏈接器和容器,鏈接器用來處理外部網絡鏈接,容器用來處理內部 Servlet,我用一張圖來表示它們的關係:apache

一個 Tomcat 表明一個 Server 服務器,一個 Server 服務器能夠包含多個 Service 服務,Tomcat 默認的 Service 服務是 Catalina,而一個 Service 服務能夠包含多個鏈接器,由於 Tomcat 支持多種網絡協議,包括 HTTP/1.一、HTTP/二、AJP 等等,一個 Service 服務還會包括一個容器,容器外部會有一層 Engine 引擎所包裹,負責與處理鏈接器的請求與響應,鏈接器與容器之間經過 ServletRequest 和 ServletResponse 對象進行交流。後端

也能夠從 server.xml 的配置結構能夠看出 tomcat 總體的內部結構:設計模式

<Server port="8005" shutdown="SHUTDOWN">

  <Service name="Catalina">

    <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="UTF-8"/>

    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>

    <Engine defaultHost="localhost" name="Catalina">

      <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true">

        <Context docBase="handler-api" path="/handler" reloadable="true" source="org.eclipse.jst.jee.server:handler-api"/>
      </Host>
    </Engine>
  </Service>
</Server>

鏈接器(Connector)

鏈接器負責將各類網絡協議封裝起來,對外部屏蔽了網絡鏈接與 IO 處理的細節,將處理獲得的 Request 對象傳遞給容器處理,Tomcat 將處理請求的細節封裝到 ProtocolHandler,ProtocolHandler 是一個接口類型,經過實現 ProtocolHandler 來實現各類協議的處理,如 Http11AprProtocol:api

ProtocolHandler 採用組件模式的設計,將處理網絡鏈接,字節流封裝成 Request 對象,再將 Request 適配成 Servlet 處理 ServletRequest 對象這幾個動做,用組件封裝起來了,ProtocolHandler 包括了三個組件:Endpoint、Processor、Adapter。tomcat

Endpoint 在 ProtocolHandler 實現類的構造方法中建立,以下:服務器

public Http11AprProtocol() {
  super(new AprEndpoint());
}

Endpoint 組件用來處理底層的 Socket 網絡鏈接,AprEndpoint 裏面有個叫 SocketProcessor 的內部類,它負責爲 AprEndpoint 將接收到的 Socket 請求轉化成 Request 對象,SocketProcessor 實現了 Runnable 接口,它會有一個專門的線程池來處理,後面我會單獨從源碼的角度分析 Endpoint 組件的設計原理。微信

org.apache.tomcat.util.net.AprEndpoint.SocketProcessor#doRun:網絡

// Process the request from this socket
SocketState state = getHandler().process(socketWrapper, event);

process 方法會建立一個 processor 對象,調用它的 process 方法將 Socket 字節流封裝成 Request 對象,在建立 Processor 組件時,會將 Adapter 組件添加到 Processor 組件中:

org.apache.coyote.http11.AbstractHttp11Protocol#createProcessor:

protected Processor createProcessor() {
  Http11Processor processor = new Http11Processor();
  // set Adapter 組件
  processor.setAdapter(getAdapter());

  return processor;
}

而 Adapter 組件在鏈接器初始化時就已經建立好了:

org.apache.catalina.connector.Connector#initInternal:

// Initialize adapter
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);

目前爲止,Tomcat 只有一個 Adapter 實現類,就是 CoyoteAdapter。Adapter 的主要做用是將 Request 對象適配成容器可以識別的 Request 對象,好比 Servlet 容器,它的只能識別 ServletRequest 對象,這時候就須要 Adapter 適配器類做一層適配。

以上鍊接器的各個組件,我用一張圖說明它們直接的關係:

容器(Container)

在 Tomcat 中一共設計了 4 種容器,它們分別爲 Engine、Host、Context、Wrapper,它們的關係以下圖所示:

  • Engine:表示一個虛擬主機的引擎,一個 Tomcat Server 只有一個 引擎,鏈接器全部的請求都交給引擎處理,而引擎則會交給相應的虛擬主機去處理請求;
  • Host:表示虛擬主機,一個容器能夠有多個虛擬主機,每一個主機都有對應的域名,在 Tomcat 中,一個 webapps 就表明一個虛擬主機,固然 webapps 能夠配置多個;
  • Context:表示一個應用容器,一個虛擬主機能夠擁有多個應用,webapps 中每一個目錄都表明一個 Context,每一個應用能夠配置多個 Servlet。

從上圖可看出,各個容器組件之間的關係是由大到小,即父子關係,它們之間關係造成一個樹狀的結構,它們的實現類都實現了 Container 接口,它有以下方法來控制容器組件之間的關係:

public interface Container extends Lifecycle {
  Container getParent();
  void setParent(Container container);
  void addChild(Container child);
  Container findChild(String name);
  Container[] findChildren();
  void removeChild(Container child);
}

容器組件之間經過以上幾個方法,便可實現它們之間的父子關係,有沒有發現,Container 接口還繼承了 Lifecycle 接口,它有以下方法:

public interface Lifecycle {   
  public static final String INIT_EVENT = "init";  
  public static final String START_EVENT = "start";   
  public static final String BEFORE_START_EVENT = "before_start";   
  public static final String AFTER_START_EVENT = "after_start";   
  public static final String STOP_EVENT = "stop";   
  public static final String BEFORE_STOP_EVENT = "before_stop";   
  public static final String AFTER_STOP_EVENT = "after_stop";   
  public static final String DESTROY_EVENT = "destroy";  

  public void addLifecycleListener(LifecycleListener listener);   
  public LifecycleListener[] findLifecycleListeners();   
  public void removeLifecycleListener(LifecycleListener listener);   
  public void start() throws LifecycleException;   
  public void stop() throws LifecycleException;   
}

Tomcat 中有不少組件,組件經過實現 Lifecycle 接口,Tomcat 經過事件機制來實現對這些組件生命週期的管理。

Tomcat 的這種容器設計思想,實際上是運用了組合設計模式的思想,組合設計模式最大的優勢是能夠自由添加節點,這樣也就使得 Tomcat 的容器組件很是地容易進行擴展,符合設計模式中的開閉原則。

如今咱們知道了 Tomcat 的容器組件的組合方式,那咱們如今就來想一個問題:

當一個請求過來時,Tomcat 是如何識別請求並將它交給特定 Servlet 來處理呢?

從容器的組合關係能夠看出,它們調用順序一定是:

Engine -> Host -> Context -> Wrapper -> Servlet

那麼 Tomcat 是如何來定位 Servlet 的呢?答案是利用 Mapper 組件來完成定位的工做。

Mapper 最主要的核心功能是保存容器組件之間訪問路徑的映射關係,它是如何作到這點的呢?

咱們不妨先從源碼入手:

org.apache.catalina.core.StandardService:

protected final Mapper mapper = new Mapper();
protected final MapperListener mapperListener = new MapperListener(this);

Service 實現類中,已經初始化了 Mapper 組件以及它的監聽類 MapperListener,這裏先說明一下,在 Tomcat 組件中,標準的實現組件類前綴會有 Standard,好比:

org.apache.catalina.core.StandardServer
org.apache.catalina.core.StandardService
org.apache.catalina.core.StandardEngine
org.apache.catalina.core.StandardHost
org.apache.catalina.core.StandardContext
org.apache.catalina.core.StandardWrapperz

在 Service 服務啓動的時候,會調用 MapperListener.start() 方法,最終會執行 MapperListener 的 startInternal 方法:

org.apache.catalina.mapper.MapperListener#startInternal:

Container[] conHosts = engine.findChildren();
for (Container conHost : conHosts) {
  Host host = (Host) conHost;
  if (!LifecycleState.NEW.equals(host.getState())) {
    // Registering the host will register the context and wrappers
    registerHost(host);
  }
}

該方法會註冊新的虛擬主機,接着 registerHost() 方法會註冊 context,以此類推,從而將容器組件直接的訪問的路徑都註冊到 Mapper 中。

定位 Servlet 的流程圖:

公衆號「後端進階」,專一後端技術分享!

相關文章
相關標籤/搜索