1. 程式人生 > 其它 >半自動化運維之動態新增資料檔案(一) (r5筆記第55天)

半自動化運維之動態新增資料檔案(一) (r5筆記第55天)

尊重原創版權: https://www.gewuweb.com/hot/9295.html

Tomcat - 如何設計一個簡單的web容器

在學習Tomcat前,很多人先入為主地對它的認知是巨複雜的;所以第一步,在學習它之前,要打破這種觀念,我們通過學習如何設計一個最基本的web容器來看它需要考慮什麼;進而在真正學習Tomcat時,多把重點放在它的頂層設計上,而不是某一塊程式碼上,
思路永遠比具體實現重要的多。

  • Tomcat - 如何設計一個簡單的web容器
    • 寫在前面
    • 基礎認知:如何實現伺服器和客戶端(瀏覽器)的互動
      • HTTP協議
      • Socket
      • SeverSocket
    • 一個簡單web容器的設計和實現:對靜態資源
      • 元件設計
      • 元件實現
    • 一個簡單web容器的設計和實現:對Servelet
      • 元件設計
      • 元件實現
      • 利用外觀模式改造
    • 總結

基礎認知:如何實現伺服器和客戶端(瀏覽器)的互動

客戶端和伺服器端之間的互動是通過Socket來實現的,它屬於應用層的協議。

¶ HTTP協議

http協議相關的內容可以參看這裡:網路協議 - HTTP 協議詳解

¶ Socket

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

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

public Socket (java.lang.String host, int port)
  
        @pdai: 程式碼已經複製到剪貼簿

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

new Socket ("yahoo.com", 80);
  
        @pdai: 程式碼已經複製到剪貼簿

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

¶ SeverSocket

Socket 類代表一個 客戶端套接字 ,即任何時候你想連線到一個遠端伺服器應用的時候你構造的套接字,現在,假如你想實施一個伺服器應用,例如一個
HTTP 伺服器或者 FTP
伺服器,你需要一種不同的做法。這是因為你的伺服器必須隨時待命,因為它不知道一個客戶端應用什麼時候會嘗試去連線它。為了讓你的應用能隨時待命,你需要使用
java.net.ServerSocket 類。這是 伺服器套接字 的實現。

ServerSocket 和 Socket 不同,伺服器套接字的角色是等待來自客戶端的連線請求。 一旦伺服器套接字獲得一個連線請求,它建立一個
Socket 例項來與客戶端進行通訊

要建立一個伺服器套接字,你需要使用 ServerSocket 類提供的四個構造方法中的一個。你 需要指定 IP
地址和伺服器套接字將要進行監聽的埠號。通常,IP 地址將會是 127.0.0.1,也 就是說,伺服器套接字將會監聽本地機器。伺服器套接字正在監聽的 IP
地址被稱為是繫結地址。 伺服器套接字的另一個重要的屬性是 backlog,這是伺服器套接字開始拒絕傳入的請求之前,傳 入的連線請求的最大佇列長度。

其中一個 ServerSocket 類的構造方法如下所示:

public ServerSocket(int port, int backLog, InetAddress bindingAddress);
  
        @pdai: 程式碼已經複製到剪貼簿

¶ 一個簡單web容器的設計和實現:對靜態資源

注意:當你跑如下程式時,可能會由於瀏覽器新版本不再支援的HTTP 0.9協議,而造成瀏覽器頁面沒有返回資訊。

¶ 元件設計

根據上述的基礎,我們可以看到,我們只需要提供三個最基本的類,分別是:

  • Request - 表示請求,這裡表示瀏覽器發起的HTTP請求
  • HttpServer - 表示處理請求的伺服器,同時這裡使用我們上面鋪墊的ServerSocket
  • Reponse - 表示處理請求後的響應, 這裡表示伺服器對HTTP請求的響應結果

¶ 元件實現

從上圖中我們可以看到,組織這幾個類的入口在Server的啟動方法中,即main方法中, 所以我們透過main方法從Server類進行分析:

  • Server是如何啟動的?

    public class HttpServer {

    // 存放靜態資源的位置
    public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator + "webroot";

    // 關閉Server的請求
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";

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

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

    public void await() {
    // 啟動ServerSocket
    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);
    }

    // 迴圈等待一個Request請求
    while (!shutdown) {
      Socket socket = null;
      InputStream input = null;
      OutputStream output = null;
      try {
        // 建立socket
        socket = serverSocket.accept();
        input = socket.getInputStream();
        output = socket.getOutputStream();
    
        // 封裝input至request, 並處理請求
        Request request = new Request(input);
        request.parse();
    
        // 封裝output至response
        Response response = new Response(output);
        response.setRequest(request);
        response.sendStaticResource();
    
        // 關閉socket
        socket.close();
    
        // 如果接受的是關閉請求,則設定關閉監聽request的標誌
        shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
      }
      catch (Exception e) {
        e.printStackTrace();
        continue;
      }
    }
    

    }
    }

        @pdai: 程式碼已經複製到剪貼簿
    
  • Request請求是如何封裝和處理的?

    public class Request {

    private InputStream input;
    private String uri;

    // 初始化Request
    public Request(InputStream input) {
    this.input = input;
    }

    // 處理request的方法
    public void parse() {
    // 從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());

    // 獲得兩個空格之間的內容, 這裡將是HttpServer.WEB_ROOT中靜態檔案的檔名稱
    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;
    }

    }

        @pdai: 程式碼已經複製到剪貼簿
    
  • Response中響應了什麼?

    public class Response {

    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;

    public Response(OutputStream output) {
    this.output = output;
    }

    // response中封裝了request,以便獲取request中的請求引數
    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 {
    // 檔案不存在時,輸出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" +
    "

    File Not Found

    ";
    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();
    }
    }
    }
        @pdai: 程式碼已經複製到剪貼簿
    
  • 啟動輸出

當我們run上面HttpServer中的main方法之後,我們就可以開啟瀏覽器http://localhost:8080,
後面新增引數看返回webroot目錄中靜態檔案的內容了(比如這裡我加了hello.txt檔案到webroot下,並訪問http://localhost:8080/hello.txt)。

¶ 一個簡單web容器的設計和實現:對Servelet

上面這個例子是不是很簡單?是否打破了對一個簡單http伺服器的認知,減少了對它的恐懼。

但是上述的例子中只處理了靜態資源,我們如果要處理Servlet呢?

¶ 元件設計

不難發現,我們只需要在HttpServer只需要請求的處理委託給ServletProcessor, 讓它接受請求,並處理Response即可。

¶ 元件實現

  • 在HttpServer中

    public void await() {
    //....

        // create Response object
        Response response = new Response(output);
        response.setRequest(request);
    
        // 不再有response自己處理
        //response.sendStaticResource();
    
        // 而是如果以/servlet/開頭,則委託ServletProcessor處理
        if (request.getUri().startsWith("/servlet/")) {
          ServletProcessor1 processor = new ServletProcessor1();
          processor.process(request, response);
        } else {
          // 原有的靜態資源處理
          StaticResourceProcessor processor = new StaticResourceProcessor();
          processor.process(request, response);
        }
    
    // ....
    

    }

        @pdai: 程式碼已經複製到剪貼簿
    
  • ServletProcessor 如何處理的?

    public class ServletProcessor1 {

    public void process(Request request, Response response) {

    // 獲取servlet名字
    String uri = request.getUri();
    String servletName = uri.substring(uri.lastIndexOf("/") + 1);
    
    // 初始化URLClassLoader
    URLClassLoader loader = null;
    try {
      // create a URLClassLoader
      URL[] urls = new URL[1];
      URLStreamHandler streamHandler = null;
      File classPath = new File(Constants.WEB_ROOT);
      // the forming of repository is taken from the createClassLoader method in
      // org.apache.catalina.startup.ClassLoaderFactory
      String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString() ;
      // the code for forming the URL is taken from the addRepository method in
      // org.apache.catalina.loader.StandardClassLoader class.
      urls[0] = new URL(null, repository, streamHandler);
      loader = new URLClassLoader(urls);
    } catch (IOException e) {
      System.out.println(e.toString() );
    }
    
    // 用classLoader載入上面的servlet
    Class myClass = null;
    try {
      myClass = loader.loadClass(servletName);
    }
    catch (ClassNotFoundException e) {
      System.out.println(e.toString());
    }
    
    // 將載入到的class轉成Servlet,並呼叫service方法處理
    Servlet servlet = null;
    try {
      servlet = (Servlet) myClass.newInstance();
      servlet.service((ServletRequest) request, (ServletResponse) response);
    } catch (Exception e) {
      System.out.println(e.toString());
    } catch (Throwable e) {
      System.out.println(e.toString());
    }
    

    }
    }

        @pdai: 程式碼已經複製到剪貼簿
    
  • Repsonse

    public class PrimitiveServlet implements Servlet {

    public void init(ServletConfig config) throws ServletException {
    System.out.println("init");
    }

    public void service(ServletRequest request, ServletResponse response)
    throws ServletException, IOException {
    System.out.println("from service");
    PrintWriter out = response.getWriter();
    out.println("Hello. Roses are red.");
    out.print("Violets are blue.");
    }

    public void destroy() {
    System.out.println("destroy");
    }

    public String getServletInfo() {
    return null;
    }
    public ServletConfig getServletConfig() {
    return null;
    }

    }

        @pdai: 程式碼已經複製到剪貼簿
    
  • 訪問 URL

¶ 利用外觀模式改造

上述程式碼存在一個問題,

// 將載入到的class轉成Servlet,並呼叫service方法處理
    Servlet servlet = null;
    try {
      servlet = (Servlet) myClass.newInstance();
      servlet.service((ServletRequest) request, (ServletResponse) response);
    } catch (Exception e) {
      System.out.println(e.toString());
    } catch (Throwable e) {
      System.out.println(e.toString());
    }
  
        @pdai: 程式碼已經複製到剪貼簿

這裡直接處理將request和response傳給servlet處理是不安全的,因為request可以向下轉型為Request類,從而ServeletRequest便具備了訪問Request中方法的能力。

public class Request implements ServletRequest {
  // 一些public方法
}
public class Response implements ServletResponse {

}
  
        @pdai: 程式碼已經複製到剪貼簿

解決的方法便是通過外觀模式進行改造:

  • RequestFacade為例

    public class RequestFacade implements ServletRequest {

    private ServletRequest request = null;

    public RequestFacade(Request request) {
    this.request = request;
    }

    /* implementation of the ServletRequest*/
    public Object getAttribute(String attribute) {
    return request.getAttribute(attribute);
    }

    public Enumeration getAttributeNames() {
    return request.getAttributeNames();
    }

    public String getRealPath(String path) {
    return request.getRealPath(path);
    }

    ...

        @pdai: 程式碼已經複製到剪貼簿
    
  • Process中又傳入外觀類

    Servlet servlet = null;
    RequestFacade requestFacade = new RequestFacade(request); // 轉換成外觀類
    ResponseFacade responseFacade = new ResponseFacade(response);// 轉換成外觀類
    try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) requestFacade, (ServletResponse) responseFacade);
    }
    catch (Exception e) {
    System.out.println(e.toString());
    }
    catch (Throwable e) {
    System.out.println(e.toString());
    }

        @pdai: 程式碼已經複製到剪貼簿
    

¶ 總結

當我們看到這麼一個簡單的web容器實現之後,我們便不再覺得Tomcat高高在上;這將為我們繼續分析Tomcat中核心原始碼提供基礎。

基礎。

更多內容參考: https://www.gewuweb.com/sitemap.html