Tomcat詳解系列(1) – 如何設計一個簡單的web容器
Tomcat – 如何設計一個簡單的web容器
在學習Tomcat前,很多人先入為主的對它的認知是巨複雜的;所以第一步,在學習它之前,要打破這種觀念,我們通過學習如何設計一個最基本的web容器來看它需要考慮什麼;進而在真正學習Tomcat時,多把重點放在它的頂層設計上,而不是某一塊代碼上, 思路永遠比具體實現重要的多。@pdai
寫在前面
我們在學習一項技術時,需要學習是它的知識體系,而不是碎片化的知識點。在構建知識體系時,我們往往需要先全局的看完一個教程或者一本書,這是構建的基礎。這裡我推薦大家看兩本書:
特別是第一本:經典的《How Tomcat Works》的中文版,它從0基礎逐步構建出Tomcat,適合新手;本節中很多內容源自這本書。
本系列在本之後,將轉為直接分析Tomcat框架。
基礎認知:如何實現服務器和客戶端(瀏覽器)的交互
客戶端和服務器端之間的交互式通過Socket來實現的,它術語應用層的協議。
HTTP協議
http協議相關的內容可以參看這裡:網絡協議 – HTTP 協議詳解
Socket
Socket是網絡連接的一個端點。套接字使得一個應用可以從網絡中讀取和寫入數據。放在兩 個不同計算機上的兩個應用可以通過連接發送和接受位元組流。為了從你的應用發送一條信息到另 一個應用,你需要知道另一個應用的 IP 地址和套接字端口。在 Java 裡邊,套接字指的是java.net.Socket
類。
要創建一個套接字,你可以使用 Socket 類眾多構造方法中的一個。其中一個接收主機名稱 和端口號:
public Socket (java.lang.String host, int port)
在這裡主機是指遠程機器名稱或者 IP 地址,端口是指遠程應用的端口號。例如,要連接 yahoo.com 的 80 端口,你需要構造以下的 Socket 對象:
new Socket ("yahoo.com", 80);
一旦你成功創建了一個 Socket 類的實例,你可以使用它來發送和接受位元組流。要發送位元組 流,你首先必須調用Socket類的getOutputStream方法來獲取一個java.io.OutputStream
對象。 要 發 送 文 本 到 一 個 遠 程 應 用 , 你 經 常 要 從 返 回 的 OutputStream 對 象 中 構 造 一 個 java.io.PrintWriter
對象。要從連接的另一端接受位元組流,你可以調用 Socket 類的 getInputStream 方法用來返回一個 java.io.InputStream
對象。
SeverSocket
Socket 類代表一個客戶端套接字,即任何時候你想連接到一個遠程服務器應用的時候你構造的套接字,現在,假如你想實施一個服務器應用,例如一個 HTTP 服務器或者 FTP 服務器,你需要一種不同的做法。這是因為你的服務器必須隨時待命,因為它不知道一個客戶端應用什麼時候會嘗試去連接它。為了讓你的應用能隨時待命,你需要使用 java.net.ServerSocket
類。這是 服務器套接字的實現。
ServerSocket
和 Socket
不同,服務器套接字的角色是等待來自客戶端的連接請求。一旦服務器套接字獲得一個連接請求,它創建一個 Socket 實例來與客戶端進行通信。
要創建一個服務器套接字,你需要使用 ServerSocket 類提供的四個構造方法中的一個。你 需要指定 IP 地址和服務器套接字將要進行監聽的端口號。通常,IP 地址將會是 127.0.0.1,也 就是說,服務器套接字將會監聽本地機器。服務器套接字正在監聽的 IP 地址被稱為是綁定地址。 服務器套接字的另一個重要的屬性是 backlog,這是服務器套接字開始拒絕傳入的請求之前,傳 入的連接請求的最大隊列長度。
其中一個 ServerSocket 類的構造方法如下所示:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
一個簡單web容器的設計和實現:對靜態資源
準備,這個例子來源於《How Tomcat Works》, 可以從這裡下載源碼
注意:當你跑如下程序時,可能會由於瀏覽器新版本不再支持的HTTP 0.9協議,而造成瀏覽器頁面沒有返回信息。
組件設計
根據上述的基礎,我們可以看到,我們只需要提供三個最基本的類,分別是:
- Request – 表示請求,這裡表示瀏覽器發起的HTTP請求
- HttpServer – 表示處理請求的服務器,同時這裡使用我們上面鋪墊的ServerSocket
- Reponse – 表示處理請求後的響應, 這裡表示服務器對HTTP請求的響應結果
組件實現
從上圖中我們可以看到,組織這幾個類的入口在Server的啟動方法中,即main方法中, 所以我們透過main方法從Server類進行分析:
- Server是如何啟動的?
public class HttpServer {
// 存放靜態資源的位置
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
// 關閉Server的請求
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
// 是否關閉Server
private boolean shutdown = false;
// 主入口
public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}
public void await() {
// 啟動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);
}
// 循環等待一個Request請求
while (!shutdown) {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
try {
// 創建socket
socket = serverSocket.accept();
input = socket.getInputStream();
output = socket.getOutputStream();
// 封裝input至request, 並處理請求
Request request = new Request(input);
request.parse();
// 封裝output至response
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
// 關閉socket
socket.close();
// 如果接受的是關閉請求,則設置關閉監聽request的標誌
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
}
catch (Exception e) {
e.printStackTrace();
continue;
}
}
}
}
- Request請求是如何封裝和處理的?
public class Request {
private InputStream input;
private String uri;
// 初始化Request
public Request(InputStream input) {
this.input = input;
}
// 處理request的方法
public void parse() {
// 從socket中讀取字符
StringBuffer request = new StringBuffer(2048);
int i;
byte[] buffer = new byte[2048];
try {
i = input.read(buffer);
}
catch (IOException e) {
e.printStackTrace();
i = -1;
}
for (int j=0; j<i; j++) {
request.append((char) buffer[j]);
}
System.out.print(request.toString());
// 獲得兩個空格之間的內容, 這裡將是HttpServer.WEB_ROOT中靜態文件的文件名稱
uri = parseUri(request.toString());
}
private String parseUri(String requestString) {
int index1, index2;
index1 = requestString.indexOf(' ');
if (index1 != -1) {
index2 = requestString.indexOf(' ', index1 + 1);
if (index2 > index1)
return requestString.substring(index1 + 1, index2);
}
return null;
}
public String getUri() {
return uri;
}
}
- Response中響應了什麼?
public class Response {
private static final int BUFFER_SIZE = 1024;
Request request;
OutputStream output;
public Response(OutputStream output) {
this.output = output;
}
// response中封裝了request,以便獲取request中的請求參數
public void setRequest(Request request) {
this.request = request;
}
public void sendStaticResource() throws IOException {
byte[] bytes = new byte[BUFFER_SIZE];
FileInputStream fis = null;
try {
// 讀取文件內容
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
fis = new FileInputStream(file);
int ch = fis.read(bytes, 0, BUFFER_SIZE);
while (ch!=-1) {
output.write(bytes, 0, ch);
ch = fis.read(bytes, 0, BUFFER_SIZE);
}
}
else {
// 文件不存在時,輸出404信息
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 23\r\n" +
"\r\n" +
"<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());
}
}
catch (Exception e) {
// thrown if cannot instantiate a File object
System.out.println(e.toString() );
}
finally {
if (fis!=null)
fis.close();
}
}
}
- 啟動輸出
當我們run上面HttpServer中的main方法之後,我們就可以打開瀏覽器//localhost:8080, 後面添加參數看返回webroot目錄中靜態文件的內容了(比如這裡我加了hello.txt文件到webroot下,並訪問//localhost:8080/hello.txt)。
一個簡單web容器的設計和實現:對Servelet
上面這個例子是不是很簡單?是否打破了對一個簡單http服務器的認知,減少了對它的恐懼。
但是上述的例子中只處理了靜態資源,我們如果要處理Servlet呢?
組件設計
不難發現,我們只需要在HttpServer只需要請求的處理委託給ServletProcessor, 讓它接受請求,並處理Response即可。
組件實現
- 在HttpServer中
public void await() {
//....
// create Response object
Response response = new Response(output);
response.setRequest(request);
// 不再有response自己處理
//response.sendStaticResource();
// 而是如果以/servlet/開頭,則委託ServletProcessor處理
if (request.getUri().startsWith("/servlet/")) {
ServletProcessor1 processor = new ServletProcessor1();
processor.process(request, response);
} else {
// 原有的靜態資源處理
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
}
// ....
}
- ServletProcessor 如何處理的?
public class ServletProcessor1 {
public void process(Request request, Response response) {
// 獲取servlet名字
String uri = request.getUri();
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
// 初始化URLClassLoader
URLClassLoader loader = null;
try {
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
// the forming of repository is taken from the createClassLoader method in
// org.apache.catalina.startup.ClassLoaderFactory
String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
// the code for forming the URL is taken from the addRepository method in
// org.apache.catalina.loader.StandardClassLoader class.
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
} catch (IOException e) {
System.out.println(e.toString() );
}
// 用classLoader加載上面的servlet
Class myClass = null;
try {
myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
System.out.println(e.toString());
}
// 將加載到的class轉成Servlet,並調用service方法處理
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
}
}
}
- Repsonse
public class PrimitiveServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {
System.out.println("init");
}
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
System.out.println("from service");
PrintWriter out = response.getWriter();
out.println("Hello. Roses are red.");
out.print("Violets are blue.");
}
public void destroy() {
System.out.println("destroy");
}
public String getServletInfo() {
return null;
}
public ServletConfig getServletConfig() {
return null;
}
}
- 訪問 URL
利用外觀模式改造
上述代碼存在一個問題,
// 將加載到的class轉成Servlet,並調用service方法處理
Servlet servlet = null;
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) request, (ServletResponse) response);
} catch (Exception e) {
System.out.println(e.toString());
} catch (Throwable e) {
System.out.println(e.toString());
}
這裡直接處理將request和response傳給servlet處理是不安全的,因為request可以向下轉型為Request類,從而ServeletRequest便具備了訪問Request中方法的能力。
public class Request implements ServletRequest {
// 一些public方法
}
public class Response implements ServletResponse {
}
解決的方法便是通過外觀模式進行改造:
- RequestFacade為例
public class RequestFacade implements ServletRequest {
private ServletRequest request = null;
public RequestFacade(Request request) {
this.request = request;
}
/* implementation of the ServletRequest*/
public Object getAttribute(String attribute) {
return request.getAttribute(attribute);
}
public Enumeration getAttributeNames() {
return request.getAttributeNames();
}
public String getRealPath(String path) {
return request.getRealPath(path);
}
...
- Process中由傳入外觀類
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request); // 轉換成外觀類
ResponseFacade responseFacade = new ResponseFacade(response);// 轉換成外觀類
try {
servlet = (Servlet) myClass.newInstance();
servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
}
catch (Exception e) {
System.out.println(e.toString());
}
catch (Throwable e) {
System.out.println(e.toString());
}
總結
當我們看到這麼一個簡單的web容器實現之後,我們便不再覺得Tomcat高高在上;這將為我們繼續分析Tomcat中核心源碼提供基礎。
更多
更多文章請參考 Java 全棧知識體系