Tomcat 第四篇:請求處理流程(上)

極客挖掘機發表於2020-09-27

1. 引言

既然是在講 Tomcat ,那麼一個 HTTP 請求的請求流程是無論如何也繞不開的。

首先拋開所有,使用我們現有的知識面,猜測一下一個請求被 Tomcat 處理的過程:

1. 客戶端(瀏覽器)傳送一個請求(HTTP)
2. 建立 Socket 連線
3. 通過 Socket 讀取資料
4. 根據協議(HTTP)解析請求
5. 呼叫對應的程式碼完成響應

上面這套流程,我相信任何一個 Java 碼農都能想得到,當 Tomcat 接受到請求後,經過一系列的基礎處理,最終會呼叫到我們自己的業務程式上,或者說是 Servlet 上,在早期,這些請求會由我們自己實現的 jsp 或者是 Servlet 進行接收,隨著時代的發展以及演進,出現了 Struts 和 Spring 等中介軟體來幫助我們完成基礎的請求處理,使得開發人員更加關注具體的業務。

我想很多人都很好奇, Tomcat 是如何將這些 HTTP 請求轉交給我們的 Servlet 的?

2. Connector 初始化

上一篇我們在聊 Tomcat 啟動流程的時候,最後執行初始化的是 org.apache.catalina.connector.Connector#initInternal() ,這時整個初始化流程到了 Connector ,看一下這段程式碼:

// 去除部分程式碼
protected void initInternal() throws LifecycleException {
    super.initInternal();
    // Initialize adapter
    adapter = new CoyoteAdapter(this);
    protocolHandler.setAdapter(adapter);
    // ......
    try {
        protocolHandler.init();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
}

這段程式碼中主要做了兩件事情:

  • 構造了 CoyoteAdapter 物件,並且將其設定為 ProtocolHandler 的 Adapter 。
  • 呼叫了 org.apache.coyote.ProtocolHandler#init() 的方法。

先說第二件事情,呼叫了 org.apache.coyote.ProtocolHandler#init()ProtocolHandler 是在構造方法中進行的初始化,這裡的核心程式碼是:

setProtocol(protocol)

再看下 setProtocol() 這個方法做了啥:

public void setProtocol(String protocol) {
    boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
            AprLifecycleListener.getUseAprConnector();
    if ("HTTP/1.1".equals(protocol) || protocol == null) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
        }
    } else if ("AJP/1.3".equals(protocol)) {
        if (aprConnector) {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
        } else {
            setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
        }
    } else {
        setProtocolHandlerClassName(protocol);
    }
}

看到這裡可以知道,主邏輯分成了兩塊,一塊是使用 HTTP/1.1 協議,另一塊是使用了 AJP/1.3 的協議,這裡通過協議的不同,最終初始化了不同的類。

如果是使用 HTTP/1.1 的協議,則採用了 org.apache.coyote.http11.Http11AprProtocol 或者 org.apache.coyote.http11.Http11NioProtocol,如果是採用 AJP/1.3 則採用 org.apache.coyote.ajp.AjpAprProtocol 或者是 org.apache.coyote.ajp.AjpNioProtocol

看下 org.apache.coyote.http11.Http11AprProtocolorg.apache.coyote.ajp.AjpAprProtocol 繼承關係圖:

可以看到這兩個類都繼承自 org.apache.coyote.AbstractProtocol ,通過檢視 org.apache.coyote.AbstractProtocol#init() 方法,可以看到是呼叫了 org.apache.tomcat.util.net.AbstractEndpoint#init() ,而 AbstractEndpoint 的例項化操作是在例項化 AjpProtocolHttp11Protocol 的時候在其建構函式中例項化的,而在 AjpProtocolHttp11Protocol 的建構函式中,實際上是都初始化了 org.apache.tomcat.util.net.JIoEndpoint ,只不過根據不同的 HTTP 或者是 AJP 協議,它們具有不同的連線處理類。其中 Http11Protocol 的連線處理類為 org.apache.coyote.http11.Http11Protocol.Http11ConnectionHandler ,而連線處理類為 org.apache.coyote.ajp.AjpProtocol.AjpConnectionHandler

除此之外, ProtocolHandler 還有其他實現,都在 org.apache.coyote 這個包中,一些常見的類圖如下:

因此到這裡我們基本清楚了 Connector 的初始化流程,總結如下:

//1 HTTP/1.1協議聯結器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.http11.Http11AprProtocol#init
-->org.apache.tomcat.util.net.AprEndpoint#init
(org.apache.coyote.http11.Http11AprProtocol.Http11ConnectionHandler)

// 2 AJP/1.3協議聯結器
org.apache.catalina.connector.Connector#init
->org.apache.coyote.ajp.AjpAprProtocol#init
-->org.apache.tomcat.util.net.AprEndpoint#init
(org.apache.coyote.ajp.AjpAprProtocol.AjpConnectionHandler)

3. Connector 啟動

ProtocolHandler 的初始化稍微有些特殊,Server、Service、Connector 這三個容器的初始化順序為: Server -> Service -> Connector 。值得注意的是, ProtocolHandler 作為 Connector 的子容器,其初始化過程並不是由 Connector 的 initInternal 方法呼叫的,而是與啟動過程一道被 Connector 的 startInternal 方法所呼叫。

@Override
protected void startInternal() throws LifecycleException {
    // Validate settings before starting
    if (getPort() < 0) {
        throw new LifecycleException(sm.getString(
                "coyoteConnector.invalidPort", Integer.valueOf(getPort())));
    }
    setState(LifecycleState.STARTING);
    try {
        protocolHandler.start();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
    }
}

這裡也總共做了兩件事兒:

  • 將 Connector 容器的狀態更改為啟動中(LifecycleState.STARTING) 。
  • 啟動 ProtocolHandler 。

簡單起見,以 Http11Protocol 為例介紹 ProtocolHandler 的 start 方法:

由於 ProtocolHandler 是一個介面,它的 start 方法有兩個抽象類進行實現:

  • org.apache.coyote.AbstractAjpProtocol
  • org.apache.coyote.ajp.AbstractProtocol

這裡我們僅討論 org.apache.coyote.ajp.AbstractProtocol ,看下它的 start 的方法:

@Override
public void start() throws Exception {
    if (getLog().isInfoEnabled()) {
        getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
    }

    endpoint.start();

    // Start timeout thread
    asyncTimeout = new AsyncTimeout();
    Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
    int priority = endpoint.getThreadPriority();
    if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
        priority = Thread.NORM_PRIORITY;
    }
    timeoutThread.setPriority(priority);
    timeoutThread.setDaemon(true);
    timeoutThread.start();
}

這裡最核心的一句程式碼是呼叫了 endpoint.start() ,這裡的 endpoint 是抽象類 AbstractEndpoint#start() :

public final void start() throws Exception {
    if (bindState == BindState.UNBOUND) {
        bind();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}

這一段也做了兩件事兒:

  • 判斷當前繫結狀態,如果沒有繫結,則會先去繫結,呼叫 bind()
  • 然後呼叫 startInternal() 進行初始化。

AbstractEndpoint 有三個子類:

我們專注於 AprEndpoint 這個子類,上面 AbstractEndpoint 抽象類中的兩個方法 bind()startInternal() 都會在這個類中進行實現。

我們先看 bind() 方法:

/**
    * Initialize the endpoint.
    */
@Override
public void bind() throws Exception {

    // Create the root APR memory pool
    try {
        rootPool = Pool.create(0);
    } catch (UnsatisfiedLinkError e) {
        throw new Exception(sm.getString("endpoint.init.notavail"));
    }

    // Create the pool for the server socket
    serverSockPool = Pool.create(rootPool);
    // Create the APR address that will be bound
    String addressStr = null;
    if (getAddress() != null) {
        addressStr = getAddress().getHostAddress();
    }
    int family = Socket.APR_INET;
    if (Library.APR_HAVE_IPV6) {
        if (addressStr == null) {
            if (!OS.IS_BSD) {
                family = Socket.APR_UNSPEC;
            }
        } else if (addressStr.indexOf(':') >= 0) {
            family = Socket.APR_UNSPEC;
        }
        }

    long inetAddress = Address.info(addressStr, family,
            getPort(), 0, rootPool);
    // Create the APR server socket
    serverSock = Socket.create(Address.getInfo(inetAddress).family,
            Socket.SOCK_STREAM,
            Socket.APR_PROTO_TCP, rootPool);
    if (OS.IS_UNIX) {
        Socket.optSet(serverSock, Socket.APR_SO_REUSEADDR, 1);
    }
    if (Library.APR_HAVE_IPV6) {
        if (getIpv6v6only()) {
            Socket.optSet(serverSock, Socket.APR_IPV6_V6ONLY, 1);
        } else {
            Socket.optSet(serverSock, Socket.APR_IPV6_V6ONLY, 0);
        }
    }
    // Deal with the firewalls that tend to drop the inactive sockets
    Socket.optSet(serverSock, Socket.APR_SO_KEEPALIVE, 1);
    // Bind the server socket
    int ret = Socket.bind(serverSock, inetAddress);
    if (ret != 0) {
        throw new Exception(sm.getString("endpoint.init.bind", "" + ret, Error.strerror(ret)));
    }
    // Start listening on the server socket
    ret = Socket.listen(serverSock, getAcceptCount());
    if (ret != 0) {
        throw new Exception(sm.getString("endpoint.init.listen", "" + ret, Error.strerror(ret)));
    }
    if (OS.IS_WIN32 || OS.IS_WIN64) {
        // On Windows set the reuseaddr flag after the bind/listen
        Socket.optSet(serverSock, Socket.APR_SO_REUSEADDR, 1);
    }

    // Enable Sendfile by default if it has not been configured but usage on
    // systems which don't support it cause major problems
    if (!useSendFileSet) {
        setUseSendfileInternal(Library.APR_HAS_SENDFILE);
    } else if (getUseSendfile() && !Library.APR_HAS_SENDFILE) {
        setUseSendfileInternal(false);
    }

    // Initialize thread count default for acceptor
    if (acceptorThreadCount == 0) {
        // FIXME: Doesn't seem to work that well with multiple accept threads
        acceptorThreadCount = 1;
    }

    // Delay accepting of new connections until data is available
    // Only Linux kernels 2.4 + have that implemented
    // on other platforms this call is noop and will return APR_ENOTIMPL.
    if (deferAccept) {
        if (Socket.optSet(serverSock, Socket.APR_TCP_DEFER_ACCEPT, 1) == Status.APR_ENOTIMPL) {
            deferAccept = false;
        }
    }

    // Initialize SSL if needed
    if (isSSLEnabled()) {
        for (SSLHostConfig sslHostConfig : sslHostConfigs.values()) {
            createSSLContext(sslHostConfig);
        }
        SSLHostConfig defaultSSLHostConfig = sslHostConfigs.get(getDefaultSSLHostConfigName());
        if (defaultSSLHostConfig == null) {
            throw new IllegalArgumentException(sm.getString("endpoint.noSslHostConfig",
                    getDefaultSSLHostConfigName(), getName()));
        }
        Long defaultSSLContext = defaultSSLHostConfig.getOpenSslContext();
        sslContext = defaultSSLContext.longValue();
        SSLContext.registerDefault(defaultSSLContext, this);

        // For now, sendfile is not supported with SSL
        if (getUseSendfile()) {
            setUseSendfileInternal(false);
            if (useSendFileSet) {
                log.warn(sm.getString("endpoint.apr.noSendfileWithSSL"));
            }
        }
    }
}

這個方法上面的註釋已經寫的比較清楚了,首先第一個方法註釋就告訴我們這個方法是用來初始化 endpoint 的,大體做了這麼幾件事兒:

  • 建立了 APR 的 rootPool ,從命名上看這應該是一個根連線池。
  • 建立一個 serverSockPool ,使用剛才建立的 rootPool 進行建立,這個命名大家就都看得懂了。
  • 建立用來做繫結的 APR 的地址。
  • 建立一個 APR server socket -> serverSock ,這裡開啟了 socket 。
  • 將剛才建立的 server 和 socket 進行繫結。
  • 開啟 server socket 上面的監聽。
  • 一些系統層面的設定。
  • 如果需要的話,還會進行一些 SSL 的相關設定。

接著看下 startInternal() 方法:

/**
  * Start the APR endpoint, creating acceptor, poller and sendfile threads.
  */
@Override
public void startInternal() throws Exception {

    if (!running) {
        running = true;
        paused = false;

        processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                socketProperties.getProcessorCache());

        // Create worker collection
        if (getExecutor() == null) {
            createExecutor();
        }

        initializeConnectionLatch();

        // Start poller thread
        poller = new Poller();
        poller.init();
        Thread pollerThread = new Thread(poller, getName() + "-Poller");
        pollerThread.setPriority(threadPriority);
        pollerThread.setDaemon(true);
        pollerThread.start();

        // Start sendfile thread
        if (getUseSendfile()) {
            sendfile = new Sendfile();
            sendfile.init();
            Thread sendfileThread =
                    new Thread(sendfile, getName() + "-Sendfile");
            sendfileThread.setPriority(threadPriority);
            sendfileThread.setDaemon(true);
            sendfileThread.start();
        }

        startAcceptorThreads();
    }
}

這個方法所有的前提條件都在於如果 AprEndpoint 尚未出於執行中,即 running == true ,首先如果沒有建立執行緒池 getExecutor() == null ,則需要呼叫 createExecutor() 方法建立執行緒池和任務佇列 TaskQueue

public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
}

剩下兩個是建立了兩個執行緒,分別是 poller 和 sendfile ,這兩個都是 AprEndpoint 的內部類,這兩個執行緒一個是用來做輪詢,另一個是用來做資料傳送。

至此, Tomcat 中為請求處理的準備工作已經完成。

相關文章