1. 程式人生 > >Tomcat詳解系列(1) - 如何設計一個簡單的web容器

Tomcat詳解系列(1) - 如何設計一個簡單的web容器

# Tomcat - 如何設計一個簡單的web容器 > 在學習Tomcat前,很多人先入為主的對它的認知是巨複雜的;所以第一步,在學習它之前,要打破這種觀念,我們通過學習如何設計一個最基本的web容器來看它需要考慮什麼;進而在真正學習Tomcat時,多把重點放在它的頂層設計上,而不是某一塊程式碼上, 思路永遠比具體實現重要的多。@pdai - [Tomcat - 如何設計一個簡單的web容器](#tomcat---如何設計一個簡單的web容器) - [寫在前面](#寫在前面) - [基礎認知:如何實現伺服器和客戶端(瀏覽器)的互動](#基礎認知如何實現伺服器和客戶端瀏覽器的互動) - [HTTP協議](#http協議) - [Socket](#socket) - [SeverSocket](#seversocket) - [一個簡單web容器的設計和實現:對靜態資源](#一個簡單web容器的設計和實現對靜態資源) - [元件設計](#元件設計) - [元件實現](#元件實現) - [一個簡單web容器的設計和實現:對Servelet](#一個簡單web容器的設計和實現對servelet) - [元件設計](#元件設計-1) - [元件實現](#元件實現-1) - [利用外觀模式改造](#利用外觀模式改造) - [總結](#總結) ## 寫在前面 我們在學習一項技術時,需要學習是它的知識體系,而不是碎片化的知識點。在構建知識體系時,我們往往需要先全域性的看完一個教程或者一本書,這是構建的基礎。這裡我推薦大家看兩本書: ![](https://pdai.tech/_images/tomcat/tomcat-x-design-7.png) 特別是第一本:經典的《How Tomcat Works》的中文版,它從0基礎逐步構建出Tomcat,適合新手;本節中很多內容源自這本書。 本系列在本之後,將轉為直接分析Tomcat框架。 ## 基礎認知:如何實現伺服器和客戶端(瀏覽器)的互動 > 客戶端和伺服器端之間的互動式通過Socket來實現的,它術語應用層的協議。 ### HTTP協議 http協議相關的內容可以參看這裡:[網路協議 - HTTP 協議詳解](https://pdai.tech/md/develop/protocol/dev-protocol-http.html) ### Socket Socket是網路連線的一個端點。套接字使得一個應用可以從網路中讀取和寫入資料。放在兩 個不同計算機上的兩個應用可以通過連線傳送和接受位元組流。為了從你的應用傳送一條資訊到另 一個應用,你需要知道另一個應用的 IP 地址和套接字埠。在 Java 裡邊,套接字指的是`java.net.Socket`類。 要建立一個套接字,你可以使用 Socket 類眾多構造方法中的一個。其中一個接收主機名稱 和埠號: ```java public Socket (java.lang.String host, int port) ``` 在這裡主機是指遠端機器名稱或者 IP 地址,埠是指遠端應用的埠號。例如,要連線 yahoo.com 的 80 埠,你需要構造以下的 Socket 物件: ```java new Socket ("yahoo.com", 80); ``` 一旦你成功建立了一個 Socket 類的例項,你可以使用它來發送和接受位元組流。要傳送位元組 流,你首先必須呼叫Socket類的getOutputStream方法來獲取一個`java.io.OutputStream`物件。 要 發 送 文 本 到 一 個 遠 程 應 用 , 你 經 常 要 從 返 回 的 OutputStream 對 象 中 構 造 一 個 `java.io.PrintWriter` 物件。要從連線的另一端接受位元組流,你可以呼叫 Socket 類的 getInputStream 方法用來返回一個 `java.io.InputStream` 物件。 ![](https://pdai.tech/_images/tomcat/tomcat-x-design-6.png) ### SeverSocket Socket 類代表一個**客戶端套接字**,即任何時候你想連線到一個遠端伺服器應用的時候你構造的套接字,現在,假如你想實施一個伺服器應用,例如一個 HTTP 伺服器或者 FTP 伺服器,你需要一種不同的做法。這是因為你的伺服器必須隨時待命,因為它不知道一個客戶端應用什麼時候會嘗試去連線它。為了讓你的應用能隨時待命,你需要使用 `java.net.ServerSocket` 類。這是 **伺服器套接字**的實現。 `ServerSocket` 和 `Socket` 不同,伺服器套接字的角色是等待來自客戶端的連線請求。**一旦伺服器套接字獲得一個連線請求,它建立一個 Socket 例項來與客戶端進行通訊**。 要建立一個伺服器套接字,你需要使用 ServerSocket 類提供的四個構造方法中的一個。你 需要指定 IP 地址和伺服器套接字將要進行監聽的埠號。通常,IP 地址將會是 127.0.0.1,也 就是說,伺服器套接字將會監聽本地機器。伺服器套接字正在監聽的 IP 地址被稱為是繫結地址。 伺服器套接字的另一個重要的屬性是 backlog,這是伺服器套接字開始拒絕傳入的請求之前,傳 入的連線請求的最大佇列長度。 其中一個 ServerSocket 類的構造方法如下所示: ```java public ServerSocket(int port, int backLog, InetAddress bindingAddress); ``` ## 一個簡單web容器的設計和實現:對靜態資源 > 準備,這個例子來源於《How Tomcat Works》, 可以從這裡下載原始碼 注意:當你跑如下程式時,可能會由於瀏覽器新版本不再支援的HTTP 0.9協議,而造成瀏覽器頁面沒有返回資訊。 ### 元件設計 根據上述的基礎,我們可以看到,我們只需要提供三個最基本的類,分別是: + **Request** - 表示請求,這裡表示瀏覽器發起的HTTP請求 + **HttpServer** - 表示處理請求的伺服器,同時這裡使用我們上面鋪墊的ServerSocket + **Reponse** - 表示處理請求後的響應, 這裡表示伺服器對HTTP請求的響應結果 ![](https://pdai.tech/_images/tomcat/tomcat-x-design-1.png) ### 元件實現 從上圖中我們可以看到,組織這幾個類的入口在Server的啟動方法中,即main方法中, 所以我們透過main方法從Server類進行分析: + Server是如何啟動的? ```java 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請求是如何封裝和處理的? ```java 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
index1) return requestString.substring(index1 + 1, index2); } return null; } public String getUri() { return uri; } } ``` + Response中響應了什麼? ```java 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" + "

File Not Found

"; 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方法之後,我們就可以開啟瀏覽器http://localhost:8080, 後面新增引數看返回webroot目錄中靜態檔案的內容了(比如這裡我加了hello.txt檔案到webroot下,並訪問http://localhost:8080/hello.txt)。 ![](https://pdai.tech/_images/tomcat/tomcat-x-design-5.png) ![](https://pdai.tech/_images/tomcat/tomcat-x-design-4.png) ## 一個簡單web容器的設計和實現:對Servelet 上面這個例子是不是很簡單?是否打破了對一個簡單http伺服器的認知,減少了對它的恐懼。 但是上述的例子中只處理了靜態資源,我們如果要處理Servlet呢? ### 元件設計 不難發現,我們只需要在HttpServer只需要請求的處理委託給ServletProcessor, 讓它接受請求,並處理Response即可。 ![](https://pdai.tech/_images/tomcat/tomcat-x-design-2.png) ### 元件實現 + 在HttpServer中 ```java 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 如何處理的? ```java 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 ```java 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 ![](https://pdai.tech/_images/tomcat/tomcat-x-design-8.png) ### 利用外觀模式改造 上述程式碼存在一個問題, ```java // 將載入到的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中方法的能力。 ```java public class Request implements ServletRequest { // 一些public方法 } public class Response implements ServletResponse { } ``` 解決的方法便是通過外觀模式進行改造: ![](https://pdai.tech/_images/tomcat/tomcat-x-design-3.png) + RequestFacade為例 ```java 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中由傳入外觀類 ```java 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 全棧知識體系](https://www.pd