Tomcat 第五篇:請求處理流程(下)
阿新 • • 發佈:2020-10-08
![](https://cdn.geekdigging.com/java/tomcat/tomcat_header.jpg)
## 1. 請求處理流程 AprEndPoint
順著上一篇接著聊,當一個請求傳送到 Tomcat 以後,會由聯結器 `Connector` 轉送至 `AprEndPoint` ,在 `AprEndPoint` 中呼叫了 `startInternal()` 方法,這個方法總共做了做了四件事兒:
- LimitLatch 限制連線次數。
- 建立了 poller 執行緒。
- 建立了 sendfile 執行緒。
- 建立了 acceptor 。
其中, `poller` 、 `sendfile` 、 `acceptor` 都是 `AprEndPoint` 的內部類,因為他們的父類都實現了 `Runnable` ,所以核心邏輯都在他們自己的 `run()` 方法中。
其中的涉及到的原始碼太多了,我就是懶得往出列了,所以畫了下面這個圖給各位做個示意。
![](https://cdn.geekdigging.com/java/tomcat/tomcat5/AprEndPoint.png)
- `LimitLatch` 是連線控制器,它負責控制最大連線數。
- `Acceptor` 跑在一個單獨的執行緒中,它在一個死迴圈裡面通過呼叫 `accept()` 方法來接收新連線,會返回一個 long 型別的 `socket` ,然後將這個 `socket` 封裝成 `AprSocketWrapper` 物件。
- `Poller` 本身也跑在一個單獨的執行緒中,它早內部維護了一個 `SocketList` 物件,這個物件中含有 `socket` 陣列,它在一個死迴圈裡不斷檢測 `socket` 的資料就緒狀態,一旦有 `socket` 可讀,就生成一個 `SocketProcessor` 任務物件扔給 `Executor` 去處理。
- `Executor` 就是一個執行緒池,負責執行 `SocketProcessor` 任務類, `SocketProcessor` 的 `run()` 方法會呼叫 `Http11Processor` 來讀取和解析請求資料。
肯能有的朋友看完了,都不知道 `AprEndPoint` 或者說 `Apr` 這種連線模式是什麼。
稍微做下簡介:
APR(Apache Portable Runtime Libraries)是 Apache 可移植執行時庫,它是用 C 語言實現的,其目的是向上層應用程式提供一個跨平臺的作業系統介面庫。Tomcat 可以用它來處理包括檔案和網路 I/O,從而提升效能。
在 Tomcat8.5.x 中,預設的 I/O 模式使用的是 NIO ,使用的連結器是 `org.apache.coyote.http11.Http11NioProtocol` ,當然,由於是預設的,無需顯示配置,在 `server.xml` 中只需要這麼寫就可以了:
```xml
```
但是如果要換成 APR ,就需要這麼寫了:
```xml
```
接下來聊一個拷問靈魂的問題, **APR 是如何提升效能的?**
跟 `NioEndpoint` 一樣, `AprEndpoint` 也實現了非阻塞 I/`O,它們的區別是:NioEndpoint` 通過呼叫 Java 的 NIO API 來實現非阻塞 I/O,而 `AprEndpoint` 是通過 JNI 呼叫 APR 本地庫而實現非阻塞 I/O 的。
![](https://cdn.geekdigging.com/java/tomcat/tomcat5/sendfile.png)
Tomcat 的 Endpoint 元件在接收網路資料時需要預先分配好一塊 Buffer,所謂的 Buffer 就是位元組陣列 byte[] ,Java 通過 JNI 呼叫把這塊 Buffer 的地址傳給 C 程式碼,C 程式碼通過作業系統 API 讀取 Socket 並把資料填充到這塊 Buffer。
Java NIO API 提供了兩種 Buffer 來接收資料: HeapByteBuffer 和 DirectByteBuffer 。
HeapByteBuffer 物件本身在 JVM 堆上分配,並且它持有的位元組陣列 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 來接收網路資料,需要把資料從核心先拷貝到一個臨時的本地記憶體,再從臨時本地記憶體拷貝到 JVM 堆,而不是直接從核心拷貝到 JVM 堆上。
資料從核心拷貝到 JVM 堆的過程中,JVM 可能會發生 GC , GC 過程中物件可能會被移動,也就是說 JVM 堆上的位元組陣列可能會被移動,這樣的話 Buffer 地址就失效了。如果這中間經過本地記憶體中轉,從本地記憶體到 JVM 堆的拷貝過程中 JVM 可以保證不做 GC。
![](https://cdn.geekdigging.com/java/tomcat/tomcat5/sendfile_1.jpg)
Tomcat 的 AprEndpoint 通過作業系統層面的 sendfile 特性解決了這個問題,sendfile 系統呼叫方式非常簡潔。
![](https://cdn.geekdigging.com/java/tomcat/tomcat5/sendfile_2.jpg)
## 2. 請求處理流程 NioEndPoint
前面介紹了 `AprEndpoint` 的請求處理流程,我們在順便看下 Tomcat 預設的 `NioEndPoint` 處理流程。
實際上這兩個處理流程非常的相似,區別基本上是因為非阻塞 I/O 的實現方式。
![](https://cdn.geekdigging.com/java/tomcat/tomcat5/NioEndpoint.png)
- 在 `Acceptor` 中的 `accept()` 方法返回一個 `Channel` 物件,接著把 `Channel` 物件交給 `Poller` 去處理。
- `Poller` 在內部維護一個 `Channel` 陣列,它在一個死迴圈裡不斷檢測 `Channel` 的資料就緒狀態,一旦有 `Channel` 可讀,就生成一個 `SocketProcessor` 任務物件扔給 `Executor` 去處理。每個 `Poller` 執行緒都有自己的 `Queue` 。每個 `Poller` 執行緒可能同時被多個 `Acceptor` 執行緒呼叫來註冊 `PollerEvent` 。 `Poller` 不斷的通過內部的 `Selector` 物件向核心查詢 `Channel` 的狀態,一旦可讀就生成任務類 `SocketProcessor` 交給 `Executor` 去處理。 `Poller` 的另一個重要任務是迴圈遍歷檢查自己所管理的 `SocketChannel` 是否已經超時,如果有超時就關閉這個 `SocketChannel` 。
- `Executor` 是執行緒池,負責執行 `SocketProcessor` 任務類, `SocketProcessor` 的 `run()` 方法會呼叫 `Http11Processor` 來讀取和解析請求資料。 `ServerSocketChannel` 通過 `accept()` 接受新的連線, `accept()` 方法返回獲得 `SocketChannel` 物件,然後將 `SocketChannel` 物件封裝在一個 `PollerEvent` 物件中,並將 `PollerEvent` 物件壓入 `Poller` 的 `Queue` 裡,這是個典型的生產者 - 消費者模式, `Acceptor` 與 `Poller` 執行緒之間通過 `Queue` 通訊。
## 參考
https://jonhuster.blog.csdn.net/article/details/