【Tomcat】Tomcat工作原理及簡單模擬實現

weknow619發表於2019-03-15

Tomcat應該都不陌生,我們經常會把寫好的程式碼打包放在Tomcat裡並啟動,然後在瀏覽器裡就能愉快的呼叫我們寫的程式碼來實現相應的功能了,那麼Tomcat是如何工作的?

一、Tomcat工作原理

我們啟動Tomcat時雙擊的startup.bat檔案的主要作用是找到catalina.bat,並且把引數傳遞給它,而catalina.bat中有這樣一段話:

image.png
Bootstrap.class是整個Tomcat 的入口,我們在Tomcat原始碼裡找到這個類,其中就有我們經常使用的main方法:
image.png
這個類有兩個作用 :1.初始化一個守護程式變數、載入類和相應引數。2.解析命令,並執行。

原始碼不過多贅述,我們在這裡只需要把握整體架構,有興趣的同學可以自己研究下原始碼。Tomcat的server.xml配置檔案中可以對應構架圖中位置,多層的表示可以配置多個:

image.png
即一個由 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的裝載、初始化、執行以及資源回收,它是最底層容器。

比如現在有以下網址,根據“/”切割的連結就會定位到具體的處理邏輯上,且每個容器都有過濾功能。

image.png

二、Tomcat實現思路

下面只是簡單實現效果,當瀏覽器訪問對應地址時:

image.png
實現以上效果整體思路如下: 1.ServerSocket佔用8080埠,用while(true)迴圈等待使用者發請求。

2.拿到瀏覽器的請求,解析並返回URL地址,用I/O輸入流讀取本地磁碟上相應檔案。

3.讀取檔案,不存在構建響應報文頭、HTML正文內容,存在則寫到瀏覽器端。

三、實現Tomcat

工程檔案結構和pom.xml檔案:

image.png
1.HttpServer核心處理類,用於接受使用者請求,傳遞HTTP請求頭資訊,關閉容器:

public class HttpServer {
  // 用於判斷是否需要關閉容器
  private boolean shutdown = false;
  
  public void acceptWait() {
    ServerSocket serverSocket = null;
    try {
        //埠號,最大連結數,ip地址
      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();
        //如果請求地址是/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地址返回:

public class Request {
  private InputStream is;
  private String url;

  public Request(InputStream input) {
    this.is = input;
  }
  public void parse() {
    //從socket中讀取一個2048長度字元
    StringBuffer request = new StringBuffer(Response.BUFFER_SIZE);
    int i;
    byte[] buffer = new byte[Response.BUFFER_SIZE];
    try {
      i = is.read(buffer);
    }
    catch (IOException e) {
      e.printStackTrace();
      i = -1;
    }
    for (int j=0; j<i; j++) {
      request.append((char) buffer[j]);
    }
    //列印讀取的socket中的內容
    System.out.print(request.toString());
    url = parseUrL(request.toString());
  }

  private String parseUrL(String requestString) {
    int index1, index2;
    index1 = requestString.indexOf(' ');//看socket獲取請求頭是否有值
    if (index1 != -1) {
      index2 = requestString.indexOf(' ', index1 + 1);
      if (index2 > index1)
        return requestString.substring(index1 + 1, index2);
    }
    return null;
  }

  public String getUrL() {
    return url;
  }

}
複製程式碼

3.建立Response類,響應請求讀取檔案並寫回到瀏覽器

public class Response {
  public static final int BUFFER_SIZE = 2048;
  //瀏覽器訪問D盤的檔案
  private static final String WEB_ROOT ="D:";
  private Request request;
  private OutputStream output;

  public Response(OutputStream output) {
    this.output = output;
  }
  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(WEB_ROOT, request.getUrL());
      //如果檔案存在,且不是個目錄
      if (file.exists() && !file.isDirectory()) {
        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 {
           //檔案不存在,返回給瀏覽器響應提示,這裡可以拼接HTML任何元素
          String retMessage = "<h1>"+file.getName()+" file or directory not exists</h1>";
          String returnMessage ="HTTP/1.1 404 File Not Found\r\n" +
                  "Content-Type: text/html\r\n" +
                  "Content-Length: "+retMessage.length()+"\r\n" +
                  "\r\n" +
                  retMessage;
        output.write(returnMessage.getBytes());
      }
    }
    catch (Exception e) {
      System.out.println(e.toString() );
    }
    finally {
      if (fis!=null)
        fis.close();
    }
  }
}
複製程式碼

四、擴充套件點

1.在WEB_INF資料夾下讀取web.xml解析,通過請求名找到對應的類名,通過類名建立物件,用反射來初始化配置資訊,如welcome頁面,Servlet、servlet-mapping,filter,listener,啟動載入級別等。

2.抽象Servlet類來轉碼處理請求和響應的業務。發過來的請求會有很多,也就意味著我們應該會有很多的Servlet,例如:RegisterServlet、LoginServlet等等還有很多其他的訪問。可以用到類似於工廠模式的方法處理,隨時產生很多的Servlet,來滿足不同的功能性的請求。

3.使用多執行緒。本文的程式碼是死迴圈,且只能有一個連結,而現實中的情況是往往會有很多很多的客戶端發請求,可以把每個瀏覽器的通訊封裝到一個執行緒當中。

相關文章