【SpringBoot + Tomcat】請求到達後端服務程序後的處理過程

酷酷-發表於2024-04-09

1 前言

這節我主要是想看下,Tomcat 如何接收到請求並且是怎麼一步步封裝並交給 SpringMVC 處理的。這塊之前一直沒太深入的瞭解過,所以這節我們來看看。

在看這節之前,你首先要清楚這兩個問題,方便你更好的去理解。

(1)SpringBoot啟動的過程中,Tomcat 的建立和啟動時機是在什麼時候呢?不知道的話,可以看我的這篇

(2)我們都知道 SpringMVC 其實可以理解為就是一個 DispatchServlet ,那麼它是如何跟 Tomcat 掛上關係的呢?不知道的話,可以看我的這篇 還有這篇現代和傳統的啟動方式的區別會說現代 DispatchServlet 是如何放入 Tomcat 中的。

如果你看完還是有點模糊的話,也不要緊,下邊我會著重挑一些細節拿出來帶大家再看一遍。

2 Tomcat 回顧

首先我們請求能過到達服務,那麼我們的服務肯定是要啟動著的吧,是不是,後端服務都沒啟,網路連線都建立不起來,更別說服務了。所以我們要看看服務啟動的過程中,Tomcat 的建立。在看之前我們先回憶下 Tomcat 的一個大致元件結構,這裡我簡單畫了個圖:

可以看到有這麼幾個概念哈:

Server:一個 Tomcat 例項就是一個 Server

Service:一個 Server裡可以有一個或者多個 Service,每個 Service 裡有一個 Container 和多個 Connector。

Connector:負責網路連線,底層就是socket來進行連線的(java中的網路通訊是透過socket實現的 socket又分為普通的socket、NioSocket),還有我們的 request、response的建立等,封裝完交給 Container 處理,處理完返回給 Connector,然後透過socket將結果返回給客戶端。它具體用 protocolhandler 處理請求的,不同的protocolhandler代表不同的連線型別,http11protocol用的是普通socket來連線的,http11nioprotocol用的是niosocket來連線的。而 protocolhandler 有三個重要的元件:endpoint、processor、adapter,endpoint 用於處理底層socket的網路連線,processor 用於將endpoint接收的socket封裝成request,adapter用於將封裝好的request交給container進行具體處理各司其職。

Container :就是servlet容器,具體處理 Servlet,它內部有四個元件:engine、host、context、wrapper,engine是引擎管理多個站點,host表示一個站點 也叫虛擬主機 appbase站點位置 預設就是我們的webapps目錄,context 就是我們webapps下的每個應用程式,wrapper 每個wrapper 封裝著一個servlet。

瞭解完這幾個概念後,我們看看 SpringBoot 啟動的時候是不是這樣的,細節、入口這裡就不一點點看了,我就直接挑關鍵程式碼給大家展示了哈:

我們可以看到建立了 Tomcat 物件、一個聯結器物件 Connector,那麼 Service、Host 怎麼建立的呢?我們繼續看看:

先看 Service 的建立,Tomcat 物件第一次內部的 Server 為空,會先建立 Server 物件,然後再建立一個 Service 物件,並把它加入到了 Server 物件中:

那我們繼續看看 Host:

這些建立我們看完捋一下,類之間的關係,大致有一個這麼個模樣:

插一個小細節:我們知道 onRefresh 的時候是建立 Web 容器,但當你看原始碼的時候,會發現在例項化TomcatWebServer 物件中就會呼叫 tomcat的 start 方法,這豈不是就啟動了麼?

哈哈哈,看啟動前的那幾行程式碼,也就是第69行,它會把 connector 聯結器先給挪出來,因為聯結器一旦啟動了,就能接收到請求了,可是這個時候我們的 Spring 容器中有的 Bean 都還沒建立完,還不能正常提供服務,所以這裡把它挪出來。然後在 finishRefresh 的時候,會把聯結器塞,並啟動聯結器:

另外關於 DispatchServlet 是放到哪個物件裡了呢?可以看這個現代和傳統的啟動方式的區別,這篇最後會一步步說它是怎麼注入到 Tomcat 的 ServletContext 中的哈。

3 聯結器

3.1 基礎認識

我們既然要網路通訊,那就涉及到網路協議,Tomcat支援的多種I/O模型和應用層協議,具體包含哪些IO模型和應用層協議,參考如下:

傳輸層協議:

IO模型描述
NIO 非阻塞I/O,採用Java NIO類庫實現。
NIO2 非同步I/O,採用JDK 7最新的NIO2類庫實現。
APR 採用Apache可移植執行庫實現,是C/C++編寫的本地庫。如果選擇該方案,需要單獨安裝APR庫。

Tomcat 支援的IO模型:自8.5/9.0 版本起,Tomcat 移除了 對 BIO 的支援.

應用層協議:

應用層協議描述
HTTP/1.1 Web應用採用的訪問協議
AJP 用於和Web伺服器整合(如Apache),以實現對靜態資源的最佳化以及叢集部署,當前支援AJP/1.3。
HTTP/2 HTTP 2.0大幅度的提升了Web效能。下一代HTTP協議 , 自8.5以及9.0版本之後支援。

在 Tomcat8.0 之前 ,Tomcat 預設採用的I/O方式為 BIO , 之後改為 NIO。 無論 NIO、NIO2還是 APR, 在效能方面均優於以往的BIO。

然後我們細細看一下聯結器的元件結構:

(1)Endpoint:

  • 通訊端點,即通訊監聽介面
  • 是具體的Socket請求的接收和傳送處理器,是對傳輸層抽象,用來實現TCP/IP協議:
  • Tomcat中並沒有Endpoint介面,而是定義了一個抽象類AbstractEndpoint, 該抽象類定義了兩個內部類:
    • Acceptor:用於監聽Socket連線請求
    • SocketProcessor:用於處理接收到的Socket請求;實現Runnable介面,在Run方法中呼叫了協議處理元件Processor進行處理;為了提高處理能力 ,socketProcessor會被提交到執行緒池來執行,該執行緒池是Tomcat擴充套件原生的 Java執行緒池,被稱作執行器Executor

(2)Processor:

  • 協議處理介面,是對應用層協議的抽象
  • Processor用來實現HTTP協議
  • Processor接收來自Endpoint的Socket資料,讀取位元組流解析成HttpRequest物件
  • 然後將HttpRequest透過Adapter轉化成ServletRequest提交給容器處理

(3)ProtocolHandler:

  • 協議介面,透過Endpoint和Processor實現針對具體協議的處理能力,預設使用的協議是Http11NioProtocol

(4)Adapter:

  • 由於協議的不同,客戶端傳輸的請求資訊也會不同 ,Tomcat自定義一個Request類來存放這些請求資訊
  • ProcotolHandler介面負責解析請求並生成Request類,但是這個Request不是標準的ServletRequest, 所有不能使用Tomcat中自定義的Request作為引數來呼叫容器進行資料處理
  • 透過運用介面卡模式,引入CoyoteAdapter來解決這樣的問題:聯結器呼叫CoyoteAdapter的Service方法,傳入Tomcat Request物件,然後CoyoteAdapter負責將Tomcat Request轉化成ServletRequest, 最後再呼叫容器的Service方法

3.2 聯結器的建立

我們再細細看看它的建立:

// Tomcat 工廠建立
public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
    // 現在預設的協議
    private String protocol = "org.apache.coyote.http11.Http11NioProtocol";
    public WebServer getWebServer(ServletContextInitializer... initializers) {
        ...
        Tomcat tomcat = new Tomcat();
        File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        // 建立聯結器
        Connector connector = new Connector(this.protocol);
        ...
}

繼續看一下他的構造方法:

public Connector(String protocol) {
    // 是否是 apr 預設是 false
    boolean apr = AprStatus.isAprAvailable() &&
        AprStatus.getUseAprConnector();
    ProtocolHandler p = null;
    try {
        // 建立 handler
        p = ProtocolHandler.create(protocol, apr);
    } catch (Exception e) {
        log.error(sm.getString(
                "coyoteConnector.protocolHandlerInstantiationFailed"), e);
    }
    if (p != null) {
        protocolHandler = p;
        protocolHandlerClassName = protocolHandler.getClass().getName();
    } else {
        protocolHandler = null;
        protocolHandlerClassName = protocol;
    }
}

繼續看下 Handler 的建立(主要就是根據協議名稱、是否開啟 apr 來選擇建立不同的 Handler),預設情況下建立的是:new org.apache.coyote.http11.Http11NioProtocol()

public static ProtocolHandler create(String protocol, boolean apr)
        throws ClassNotFoundException, InstantiationException, IllegalAccessException,
        IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
    if (protocol == null || "HTTP/1.1".equals(protocol)
            || (!apr && org.apache.coyote.http11.Http11NioProtocol.class.getName().equals(protocol))
            || (apr && org.apache.coyote.http11.Http11AprProtocol.class.getName().equals(protocol))) {
        if (apr) {
            return new org.apache.coyote.http11.Http11AprProtocol();
        } else {
            return new org.apache.coyote.http11.Http11NioProtocol();
        }
    } else if ("AJP/1.3".equals(protocol)
            || (!apr && org.apache.coyote.ajp.AjpNioProtocol.class.getName().equals(protocol))
            || (apr && org.apache.coyote.ajp.AjpAprProtocol.class.getName().equals(protocol))) {
        if (apr) {
            return new org.apache.coyote.ajp.AjpAprProtocol();
        } else {
            return new org.apache.coyote.ajp.AjpNioProtocol();
        }
    } else {
        // Instantiate protocol handler
        Class<?> clazz = Class.forName(protocol);
        return (ProtocolHandler) clazz.getConstructor().newInstance();
    }
}

protocolHandler 依賴的 EndPoint:

public class Http11NioProtocol extends AbstractHttp11JsseProtocol<NioChannel> {

    private static final Log log = LogFactory.getLog(Http11NioProtocol.class);


    public Http11NioProtocol() {
        super(new NioEndpoint());
    }
}

3.3 聯結器的啟動

接下來,我們就看看聯結器的啟動:

// Connector 聯結器
protected void startInternal() throws LifecycleException {
    // 埠小於0 直接報錯
    if (getPortWithOffset() < 0) {
        throw new LifecycleException(sm.getString(
                "coyoteConnector.invalidPort", Integer.valueOf(getPortWithOffset())));
    }
    // 設定狀態啟動中
    setState(LifecycleState.STARTING);
    try {
        // 內部的 protocolHandler 啟動
        protocolHandler.start();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
    }
}

Http11NioProtocol 啟動,由於它間接繼承的是 AbstractProtocol,這裡則呼叫的是 AbstractProtocol 的啟動方法:

@Override
public void start() throws Exception {
    ...
    // 內部的 endpoint 啟動 這裡的話也就是 NioEndPoint
    endpoint.start();
    // endpoint 內部的建立固定間隔60秒的執行緒池,用來啟動非同步超時檢查
    monitorFuture = getUtilityExecutor().scheduleWithFixedDelay(
            new Runnable() {
                @Override
                public void run() {
                    if (!isPaused()) {
                        startAsyncTimeout();
                    }
                }
            }, 0, 60, TimeUnit.SECONDS);
}

可以看到主要就兩步,一個是將內部的 endpoint 啟動,並且會啟動一個間隔60s的任務,校驗超時。那我們繼續看看 endpoint 的啟動:

// AbstractEndpoint
// endpoint 的啟動並繫結
public final void start() throws Exception {
    // 第一次的 endpoint 還未繫結
    if (bindState == BindState.UNBOUND) {
        // 開始繫結
        bindWithCleanup();
        bindState = BindState.BOUND_ON_START;
    }
    startInternal();
}

可以看到主要做了兩件事情,一個是如果還未繫結的話會進行繫結(比如監聽哪個埠呀)然後呼叫模板方法啟動。那我們先看一下繫結方法都做了哪些事情:

// AbstractEndpoint 繫結
private void bindWithCleanup() throws Exception {
    try {
        // 抽象方法 呼叫子類具體實現
        bind();
    } catch (Throwable t) {
        // Ensure open sockets etc. are cleaned up if something goes
        // wrong during bind
        ExceptionUtils.handleThrowable(t);
        unbind();
        throw t;
    }
}
// NioEndpoint#bind 
/**
 * Initialize the endpoint.
 */
@Override
public void bind() throws Exception {
    // 初始化並繫結通道
    initServerSocket();
    // 停止的計數器用於關閉 poller 執行緒
    setStopLatch(new CountDownLatch(1));
    // Initialize SSL if needed
    initialiseSsl();
    // 初始化並開啟共享的 Selector
    selectorPool.open(getName());
}

哇,這裡其實就涉及到 NioSocket 的相關東西了,比如 Buffer、Channel、Selector,這個你要是一點兒都不知道的話,那看起來確實有點困難,可以參考我的這篇會講 Java裡邊的兩種 Socket哈,有個簡單的認識,再繼續往下看。

我們接著往裡看看初始化通道:

protected void initServerSocket() throws Exception {
    if (!getUseInheritedChannel()) {
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
        serverSock.socket().bind(addr,getAcceptCount());
    } else {
        // Retrieve the channel provided by the OS
        Channel ic = System.inheritedChannel();
        if (ic instanceof ServerSocketChannel) {
            serverSock = (ServerSocketChannel) ic;
        }
        if (serverSock == null) {
            throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
        }
    }
    serverSock.configureBlocking(true); //mimic APR behavior
}
  • getUseInheritedChannel() 方法預設為 false ,所以會走 if 分支。
  • 在上面的 if 分支裡, 會利用 java NIO 物件 ServerSocketChannel 建立 server socket ,繫結監聽地址和埠,設定 socket 的 backlog 以及其他屬性。
  • 最後呼叫 serverSock.configureBlocking(true) 來設定監聽的 socket 為阻塞 scoket ,即該 socket 在 accept 的時候,如果沒有連線就使當前執行緒阻塞。

繫結看的差不多了,我們繼續回到 endpoint的啟動,看看模板的方法 startInternal 都做了哪些事情:

/**
 * Start the NIO endpoint, creating acceptor, poller threads.
 */
@Override
public void startInternal() throws Exception {
    if (!running) {
        running = true;
        paused = false;
        if (socketProperties.getProcessorCache() != 0) {
            processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getProcessorCache());
        }
        if (socketProperties.getEventCache() != 0) {
            eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getEventCache());
        }
        if (socketProperties.getBufferPool() != 0) {
            nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
                    socketProperties.getBufferPool());
        }
        // Create worker collection  初始化工作執行緒池
        if (getExecutor() == null) {
            createExecutor();
        }
        // 初始化控制連線數
        initializeConnectionLatch();
        // Start poller thread 又來了一個 poller
        poller = new Poller();
        Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
        pollerThread.setPriority(threadPriority);
        pollerThread.setDaemon(true);
        pollerThread.start();
        // 啟動 accept 用來接收請求
        startAcceptorThread();
    }
}

兄弟們我發現這傢伙不是一篇兩篇就能寫清楚的,這東西太多了,越往裡看東西越多,還涉及到 Tomcat 的好幾種執行緒,你看我們看到這裡我就發現了兩種 poller執行緒、還有這裡的 accept 用於處理接收請求的執行緒,我這裡就不往裡深究了。分析一個大佬的一套文章,會詳細講解 Tomcat NIO的處理,大家想詳細瞭解的話,可以看看。它是一套文章,可以從第一章開始看哈。

我這裡就小小的引用一下:對於 tomcat NIO 來說,是由一系列框架類和資料讀寫類來組成的,同時這些類執行在不同的執行緒中,共同維持整個 tomcat NIO 架構。包括原始 socket 監聽的acceptor 執行緒,監測註冊在原始 scoket 上的事件是否發生的 poller thread 事件執行緒,進行資料讀寫和執行 servlet API 的 tomcat io 執行緒。當資料需要多次讀寫的時候,監測註冊在原始 scoket 上的讀寫事件的 block poller 事件執行緒。這些類和執行緒共同組成的 tomcat NIO 整體結構如下所示:

上面我們可以發現整體架構執行著4種執行緒:

  • Acceptor 執行緒
  • Poller 執行緒
  • Tomcat IO 執行緒
  • BlockPoller 執行緒

Acceptor執行緒

  • tomcat NIO 架構中會有一個 acceptor 執行緒,這個執行緒主要監聽埠。當有請求過來的時候,完成 tcp 三次握手,將 accept 過來的 socket 註冊OP_REGISTER 事件,並將該事件提交到 Poller 執行緒的事件佇列 PollerEventQueue中 。

Poller執行緒

  • 在 tomcat NIO 架構中會有 poller 執行緒,在 tomcat8 及以前的版本之中,可以透過 pollerThreadCount 配置 poller thread 的數目,但是在 tomcat 9.0.21 中 poller thread 數目始終會為 1。
  • poller thread 對於每一個 poller 例項都有一個 NIO selector 例項,同時也有一個事件佇列SynchronizedQueue<PollerEvent>。
  • 對於每一個 poller thread 來說,都會去輪詢佇列 SynchronizedQueue<PollerEvent>,該佇列的事件由 acceptor 執行緒(監聽到新連線時)或者 tomcat io 執行緒(處理完請求之後保持長連線,新增讀事件)放入,然後根據不同的事件對原始socket註冊相應的讀寫事件。
  • 每一個poller thread 來說,會呼叫 java NIO 物件 selector,發起系統呼叫,來監測原始 scoket 是否有讀寫事件發生,如果有則將原始 scoket 的封裝物件交由 tomcat io 執行緒處理。

Tomcat IO執行緒

  • tomcat io 執行緒是一個執行緒池,執行緒池大小可由 tomcat 相關引數配置。
  • tomcat io 執行緒會接受 poller thread 傳入的 scoket 封裝物件後,依次呼叫 SocketProcessor/ConnectionHanlder(global instance)/Http11Processor/CoyoteAdapter,最後交由 servlet container 完成servlet API 的呼叫。
  • 對於request header 的解析,request body 的獲取,servlet API 的呼叫,response 資料的寫入傳送,這一系列過程都是在 tomcat io 執行緒中完成的。
  • 對於request header,request body 有時候需要多次讀取操作,這個時候 tomcat io 執行緒會將原始 socket 再次進行讀事件的註冊,並交由 BlockPoller 執行緒處理,然後在 readLatch (CountDownLatch) 物件上等待。
  • 對於 response data 有時候不可寫,比如原始 scoket 的傳送緩衝區滿了,這個時候 tomcat io 執行緒同樣會將原始 socket 再次註冊寫事件,並交由 BlockPoller 執行緒處理,然後在 writeLatch 物件上等待。

BlockPoller執行緒

  • tomcat NIO 架構中會有 block poller 執行緒,其核心功能由 BlockPoller 類來實現,BlockPoller 例項會有一個 NIO selector 例項,同時也會擁有一個自己的事件佇列例項SynchronizedQueue<Runnable>。
  • 對於BlockPoller thread來說, 會去輪詢佇列 SynchronizedQueue<Runnable>,該佇列的物件(RunnableAdd型別)由 tomcat io 執行緒放入,然後根據不同的物件對原始 socket 註冊相應的讀寫事件。
  • 對於BlockPoller thread來說, 會呼叫 java NIO 物件 selector,發起系統呼叫,來監測原始 scoket 是否有讀寫事件發生。如果有,則將 readLatch 或者 writeLatch 物件上等待的 tomcat io 執行緒喚醒,然後 tomcat io 執行緒繼續完成讀寫操作。

它的啟動,我們就暫時看到這裡,是不是有點枯燥,我們就真實的 debug 看一下一次請求的處理。

4 請求過程

我們傳送請求,首先就是連線的建立。那我們直接在它這個 accpet 執行緒裡邊打個斷點:

當我傳送一個請求後,這就是我們請求的 socket :

endpoint.serverSocketAccept() 方法來獲取已經完成 tcp 三次握手的socket 連線,如果沒有發生異常,並且 endpoint 正在 running 狀態,同時也沒有暫停,那麼該邏輯就會把針對新進入連線所建立的原始 java socket 物件交由 endpoint.setSocketOptions(socket) 方法去處理。

上面我們也看到socket 初始化過程,server 端的監聽 socket 是被設定為阻塞 socket ,所以endpoint.serverSocketAccept() 方法在沒有可用連線的時候 acceptor 執行緒是阻塞的。

  • 上述邏輯會在 setSocketOptions() 方法裡進行構造 SocketBufferHandler 物件,主要設定讀寫 buffer 大小,以及是否使用 DirectBuffer (預設使用)。
  • 構造 NioChannel 物件,該物件封裝了基於原始 socket 去進行構造的 SocketBufferHandler 物件。
  • 構造 NioSocketWrapper 物件,該物件封裝了上面構造的 NioChannel 物件和當前 NioEndpoint 物件。
  • 呼叫 poller 的 register(channel, socketWrapper) 方法將上面構造的 NioChannel 物件和 NioSocketWrapper 物件註冊到 poller 執行緒的事件佇列裡。
  • 由poller 物件的 register() 方法分析可知,會註冊 OP_REGISTER 型別的PollerEvent 到 poller 物件的事件佇列裡。

5 小結

鐵子們,暫時看到這裡哈。

相關文章