深入理解 Tomcat(三)Tomcat 底層實(shí)現(xiàn)原理

又是一個(gè)周末,這篇文章將從一個(gè)簡單的例子來理解tomcat的底層設(shè)計(jì);

本文將介紹 Java Web 服務(wù)器是如何運(yùn)行的, Web 服務(wù)器也稱為超文本傳輸協(xié)議( HyperText Transfer Protocol, HTTP)服務(wù)器, 因?yàn)樗褂?Http 與其客戶端(通常是 Web 瀏覽器)進(jìn)行通信, 基于 Java 的 Web 服務(wù)器會使用兩個(gè)重要的類: java.net.Socket 類和 java.net.ServerSocket 類, 并通過發(fā)送 Http 消息進(jìn)行通信. 我們先花一些篇幅介紹 Http 協(xié)議(如果同學(xué)們熟悉HTTP協(xié)議可直接跳過)和這兩個(gè)類, 然后寫一個(gè)簡單的 Web 服務(wù)器.

HTTP

Http : Http 允許 Web 服務(wù)器和瀏覽器通過 Internet 發(fā)送并接受數(shù)據(jù), 是一種基于"請求---響應(yīng)"的協(xié)議, 客戶端請求一個(gè)文件, 服務(wù)器端對該請求進(jìn)行響應(yīng). Http 使用可靠的 tcp 連接, tcp 協(xié)議默認(rèn)使用 tcp 80端口, http協(xié)議的第一個(gè)版本是 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 中, 總是由客戶端通過建立連接并發(fā)送 http 請求來初始化一個(gè)事務(wù)的. Web 服務(wù)器端并不負(fù)責(zé)聯(lián)系客戶端或建立一個(gè)到客戶端的回調(diào)連接.客戶端或服務(wù)器端可提前關(guān)閉連接, 例如, 當(dāng)使用 Web 瀏覽器瀏覽網(wǎng)頁時(shí), 可以單擊瀏覽器上的 stop 按鈕來停止下載文件, 這樣就有效的關(guān)閉了一個(gè) Web 服務(wù)器的 http 連接.

HTTP 請求

一個(gè) HTTP 請求包含以下三部分:

  • 請求方法----統(tǒng)一資源標(biāo)識符(Uniform Resource Identifier, URI)------協(xié)議/版本
  • 請求頭
  • 實(shí)體

下面是一個(gè) 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  

方法—統(tǒng)一資源標(biāo)識符(URI)—協(xié)議/版本出現(xiàn)在請求的第一行。
POST /examples/default.jsp HTTP/1.1

這里 POST 是請求方法,/examples/default.jsp 是 URI,而 HTTP/1.1 是協(xié)議/版本部分。 每個(gè) HTTP 請求可以使用 HTTP 標(biāo)準(zhǔn)里邊提到的多種方法之一。HTTP 1.1 支持 7 種類型的請 求:GET, POST, HEAD, OPTIONS, PUT, DELETE 和 TRACE。GET 和 POST 在互聯(lián)網(wǎng)應(yīng)用里邊最普遍使用的。

URI 完全指明了一個(gè)互聯(lián)網(wǎng)資源。URI 通常是相對服務(wù)器的根目錄解釋的。因此,始終一斜 線/開頭。統(tǒng)一資源定位器(URL)其實(shí)是一種 URI(查看 http://www.ietf.org/rfc/rfc2396.txt)
來的。該協(xié)議版本代表了正在使用的 HTTP 協(xié)議的版本。

請求的頭部包含了關(guān)于客戶端環(huán)境和請求的主體內(nèi)容的有用信息。例如它可能包括瀏覽器設(shè) 置的語言,主體內(nèi)容的長度等等。每個(gè)頭部通過一個(gè)回車換行符(CRLF)來分隔的。

對于 HTTP 請求格式來說,頭部和主體內(nèi)容之間有一個(gè)回車換行符(CRLF)是相當(dāng)重要的。CRLF 告訴HTTP服務(wù)器主體內(nèi)容是在什么地方開始的。在一些互聯(lián)網(wǎng)編程書籍中,CRLF還被認(rèn)為是HTTP 請求的第四部分。

在前面一個(gè) HTTP 請求中,主體內(nèi)容只不過是下面一行:

lastName=Franks&firstName=Michael

實(shí)體內(nèi)容在一個(gè)典型的 HTTP 請求中可以很容易的變得更長。

HTTP 響應(yīng)

類似于 HTTP 請求,一個(gè) HTTP 響應(yīng)也包括三個(gè)組成部分:

  • 方法—統(tǒng)一資源標(biāo)識符(URI)—協(xié)議/版本
  • 響應(yīng)的頭部
  • 主體內(nèi)容

下面是一個(gè) HTTP 響應(yīng)的例子:

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> 

響應(yīng)頭部的第一行類似于請求頭部的第一行。第一行告訴你該協(xié)議使用 HTTP 1.1,請求成 功(200=成功),表示一切都運(yùn)行良好。

響應(yīng)頭部和請求頭部類似,也包括很多有用的信息。響應(yīng)的主體內(nèi)容是響應(yīng)本身的 HTML 內(nèi) 容。頭部和主體內(nèi)容通過 CRLF 分隔開來。

Socket 類

套接字是網(wǎng)絡(luò)連接的一個(gè)端點(diǎn)。套接字使得一個(gè)應(yīng)用可以從網(wǎng)絡(luò)中讀取和寫入數(shù)據(jù)。放在兩 個(gè)不同計(jì)算機(jī)上的兩個(gè)應(yīng)用可以通過連接發(fā)送和接受字節(jié)流。為了從你的應(yīng)用發(fā)送一條信息到另 一個(gè)應(yīng)用,你需要知道另一個(gè)應(yīng)用的 IP 地址和套接字端口。在 Java 里邊,套接字指的是java.net.Socket類。

要?jiǎng)?chuàng)建一個(gè)套接字,你可以使用 Socket 類眾多構(gòu)造方法中的一個(gè)。其中一個(gè)接收主機(jī)名稱 和端口號:

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

在這里主機(jī)是指遠(yuǎn)程機(jī)器名稱或者 IP 地址,端口是指遠(yuǎn)程應(yīng)用的端口號。例如,要連接 yahoo.com 的 80 端口,你需要構(gòu)造以下的 Socket 對象:

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

一旦你成功創(chuàng)建了一個(gè) Socket 類的實(shí)例,你可以使用它來發(fā)送和接受字節(jié)流。要發(fā)送字節(jié) 流,你首先必須調(diào)用Socket類的getOutputStream方法來獲取一個(gè)java.io.OutputStream對象。 要 發(fā) 送 文 本 到 一 個(gè) 遠(yuǎn) 程 應(yīng) 用 , 你 經(jīng) 常 要 從 返 回 的 OutputStream 對 象 中 構(gòu) 造 一 個(gè) java.io.PrintWriter 對象。要從連接的另一端接受字節(jié)流,你可以調(diào)用 Socket 類的 getInputStream 方法用來返回一個(gè) java.io.InputStream 對象。

ServerSocket 類

Socket 類代表一個(gè)客戶端套接字,即任何時(shí)候你想連接到一個(gè)遠(yuǎn)程服務(wù)器應(yīng)用的時(shí)候你構(gòu) 造的套接字,現(xiàn)在,假如你想實(shí)施一個(gè)服務(wù)器應(yīng)用,例如一個(gè) HTTP 服務(wù)器或者 FTP 服務(wù)器,你 需要一種不同的做法。這是因?yàn)槟愕姆?wù)器必須隨時(shí)待命,因?yàn)樗恢酪粋€(gè)客戶端應(yīng)用什么時(shí) 候會嘗試去連接它。為了讓你的應(yīng)用能隨時(shí)待命,你需要使用 java.net.ServerSocket 類。這是 服務(wù)器套接字的實(shí)現(xiàn)。

ServerSocket 和 Socket 不同,服務(wù)器套接字的角色是等待來自客戶端的連接請求。一旦服 務(wù)器套接字獲得一個(gè)連接請求,它創(chuàng)建一個(gè) Socket 實(shí)例來與客戶端進(jìn)行通信。

要?jiǎng)?chuàng)建一個(gè)服務(wù)器套接字,你需要使用 ServerSocket 類提供的四個(gè)構(gòu)造方法中的一個(gè)。你 需要指定 IP 地址和服務(wù)器套接字將要進(jìn)行監(jiān)聽的端口號。通常,IP 地址將會是 127.0.0.1,也 就是說,服務(wù)器套接字將會監(jiān)聽本地機(jī)器。服務(wù)器套接字正在監(jiān)聽的 IP 地址被稱為是綁定地址。 服務(wù)器套接字的另一個(gè)重要的屬性是 backlog,這是服務(wù)器套接字開始拒絕傳入的請求之前,傳 入的連接請求的最大隊(duì)列長度。

其中一個(gè) ServerSocket 類的構(gòu)造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress); 

應(yīng)用程序

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

我們的 web 服務(wù)器應(yīng)用程序放在 cxs01.pyrmont(編譯的時(shí)候因?yàn)殄e(cuò)誤改名字了,也就懶得改回來了) 包里邊,由三個(gè)類組成:

  • HttpServer
  • Request
  • Response

這個(gè)應(yīng)用程序的入口點(diǎn)(靜態(tài) main 方法)可以在 HttpServer 類里邊找到。main 方法創(chuàng)建了 一個(gè) HttpServer 的實(shí)例并調(diào)用了它的 await 方法。await 方法,顧名思義就是在一個(gè)指定的端 口上等待 HTTP 請求,處理它們并發(fā)送響應(yīng)返回客戶端。它一直等待直至接收到 shutdown 命令。

應(yīng)用程序不能做什么,除了發(fā)送靜態(tài)資源,例如放在一個(gè)特定目錄的 HTML 文件和圖像文件。 它也在控制臺上顯示傳入的 HTTP 請求的字節(jié)流。不過,它不給瀏覽器發(fā)送任何的頭部例如日期 或者 cookies。

下面我們來看看我們今天的重點(diǎn),這三個(gè)類,也就是tomcat的雛形代碼
HttpServer 類

HttpServer 類代表一個(gè) web 服務(wù)器,也就是程序的入口,看代碼:

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

  // 關(guān)閉命令
  private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

  // 是否關(guān)閉
  private boolean shutdown = false;

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

main 方法中創(chuàng)建了一個(gè)HttpServer對象,并調(diào)用了該對象的await方法??疵郑摲椒☉?yīng)該是等待http請求之類的東東。我們來看看方法內(nèi)部:

public void await() {
    ServerSocket serverSocket = null;
    int port = 8080;
    try {
      // 創(chuàng)建一個(gè)socket服務(wù)器
      serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
    }
    catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }

    // 循環(huán)等待http請求
    while (!shutdown) {
      Socket socket = null;
      InputStream input = null;
      OutputStream output = null;
      try {
        // 阻塞等待http請求
        socket = serverSocket.accept();
        input = socket.getInputStream();
        output = socket.getOutputStream();

        // 創(chuàng)建一個(gè)Request對象用于解析http請求內(nèi)容
        Request request = new Request(input);
        request.parse();

        // 創(chuàng)建一個(gè)Response 對象,用于發(fā)送靜態(tài)文本
        Response response = new Response(output);
        response.setRequest(request);
        response.sendStaticResource();

        // 關(guān)閉流
        socket.close();

        //檢查URI中是否有關(guān)閉命令
        shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
      }
      catch (Exception e) {
        e.printStackTrace();
        continue;
      }
    }
  }

我們看到,該方法創(chuàng)建了一個(gè)Socket服務(wù)器,并循環(huán)阻塞監(jiān)聽http請求,當(dāng)有http請求到來時(shí), 該方法便創(chuàng)建一個(gè)Request對象,構(gòu)造參數(shù)是socket獲取的輸入流對象, 用于讀取客戶端請求的數(shù)據(jù)并解析。 然后再創(chuàng)建一個(gè)Response對象,構(gòu)造參數(shù)是socket的輸出流對象, 并含有一個(gè)Request對象的成員變量。Response對象用于將靜態(tài)頁面發(fā)送給瀏覽器或者是其他的客戶端。最后, 該方法校驗(yàn)請求中是否含有關(guān)閉命令的字符串,如果有,就停止服務(wù)器的運(yùn)行。

這就是一個(gè)簡單的服務(wù)器, 當(dāng)我第一次看到的時(shí)候,心想: 真TMD簡單啊。原來沒那么復(fù)雜嘛。我想同學(xué)們心里想的跟我也一樣吧。so, 不論多么龐大的代碼,底層原理都是很簡單的,只要我們學(xué)好了基礎(chǔ),學(xué)習(xí)起來就會輕松很多。

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

Request 類

類結(jié)構(gòu)圖如下:


image

Request 類代表一個(gè) HTTP 請求。從負(fù)責(zé)與客戶端通信的 Socket 中傳遞過來 InputStream 對象來構(gòu)造這個(gè)類的一個(gè)實(shí)例。你調(diào)用 InputStream 對象其中一個(gè) read 方法來獲 取 HTTP 請求的原始數(shù)據(jù)。其中最主要的方法就是parse 和 parseUri ,他們用于逐個(gè)解析每個(gè)從客戶端傳遞過來的字節(jié),我們先看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 {
      // 讀取流中內(nèi)容
      i = input.read(buffer);
    }
    catch (IOException e) {
      e.printStackTrace();
      i = -1;
    }
    for (int j=0; j<i; j++) {
     // 將每個(gè)字節(jié)轉(zhuǎn)換為字符
      request.append((char) buffer[j]);
    }
    // 打印字符串
    System.out.print(request.toString());
    // 根據(jù)轉(zhuǎn)換出來的字符解析URI
    uri = parseUri(request.toString());
  }

我們也看到該方法是十分的簡單, 創(chuàng)建一個(gè)StringBuffer 對象,然后從流中讀取字節(jié),然后循環(huán)將字節(jié)轉(zhuǎn)成字符寫入到Stringbuffer對象中。最后傳入到parseUri方法中進(jìn)行解析。

我們再看看parseUri方法, 這個(gè)方法中,我們前面學(xué)習(xí)的關(guān)于HTTP的知識會起作用:

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

該方法從請求行里邊獲得 URI。parseUri 方法搜索請求里邊的第一個(gè)和第二個(gè)空格并從中獲取 URI。
為什么是第一個(gè)空格和第二個(gè)空格之間的內(nèi)容呢?我們看看前面的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了。

我們總結(jié)一下Request類,這個(gè)類其實(shí)就是解析HTTP 消息頭內(nèi)容的,先將流中數(shù)據(jù)轉(zhuǎn)成字節(jié),然后將轉(zhuǎn)成字符,最后將字符解析,得到自己感興趣的內(nèi)容。奏是這么簡單。好了,我們再看看Response類??纯此窃趺磳?shí)現(xiàn)的。

Response類

我們先看看這個(gè)類的結(jié)構(gòu)圖:


Response 代表了Http請求中的一個(gè)響應(yīng)。我們關(guān)注其中的 sendStaticResource 方法,看名字,該方法應(yīng)該是發(fā)送靜態(tài)資源給客戶端。我們看看代碼:

  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 方法是用來發(fā)送一個(gè)靜態(tài)資源,例如一個(gè) HTML 文件。它首先通過傳遞 上一級目錄的路徑和子路徑給 File 累的構(gòu)造方法來實(shí)例化 java.io.File 類。

然后它檢查該文件是否存在。假如存在的話,通過傳遞 File 對象讓 sendStaticResource 構(gòu)造一個(gè) java.io.FileInputStream 對象。然后,它調(diào)用 FileInputStream 的 read 方法并把字 節(jié)數(shù)組寫入 OutputStream 對象。請注意,這種情況下,靜態(tài)資源是作為原始數(shù)據(jù)發(fā)送給瀏覽器 的。

假如文件并不存在,sendStaticResource 方法發(fā)送一個(gè)錯(cuò)誤信息到瀏覽器

運(yùn)行程序,啟動HttpServer mian方法,使用Edge瀏覽器在地址欄敲入:http://localhost:8080/index.html
返回:

表示文件存在, 再看看我們的后臺控制臺:


如期打印了http請求頭中的內(nèi)容。并且下面還請求了一張圖片。

總結(jié)

至此,我們已經(jīng)知道了一個(gè)簡單的Web服務(wù)器是如何工作的。破除了我們之前的疑惑,實(shí)際上tomcat底層就是這么實(shí)現(xiàn)的,可能關(guān)于阻塞IO和非阻塞NIO會有區(qū)別,但總體上還是這個(gè)思路,然后其余的組件都是針對優(yōu)化性能,提高擴(kuò)展性來設(shè)計(jì)新的架構(gòu)。所以,我們明白了底層設(shè)計(jì),再去學(xué)習(xí)他的設(shè)計(jì),就不會那么迷茫。從而感到泄氣。畢竟每個(gè)夜晚,我們孤獨(dú)的學(xué)習(xí),不想徒勞無功。

好了,本文結(jié)束!?。?good luck ?。。?/p>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,506評論 19 139
  • 1、OC中創(chuàng)建線程的方法是什么?如果指定在主線程中執(zhí)行代碼?如何延時(shí)執(zhí)行代碼?!倦y度系數(shù)★★】 1)創(chuàng)建線程的方法...
    木旁_G閱讀 2,077評論 2 16
  • 多線程、特別是NSOperation 和 GCD 的內(nèi)部原理。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場景。SDWebImage的原...
    LZM輪回閱讀 2,108評論 0 12
  • 參考:http://www.2cto.com/net/201611/569006.html TCP HTTP UD...
    F麥子閱讀 3,067評論 0 14
  • 審美觀的審美對象范圍不只局限在繪畫和藝術(shù)里,因?yàn)槊朗菬o處不在的。一個(gè)人審美觀的形成伴隨著他的成長,隨著他越長越大,...
    lilywright閱讀 200評論 0 0

友情鏈接更多精彩內(nèi)容