這一節你們能夠學到不少東西,不過若是不懂的話可能會很困惑。html
本節對應《深刻剖析 Tomcat》第四章:Tomcat 默認鏈接器。java
你們必定要先去下載 Tomcat4 相關代碼,不然確定會不知所云。 代碼能夠在個人github上下載git
在第 3 節中的鏈接器已經能較好的運行,既使用了線程啓動,也完成了對大部分請求解析,可是仍而後不少不足。github
一個 Tomcat 鏈接器必須符合如下條件:web
能夠看到,上節的簡單鏈接器基本的條件都沒有實現。apache
Tomcat4 的默認鏈接器相似於上節的簡單鏈接器。它等待前來的 HTTP 請求,建立 request和 response 對象,而後把 request 和 response 對象傳遞給容器(上節只是交給響應的處理器Processor處理)。鏈接器是經過調用接口org.apache.catalina.Container 的 invoke 方法來傳遞 request 和 response 對象的。服務器
寫了一半才發現要寫徹底部知識點須要用灰常灰常大的篇幅,因此有些內容留到下一節再講.eclipse
首先須要牢記鏈接器Connector的功能是什麼:建立ServerSocket,等待請求,解析請求並傳遞給容器(由容器處理請求)。socket
這一節講解:建立ServerSocket和等待請求部分,其中重點提到經過池實現多個請求的處理,下一節的補充內容爲解析請求部分和其餘一些瑣碎的問題。學習
整體來講,處理用戶請求有如下幾個步驟:
核心類:
Bootstrap.java做爲啓動類
爲何是先從啓動類開始講呢?由於能夠經過啓動過程比較完整的分析代碼。
對於什麼是SimpleContainer,爲何一個鏈接器要setContainer(container)將指定容器設置到鏈接器中,你們能夠先無論,在下一節對描述處理請求過程會用到Container,因此下一節還會再回頭看這個類。 還有須要先提早注意一點,這裏的HttpConnector已是Tomcat4中核心包中的代碼,再也不是屬於ex**中的類,有點高級,哈哈。
package ex04.pyrmont.startup; import ex04.pyrmont.core.SimpleContainer; import org.apache.catalina.connector.http.HttpConnector; public final class Bootstrap { public static void main(String[] args) { HttpConnector connector = new HttpConnector(); SimpleContainer container = new SimpleContainer(); connector.setContainer(container); try { connector.initialize(); connector.start(); System.in.read(); } catch (Exception e) { e.printStackTrace(); } } }
接下來你們看看HttpConnector.java,能夠發現實現除了Connector接口外,還實現了Runnable接口和Lifecycle接口。
爲何要實現Runnable接口,由於它的實例能夠運行在本身的線程上,也就是須要啓動線程。
Lifecycle 將之後解釋,如今你不須要擔憂它,只要明白經過實現 Lifecycle,在你建立 HttpConnector 實例以後,你應該調用它的 initialize 和 start 方法。這兩個方法在組件的 生命週期裏必須只調用一次。
功能:完成ServerSocket建立
對應connector.initialize()
能夠看到HttpConnector.java的initialize方法的做用是完成服務器端的ServerSocket的建立,並賦值給HttpConnector的實例變量serversocket中,裏面調用了這個類的私有方法open();
serverSocket = open();
看看open(),open()方法負責建立SeverSocket這個方法有下面這些關鍵語句
private ServerSocket open(){ ServerSocketFactory factory = getFactory(); try { return (factory.createSocket(port, acceptCount, is)); } catch (BindException be) { throw new BindException(be.getMessage() + ":" + address + ":" + port); } }
getFactory()是用單例模式返回具體工廠,即代碼中的DefaultServerSocketFactory實例。DefaultServerSocketFactory對象負責建立ServerSocket對象,也就factory.createSocket(...)。
public ServerSocketFactory getFactory() { if (this.factory == null) { synchronized (this) { this.factory = new DefaultServerSocketFactory(); } } return (this.factory); }
經過對比前一節或者ex03包下的HttpConnector類,能夠發現一個簡單的建立ServerSocket對象變得複雜許多,惟一不變的就是建立的地點都是在HttpConnector.java中,只是再也不是在啓動HttpConnector線程時建立,而是在以前建立,不依賴於HttpConnector線程的啓動。
下面是上一節建立 ServerSocket對象的方式。
ServerSocket serverSocket = null; int port = 8080; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); }
對應 connector.start();
下面是這個方法關鍵代碼。
public void start(){ threadStart();//啓動HttpConnector線程 while (curProcessors < minProcessors) { if ((maxProcessors > 0) && (curProcessors >= maxProcessors)) break; HttpProcessor processor = newProcessor();//建立HttpProcessor池 recycle(processor); } }
啓動 HttpConnector 線程 threadStart() 放在本小節後半部分說,先介紹處理器池。
HttpConnector 維護一個 HttpProcessor 的實例池,從而避免每次建立 HttpProcessor 實例。
這些 HttpProcessor 對象是存放在 HttpConnector.java 中一個叫 processors 實例的 java.io.Stack 中:
private Stack processors = new Stack();
池的實現通常都是享元模式和單例模式的綜合使用哦。
啓動鏈接器歸納1:啓動 HttpConnector 線程,每一趟 while 循環 new 出 HttpProcessor 對象,並調用 recyle 方法將對象放入表明池的 processors 棧中。
void recycle(HttpProcessor processor) { processors.push(processor);//將建立的實例壓入棧中 }
如何建立 HttpProcessor 實例的: newProcessor() 方法
這個方法獲取到的信息是:建立出一個 HttpProcessor 對象後就當即啓動這個線程
啓動鏈接器歸納2:啓動 HttpConnector 線程,一個 while 循環事後,建立了一些 HttpProcessor 實例,而後啓動它們的線程,最後經過 recyle() 方法放入 processors 實例中
啓動 Processor 線程後發生了什麼,稍後再說。
private HttpProcessor newProcessor() { //建立實例 HttpProcessor processor = new HttpProcessor(this, curProcessors++); if (processor instanceof Lifecycle) { try { ((Lifecycle) processor).start();//啓動處理器線程 } catch (LifecycleException e) { log("newProcessor", e); return (null); } } created.addElement(processor); return (processor); }
看看 HttpProcesor.jav a的構造方法
構造方法中,從 connector 中獲取端口,包括代理端口
同時建立HttpRequestImpl 和 HttpResponseImpl 實例,以便於請求到來時不用再建立這些對象,只須要再放入相應的請求信息就行。
同時 HttpConnector.java 經過建立處理器實例,在這個過程綁定了請求和 connector 之間的關係,即 request.setConnector(this);
好了,如今對啓動鏈接器過程發生了什麼能夠歸納爲:
啓動鏈接器歸納3:啓動 HttpConnector 線程,一個 while 循環事後,建立了一些 HttpProcessor 實例;建立每個 HttpProcessor 實例過程當中,都會建立請求和響應對象,避免請求到來時再建立,並綁定鏈接器與請求之間的關係;而後啓動處理器線程,最後經過 recyle() 方法放入 processors 實例中。
public HttpProcessor(HttpConnector connector, int id) { super(); this.connector = connector; this.debug = connector.getDebug(); this.id = id; this.proxyName = connector.getProxyName(); this.proxyPort = connector.getProxyPort(); this.request = (HttpRequestImpl) connector.createRequest(); this.response = (HttpResponseImpl) connector.createResponse(); this.serverPort = connector.getPort(); this.threadName = "HttpProcessor[" + connector.getPort() + "][" + id + "]"; } public Request createRequest() { HttpRequestImpl request = new HttpRequestImpl(); request.setConnector(this); return (request); }
接下來看看 HttpProcessor 線程啓動後發生了什麼事,固然就是它的run方法。
如今好比有10個處理器線程,那麼從註釋中能夠看到,啓動一個處理器線程後,這個線程經過await就一直處於等待請求狀態了,請求到來後就能夠調用 process方法處理
啓動鏈接器歸納4:啓動 HttpConnector 線程,一個 while 循環事後,建立了一些 HttpProcessor 實例,這些實例中沒有用戶的 socket 信息;建立每個 HttpProcessor 實例過程當中,都會建立請求和響應對象,避免請求到來時再建立,並綁定鏈接器與請求之間的關係;而後啓動處理器線程,這個線程啓動後 await 方法進入阻塞狀態等待請求;經過 recyle() 方法放入 processors 實例中;若是請求到來,獲取一個處理器,處理這個請求。
public void run() { while (!stopped) { Socket socket = await(); //阻塞,直到用戶請求到來獲取到這個處理器才被喚醒 if (socket == null) continue; try { process(socket); //處理用戶請求 } catch (Throwable t) { log("process.invoke", t); } connector.recycle(this); //鏈接器回收處理器 } }
聽起來可能有些迷糊,由於還沒將鏈接器線程的啓動。
咱們知道 HttpConnector 實現了 Runnable 接口,Bootstrap.java 中調用 connector.start(),最後經過 threadStart方法 啓動 HttpConnector 線程
threadStart方法就是建立建立了 HttpConnector 線程,因此會調用 HttpConnector 類的run方法。
private void threadStart() { log(sm.getString("httpConnector.starting")); thread = new Thread(this, threadName); thread.setDaemon(true); thread.start(); }
HttpConnector 的 run 方法究竟作了什麼呢?鏈接器的用途是什麼還記得嗎?就是建立ServerSocket,等待請求,解析請求,傳遞請求響應對象給容器(固然也能夠認爲是 Processor 作的,可是通常認爲Tomcat的組件核心是 Connector 和 Container,能夠將 Processor 看成 Connector 的一部分)。
前面講了建立 ServerSocket,那剩下的功能固然是等待請求部分。(解析請求下一節講)
還記得上一節中是如何等待客戶端請求的嗎?固然就是調用 serverSocket 的 accept 方法
通過默認鏈接器改進了,可是在啓動 HttpConnector 線程後,run 方法依然是使用一樣的方式等待用戶請求。只是更加具體。
下面貼上 theadStart 方法中的關鍵代碼。
public void run() { while (!stopped) { Socket socket = null; try { socket = serverSocket.accept();//等待用戶請求,阻塞 } catch (AccessControlException ace) { continue; } catch (IOException e) { //省略 continue; } HttpProcessor processor = createProcessor(); //從池中獲取處理器實例 if (processor == null) { try { socket.close(); } catch (IOException e) { ; } continue; } processor.assign(socket); //將請求的socket交給獲得的處理器實例中 } }
簡單說說 createProcessor 方法:
在 HttpConnector 中,建立的 HttpProcessor 實例數量是有兩個變量決定的: minProcessors和 maxProcessors。默認狀況下, minProcessors 爲 5 而 maxProcessors 爲 20,可是你能夠經過setMinProcessors 和 setMaxProcessors 方法來改變他們的值。
protected int minProcessors = 5; private int maxProcessors = 20;
開始的時候, HttpConnector 對象建立 minProcessors 個 HttpProcessor 實例。
若是一次有比 HtppProcessor 實例更多的請求須要處理時, HttpConnector 建立更多的 HttpProcessor 實例,直到實例數量達到 maxProcessors 個。
在到達這點以後,仍不夠 HttpProcessor 實例的話,請來的請求將會給忽略掉。
若是你想讓 HttpConnector 繼續建立 HttpProcessor 實例的話,把maxProcessors 設置爲一個負數。還有就是變量 curProcessors 保存了 HttpProcessor 實例的當前數量。
private HttpProcessor createProcessor() { synchronized (processors) { if (processors.size() > 0) { return ((HttpProcessor) processors.pop()); //從池中拿處理器對象 } if ((maxProcessors > 0) && (curProcessors < maxProcessors)) { return (newProcessor()); //沒有超過上界,建立新的處理器 } else { if (maxProcessors < 0) { return (newProcessor()); //若是沒有上界,能夠隨意建立處理器實例 } else { return (null); } } }
}
從上面的遞推的說明中。能夠知道整個流程是這樣子的:
首先 connector.initialize 方法建立 ServerSocket 實例。connector.start() 方法中啓動 HttpConnector 線程,這個線程經過 serverSocket.accept 方法進入等待用戶請求狀態。
-隨後 connector.start 方法建立了若干個 HttpProcessor 處理器實例,同時啓動了處理器線程,因爲這些處理器實例中沒有用戶的 socket 信息,沒法處理請求,因此所有進入阻塞狀態,即 await 方法。當用戶請求到來時,經過 assign 方法將 socket 放入處理器實例中,以便讓處理器處理用戶請求。
那Tomcat4是如何實現同時處理多個請求的呢?
就要看assgin和await方法了。
用戶請求到來,獲得了用戶的 socket,調用 assign 方法,由於 avaiable 默認是 false,因此跳過 while 須要,將 socket 放入獲取到的處理器實例中,同時將 avaiable 設爲 true,喚醒線程,此時 await 方法中的 wait 方法被喚醒了,同時由於 avaliable 爲 true,跳出循環,將 avaiable 設爲 false 從新進入阻塞,獲得用戶的返回用戶的 socket,最後就可以經過 process 處理請求了。
synchronized void assign(Socket socket) { while (available) { //avaiable默認是false,,第一次執行時跳過while try { wait(); } catch (InterruptedException e) { } } this.socket = socket; //將從池中獲取到的HttpProcessor實例中的socket變量賦值 available = true; notifyAll();//喚醒線程。 } private synchronized Socket await() { while (!available) {//默認是false,因此進入循環阻塞,由於處理器實例沒有socket信息, try { wait(); } catch (InterruptedException e) { } } Socket socket = this.socket; //獲得了socket available = false; //從新進入阻塞 notifyAll(); if ((debug >= 1) && (socket != null)) log(" The incoming request has been awaited"); return (socket); }
疑問:
爲何 await 須要使用一個本地變量(socket)而不是返回實例的 socket 變量呢?
爲何 await 方法須要調用 notifyAll 呢?
process方法處理請求後,最後經過connecotr.recyle()方法回收處理器,即將處理器壓回處理器池中
void recycle(HttpProcessor processor) { processors.push(processor);//將建立的實例壓入棧中 }
本節講解了Tomcat默認鏈接器是如何實現多個請求處理
並無涉及process方法具體是如何處理請求的,在下一節補充。
但願你們看過這篇博客以後有所幫助。
若是以爲還不錯,能夠推薦一下或加關注喲,以爲寫得很差的也體諒一下。
附
相應代碼能夠在個人github上找到下載,拷貝到eclipse,而後打開對應包的代碼便可。
如發現編譯錯誤,多是因爲jdk不一樣版本對編譯的要求不一樣致使的,能夠無論,供學習研究使用。