1. 程式人生 > >HowTomcatWorks學習筆記--Tomcat的預設聯結器(續)

HowTomcatWorks學習筆記--Tomcat的預設聯結器(續)

這本書(How Tomcat Works 中文下載地址)之前就看過,然而也就開了個頭就廢棄了,所以一直耿耿於懷。這次決定重新開始,在此記錄一些學習心得和本書的主要知識點。
所有程式碼也將託管在GitHub上面。O(∩_∩)O

上節回顧

剖析了Tomcat的HttpConnector類。

工作原理梗概如下:

  1. 用工廠模式創建出ServerSocket。
  2. 建立特定數量的HttpProcessor物件池,用於處理Socket請求;
  3. 同時,會啟動HttpProcessor執行緒。由於沒有請求,所以所有的HttpProcessor都會阻塞在那裡。
  4. 呼叫HttpConnector的run方法,等待Socket請求。
  5. 當請求來臨,從物件池中pop出一個HttpProcessor例項。
  6. 呼叫HttpProcessor的assign方法,將Socket例項傳遞給HttpProcessor;
  7. 並且,喚醒在HttpProcessor中被阻塞的執行緒,用於處理Socket請求。

概要

這裡會主要關注HttpProcessor非同步處理請求的方法。

HttpProcessor類

在前幾個章節中,我們自己寫的處理請求的程式碼是同步的。他必須等待處理完process方法,才能接收新的Socket。

public void run() {
    ...
    while (!stopped) {
        Socket socket = null
; try { socket = serversocket.accept(); } catch (Exception e) { continue; } // Hand this socket off to an Httpprocessor HttpProcessor processor = new Httpprocessor(this); processor.process(socket); } }

而Tomcat中,這是非同步的。

run方法

它的邏輯是,接收Socket,然後處理請求,最後將HttpConnector例項放回到物件池中。

public void run() {
    // Process requests until we receive a shutdown signal
    while (!stopped) {
        // Wait for the next socket to be assigned
        Socket socket = await();
        if (socket == null)
            continue;
        // Process the request from this socket
        try {
            process(socket);
        } catch (Throwable t) {
            log("process.invoke", t);
        }
        // Finish up this request
        connector.recycle(this);
    }
    // Tell threadStop() we have shut ourselves down successfully
    synchronized (threadSync) {
        threadSync.notifyAll();
    }
}

while方法做迴圈,很常見。

在上一章節中我們已經說過,HttpConnector會建立HttpProcessor,並且啟動執行緒的run方法,同時它被放在物件池中,一直阻塞著。

那麼run方法為何會一直阻塞著呢?很簡單,因為在while迴圈中的await方法。

await方法

執行的流程如下:

  1. while迴圈中available條件的初始狀態是false,表示沒有請求。所以它就一直處於wait等待狀態。
  2. 一旦有請求(在assign方法中,會將available設定為true,並且釋放執行緒鎖。await得以呼叫。),await將會獲取執行緒鎖,程式碼跳出迴圈得以向下執行。
  3. 然後,它將available條件設定為false,通過notifyAll釋放執行緒鎖,返回Socket例項。
  4. 這樣,當前Scoket的await方法就執行完成,返回run方法中。
  5. 在run方法中,繼續其他操作。
private synchronized Socket await() {
    // Wait for the Connector to provide a new Socket
    while (!available) {
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }
    // Notify the Connector that we have received this Socket
    Socket socket = this.socket;
    available = false;
    notifyAll();

    if ((debug >= 1) && (socket != null))
        log("  The incoming request has been awaited");
    return (socket);
}

一個問題

這裡有個問題值得注意一下,為何需要呼叫notifyAll方法,作用是什麼,是否多此一舉呢?

書上是這麼解釋的:

為什麼 await 需要使用一個本地變數(socket)而不是返回例項的 socket 變數呢?因為這樣一來,在當前 socket 被完全處理之前,例項的 socket 變數可以賦給下一個前來的 socket。

為什麼 await 方法需要呼叫 notifyAll 呢? 這是為了防止在 available 為 true 的時候另一個 socket 到來。在這種情況下,聯結器執行緒將會在 assign 方法的 while 迴圈中停止,直到接收到處理器執行緒的 notifyAll 呼叫。

意思大概就是,一個HttpProcessor例項在同一時間只能處理一個Socket的請求,但是設定一個本地socket變數,可以讓HttpProcessor提前儲存下一個Socket請求。

可是將Socket傳遞給HttpProcessor的時候,HttpProcessor是需要從物件池中獲取而來的。在處理當前請求的HttpProcessor明顯不在物件池中,它是如何能夠提前儲存下一個Socket請求呢?

這個問題我還未找到答案。待以後處理。

assign方法

剛剛我們說了,await方法會阻塞。直到assign方法被呼叫,釋放了執行緒鎖,await才得以繼續執行。

assign方法的作用就是,

  1. 接收由HttpConnector傳遞的Socket例項。
  2. 釋放執行緒鎖,讓await方法得以呼叫。

available屬性是false,我們得知。

synchronized void assign(Socket socket) {
    // Wait for the Processor to get the previous Socket
    while (available) {
        try {
                wait();
            } catch (InterruptedException e) {
        }
    }
    // Store the newly available Socket and notify our thread
    this.socket = socket;
    available = true;
    notifyAll();

    if ((debug >= 1) && (socket != null))
        log(" An incoming request is being assigned");
}

處理請求

pocess方法,會做:

  1. 解析連線
  2. 解析請求
  3. 解析頭部

解析資料是在一個white迴圈中進行的。

迴圈條件中keepAlive是由Http請求控制的。

keepAlive = true;
while (!stopped && ok && keepAlive) {
...
  if ( "close".equals(response.getHeader("Connection")) ) {
                keepAlive = false;
  }
...
}

裡面的處理資料程式碼主要是:

if (ok) {
    parseConnection(socket);
    parseRequest(input, output);
    if (!request.getRequest().getProtocol()
        .startsWith("HTTP/0"))
        parseHeaders(input);
    if (http11) {
        // Sending a request acknowledge back to the client if
        // requested.
        ackRequest(output);
        // If the protocol is HTTP/1.1, chunking is allowed.
        if (connector.isChunkingAllowed())
            response.setAllowChunking(true);
    }
}

都是在處理資料。其他的略。

解析連線

parseConnection方法從套接字中獲取到網路地址並把它賦予 HttpRequestImpl 物件。

private void parseConnection(Socket socket) throws IOException, ServletException {
    if (debug >= 2)
        log(" parseConnection: address=" + socket.getInetAddress() +
                ", port=" + connector.getPort());
    ((HttpRequestImpl) request).setInet(socket.getInetAddress()); 
    if (proxyPort != 0)
        request.setServerPort(proxyPort); 
    else
        request.setServerPort(serverPort);
    request.setSocket(socket); 
}

解析請求

parseRequest方法和我們之前章節實現的差不多。

解析頭部

parseHeaders方法使用包org.apache.catalina.connector.http裡邊的 HttpHeader 和 DefaultHeaders 類。類 HttpHeader 指代一個 HTTP 請求頭部。內容也類似,略過。

簡單的容器

容器的作用,

  1. 用來動態load出Servlet,
  2. 並且將Request和Response傳遞給它,執行它的service方法。

它需要實現org.apache.catalina.Container介面。

現在,我們自己實現一個容器,然後將這個容器傳遞給org.apache.catalina.connector.http.HttpConnector

程式碼和之前的很類似,只是從將一些功能從ServeletProcessor中分離了出來。

public class SimpleContainer implements Container {
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator           + "webroot";
    ...
    public void invoke(Request request, Response response)
    throws IOException, ServletException {

    String servletName = ( (HttpServletRequest) request).getRequestURI();
    servletName = servletName.substring(servletName.lastIndexOf("/") + 1);
    URLClassLoader loader = null;
    try {
      URL[] urls = new URL[1];
      URLStreamHandler streamHandler = null;
      File classPath = new File(WEB_ROOT);
      String repository = (new URL("file", null, classPath.getCanonicalPath() 
                                   + File.separator)).toString() ;
      urls[0] = new URL(null, repository, streamHandler);
      loader = new URLClassLoader(urls);
    }
    catch (IOException e) {
      System.out.println(e.toString() );
    }
    Class myClass = null;
    try {
      myClass = loader.loadClass(servletName);
    }
    catch (ClassNotFoundException e) {
      System.out.println(e.toString());
    }

    Servlet servlet = null;

    try {
      servlet = (Servlet) myClass.newInstance();
      servlet.service((HttpServletRequest) request, (HttpServletResponse) response);
    }
    catch (Exception e) {
      System.out.println(e.toString());
    }
    catch (Throwable e) {
      System.out.println(e.toString());
    }
  }
  ...
} 

啟動類

package com.ext04.pyrmont.startup;

import com.ext04.pyrmont.core.SimpleContainer;
import org.apache.catalina.connector.http.HttpConnector;


/**
 * Created by laiwenqiang on 2017/5/25.
 */
public class BootStrap {
    public static void main(String[] args) {
        HttpConnector connector = new HttpConnector();
        SimpleContainer container = new SimpleContainer();
        connector.setContainer(container);

        try {
            connector.initialize();
            connector.start();

            // 加上這句話,防止main方法退出。阻塞在那。
            System.in.read();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

開啟瀏覽器,輸入http://localhost:8080/servlet/PrimitiveServlet,得出結果。

一步一個腳印
本章完成,(^__^)