Tomcat架構之為Bypass記憶體馬檢測鋪路(記憶體馬系列篇四)

語言: CN / TW / HK

寫在前面

繼前面三種常見的Tomcat的記憶體馬構造方式,後面將會講解更加獨特的Tomcat記憶體馬構造,這是記憶體馬系列文章第四篇,主要講解Tomcat的架構,為後面Tomcat元件記憶體馬做鋪墊(不是在容器中實現的記憶體馬,可以一定程度避免查殺)。

前置

頂層架構

我們可以通過一張圖來表示。

架構足夠模組化,從圖中我們可以知道最上層為Server伺服器,為Service服務提供一個生存環境,掌握每個Service服務的生命週期,至於Service則是嘴歪提供服務。

而在每隔Service中有著多個Connector和一個Container,他們的作用分別是Connector:負責接收瀏覽器的TCP連線請求,提供Socket與Request、Response的相關轉化,與請求端交換資料,Container:用於封裝和管理Servlet,以及具體處理Request請求,是所有子容器的父介面(包括Engine、Host、Context、Wrapper)。

  • Engine -- 引擎

  • Host -- 主機

  • Context -- 上下文

  • Wrapper -- 包裝器

Service服務之下還有各種 支撐元件 ,下面簡單羅列一下這些元件。

  • Manager -- 管理器,用於管理會話Session

  • Logger -- 日誌器,用於管理日誌

  • Loader -- 載入器,和類載入有關,只會開放給Context所使用

  • Pipeline -- 管道元件,配合Valve實現過濾器功能

  • Valve -- 閥門元件,配合Pipeline實現過濾器功能

  • Realm -- 認證授權元件

同樣可以在tomcat的server.xml中看到一些配置。

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!-- APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">

    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t "%r" %s %b" />

      </Host>
    </Engine>
  </Service>
</Server>

我們可以知道開放的埠,第一部分就是tomcat預設的監聽器,第二部分就是資源,第三部分就是一些Service服務,上面的這個xml檔案只配置了一個Connector,提供了HTTP/1.1的連線支援,但是Tomcat是支援多個Connector配置的,比如:

存在多個配置

之後還存在一些Engine等等結構,當然除了這些還有著負責jsp頁面解析、jsp屬性驗證,同時也負責將jsp頁面動態轉換為java程式碼並編譯為class檔案的Jasper元件,提供命名服務的Naming元件,提供Session服務的Session元件,負責日誌記錄的Logging元件,提供JVM監控服務的JMX元件。

生命週期

整個Tomcat的生命週期以觀察者模式為基礎

Subject 抽象主題:負責管理所有觀察者的引用,同時定義主要的事件操作,而Tomcat核心類LifeCycle就是這個抽象主題。

ConcretSubject 具體主題:實現抽象主題定義的所有介面,發生變化通知觀察者,如StandardServer

Observer 觀察者:監聽主題變化的操作介面,LifeCycleListener為這個抽象觀察者

我們可以來看看 Lifecycle 的方法。

定義了許多的方法,分別為新增監聽器,獲取監聽器,刪除監聽器或者是初始化,啟動方法和停止消毀相關的方法。

存在有一個對 LifecycleBase 類對 Lifecycle 進行了實現,對各個方法進行了重寫,我們來看看其中的幾個方法。

init()

這裡判斷了state只有為NEW的時候才會進行初始化,首先將其狀態設定為 INITIALIZING ,之後呼叫 initInternal 進行初始化操作,在初始化完成之後設定狀態,如果在過程中出現錯誤,將會丟擲異常

值得注意的是 initInternal 方法是一個抽象方法,需要各個元件進行重寫,其他的方法也就不詳細寫了,師傅們可以跟一下。

從上述原始碼看得出來, LifecycleBase 是使用了狀態機+模板模式來實現的。模板方法有下面這幾個:

// 初始化方法
protected abstract void initInternal() throws LifecycleException;
// 啟動方法
protected abstract void startInternal() throws LifecycleException;
// 停止方法
protected abstract void stopInternal() throws LifecycleException;
// 銷燬方法
protected abstract void destroyInternal() throws LifecycleException;

Tomcat類載入機制

Java預設的類載入機制是通過 雙親委派模型 來實現的。而Tomcat實現的方式又和 雙親委派模型 有所區別。原因在於一個Tomcat容器允許同時執行多個Web程式,每個Web程式依賴的類又必須是相互隔離的。因此,如果Tomcat使用 雙親委派模式 來載入類的話,將導致Web程式依賴的類變為共享的。對於雙親委派模型的機制我們是非常熟悉的了,也不過多的講解了。

貼個圖

大概就是這樣個一類載入順序,同樣存在有另一中缺陷,如果在程式中使用了第三方實現類,如果使用雙親委派模型,那麼第三方實現類也需要放在Java核心類裡面才可以,不然的話第三方實現類將不能被載入使用,但是在 java.lang.Thread 類中存在有兩個方法能夠獲取到上下文類載入器。

我們可以通過在SPI類裡面呼叫 getContextClassLoader 來獲取第三方實現類的類載入器。由第三方實現類通過呼叫 setContextClassLoader 來傳入自己實現的類載入器。

接下來來看看Tomcat獨有的類載入機制模型。

我們可以從 官方文件 找到說明

這個類載入器結構圖和之前的雙親委派機制的架構圖有著很大的區別。

每個類載入器的作用如下Common類載入器,負責載入Tomcat和Web應用都複用的類

Catalina類載入器,負責載入Tomcat專用的類,而這些被載入的類在Web應用中將不可見

Shared類載入器,負責載入Tomcat下所有的Web應用程式都複用的類,而這些被載入的類在Tomcat中將不可見。

WebApp類載入器,負責載入具體的某個Web應用程式所使用到的類,而這些被載入的類在Tomcat和其他的Web應用程式都將不可見。

Jsp類載入器,每個jsp頁面一個類載入器,不同的jsp頁面有不同的類載入器,方便實現jsp頁面的熱插拔

直接來分析一下Tomcat啟動所呼叫的流程。

如果在 org.apache.catalina.startup.Bootstrap 類中,來看一下類中存在的方法

其中存在main方法,在啟動tomcat的同時將會呼叫這個方法,但是在這個類最後面存在一個靜態程式碼塊將會首先執行。

從環境變數中獲取catalina.home,在沒有獲取到的時候將執行後面的獲取操作
在第一步沒獲取的時候,從bootstrap.jar所在目錄的上一級目錄獲取
第二步中的bootstrap.jar可能不存在,這時我們直接把user.dir作為我們的home目錄
重新設定catalinaHome屬性
接下來獲取CATALINA_BASE(從系統變數中獲取),若不存在,則將CATALINA_BASE保持和CATALINA_HOME相同
重新設定catalinaBase屬性

而在main方法中,main方法大體分成兩塊,一塊為init,另一塊為load+start,我們可以來看看init中的的程式碼。

public void init() throws Exception {
    // 非常關鍵的地方,初始化類載入器s,後面我們會詳細具體地分析這個方法
    initClassLoaders();

    // 設定上下文類載入器為catalinaLoader,這個類載入器負責載入Tomcat專用的類
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 使用catalinaLoader載入我們的Catalina類
    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
        log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 設定Catalina類的parentClassLoader屬性為sharedLoader
    // Set the shared extensions class loader
    if (log.isDebugEnabled())
        log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    Method method =
        startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    // catalina守護物件為剛才使用catalinaLoader載入類、並初始化出來的Catalina物件
    catalinaDaemon = startupInstance;
}

跟進一下 initClassLoaders

private void initClassLoaders() {
    try {
        // 建立commonLoader,如果未建立成果的話,則使用應用程式類載入器作為commonLoader
        commonLoader = createClassLoader("common", null);
        if( commonLoader == null ) {
            // no config file, default to this loader - we might be in a 'single' env.
            commonLoader=this.getClass().getClassLoader();
        }
        // 建立catalinaLoader,父類載入器為commonLoader
        catalinaLoader = createClassLoader("server", commonLoader);
        // 建立sharedLoader,父類載入器為commonLoader
        sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
        // 如果建立的過程中出現異常了,日誌記錄完成之後直接系統退出
        handleThrowable(t);
        log.error("Class loader creation threw exception", t);
        System.exit(1);
    }
}

接下來來看看 SecurityClassLoad.securityClassLoad 的使用其實就是使用catalinaLoader載入tomcat原始碼裡面的各個專用類。我們大致羅列一下待載入的類所在的package。

org.apache.catalina.core.*

org.apache.coyote.*

org.apache.catalina.loader.*

org.apache.catalina.realm.*

org.apache.catalina.servlets.*

org.apache.catalina.session.*

org.apache.catalina.util.*

org.apache.catalina.valves.*

javax.servlet.http.Cookie

org.apache.catalina.connector.*

org.apache.tomcat.*

元件

好不容易跟進了前面的tomcat原始碼,終於來到了我們的目的地元件的分析。

Server

在跟進前面的Tomcat啟動類中呼叫了,server.init方法和server.start方法,我們來看看Server的原始碼實現

org.apache.catalina.Server 介面中有著對方法的定義。

我們可以觀察到其繼承了我們前面分析了的生命週期的介面 Lifecycle ,這就意味著,在在呼叫了server.init或者start方法的同時同樣會呼叫所有的service的方法。

來看看Server的實現類,類為 org.apache.catalina.core.StandardServer 下,在初始化的時候將會呼叫 initInternal 方法。

從這裡的迴圈語句中也可以證實確實將會呼叫所有存在service的init方法,同樣在呼叫servser.start方法的時候將會呼叫 startInternal 方法。

同樣通過了迴圈的方式進行呼叫,我們可以通過idea檢視該類的繼承實現結構關係圖。

Service

其介面在 org.apache.catalina.Service 中,存在方法的定義,同樣繼承了Lifecycle介面。我們來到Service的實現類 org.apache.catalina.core.StandardService

首先上圖看一下類的關係

既然在呼叫Server.init中會呼叫service.init方法,那我們就跟進一下service.init講了個什麼。

其中存在對Engine的初始化 / Executor的初始化(後面會提到) / connector的初始化。同樣的,跟進一下start方法的邏輯和上面呼叫init方法進行初始化差不多,分別呼叫了對應的start方法。

Container

對於Container的介面在 org.apache.catalina.Container 中,同樣繼承了 Lifecycle 介面

首先看一下他的繼承關係。

由上圖我們可以知道Engine包含多個Host,Host包含多個Context,Context包含多個Wrapper,每個Wrapper對應一個Servlet。

分別的說明

Engine,我們可以看成是容器對外提供功能的入口,每個Engine是Host的集合,用於管理各個Host。

Host,我們可以看成 虛擬主機 ,一個tomcat可以支援多個虛擬主機。

Context,又叫做上下文容器,我們可以看成 應用服務 ,每個Host裡面可以執行多個應用服務。同一個Host裡面不同的Context,其contextPath必須不同,預設Context的contextPath為空格("")或斜槓(/)。

Wrapper,是Servlet的抽象和包裝,每個Context可以有多個Wrapper,用於支援不同的Servlet。另外,每個JSP其實也是一個個的Servlet。

Connector

最後來到了最關鍵的Connector部分了,也是構造記憶體馬的關鍵位置。

首先來看看整體的架構流程圖。

描述了請求到達Container進行處理的全過程。

不同協議 ProtocolHandler 會有不同的實現。

  1. ajp和http11是兩種不同的協議

  2. nio、nio2和apr是不同的通訊方式

  3. 協議和通訊方式可以相互組合

ProtocolHandler 包含三個部件: EndpointProcessorAdapter

  1. Endpoint 用來處理底層Socket的網路連線, Processor 用於將 Endpoint 接收到的Socket封裝成Request, Adapter 用於將Request交給Container進行具體的處理。

  2. Endpoint 由於是處理底層的Socket網路連線,因此 Endpoint 是用來實現 TCP/IP協議 的,而 Processor 用來實現 HTTP協議 的, Adapter 將請求適配到Servlet容器進行具體的處理。

  3. Endpoint 的抽象實現類AbstractEndpoint裡面定義了 AcceptorAsyncTimeout 兩個內部類和一個 Handler介面Acceptor 用於監聽請求, AsyncTimeout 用於檢查非同步Request的超時, Handler 用於處理接收到的Socket,在內部呼叫 Processor 進行處理。

接下來深入原始碼進行一下分析

跟進 org.apache.catalina.connector.Connector 類的程式碼邏輯

發現以下幾點

  1. 無參構造方法,傳入引數為空協議,會預設使用 HTTP/1.1

  2. HTTP/1.1null ,protocolHandler使用 org.apache.coyote.http11.Http11NioProtocol ,不考慮apr

  3. AJP/1.3 ,protocolHandler使用 org.apache.coyote.ajp.AjpNioProtocol ,不考慮apr

  4. 其他情況,使用傳入的protocol作為protocolHandler的類名

  5. 使用protocolHandler的類名構造ProtocolHandler的例項

接下來來到了 Connector#initInternal 方法的呼叫。

總結一下就是初始化了一個CoyoteAdapter,並將其設定進入了protocolHandler中,之後接受了body的method列表,預設為POST。

最後初始化了初始化protocolHandler

之後將會呼叫 protocolHandler#init 方法

而對於start方法的呼叫,在 Connector

在設定了狀態為 STARTING 之後就呼叫了 protocolHandler#start 方法。

將會呼叫endpoint#start方法。

跟進一下

呼叫bind方法

建立工作者執行緒池。

初始化連線latch,用於限制請求的併發量。

開啟poller執行緒。poller用於對接受者執行緒生產的訊息(或事件)進行處理,poller最終呼叫的是Handler的程式碼。

開啟acceptor執行緒

Acceptor

請求的入口是在 AcceptorEndpoint.start() 方法會開啟 Acceptor執行緒 來處理請求

那麼我們接下來就要分析一下 Acceptor執行緒 中的執行邏輯

在其run方法中存在

  1. 執行過程中,如果 Endpoint 暫停了,則 Acceptor 進行自旋(間隔50毫秒)

  2. 如果 Endpoint 終止運行了,則 Acceptor 也會終止

  3. 如果請求達到了最大連線數,則wait直到連線數降下來

  4. 接受下一次連線的socket

存在這樣的邏輯

setSocketOptions() 這兒是關鍵,會將socket以事件的方式傳遞給poller。

跟進一下 setSocketOptions

其中存在 this.getPoller0.register 的呼叫將channel註冊到poller,注意關鍵的兩個方法, getPoller0()Poller.register() 。先來分析一下 getPoller0() ,該方法比較關鍵的一個地方就是 以取模的方式 對poller數量進行輪詢獲取。

/**
 * The socket poller.
 */
private Poller[] pollers = null;
private AtomicInteger pollerRotater = new AtomicInteger(0);
/**
 * Return an available poller in true round robin fashion.
 *
 * @return The next poller in sequence
 */
public Poller getPoller0() {
    int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
    return pollers[idx];
}

接下來我們分析一下 Poller.register() 方法。因為 Poller 維持了一個 events同步佇列 ,所以 Acceptor 接受到的channel會放在這個佇列裡面。

Poller

Acceptor 生成了事件 PollerEvent ,那麼 Poller 必然會對這些事件進行消費。我們來分析一下 Poller.run() 方法。

在其中存在有

呼叫了processKey(sk, socketWrapper)進行處理。

該方法又會根據key的型別,來分別處理讀和寫。

  1. 處理讀事件,比如生成Request物件

  2. 處理寫事件,比如將生成的Response物件通過socket寫回客戶端

跟進 processSocket 方法

  1. processorCache 裡面拿一個 Processor 來處理socket, Processor 的實現為 SocketProcessor

  2. Processor 放到工作執行緒池中執行

Processor

呼叫 service() 方法

  1. 生成Request和Response物件

  2. 呼叫 Adapter.service() 方法,將生成的Request和Response物件傳進去

Adapter

Adapter 用於連線 ConnectorContainer ,起到承上啟下的作用。 Processor 會呼叫 Adapter.service() 方法主要做了下面幾件事情

根據coyote框架的request和response物件,生成connector的request和response物件(是HttpServletRequest和HttpServletResponse的封裝)

補充header

解析請求,該方法會出現代理伺服器、設定必要的header等操作

真正進入容器的地方,呼叫Engine容器下pipeline的閥門

總結

就這樣,深入程式碼的瞭解了整個Tomcat的架構(當然,還是有很多的程式碼沒有貼出來,太長了,需要師傅們自己跟一下),跟完之後大吸一口涼氣。真難》

Ref

https://www.jianshu.com/u/794bce41b31b

http://chujunjie.top/2019/04/30/Tomcat原始碼學習筆記-Connector元件-二/