手寫一個簡化版Tomcat
一、Tomcat工作原理
我們啟動Tomcat時雙擊的startup.bat文件的主要作用是找到catalina.bat,並且把參數傳遞給它,而catalina.bat中有這樣一段話:
Bootstrap.class是整個Tomcat 的入口,我們在Tomcat源碼裏找到這個類,其中就有我們經常使用的main方法:
這個類有兩個作用 :1.初始化一個守護進程變量、加載類和相應參數。2.解析命令,並執行。
源碼不過多贅述,我們在這裏只需要把握整體架構,有興趣的同學可以自己研究下源碼。Tomcat的server.xml配置文件中可以對應構架圖中位置,多層的表示可以配置多個:
即一個由 Server->Service->Engine->Host->Context 組成的結構,從裏層向外層分別是:
-
Server:服務器Tomcat的頂級元素,它包含了所有東西。
-
Service:一組 Engine(引擎) 的集合,包括線程池 Executor 和連接器 Connector 的定義。
-
Engine(引擎):一個 Engine代表一個完整的 Servlet 引擎,它接收來自Connector的請求,並決定傳給哪個Host來處理。
-
Container(容器):Host、Context、Engine和Wraper都繼承自Container接口,它們都是容器。
-
Connector(連接器):將Service和Container連接起來,註冊到一個Service,把來自客戶端的請求轉發到Container。
-
Host:即虛擬主機,所謂的”一個虛擬主機”可簡單理解為”一個網站”。
-
Context(上下文 ): 即 Web 應用程序,一個 Context 即對於一個 Web 應用程序。Context容器直接管理Servlet的運行,Servlet會被其給包裝成一個StandardWrapper類去運行。Wrapper負責管理一個Servlet的裝載、初始化、執行以及資源回收,它是最底層容器。
比如現在有以下網址,根據“/”切割的鏈接就會定位到具體的處理邏輯上,且每個容器都有過濾功能。
二、梳理自己的Tomcat實現思路
一個請求要請求服務器端的一個文件,服務端根據路徑查找該文件,如果有則讀取給文件並把文件內容響應回客戶端。
實現以上效果整體思路如下:
1.ServerSocket占用8080端口,用while(true)循環等待用戶發請求。
2.拿到瀏覽器的請求,解析並返回URL地址,用I/O輸入流讀取本地磁盤上相應文件。
3.讀取文件,如果文件不存在則構建響應報文頭、HTML正文內容,如果存在則把文件寫到瀏覽器端。
三、實現自己的Tomcat
工程文件結構:
1.HttpServer核心處理類,用於接受用戶請求,傳遞HTTP請求頭信息,關閉容器:
package com.jp.jpHttp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; /** * HttpServer核心處理類,用於接受用戶請求,傳遞HTTP請求頭信息,關閉容器 * */ public class HttpServer { // 用於判斷是否需要關閉容器 private boolean shutdown = false; public void acceptWait() { ServerSocket serverSocket = null; try { /** * serverSocket的三個參數 * TCP端口號:0-65535,端口號 0 在所有空閑端口上創建套接字 * 最大連接數:傳入連接指示(對連接的請求)的最大隊列長度被設置為 backlog 參數。如果隊列滿時收到連接指示,則拒絕該連接。 * ip地址: 參數可以在 ServerSocket 的多宿主主機 (multi-homed host) 上使用,ServerSocket 僅接受對其地址之一的連接請求。如果 bindAddr 為 null,則默認接受任何/所有本地地址上的連接 */ serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException e) { e.printStackTrace(); System.exit(1); } // 等待用戶發請求 while (!shutdown) { try { Socket socket = serverSocket.accept(); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); // 接受請求參數 Request request = new Request(is); request.parse(); // 創建用於返回瀏覽器的對象 Response response = new Response(os); response.setRequest(request); response.sendStaticResource(); // 關閉一次請求的socket,因為http請求就是采用短連接的方式 socket.close(); System.out.println("服務端的serverSocket關閉"); // 如果請求地址是/shutdown 則關閉容器 if (null != request) { shutdown = request.getUrL().equals("/shutdown"); } } catch (Exception e) { e.printStackTrace(); continue; } } } public static void main(String[] args) { HttpServer server = new HttpServer(); server.acceptWait(); } }
2.創建Request類,獲取HTTP的請求頭所有信息並截取URL地址返回:
1 package com.jp.jpHttp; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 6 /** 7 * 創建Request類,獲取HTTP的請求頭所有信息並截取URL地址返回 8 * 9 */ 10 public class Request { 11 private InputStream is; 12 private String url; 13 14 public Request(InputStream input) { 15 this.is = input; 16 } 17 18 public void parse() { 19 // 從socket中讀取一個2048長度字符 20 StringBuffer request = new StringBuffer(Response.BUFFER_SIZE); 21 int i; 22 byte[] buffer = new byte[Response.BUFFER_SIZE]; 23 try { 24 i = is.read(buffer);//從輸入流is讀取一定數量的字節,並存儲到buffer字節數組中 25 } catch (IOException e) { 26 e.printStackTrace(); 27 i = -1; 28 } 29 for (int j = 0; j < i; j++) { 30 request.append((char) buffer[j]);//把buffer字符數組中的字節拼成字符串 request 31 } 32 // 打印讀取的socket中的內容 33 System.out.println("打印socket的輸入流中的內容"); 34 System.out.print(request.toString());//打印字符串request,就是socket裏的輸入流,即來自客戶端的內容 35 url = parseUrL(request.toString()); //從字符串request中抽取出請求路徑 36 } 37 38 //從字符串request中抽取出請求路徑的函數 39 private String parseUrL(String requestString) { 40 int index1, index2; 41 index1 = requestString.indexOf(‘ ‘);// 看socket獲取請求頭是否有值 42 if (index1 != -1) { 43 index2 = requestString.indexOf(‘ ‘, index1 + 1); 44 if (index2 > index1) 45 System.out.println(); 46 System.out.println("獲取到請求文件的路徑(url)" + requestString.substring(index1 + 1, index2)); 47 return requestString.substring(index1 + 1, index2); 48 } 49 return null; 50 } 51 52 public String getUrL() { 53 return url; 54 } 55 56 }
3.創建Response類,響應請求讀取文件並寫回到瀏覽器
1 package com.jp.jpHttp; 2 3 import java.io.File; 4 import java.io.FileInputStream; 5 import java.io.IOException; 6 import java.io.OutputStream; 7 8 /** 9 * 創建Response類,響應請求讀取文件並寫回到瀏覽器 10 */ 11 public class Response { 12 public static final int BUFFER_SIZE = 2048; 13 // 瀏覽器訪問D盤的文件 14 private static final String WEB_ROOT = "D:"; 15 private Request request; 16 private OutputStream output; 17 18 public Response(OutputStream output) { 19 this.output = output; 20 } 21 22 public void setRequest(Request request) { 23 this.request = request; 24 } 25 26 public void sendStaticResource() throws IOException { 27 byte[] bytes = new byte[BUFFER_SIZE]; 28 FileInputStream fis = null; 29 try { 30 // 拼接本地目錄和瀏覽器端口號後面的目錄 31 File file = new File(WEB_ROOT, request.getUrL()); 32 System.out.println("請求路徑拼接為服務器內的文件路徑:"+ file.getAbsolutePath()); 33 //System.out.println("測試應用程序是否可以讀取此抽象路徑名表示的文件:" + file.canRead()); 34 // 如果文件存在,且不是個目錄 35 if (file.exists() && !file.isDirectory()) { 36 fis = new FileInputStream(file); 37 int ch = fis.read(bytes, 0, BUFFER_SIZE); 38 while (ch != -1) { 39 output.write(bytes, 0, ch); 40 ch = fis.read(bytes, 0, BUFFER_SIZE); 41 } 42 System.out.println("請求的文件存在,正在把文件作為響應寫進socket的輸出流"); 43 } else { 44 // 文件不存在,返回給瀏覽器響應提示,這裏可以拼接HTML任何元素 45 String retMessage = "<h1>" + file.getName() + " file or directory not exists</h1>"; 46 String returnMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" 47 + "Content-Length: " + retMessage.length() + "\r\n" + "\r\n" + retMessage; 48 output.write(returnMessage.getBytes()); 49 System.out.println("請求的文件不存在,返回404"); 50 } 51 } catch (Exception e) { 52 System.out.println(e.toString()); 53 } finally { 54 if (fis != null) 55 fis.close(); 56 } 57 } 58 }
實驗文件
實驗結果
請求文件不存在
當請求http://localhost:8080/shutdown 時,關閉容器,即不再監聽端口
四、讀者可以自己做的優化,擴展的點
1.在WEB_INF文件夾下讀取web.xml解析,通過請求名找到對應的類名,通過類名創建對象,用反射來初始化配置信息,如welcome頁面,Servlet、servlet-mapping,filter,listener,啟動加載級別等。
2.抽象Servlet類來轉碼處理請求和響應的業務。發過來的請求會有很多,也就意味著我們應該會有很多的Servlet,例如:RegisterServlet、LoginServlet等等還有很多其他的訪問。可以用到類似於工廠模式的方法處理,隨時產生很多的Servlet,來滿足不同的功能性的請求。
3.使用多線程技術。本文的代碼是死循環,且只能有一個鏈接,而現實中的情況是往往會有很多很多的客戶端發請求,可以把每個瀏覽器的通信封裝到一個線程當中。
https://my.oschina.net/liughDevelop/blog/1790893
手寫一個簡化版Tomcat