Tomcat 第五篇:請求處理流程(下)

1. 請求處理流程 AprEndPoint
順著上一篇接著聊,當一個請求發(fā)送到 Tomcat 以后,會由連接器 Connector 轉(zhuǎn)送至 AprEndPoint ,在 AprEndPoint 中調(diào)用了 startInternal() 方法,這個方法總共做了做了四件事兒:
LimitLatch 限制連接次數(shù)。 創(chuàng)建了 poller 線程。 創(chuàng)建了 sendfile 線程。 創(chuàng)建了 acceptor 。
其中, poller 、 sendfile 、 acceptor 都是 AprEndPoint 的內(nèi)部類,因為他們的父類都實現(xiàn)了 Runnable ,所以核心邏輯都在他們自己的 run() 方法中。
其中的涉及到的源代碼太多了,我就是懶得往出列了,所以畫了下面這個圖給各位做個示意。

LimitLatch是連接控制器,它負責控制最大連接數(shù)。Acceptor跑在一個單獨的線程中,它在一個死循環(huán)里面通過調(diào)用accept()方法來接收新連接,會返回一個 long 類型的socket,然后將這個socket封裝成AprSocketWrapper對象。Poller本身也跑在一個單獨的線程中,它早內(nèi)部維護了一個SocketList對象,這個對象中含有socket數(shù)組,它在一個死循環(huán)里不斷檢測socket的數(shù)據(jù)就緒狀態(tài),一旦有socket可讀,就生成一個SocketProcessor任務對象扔給Executor去處理。Executor就是一個線程池,負責運行SocketProcessor任務類,SocketProcessor的run()方法會調(diào)用Http11Processor來讀取和解析請求數(shù)據(jù)。
肯能有的朋友看完了,都不知道 AprEndPoint 或者說 Apr 這種連接模式是什么。
稍微做下簡介:
APR(Apache Portable Runtime Libraries)是 Apache 可移植運行時庫,它是用 C 語言實現(xiàn)的,其目的是向上層應用程序提供一個跨平臺的操作系統(tǒng)接口庫。Tomcat 可以用它來處理包括文件和網(wǎng)絡 I/O,從而提升性能。
在 Tomcat8.5.x 中,默認的 I/O 模式使用的是 NIO ,使用的鏈接器是 org.apache.coyote.http11.Http11NioProtocol ,當然,由于是默認的,無需顯示配置,在 server.xml 中只需要這么寫就可以了:
<Connector?port="8080"?protocol="HTTP/1.1"
????????????connectionTimeout="20000"
????????????redirectPort="8443"?/>
但是如果要換成 APR ,就需要這么寫了:
<Connector?port="8443"?protocol="org.apache.coyote.http11.Http11AprProtocol"
????????????maxThreads="150"?SSLEnabled="true"?>
????<UpgradeProtocol?className="org.apache.coyote.http2.Http2Protocol"?/>
????<SSLHostConfig>
????????<Certificate?certificateKeyFile="conf/localhost-rsa-key.pem"
????????????????????????certificateFile="conf/localhost-rsa-cert.pem"
????????????????????????certificateChainFile="conf/localhost-rsa-chain.pem"
????????????????????????type="RSA"?/>
????SSLHostConfig>
Connector>
接下來聊一個拷問靈魂的問題, 「APR 是如何提升性能的?」
跟 NioEndpoint 一樣, AprEndpoint 也實現(xiàn)了非阻塞 I/O,它們的區(qū)別是:NioEndpoint 通過調(diào)用 Java 的 NIO API 來實現(xiàn)非阻塞 I/O,而 AprEndpoint 是通過 JNI 調(diào)用 APR 本地庫而實現(xiàn)非阻塞 I/O 的。

Tomcat 的 Endpoint 組件在接收網(wǎng)絡數(shù)據(jù)時需要預先分配好一塊 Buffer,所謂的 Buffer 就是字節(jié)數(shù)組 byte[] ,Java 通過 JNI 調(diào)用把這塊 Buffer 的地址傳給 C 代碼,C 代碼通過操作系統(tǒng) API 讀取 Socket 并把數(shù)據(jù)填充到這塊 Buffer。
Java NIO API 提供了兩種 Buffer 來接收數(shù)據(jù):HeapByteBuffer 和 DirectByteBuffer 。
HeapByteBuffer 對象本身在 JVM 堆上分配,并且它持有的字節(jié)數(shù)組 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 來接收網(wǎng)絡數(shù)據(jù),需要把數(shù)據(jù)從內(nèi)核先拷貝到一個臨時的本地內(nèi)存,再從臨時本地內(nèi)存拷貝到 JVM 堆,而不是直接從內(nèi)核拷貝到 JVM 堆上。
數(shù)據(jù)從內(nèi)核拷貝到 JVM 堆的過程中,JVM 可能會發(fā)生 GC , GC 過程中對象可能會被移動,也就是說 JVM 堆上的字節(jié)數(shù)組可能會被移動,這樣的話 Buffer 地址就失效了。如果這中間經(jīng)過本地內(nèi)存中轉(zhuǎn),從本地內(nèi)存到 JVM 堆的拷貝過程中 JVM 可以保證不做 GC。

Tomcat 的 AprEndpoint 通過操作系統(tǒng)層面的 sendfile 特性解決了這個問題,sendfile 系統(tǒng)調(diào)用方式非常簡潔。

2. 請求處理流程 NioEndPoint
前面介紹了 AprEndpoint 的請求處理流程,我們在順便看下 Tomcat 默認的 NioEndPoint 處理流程。
實際上這兩個處理流程非常的相似,區(qū)別基本上是因為非阻塞 I/O 的實現(xiàn)方式。

在 Acceptor中的accept()方法返回一個Channel對象,接著把Channel對象交給Poller去處理。Poller在內(nèi)部維護一個Channel數(shù)組,它在一個死循環(huán)里不斷檢測Channel的數(shù)據(jù)就緒狀態(tài),一旦有Channel可讀,就生成一個SocketProcessor任務對象扔給Executor去處理。每個Poller線程都有自己的Queue。每個Poller線程可能同時被多個Acceptor線程調(diào)用來注冊PollerEvent。Poller不斷的通過內(nèi)部的Selector對象向內(nèi)核查詢Channel的狀態(tài),一旦可讀就生成任務類SocketProcessor交給Executor去處理。Poller的另一個重要任務是循環(huán)遍歷檢查自己所管理的SocketChannel是否已經(jīng)超時,如果有超時就關(guān)閉這個SocketChannel。Executor是線程池,負責運行SocketProcessor任務類,SocketProcessor的run()方法會調(diào)用Http11Processor來讀取和解析請求數(shù)據(jù)。ServerSocketChannel通過accept()接受新的連接,accept()方法返回獲得SocketChannel對象,然后將SocketChannel對象封裝在一個PollerEvent對象中,并將PollerEvent對象壓入Poller的Queue里,這是個典型的生產(chǎn)者 - 消費者模式,Acceptor與Poller線程之間通過Queue通信。
參考
https://jonhuster.blog.csdn.net/article/details/93297251

