1. 程式人生 > >深入理解 Tomcat(三)Tomcat 底層實現原理

深入理解 Tomcat(三)Tomcat 底層實現原理

轉載自:https://blog.csdn.net/qq_38182963/article/details/78660777

又是一個週末,這篇文章將從一個簡單的例子來理解tomcat的底層設計;

本文將介紹 Java Web 伺服器是如何執行的, Web 伺服器也稱為超文字傳輸協議( HyperText Transfer Protocol, HTTP)伺服器, 因為它使用 Http 與其客戶端(通常是 Web 瀏覽器)進行通訊, 基於 Java 的 Web 伺服器會使用兩個重要的類: java.net.Socket 類和 java.net.ServerSocket 類, 並通過傳送 Http 訊息進行通訊. 我們先花一些篇幅介紹 Http 協議(如果同學們熟悉HTTP協議可直接跳過)和這兩個類, 然後寫一個簡單的 Web 伺服器.

HTTP

Http : Http 允許 Web 伺服器和瀏覽器通過 Internet 傳送並接受資料, 是一種基於”請求—響應”的協議, 客戶端請求一個檔案, 伺服器端對該請求進行響應. Http 使用可靠的 tcp 連線, tcp 協議預設使用 tcp 80埠, http協議的第一個版本是 http/0.9, 後來被 http/1.0取代, 隨後 http/1.0又被http/1.1取代, http/1.1 定義域 RFC(Request for Comment, 請求註解)2616中.

如果各位對 Http1.1 有更多興趣, 請閱讀 RFC 2616.

在 Http 中, 總是由客戶端通過建立連線併發送 http 請求來初始化一個事務的. Web 伺服器端並不負責聯絡客戶端或建立一個到客戶端的回撥連線.客戶端或伺服器端可提前關閉連線, 例如, 當使用 Web 瀏覽器瀏覽網頁時, 可以單擊瀏覽器上的 stop 按鈕來停止下載檔案, 這樣就有效的關閉了一個 Web 伺服器的 http 連線.

HTTP 請求

一個 HTTP 請求包含以下三部分:
* 請求方法—-統一資源識別符號(Uniform Resource Identifier, URI)——協議/版本
* 請求頭
* 實體

下面是一個 HTTP 請求的例子:

POST /examples/default.jsp HTTP/1.1 
Accept: text/plain; text/html 
Accept-Language: en-gb 
Connection: Keep-Alive 
Host: localhost 
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98
) Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate

lastName=Franks&firstName=Michael

方法—統一資源識別符號(URI)—協議/版本出現在請求的第一行。
POST /examples/default.jsp HTTP/1.1

這裡 POST 是請求方法,/examples/default.jsp 是 URI,而 HTTP/1.1 是協議/版本部分。 每個 HTTP 請求可以使用 HTTP 標準裡邊提到的多種方法之一。HTTP 1.1 支援 7 種類型的請 求:GET, POST, HEAD, OPTIONS, PUT, DELETE 和 TRACE。GET 和 POST 在網際網路應用裡邊最普遍使用的。

URI 完全指明瞭一個網際網路資源。URI 通常是相對伺服器的根目錄解釋的。因此,始終一斜 線/開頭。統一資源定位器(URL)其實是一種 URI(檢視 http://www.ietf.org/rfc/rfc2396.txt)
來的。該協議版本代表了正在使用的 HTTP 協議的版本。

請求的頭部包含了關於客戶端環境和請求的主體內容的有用資訊。例如它可能包括瀏覽器設 置的語言,主體內容的長度等等。每個頭部通過一個回車換行符(CRLF)來分隔的。

對於 HTTP 請求格式來說,頭部和主體內容之間有一個回車換行符(CRLF)是相當重要的。CRLF 告訴HTTP伺服器主體內容是在什麼地方開始的。在一些網際網路程式設計書籍中,CRLF還被認為是HTTP 請求的第四部分。

在前面一個 HTTP 請求中,主體內容只不過是下面一行:

lastName=Franks&firstName=Michael

實體內容在一個典型的 HTTP 請求中可以很容易的變得更長。

HTTP 響應

類似於 HTTP 請求,一個 HTTP 響應也包括三個組成部分:
* 方法—統一資源識別符號(URI)—協議/版本
* 響應的頭部
* 主體內容

下面是一個 HTTP 響應的例子:

HTTP/1.1 200 OK 
Server: Microsoft-IIS/4.0 
Date: Mon, 5 Jan 2004 13:13:33 GMT 
Content-Type: text/html 
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT 
Content-Length: 112 

<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>

響應頭部的第一行類似於請求頭部的第一行。第一行告訴你該協議使用 HTTP 1.1,請求成 功(200=成功),表示一切都執行良好。

響應頭部和請求頭部類似,也包括很多有用的資訊。響應的主體內容是響應本身的 HTML 內 容。頭部和主體內容通過 CRLF 分隔開來。

Socket 類

套接字是網路連線的一個端點。套接字使得一個應用可以從網路中讀取和寫入資料。放在兩 個不同計算機上的兩個應用可以通過連線傳送和接受位元組流。為了從你的應用傳送一條資訊到另 一個應用,你需要知道另一個應用的 IP 地址和套接字埠。在 Java 裡邊,套接字指的是java.net.Socket類。

要建立一個套接字,你可以使用 Socket 類眾多構造方法中的一個。其中一個接收主機名稱 和埠號:

public Socket (java.lang.String host, int port)

在這裡主機是指遠端機器名稱或者 IP 地址,埠是指遠端應用的埠號。例如,要連線 yahoo.com 的 80 埠,你需要構造以下的 Socket 物件:

new Socket ("yahoo.com", 80);

一旦你成功建立了一個 Socket 類的例項,你可以使用它來發送和接受位元組流。要傳送位元組 流,你首先必須呼叫Socket類的getOutputStream方法來獲取一個java.io.OutputStream物件。 要 發 送 文 本 到 一 個 遠 程 應 用 , 你 經 常 要 從 返 回 的 OutputStream 對 象 中 構 造 一 個 java.io.PrintWriter 物件。要從連線的另一端接受位元組流,你可以呼叫 Socket 類的 getInputStream 方法用來返回一個 java.io.InputStream 物件。

ServerSocket 類

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);

應用程式

如果同學們下載過我們在第一篇文章提供的原始碼(How Tomcat Works)的話, 我們可以看一看我們的目錄:

我們的 web 伺服器應用程式放在 cxs01.pyrmont(編譯的時候因為錯誤改名字了,也就懶得改回來了) 包裡邊,由三個類組成:
* HttpServer
* Request
* Response

這個應用程式的入口點(靜態 main 方法)可以在 HttpServer 類裡邊找到。main 方法建立了 一個 HttpServer 的例項並呼叫了它的 await 方法。await 方法,顧名思義就是在一個指定的端 口上等待 HTTP 請求,處理它們併發送響應返回客戶端。它一直等待直至接收到 shutdown 命令。

應用程式不能做什麼,除了傳送靜態資源,例如放在一個特定目錄的 HTML 檔案和影象檔案。 它也在控制檯上顯示傳入的 HTTP 請求的位元組流。不過,它不給瀏覽器傳送任何的頭部例如日期 或者 cookies。

下面我們來看看我們今天的重點,這三個類,也就是tomcat的雛形程式碼
HttpServer 類

HttpServer 類代表一個 web 伺服器,也就是程式的入口,看程式碼:

public class HttpServer {
  public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator  + "webroot";

// 關閉命令
private static final String SHUTDOWN_COMMAND = “/SHUTDOWN”;

// 是否關閉
private boolean shutdown = false;

public static void main(String[] args) {
HttpServer server = new HttpServer();
server.await();
}

main 方法中建立了一個HttpServer物件,並呼叫了該物件的await方法。看名字,該方法應該是等待http請求之類的東東。我們來看看方法內部:

public void await() {
    ServerSocket serverSocket = null;
    int port = 8080;
    try {
      // 建立一個socket伺服器
      serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
    }
    catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }
<span class="hljs-comment">// 迴圈等待http請求</span>
<span class="hljs-keyword">while</span> (!shutdown) {
  Socket socket = <span class="hljs-keyword">null</span>;
  InputStream input = <span class="hljs-keyword">null</span>;
  OutputStream output = <span class="hljs-keyword">null</span>;
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// 阻塞等待http請求</span>
    socket = serverSocket.accept();
    input = socket.getInputStream();
    output = socket.getOutputStream();

    <span class="hljs-comment">// 建立一個Request物件用於解析http請求內容</span>
    Request request = <span class="hljs-keyword">new</span> Request(input);
    request.parse();

    <span class="hljs-comment">// 建立一個Response 物件,用於傳送靜態文字</span>
    Response response = <span class="hljs-keyword">new</span> Response(output);
    response.setRequest(request);
    response.sendStaticResource();

    <span class="hljs-comment">// 關閉流</span>
    socket.close();

    <span class="hljs-comment">//檢查URI中是否有關閉命令</span>
    shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
  }
  <span class="hljs-keyword">catch</span> (Exception e) {
    e.printStackTrace();
    <span class="hljs-keyword">continue</span>;
  }
}

}

我們看到,該方法建立了一個Socket伺服器,並迴圈阻塞監聽http請求,當有http請求到來時, 該方法便建立一個Request物件,構造引數是socket獲取的輸入流物件, 用於讀取客戶端請求的資料並解析。 然後再建立一個Response物件,構造引數是socket的輸出流物件, 並含有一個Request物件的成員變數。Response物件用於將靜態頁面傳送給瀏覽器或者是其他的客戶端。最後, 該方法校驗請求中是否含有關閉命令的字串,如果有,就停止伺服器的執行。

這就是一個簡單的伺服器, 當我第一次看到的時候,心想: 真TMD簡單啊。原來沒那麼複雜嘛。我想同學們心裡想的跟我也一樣吧。so, 不論多麼龐大的程式碼,底層原理都是很簡單的,只要我們學好了基礎,學習起來就會輕鬆很多。

廢話不多說,我們繼續看看Request 是如何解析Http請求的吧。

Request 類

類結構圖如下:
image

Request 類代表一個 HTTP 請求。從負責與客戶端通訊的 Socket 中傳遞過來 InputStream 物件來構造這個類的一個例項。你呼叫 InputStream 物件其中一個 read 方法來獲 取 HTTP 請求的原始資料。其中最主要的方法就是parse 和 parseUri ,他們用於逐個解析每個從客戶端傳遞過來的位元組,我們先看parse方法:

  public void parse() {
    // Read a set of characters from the 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<i; j++) {
     // 將每個位元組轉換為字元
      request.append((char) buffer[j]);
    }
    // 列印字串
    System.out.print(request.toString());
    // 根據轉換出來的字元解析URI
    uri = parseUri(request.toString());
  }

我們也看到該方法是十分的簡單, 建立一個StringBuffer 物件,然後從流中讀取位元組,然後迴圈將位元組轉成字元寫入到Stringbuffer物件中。最後傳入到parseUri方法中進行解析。

我們再看看parseUri方法, 這個方法中,我們前面學習的關於HTTP的知識會起作用:

  private String parseUri(String requestString) {
    int index1, index2;
    // 找到第一個空格
    index1 = requestString.indexOf(' ');
    if (index1 != -1) {
      // 找到第二個空格
      index2 = requestString.indexOf(' ', index1 + 1);
      if (index2 > index1)
        // 擷取第一個空格到第二個空格之間的內容
        return requestString.substring(index1 + 1, index2);
    }
    return null;
  }

該方法從請求行裡邊獲得 URI。parseUri 方法搜尋請求裡邊的第一個和第二個空格並從中獲取 URI。
為什麼是第一個空格和第二個空格之間的內容呢?我們看看前面的Http請求的例子:

POST /examples/default.jsp HTTP/1.1 
Accept: text/plain; text/html 
Accept-Language: en-gb 
Connection: Keep-Alive 
Host: localhost 
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
Content-Length: 33 Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate 

lastName=Franks&firstName=Michael

我們看第一行:

POST 和 HTTP/1.1之間的就是我們需要的URI。so, 我們只需要將中間那段字串擷取就OK了。

我們總結一下Request類,這個類其實就是解析HTTP 訊息頭內容的,先將流中資料轉成位元組,然後將轉成字元,最後將字元解析,得到自己感興趣的內容。奏是這麼簡單。好了,我們再看看Response類。看看他是怎麼實現的。

Response類

我們先看看這個類的結構圖:

Response 代表了Http請求中的一個響應。我們關注其中的 sendStaticResource 方法,看名字,該方法應該是傳送靜態資源給客戶端。我們看看程式碼:

  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" +
          "<h1>File Not Found</h1>";
        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();
    }
  }

可以看到,該方法也非常的簡單, sendStaticResource 方法是用來發送一個靜態資源,例如一個 HTML 檔案。它首先通過傳遞 上一級目錄的路徑和子路徑給 File 累的構造方法來例項化 java.io.File 類。

然後它檢查該檔案是否存在。假如存在的話,通過傳遞 File 物件讓 sendStaticResource 構造一個 java.io.FileInputStream 物件。然後,它呼叫 FileInputStream 的 read 方法並把字 節陣列寫入 OutputStream 物件。請注意,這種情況下,靜態資源是作為原始資料傳送給瀏覽器 的。

假如檔案並不存在,sendStaticResource 方法傳送一個錯誤資訊到瀏覽器

執行程式,啟動HttpServer mian方法,使用Edge瀏覽器在位址列敲入:http://localhost:8080/index.html
返回:

表示檔案存在, 再看看我們的後臺控制檯:

如期列印了http請求頭中的內容。並且下面還請求了一張圖片。

總結

至此,我們已經知道了一個簡單的Web伺服器是如何工作的。破除了我們之前的疑惑,實際上tomcat底層就是這麼實現的,可能關於阻塞IO和非阻塞NIO會有區別,但總體上還是這個思路,然後其餘的元件都是針對優化效能,提高擴充套件性來設計新的架構。所以,我們明白了底層設計,再去學習他的設計,就不會那麼迷茫。從而感到洩氣。畢竟每個夜晚,我們孤獨的學習,不想徒勞無功。

好了,本文結束!!! good luck !!!