1. 程式人生 > >【Tomcat】手寫迷你版Tomcat

【Tomcat】手寫迷你版Tomcat

[Toc] ## 原始碼地址 https://github.com/CoderXiaohui/mini-tomcat ## 一,分析 ### Mini版Tomcat需要實現的功能 作為一個伺服器軟體提供服務(通過瀏覽器客戶端傳送Http請求,它可以接收到請求進行處理,處理之後的結果返回瀏覽器客戶端)。 1. 提供服務,接收請求(socket通訊) 2. 請求資訊封裝成Request物件,封裝響應資訊Response物件 3. 客戶端請求資源,資源分為靜態資源(html)和動態資源(servlet) 4. 資源返回給客戶端瀏覽器 ***Tomcat的入口就是一個main函式** ## 二,開發——準備工作 ### 2.1 新建Maven工程 ![image-20201227160427940](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227160427940.png) ![image-20201227161023307](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227161023307.png) ### 2.2 定義編譯級別 ```xml 4.0.0 com.dxh MiniCat 1.0-SNAPSHOT org.apache.maven.plugins maven-compiler-plugin 3.1 11 11 utf-8
``` ### 2.3 新建主類編寫啟動入口和埠 這裡我們把socket監聽的埠號定義在主類中。 ``` java package server; /** * Minicat的主類 */ public class Bootstrap { /** * 定義Socket監聽的埠號 */ private int port = 8080; public int getPort() { return port; } public void setPort(int port) { this.port = port; } /** * Minicat的啟動入口 * @param args */ public static void main(String[] args) { } } ``` ## 三,開發——1.0版本 循序漸進,一點一點的完善,1.0版本我們需要的需求是: - 瀏覽器請求http://localhost:8080,返回一個固定的字串到頁面“Hello Minicat” ### 3.1 編寫start方法以及遇到的問題 start方法主要就是監聽上面配置的埠,然後得到其輸出流,最後寫出。 ``` java /** * MiniCat啟動需要初始化展開的一些操作 */ public void start() throws IOException { /* 完成Minicat 1.0版本 需求:瀏覽器請求http://localhost:8080,返回一個固定的字串到頁面“Hello Minicat!” */ ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>
>Minicat start on port:"+port); while(true){ Socket socket = serverSocket.accept(); //有了socket,接收到請求,獲取輸出流 OutputStream outputStream = socket.getOutputStream(); outputStream.write("Hello Minicat!".getBytes()); socket.close(); } } ``` **完整的程式碼:** ```java /** * Minicat的主類 */ public class Bootstrap { /** * 定義Socket監聽的埠號 */ private int port = 8080; public int getPort() { return port; } public void setPort(int port) { this.port = port; } /** * Minicat的啟動入口 * @param args */ public static void main(String[] args) { Bootstrap bootstrap = new Bootstrap(); try { //啟動Minicat bootstrap.start(); } catch (IOException e) { e.printStackTrace(); } } /** * MiniCat啟動需要初始化展開的一些操作 */ public void start() throws IOException { ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>
>Minicat start on port:"+port); while(true){ Socket socket = serverSocket.accept(); //有了socket,接收到請求,獲取輸出流 OutputStream outputStream = socket.getOutputStream(); outputStream.write("Hello Minicat!".getBytes()); socket.close(); } } } ``` > 此時,如果啟動專案,從瀏覽器中輸入http://localhost:8080/,能夠正常接收到請求嗎? > > 不能! **問題分析:** 啟動專案,從瀏覽器中輸入http://localhost:8080/,可看到返回結果如下圖: ![image-20201227165334660](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227165334660.png) **因為Http協議是一個應用層協議,其規定了請求頭、請求體、響應同樣,如果沒有這些東西的話瀏覽器無法正常顯示**。程式碼中直接把”Hello Minicat!“直接輸出了, ### 3.2 解決問題,修改程式碼: 1. 新建一個工具類,主要提供響應頭資訊 ```java package server; /** * http協議工具類,主要提供響應頭資訊,這裡我們只提供200和404的情況 */ public class HttpProtocolUtil { /** * 為響應碼200提供請求頭資訊 */ public static String getHttpHeader200(long contentLength){ return "HTTP/1.1 200 OK \n" + "Content-Type: text/html \n" + "Content-Length: "+contentLength +"\n"+ "\r\n"; } /** * 為響應碼404提供請求頭資訊(也包含了資料內容) */ public static String getHttpHeader404(){ String str404="

404 not found

"; return "HTTP/1.1 404 NOT Found \n" + "Content-Type: text/html \n" + "Content-Length: "+str404.getBytes().length +"\n"+ "\r\n" + str404; } } ``` 2. 修改start方法 ```java public void start() throws IOException { ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port:"+port); while(true){ Socket socket = serverSocket.accept(); OutputStream outputStream = socket.getOutputStream(); String data = "Hello Minicat!"; String responseText = HttpProtocolUtil.getHttpHeader200(data.getBytes().length)+data; outputStream.write(responseText.getBytes()); socket.close(); } } ``` 3. 訪問~ ![image-20201227172649749](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227172649749.png) 成功。 ## 四,開發——2.0版本 需求: - 封裝Request和Response物件 - 返回html靜態資原始檔 ### 4.1 封裝前準備 新建一個類,Bootstrap2 (為了方便與1.0版本做對比)。獲得輸入流,並打印出來看看。 ```java package server; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * Minicat的主類 */ public class Bootstrap2 { /** * 定義Socket監聽的埠號 */ private int port = 8080; public int getPort() { return port; } public void setPort(int port) { this.port = port; } /** * Minicat的啟動入口 * @param args */ public static void main(String[] args) { Bootstrap2 bootstrap = new Bootstrap2(); try { //啟動Minicat bootstrap.start(); } catch (IOException e) { e.printStackTrace(); } } /** * MiniCat啟動需要初始化展開的一些操作 */ public void start() throws IOException { ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port:"+port); while (true){ Socket socket = serverSocket.accept(); InputStream inputStream = socket.getInputStream(); //從輸入流中獲取請求資訊 int count = 0 ; while (count==0){ count = inputStream.available(); } byte[] bytes = new byte[count]; inputStream.read(bytes); System.out.println("請求資訊=====>>"+new String(bytes)); socket.close(); } } } ``` 打印出來的資訊: ![image-20201227191058136](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227191058136.png) 這裡我們需要得到的是 請求方式(GET) 和 url (/) ,接下來封裝Request的時候也是隻封裝這兩個屬性 ### 4.2封裝Request、Response物件 #### 4.2.1 封裝Request 只封裝兩個引數——method和url 1. 新建Request類 2. 該類有三個屬性(`String method`、`String url`、`InputStream inputStream`) method和url都是從input流中解析出來的。 3. GET SET方法 4. 編寫有參構造 ``` java /** * 構造器 輸入流傳入 */ public Request(InputStream inputStream) throws IOException { this.inputStream = inputStream; //從輸入流中獲取請求資訊 int count = 0 ; while (count==0){ count = inputStream.available(); } byte[] bytes = new byte[count]; inputStream.read(bytes); String inputsStr = new String(bytes); //獲取第一行資料 String firstLineStr = inputsStr.split("\\n")[0]; //GET / HTTP/1.1 String[] strings = firstLineStr.split(" "); //把解析出來的資料賦值 this.method=strings[0]; this.url= strings[1]; System.out.println("method=====>>"+method); System.out.println("url=====>>"+url); } ``` 5. 無參構造 **完整的Request.java**: ``` java package server; import java.io.IOException; import java.io.InputStream; /** * 把我們用到的請求資訊,封裝成Response物件 (根據inputSteam輸入流封裝) */ public class Request { /** * 請求方式 例如:GET/POST */ private String method; /** * / , /index.html */ private String url; /** * 其他的屬性都是通過inputStream解析出來的。 */ private InputStream inputStream; /** * 構造器 輸入流傳入 */ public Request(InputStream inputStream) throws IOException { this.inputStream = inputStream; //從輸入流中獲取請求資訊 int count = 0 ; while (count==0){ count = inputStream.available(); } byte[] bytes = new byte[count]; inputStream.read(bytes); String inputsStr = new String(bytes); //獲取第一行資料 String firstLineStr = inputsStr.split("\\n")[0]; //GET / HTTP/1.1 String[] strings = firstLineStr.split(" "); this.method=strings[0]; this.url= strings[1]; System.out.println("method=====>>"+method); System.out.println("url=====>>"+url); } public Request() { } public String getMethod() { return method; } public void setMethod(String method) { this.method = method; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } a public InputStream getInputStream() { return inputStream; } public void setInputStream(InputStream inputStream) { this.inputStream = inputStream; } } ``` #### 4.2.2 封裝Response ``` java package server; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; /** * 封裝Response物件,需要依賴於OutputStream * */ public class Response{ private OutputStream outputStream; public Response(OutputStream outputStream) { this.outputStream = outputStream; } public Response() { } /** * @param path 指的就是 Request中的url ,隨後要根據url來獲取到靜態資源的絕對路徑,進一步根據絕對路徑讀取該靜態資原始檔,最終通過輸出流輸出 */ public void outputHtml(String path) throws IOException { //獲取靜態資源的絕對路徑 String absoluteResourcePath = StaticResourceUtil.getAbsolutePath(path); //輸出靜態資原始檔 File file = new File(absoluteResourcePath); if (file.exists() && file.isFile()){ //讀取靜態資原始檔,輸出靜態資源 StaticResourceUtil.outputStaticResource(new FileInputStream(file),outputStream); }else{ //輸出404 output(HttpProtocolUtil.getHttpHeader404()); } } //使用輸出流輸出指定字串 public void output(String context) throws IOException { outputStream.write(context.getBytes()); } } ``` **2.0版本只考慮輸出靜態資原始檔** 我們來分析一下`outputHtml(String path)`這個方法 首先,path就指 Request中的url,我們要用這個url找到該資源的絕對路徑: 1. 根據path,獲取靜態資源的絕對路徑 ```java public static String getAbsolutePath(String path){ String absolutePath = StaticResourceUtil.class.getResource("/").getPath(); return absolutePath.replaceAll("\\\\","/")+path; } ``` 2. 判斷靜態資源是否存在 - **不存在**:輸出404 3. **存在**:讀取靜態資原始檔,輸出靜態資源 ```java public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException { int count = 0 ; while (count==0){ count=inputStream.available(); } //靜態資源長度 int resourceSize = count; //輸出Http請求頭 , 然後再輸出具體的內容 outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes()); //讀取內容輸出 long written = 0; //已經讀取的內容長度 int byteSize = 1024; //計劃每次緩衝的長度 byte[] bytes = new byte[byteSize]; while (writtenresourceSize){ //剩餘未讀取大小不足一個1024長度,那就按照真實長度處理 byteSize= (int)(resourceSize-written); //剩餘的檔案內容長度 bytes=new byte[byteSize]; } inputStream.read(bytes); outputStream.write(bytes); outputStream.flush(); written+=byteSize; } } ``` 把上述的第一步和第三步的方法封裝到一個類中: ```java package server; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class StaticResourceUtil { /** * 獲取靜態資源方法的絕對路徑 */ public static String getAbsolutePath(String path){ String absolutePath = StaticResourceUtil.class.getResource("/").getPath(); return absolutePath.replaceAll("\\\\","/")+path; } /** * 讀取靜態資原始檔輸入流,通過輸出流輸出 */ public static void outputStaticResource(InputStream inputStream, OutputStream outputStream) throws IOException { int count = 0 ; while (count==0){ count=inputStream.available(); } //靜態資源長度 int resourceSize = count; //輸出Http請求頭 , 然後再輸出具體的內容 outputStream.write(HttpProtocolUtil.getHttpHeader200(resourceSize).getBytes()); //讀取內容輸出 long written = 0; //已經讀取的內容長度 int byteSize = 1024; //計劃每次緩衝的長度 byte[] bytes = new byte[byteSize]; while (writtenresourceSize){ //剩餘未讀取大小不足一個1024長度,那就按照真實長度處理 byteSize= (int)(resourceSize-written); //剩餘的檔案內容長度 bytes=new byte[byteSize]; } inputStream.read(bytes); outputStream.write(bytes); outputStream.flush(); written+=byteSize; } } } ``` ### 測試: 1. 修改**Bootstrap2.java**中的`start()`方法 ```java public void start() throws IOException { ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port:"+port); while (true){ Socket socket = serverSocket.accept(); InputStream inputStream = socket.getInputStream(); //封裝Resuest物件和Response物件 Request request = new Request(inputStream); Response response = new Response(socket.getOutputStream()); response.outputHtml(request.getUrl()); socket.close(); } } ``` 2. 在專案的resources資料夾新建`index.html`檔案 ```html Static resource Hello ~ Static resource ``` 3. 執行main方法 4. 瀏覽器輸入:http://localhost:8080/index.html 5. 結果展現: ![image-20201227205755188](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227205755188.png) ## 五,開發——3.0版本 3.0版本就要定義Servlet了,大致分為以下幾步: 1. 定義servlet規範 2. 編寫Servlet 3. 載入解析Servlet配置 ### 5.1 定義servlet規範 ``` java public interface Servlet { void init() throws Exception; void destroy() throws Exception; void service(Request request,Response response) throws Exception; } ``` 定義一個抽象類,實現Servlet,並且增加兩個抽象方法`doGet` , `doPost`. ``` java public abstract class HttpServlet implements Servlet{ public abstract void doGet(Request request,Response response); public abstract void doPost(Request request,Response response); @Override public void init() throws Exception { } @Override public void destroy() throws Exception { } @Override public void service(Request request, Response response) throws Exception { if ("GET".equals(request.getMethod())){ doGet(request, response); }else{ doPost(request, response); } } } ``` ### 5.2 編寫Servlet繼承HttpServlet 新建`DxhServlet.java`,並繼承`HttpServlet`重寫doGet和doPost方法 ``` java package server; import java.io.IOException; public class DxhServlet extends HttpServlet{ @Override public void doGet(Request request, Response response) { String content="

DxhServlet get

"; try { response.output(HttpProtocolUtil.getHttpHeader200(content.getBytes().length)+content); } catch (IOException e) { e.printStackTrace(); } } @Override public void doPost(Request request, Response response) { String content="

DxhServlet post

"; try { response.output(HttpProtocolUtil.getHttpHeader200(content.getBytes().length)+content); } catch (IOException e) { e.printStackTrace(); } } @Override public void init() throws Exception { super.init(); } @Override public void destroy() throws Exception { super.destroy(); } } ``` 接下來要把`DxhServlet`配置到一個配置檔案中,當MiniCat啟動時,載入進去。 ### 5.3 載入解析Servlet配置 #### 5.3.1 配置檔案 在**resources**目錄下,新建`web.xml` ``` xml dxh server.DxhServlet dxh /dxh ``` 標準的配置Servlet的標籤。`servlet-class`改成自己寫的Servlet全限定類名,`url-pattern`為`/dxh`,一會請求http://localhost:8080/dxh,來訪問這個servlet #### 5.3.2 解析配置檔案 **複製一份Bootstrap2.java,命名為Bootstrap3.java** 1. 載入解析相關的配置 ,web.xml 引入dom4j和jaxen的jar包 ```xml dom4j dom4j 1.6.1 jaxen jaxen 1.1.6 ``` 2. 在`Bootstrap3.java`中增加一個方法 ```java //用於下面儲存url-pattern以及其對應的servlet-class的例項化物件 private Map servletMap = new HashMap<>(); private void loadServlet(){ InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml"); SAXReader saxReader = new SAXReader(); try { Document document = saxReader.read(resourceAsStream); //根元素 Element rootElement = document.getRootElement(); /** * 1, 找到所有的servlet標籤,找到servlet-name和servlet-class * 2, 根據servlet-name找到中與其匹配的 */ List selectNodes = rootElement.selectNodes("//servlet"); for (int i = 0; i < selectNodes.size(); i++) { Element element = selectNodes.get(i); /** * 1, 找到所有的servlet標籤,找到servlet-name和servlet-class */ //dxh Element servletNameElement =(Element)element.selectSingleNode("servlet-name"); String servletName = servletNameElement.getStringValue(); //server.DxhServlet Element servletClassElement =(Element)element.selectSingleNode("servlet-class"); String servletClass = servletClassElement.getStringValue(); /** * 2, 根據servlet-name找到中與其匹配的 */ //Xpath表示式:從/web-app/servlet-mapping下查詢,查詢出servlet-name=servletName的元素 Element servletMapping =(Element)rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']'"); // /dxh String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue(); servletMap.put(urlPattern,(HttpServlet) Class.forName(servletClass).newInstance()); } } catch (DocumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } ``` 這段程式碼的意思就是讀取web.xml轉換成Document,然後遍歷根元素內中的**servlet**標籤(servlet是可以配置多個的),通過XPath表示式獲得`servlet-name`、`servlet-class`,以及與其對應的``標籤下的`url-pattern`,然後存在Map中。注意,這裡Map的**Key是url-pattern**,**Value是servlet-class的例項化物件**。 ### 5.4 接收請求,處理請求改造 ![image-20201227230820862](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227230820862.png) ![image-20201227231617987](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227231617987.png) 這裡進行了判斷,判斷`servletMap`中是否存在url所對應的value,如果沒有,當作靜態資源訪問,如果有,取出並呼叫service方法,在HttpServlet的service方法中已經做了根據request判斷具體呼叫的是doGet還是doPost方法。 ### 測試: 在瀏覽器中輸入: http://localhost:8080/index.html,可以訪問靜態資源 ![image-20201227231858786](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227231858786.png) 輸入:http://localhost:8080/dxh ![image-20201227231925246](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227231925246.png) 可以訪問【5.2中編寫的Servlet】動態資源~ **到此位置,一個簡單的Tomcat Demo已經完成。** ## 六,優化——多執行緒改造(不使用執行緒池) ### 6.1 問題分析 在現有的程式碼中,接收請求這部分它是一個IO模型——BIO,阻塞IO。 它存在一個問題,當一個請求還未處理完成時,再次訪問,會出現阻塞的情況。 **可以在`DxhServlet`的`doGet`方法中加入`Thread.sleep(10000);`然後訪問`http://localhost:8080/dxh`和`http://localhost:8080/index.html`做個測試** 那麼我們可以使用多執行緒對其進行改造。 ![image-20201227233057004](https://typora-files.oss-cn-beijing.aliyuncs.com/file/image-20201227233057004.png) 把上述程式碼放到一個新的執行緒中處理。 ### 6.2 複製Bootstrap3 複製Bootstrap3,命名為Bootstrap4。把start()方法中上圖的部分(包括`socket.close()`)**剪下**到下面的**執行緒處理類的run方法**中: ### 6.3 定義一個執行緒處理類 ```java package server; import java.io.InputStream; import java.net.Socket; import java.util.Map; /** * 執行緒處理類 */ public class RequestProcessor extends Thread{ private Socket socket; private Map servletMap; public RequestProcessor(Socket socket, Map servletMap) { this.socket = socket; this.servletMap = servletMap; } @Override public void run() { try{ InputStream inputStream = socket.getInputStream(); //封裝Resuest物件和Response物件 Request request = new Request(inputStream); Response response = new Response(socket.getOutputStream()); String url = request.getUrl(); //靜態資源處理 if (servletMap.get(url)==null){ response.outputHtml(request.getUrl()); }else{ //動態資源處理 HttpServlet httpServlet = servletMap.get(url); httpServlet.service(request,response); } socket.close(); }catch (Exception e){ } } } ``` ### 6.4 修改Bootstrap4的start()方法 ``` java public void start() throws Exception { //載入解析相關的配置 ,web.xml,把配置的servlet存入servletMap中 loadServlet(); ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port:"+port); /** * 可以請求動態資源 */ while (true){ Socket socket = serverSocket.accept(); //使用多執行緒處理 RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap); requestProcessor.start(); } } ``` 再次做6.1章節的測試, OK 沒有問題了。 ## 七,優化——多執行緒改造(使用執行緒池) 這一步,我們使用執行緒池進行改造。 複製Bootstrap4,命名為Bootstrap5。 修改start()方法。執行緒池的使用不再贅述。程式碼如下: ``` java public void start() throws Exception { //載入解析相關的配置 ,web.xml,把配置的servlet存入servletMap中 loadServlet(); /** * 定義執行緒池 */ //基本大小 int corePoolSize = 10; //最大 int maxPoolSize = 50; //如果執行緒空閒的話,超過多久進行銷燬 long keepAliveTime = 100L; //上面keepAliveTime的單位 TimeUnit unit = TimeUnit.SECONDS; //請求佇列 BlockingQueue workerQueue = new ArrayBlockingQueue<>(50); //執行緒工廠,使用預設的即可 ThreadFactory threadFactory = Executors.defaultThreadFactory(); //拒絕策略,如果任務太多處理不過來了,如何拒絕 RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize ,maxPoolSize ,keepAliveTime ,unit ,workerQueue ,threadFactory ,handler); ServerSocket serverSocket = new ServerSocket(port); System.out.println("========>>Minicat start on port(多執行緒):"+port); /** * 可以請求動態資源 */ while (true){ Socket socket = serverSocket.accept(); RequestProcessor requestProcessor = new RequestProcessor(socket,servletMap); threadPoolExecutor.execute(requestProcessor); } } ``` OK ,再次測試,成功~ MINI版Tomcat到此完成。 ## 八,總結 總結一下編寫一個MINI版本的Tomcat都需要做些什麼: 1. 定義一個入口類,需要監聽的埠號和入口方法——main方法 2. 定義servlet規範(介面),並實現它——HttpServlet 3. 編寫http協議工具類,主要提供響應頭資訊 4. 在main方法中呼叫start()方法用於啟動初始化和請求進來時的操作 5. 載入解析配置檔案(web.xml) 6. 當請求進來時,解析inputStream,並封裝為Request和Response物件。 7. 判斷請求資源的方式(動態資源還是靜態