1. 程式人生 > >手寫一個簡化版Tomcat

手寫一個簡化版Tomcat

exc ext login 變量 請求參數 finally engine catch container

一、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