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 小結
鐵子們,暫時看到這裡哈。