Tomcat原始碼解析-整體流程介紹
一、架構
下面談談我對Tomcat架構的理解
總體架構:
1、面向元件架構
2、基於JMX
3、事件偵聽
1)面向元件架構
tomcat程式碼看似很龐大,但從結構上看卻很清晰和簡單,它主要由一堆元件組成,如Server、Service、Connector等,並基於JMX管理這些元件,另外實現以上介面的元件也實現了代表生存期的介面Lifecycle,使其元件履行固定的生存期,在其整個生存期的過程中通過事件偵聽LifecycleEvent實現擴充套件。Tomcat的核心類圖如下所示:
1、Catalina:與開始/關閉shell指令碼互動的主類,因此如果要研究啟動和關閉的過程,就從這個類開始看起。
2、Server:是整個Tomcat元件的容器,包含一個或多個Service。
3、Service:Service是包含Connector和Container的集合,Service用適當的Connector接收使用者的請求,再發給相應的Container來處理。
4、Connector:實現某一協議的聯結器,如預設的有實現HTTP、HTTPS、AJP協議的。
5、Container:可以理解為處理某型別請求的容器,處理的方式一般為把處理請求的處理器包裝為Valve物件,並按一定順序放入型別為Pipeline的管道里。Container有多種子型別:Engine、Host、Context和Wrapper,這幾種子型別Container依次包含,處理不同粒度的請求。另外Container裡包含一些基礎服務,如Loader、Manager和Realm。
6、Engine:Engine包含Host和Context,接到請求後仍給相應的Host在相應的Context裡處理。
7、Host:就是我們所理解的虛擬主機。
8、Context:就是我們所部屬的具體Web應用的上下文,每個請求都在是相應的上下文裡處理的
9、Wrapper:Wrapper是針對每個Servlet的Container,每個Servlet都有相應的Wrapper來管理。
可以看出Server、Service、Connector、Container、Engine、Host、Context和Wrapper這些核心元件的作用範圍是逐層遞減,並逐層包含。
下面就是些被Container所用的基礎元件:
1、Loader:是被Container用來載入各種所需的Class。
2、Manager:是被Container用來管理Session池。
3、Realm:是用來處理安全裡授權與認證。
分析完核心類後,再看看Tomcat啟動的過程,Tomcat啟動的時序圖如下所示:
從上圖可以看出,Tomcat啟動分為init和start兩個過程,核心元件都實現了Lifecycle介面,都需實現start方法,因此在start過程中就是從Server開始逐層呼叫子元件的start過程。
2)基於JMX
Tomcat會為每個元件進行註冊過程,通過Registry管理起來,而Registry是基於JMX來實現的,因此在看元件的init和start過程實際上就是初始化MBean和觸發MBean的start方法,會大量看到形如:
Registry.getRegistry(null, null).invoke(mbeans, "init", false); Registry.getRegistry(null, null).invoke(mbeans, "start", false); |
這樣的程式碼,這實際上就是通過JMX管理各種元件的行為和生命期。
不熟悉JMX可見:
3)事件偵聽
各個元件在其生命期中會有各種各樣行為,而這些行為都有觸發相應的事件,Tomcat就是通過偵聽這些時間達到對這些行為進行擴充套件的目的。在看元件的init和start過程中會看到大量如:
lifecycle.fireLifecycleEvent(AFTER_START_EVENT, null); |
這樣的程式碼,這就是對某一型別事件的觸發,如果你想在其中加入自己的行為,就只用註冊相應型別的事件即可。
二、一次完整請求的裡裡外外
前幾天分析了一下Tomcat的架構和啟動過程,今天開始研究它的運轉機制。Tomcat最本質就是個能執行JSP/Servlet的Web伺服器 ,因此最典型的應用就是使用者通過瀏覽器訪問伺服器,Tomcat接收到請求後轉發給Servlet,由Servlet處理完後,把結果返回給客戶端。今天就專門解析一下這麼一個完整的請求的內部機理。
通過DEBUG,一路跟下來,發現Tomcat處理請求的核心過程是以下幾點:
1、啟動的時候啟動預支援協議的Endpoint,Endpoint會起專門的執行緒監聽相應協議的請求,預設的情況下,會啟動JIoEndpoint,JIoEndpoint基於Java ServerSocket接收Http的請求
2、ServerSocket接收到客戶端請求的Socket後,一路包裝,並一路從Host一直傳遞到Wrapper,再請求到相應的Servlet
下面將重點解析以上兩個過程。
通過以前的分析(Tomcat原始碼分析一)可知道當Tomcat啟動的時候會啟動Connector,此時Connector會通過ProtocolHandler把Endpoint啟動起來。預設情況下,Tomcat會啟動兩種Connector,分別是Http協議和AJP協議的,依次對應Http11Protocol和AjpProtocol,兩者都是啟動JIoEndpoint。下面看看JIoEndpoint的start方法:
public void start() throws Exception {
// Initialize socket if not done before
if (!initialized) {
init();
}
if (!running) {
running = true;
paused = false;
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
// Start acceptor threads
for (int i = 0; i < acceptorThreadCount; i++) {
Thread acceptorThread = new Thread(new Acceptor(), getName() + "-Acceptor-" + i);
acceptorThread.setPriority(threadPriority);
acceptorThread.setDaemon(getDaemon());
acceptorThread.start();
}
}
}
|
以上程式碼很清晰地表示啟動acceptorThreadCount個執行緒,每個執行緒由Acceptor代理,具體看看Acceptor的run方法:
public void run() { // Loop until we receive a shutdown command while (running) { // Loop if endpoint is paused while (paused) { try { Thread.sleep(1000); } catch (InterruptedException e) { // Ignore } } // Accept the next incoming connection from the server socket try { Socket socket = serverSocketFactory.acceptSocket(serverSocket); serverSocketFactory.initSocket(socket); // Hand this socket off to an appropriate processor //處理socket
if (!processSocket(socket)) {
// Close socket right away
try {
socket.close();
} catch (IOException e) {
// Ignore
}
}
}catch ( IOException x ) {
if ( running ) log.error(sm.getString("endpoint.accept.fail"), x);
} catch (Throwable t) {
log.error(sm.getString("endpoint.accept.fail"), t);
}
// The processor will recycle itself when it finishes
}
}
|
由此可得到這麼一個結論:Tomcat就是通過ServerSocket監聽Socket的方式來接收客戶端請求的。具體程式碼就無需我解析了,稍微瞭解Java net的人都能看懂以上程式碼,Tomcat就是用最標準和最基礎的Socket呼叫方法來處理網路請求的。找到處理請求的源頭後下面要做的是事情就簡單了,打好斷點,在瀏覽器裡請求一個最簡單的Hello world,一路debug下去。一路跟下來,主流程的時序圖如下所示:
從上圖可知,以上過程可分解成以下三個最主要的核心點:
1、基於Http1.1協議對Socket的解析和包裝
2、StandardEngineValve、StandardHostValve、StandardContextValve和StandardWrapperValve四種Valve的一路inoke。四種不同層次的Valve做了不同層次的處理和封裝
3、基於責任鏈模式ApplicationFilterChain實現Filter攔截和實際Servlet的請求
以上三個核心點都是內容非常豐富的可研究點,會在以後幾天逐一進行剖析。
三、可攜帶狀態的執行緒池
最近想實現一個可攜帶狀態的執行緒池,具體需求就是池中的執行緒被用來處理某種資訊,而此資訊可視為執行緒所依賴的外部狀態。如果用簡單的執行緒池來實現,執行緒初始化時就得賦予某些資訊,使得執行緒無法被再次利用。在看老版Tomcat的原始碼時,找到了答案,其實現思路主要是利用了執行緒的等待和喚起,HttpProcessor的實現正好基於此思路,時序圖如下所示:
初始化HttpProcessor執行緒時,沒法賦予所需的Socket物件,因為如果在初始化階段就賦予Socket會導致此執行緒沒法回收用來處理其他Socket。因此,在HttpProcessor的run階段,先把執行緒給wait住,具體在await方法裡體現,程式碼如下所示:
/**
* Await a newly assigned Socket from our Connector, or
|
當HttpConnector呼叫HttpProcessor.assign(socket)方法時,會給此執行緒賦予Socket物件,並喚起此執行緒,使其繼續執行,assign方法的原始碼如下所示:
/**
* Process an incoming TCP/IP connection on the specified socket. Any
* exception that occurs during processing must be logged and swallowed.
* NOTE: This method is called from our Connector's thread. We
* must assign it to our own thread so that multiple simultaneous
* requests can be handled.
*
* @param socket TCP socket to process
*/
synchronized void assign(Socket socket) {
// Wait for the Processor to get the previous Socket
while (available) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Store the newly available Socket and notify our thread
this.socket = socket;
available = true;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" An incoming request is being assigned");
}
|
執行緒被喚起和賦予socket物件後,繼續執行核心的process方法,HttpProcessor.run的完整原始碼如下所示:
/**
* The background thread that listens for incoming TCP/IP connections and
* hands them off to an appropriate processor.
*/
public void run() {
// Process requests until we receive a shutdown signal
while (!stopped) {
// Wait for the next socket to be assigned
Socket socket = await();
if (socket == null)
continue;
// Process the request from this socket
try {
process(socket);
} catch (Throwable t) {
log("process.invoke", t);
}
// Finish up this request
connector.recycle(this);
}
// Tell threadStop() we have shut ourselves down successfully
synchronized (threadSync) {
threadSync.notifyAll();
}
}
|
四、Request和Response處理的全過程
從Tomcat原始碼分析(二)可知,使用者的一個請求會經過n個環節的處理,最後到達開發人員寫的Servlet,傳給Servlet也就是HttpServletRequest和HttpServletResponse,因此可以認為這一路走下來無非就是把最原始的Socket包裝成Servlet裡用到的HttpServletRequest和HttpServletResponse,只不過每個環節完成的包裝功能和部分不一樣而已,資訊流如下圖所示:
其中,Request與Response的類圖如下所示:
org.apache.coyote.Request和org.apache.coyote.Response是Tomcat內部使用的,不提供給開發者呼叫,類是final型別的。下面結合一次完整請求的時序圖來看看從Socket到org.apache.catalina.connector.Request的加工過程:
由上圖可見,Request的解析和加工過程不是在一個方法裡搞定,而是資訊流動過程中逐步解析的,不同層次的處理器解析不同層次的資訊,在解析過程同時做了些判斷和攔截的工作,比如當發現是要訪問WEB-INF的資源,會直接返回錯誤給客戶端等等。
Note:僅當做參考,記錄在此