探秘Tomcat——從一個簡陋的Web服務器開始


前言:

  無論是之前所在實習單位小到一個三五個人做的項目,還是如今一個在做的百人以上的產品,一直都能看到tomcat的身影。工作中經常遇到的操作就是啟動和關閉tomcat服務,或者修改了摸個java文件,編譯該文件,將生成的class文件塞到tomcat目錄下相應的jar包中去,以使其生效,但是也可以熱部署,不需要這么繁瑣的操作。

  總之,一直以來都是習慣了tomcat的存在,沒有深究tomcat的運行機制和原理,上一次對於tomcat源碼的躍躍欲試還是去年的事兒了——《探秘Tomcat(一)——Myeclipse中導入Tomcat源碼》。在這篇文章中,我下載了tomcat6版本的代碼,並將其導入到eclipse中,時隔一年多,原來的項目還在,但是為顯誠意,我還是重頭做了一遍導入tomcat源碼到eclipse的操作。

  對於tomcat的崇敬之情讓我決定深入其中,一探究竟。於是我找到了網上大家首推的教材——《深入剖析tomcat》,書比較老,但是很權威,不影響原理性東西的理解。目前已經看到第二章。

 

  讀過或者了解該書的應該都知道,這不是一本上來就直接告訴你tomcat的設計思想,用到的什么設計模式或者源碼中某一行有什么匠心獨運的地方。該書采用一個循序漸進的方式從一個簡單的不能再簡單的servlet容器開始,之后慢慢豐富,添加功能模塊,最終形成我們想知道的tomcat的模樣。

 

背景知識:

  • HTTP請求:

    請求方法——統一資源標識符URI——協議/版本

    請求頭

    實體

比如這里我們可以看到請求的方法Request Method是GET, Request URL為http://tech.qq.com/a/20160604/007535.htm,並且分別有Request和下面要講的Response的請求頭信息,如Content-Type等。

 

  • HTTP共支持7中請求方法

    GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE

 

  • HTTP響應

    協議——狀態碼——描述

    響應頭

    響應實體段

 

  • Socket

    socket在應用程序中用於從網絡中讀取數據,實現不同計算機之間的通訊,實現一個socket需要知道對應應用程序的ip地址和端口號。

    首先創建該socket類的實例,有了該實例后,就可以使用它實現發送或接收字節流。如果想要發送字節流,需要調用socket類的getOutputStream來獲取一個java.io.OutputStream對象;要發送文本到遠程應用程序,需要使用返回的OutputStream對象創建一個java.io.PrintWriter對象;要從連接的另一端接收字節流,需要調用Socket類的getInputStream方法,其會返回一個java.io.InputStream對象。

 

  • ServerSocket類

    有了客戶端的socket可以發送請求,如果沒有服務端來響應,發送的請求也是肉包子打狗,ServerSocket就是充當服務端的角色,serversocket出於隨時待命的狀態,一旦有客戶端發出請求,serversocket就要給出響應,從而實現與客戶端的通信。

 

請求響應模型

  有了以上的背景知識,我們就可以實現一個簡單到爆的通訊模型,新建一個socket客戶端通訊類,用於發送和接收數據,還需要創建一個服務端的ServerSocket用於監聽和響應客戶端的請求。

  主要包含以下三個類:

  HttpServer:模擬一個Web服務器

 

package myTest;

import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;

public class HttpServer {

  /** WEB_ROOT is the directory where our HTML and other files reside.
   *  For this package, WEB_ROOT is the "webroot" directory under the working
   *  directory.
   *  The working directory is the location in the file system
   *  from where the java command was invoked.
   */
  public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator  + "webroot";

  // shutdown command
  private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

  // the shutdown command received
  private boolean shutdown = false;

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

  public void await() {
    ServerSocket serverSocket = null;
    int port = 8080;
    try {
      serverSocket =  new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
    }
    catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }

    // Loop waiting for a request
    while (!shutdown) {
      Socket socket = null;
      InputStream input = null;
      OutputStream output = null;
      try {
        socket = serverSocket.accept();
        input = socket.getInputStream();
        output = socket.getOutputStream();

        // create Request object and parse
        Request request = new Request(input);
        request.parse();

        // create Response object
        Response response = new Response(output);
        response.setRequest(request);
        response.sendStaticResource();

        // Close the socket
        socket.close();

        //check if the previous URI is a shutdown command
        shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
      }
      catch (Exception e) {
        e.printStackTrace();
        continue;
      }
    }
  }
}

  

從代碼可以看出:

  1. 類中分別創建了socket和serversocket;
  2. 定義了一個WEB_ROOT目錄,其中存放了對應請求的相應結果文件;
  3. 定義了一個關閉命令,通過在瀏覽器中輸入類似.../SHUTDOWN來關閉Web服務器
  4. 創建了一個await方法,一直監聽127.0.0.1的8080端口,如果有請求產生(比如在瀏覽器中輸入一個請求地址),則會進入await方法並執行serverSocket的accept方法。

 

  Request類:

  模擬一個HTTP請求。

 

package myTest;

import java.io.InputStream;
import java.io.IOException;

public class Request {

  private InputStream input;
  private String uri;

  public Request(InputStream input) {
    this.input = input;
  }

  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 = parseUri(request.toString());
  }

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

  public String getUri() {
    return uri;
  }

}

  

從代碼中可以發現:

  1. 可以實現傳遞InputStream對象,在處理與客戶端通訊的Socket對象中獲取;
  2. 調用InputStream對象的read來獲取HTTP請求的原始數據;
  3. parse方法用於解析HTTP請求中的原始數據(原始數據由上面的getInputStream中獲得);
  4. parseUri作為一個私有方法被parse調用,用於解析HTTP請求的URI

 

  Response類:

  模擬HTTP的響應。

 

package myTest;

import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;

/*
  HTTP Response = Status-Line
    *(( general-header | response-header | entity-header ) CRLF)
    CRLF
    [ message-body ]
    Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/

public class Response {

  private static final int BUFFER_SIZE = 1024;
  Request request;
  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(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 {
        // file not found
        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();
    }
  }
}

  

從代碼可以發現:

  1. Request類似,這里通過接受OutputStream構造了Response對象;
  2. setRequest方法用於接收Request對象,因為在Response中需要用到Request的getUri方法;
  3. sendStaticResource方法主要用於處理請求的響應,如這里發送一個靜態資源html作為請求的結果

 

至此, 本篇主要提到:

  • 一些基本概念如http請求、http響應、socket等;
  • 對於一個超級簡陋的web服務器有了基本的認識;
  • 明確了客戶端和服務端各自的角色和職責。

 

如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。

 

  


友情贊助

如果你覺得博主的文章對你那么一點小幫助,恰巧你又有想打賞博主的小沖動,那么事不宜遲,趕緊掃一掃,小額地贊助下,攢個奶粉錢,也是讓博主有動力繼續努力,寫出更好的文章^^。

    1. 支付寶                          2. 微信

                      

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM