HowTomcatWorks學習筆記--Tomcat的預設聯結器(續)
這本書(How Tomcat Works 中文下載地址)之前就看過,然而也就開了個頭就廢棄了,所以一直耿耿於懷。這次決定重新開始,在此記錄一些學習心得和本書的主要知識點。
所有程式碼也將託管在GitHub上面。O(∩_∩)O
上節回顧
剖析了Tomcat的HttpConnector
類。
工作原理梗概如下:
- 用工廠模式創建出ServerSocket。
- 建立特定數量的HttpProcessor物件池,用於處理Socket請求;
- 同時,會啟動HttpProcessor執行緒。由於沒有請求,所以所有的HttpProcessor都會阻塞在那裡。
- 呼叫HttpConnector的run方法,等待Socket請求。
- 當請求來臨,從物件池中pop出一個HttpProcessor例項。
- 呼叫HttpProcessor的
assign
方法,將Socket例項傳遞給HttpProcessor; - 並且,喚醒在HttpProcessor中被阻塞的執行緒,用於處理Socket請求。
概要
這裡會主要關注HttpProcessor
非同步處理請求的方法。
HttpProcessor類
在前幾個章節中,我們自己寫的處理請求的程式碼是同步的。他必須等待處理完process
方法,才能接收新的Socket。
public void run() {
...
while (!stopped) {
Socket socket = null ;
try {
socket = serversocket.accept();
}
catch (Exception e) {
continue;
}
// Hand this socket off to an Httpprocessor
HttpProcessor processor = new Httpprocessor(this);
processor.process(socket);
}
}
而Tomcat中,這是非同步的。
run方法
它的邏輯是,接收Socket,然後處理請求,最後將HttpConnector例項放回到物件池中。
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();
}
}
while方法做迴圈,很常見。
在上一章節中我們已經說過,HttpConnector會建立HttpProcessor,並且啟動執行緒的run方法,同時它被放在物件池中,一直阻塞著。
那麼run方法為何會一直阻塞著呢?很簡單,因為在while迴圈中的await
方法。
await方法
執行的流程如下:
- while迴圈中
available
條件的初始狀態是false,表示沒有請求。所以它就一直處於wait
等待狀態。 - 一旦有請求(在assign方法中,會將available設定為true,並且釋放執行緒鎖。await得以呼叫。),await將會獲取執行緒鎖,程式碼跳出迴圈得以向下執行。
- 然後,它將
available
條件設定為false,通過notifyAll
釋放執行緒鎖,返回Socket例項。 - 這樣,當前Scoket的await方法就執行完成,返回run方法中。
- 在run方法中,繼續其他操作。
private synchronized Socket await() {
// Wait for the Connector to provide a new Socket
while (!available) {
try {
wait();
} catch (InterruptedException e) {
}
}
// Notify the Connector that we have received this Socket
Socket socket = this.socket;
available = false;
notifyAll();
if ((debug >= 1) && (socket != null))
log(" The incoming request has been awaited");
return (socket);
}
一個問題
這裡有個問題值得注意一下,為何需要呼叫notifyAll方法,作用是什麼,是否多此一舉呢?
書上是這麼解釋的:
為什麼 await 需要使用一個本地變數(socket)而不是返回例項的 socket 變數呢?因為這樣一來,在當前 socket 被完全處理之前,例項的 socket 變數可以賦給下一個前來的 socket。
為什麼 await 方法需要呼叫 notifyAll 呢? 這是為了防止在 available 為 true 的時候另一個 socket 到來。在這種情況下,聯結器執行緒將會在 assign 方法的 while 迴圈中停止,直到接收到處理器執行緒的 notifyAll 呼叫。
意思大概就是,一個HttpProcessor例項在同一時間只能處理一個Socket的請求,但是設定一個本地socket變數,可以讓HttpProcessor提前儲存下一個Socket請求。
可是將Socket傳遞給HttpProcessor的時候,HttpProcessor是需要從物件池中獲取而來的。在處理當前請求的HttpProcessor明顯不在物件池中,它是如何能夠提前儲存下一個Socket請求呢?
這個問題我還未找到答案。待以後處理。
assign方法
剛剛我們說了,await方法會阻塞。直到assign方法被呼叫,釋放了執行緒鎖,await才得以繼續執行。
assign
方法的作用就是,
- 接收由HttpConnector傳遞的Socket例項。
- 釋放執行緒鎖,讓
await
方法得以呼叫。
available
屬性是false,我們得知。
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");
}
處理請求
pocess
方法,會做:
- 解析連線
- 解析請求
- 解析頭部
解析資料是在一個white迴圈中進行的。
迴圈條件中keepAlive
是由Http請求控制的。
keepAlive = true;
while (!stopped && ok && keepAlive) {
...
if ( "close".equals(response.getHeader("Connection")) ) {
keepAlive = false;
}
...
}
裡面的處理資料程式碼主要是:
if (ok) {
parseConnection(socket);
parseRequest(input, output);
if (!request.getRequest().getProtocol()
.startsWith("HTTP/0"))
parseHeaders(input);
if (http11) {
// Sending a request acknowledge back to the client if
// requested.
ackRequest(output);
// If the protocol is HTTP/1.1, chunking is allowed.
if (connector.isChunkingAllowed())
response.setAllowChunking(true);
}
}
都是在處理資料。其他的略。
解析連線
parseConnection
方法從套接字中獲取到網路地址並把它賦予 HttpRequestImpl 物件。
private void parseConnection(Socket socket) throws IOException, ServletException {
if (debug >= 2)
log(" parseConnection: address=" + socket.getInetAddress() +
", port=" + connector.getPort());
((HttpRequestImpl) request).setInet(socket.getInetAddress());
if (proxyPort != 0)
request.setServerPort(proxyPort);
else
request.setServerPort(serverPort);
request.setSocket(socket);
}
解析請求
parseRequest
方法和我們之前章節實現的差不多。
解析頭部
parseHeaders
方法使用包org.apache.catalina.connector.http
裡邊的 HttpHeader 和 DefaultHeaders 類。類 HttpHeader 指代一個 HTTP 請求頭部。內容也類似,略過。
簡單的容器
容器的作用,
- 用來動態load出Servlet,
- 並且將Request和Response傳遞給它,執行它的service方法。
它需要實現org.apache.catalina.Container
介面。
現在,我們自己實現一個容器,然後將這個容器傳遞給org.apache.catalina.connector.http.HttpConnector
。
程式碼和之前的很類似,只是從將一些功能從ServeletProcessor
中分離了出來。
public class SimpleContainer implements Container {
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
...
public void invoke(Request request, Response response)
throws IOException, ServletException {
String servletName = ( (HttpServletRequest) request).getRequestURI();
servletName = servletName.substring(servletName.lastIndexOf("/") + 1);
URLClassLoader loader = null;
try {
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(WEB_ROOT);
String repository = (new URL("file", null, classPath.getCanonicalPath()
+ File.separator)).toString() ;
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
}
catch (IOException e) {
System.out.println(e.toString() );
}
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((HttpServletRequest) request, (HttpServletResponse) response);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
}
...
}
啟動類
package com.ext04.pyrmont.startup;
import com.ext04.pyrmont.core.SimpleContainer;
import org.apache.catalina.connector.http.HttpConnector;
/**
* Created by laiwenqiang on 2017/5/25.
*/
public class BootStrap {
public static void main(String[] args) {
HttpConnector connector = new HttpConnector();
SimpleContainer container = new SimpleContainer();
connector.setContainer(container);
try {
connector.initialize();
connector.start();
// 加上這句話,防止main方法退出。阻塞在那。
System.in.read();
} catch (Exception e) {
e.printStackTrace();
}
}
}
開啟瀏覽器,輸入http://localhost:8080/servlet/PrimitiveServlet
,得出結果。
一步一個腳印
本章完成,(^__^)