Tomcat剖析(四):Tomcat默認鏈接器(2)

Tomcat剖析(四):Tomcat默認鏈接器(2)

第一部分:概述

這一節基於《深度剖析Tomcat》第四章:Tomcat的默認鏈接器 總結而成html

最好先到個人github上下載本書相關的代碼,同時去上網下載這本書。java

通過上一節的講解,你已經理解了請求和響應對象,而且知道 HttpConnector 對象是如何建立它們的。Tomcat經過多個HttpProcessor實例實現多請求的實例,算是擁有一個比較完整的鏈接器了。git

這一節,咱們看看Tomcat中默認鏈接器中剩下的未講的功能:處理請求github

這一節補充的內容有些內容是從書上copy下來的。web

說到請求和響應,固然先要了解請求對象和響應對象apache

  • 默認鏈接器裏 HTTP 請求對象實現org.apache.catalina.Request 接口。這個接口被類RequestBase 直接實現了,也是 HttpRequest 的父接口。最終的實現是繼承於 HttpRequest 的HttpRequestImpl。像第 3 節的同樣,有幾個 facade 類:RequestFacade 和 HttpRequestFacade。須要注意的是,這裏除了屬於 javax.servlet和 javax.servlet.http 包的類,前綴 org.apache.catalina 已經被省略了。對HTTP響應對象也是相似的。

爲何要用Façade類?在第2節的安全性問題上已經說明。而這個類層次結構仍是比較容易理解的。數組

圖有些模糊,但仍是能夠看的。安全

在這節中咱們關注 HttpProcessor 類的 process 方法。 處理請求是經過HttpProcessor類的process方法實現。 它是一個套接字賦給它以後,在 HttpProcessor 類的 run 方法中調用的,而run方法在處理器對象建立時就調用了,只是在等待請求。服務器

process 方法的工做:cookie

  • 解析鏈接
  • 解析請求
  • 解析頭部

第二部分:代碼講解

處理請求

和第3節的同樣,有一個 SocketInputStream 實例用來包裝套接字的輸入流。注意的是,SocketInputStream 的 構 造 方 法 同 樣 傳 遞 了 從 連 接 器 獲 得 的 緩 衝 區 大 小 , 而 不 是 從HttpProcessor 的本地變量得到(第3節中是指定的2048)。這是由於對於默認鏈接器的用戶而言,HttpProcessor 是不可訪問的。經過傳遞 Connector 接口的緩衝區大小,這就使得使用鏈接器的任何人均可以設置緩衝大小。

SocketInputStream input = null;
OutputStream output = null;

// Construct and initialize the objects we will need
try {
    input = new SocketInputStream(socket.getInputStream(),
                                  connector.getBufferSize());
} catch (Exception e) {
    log("process.create", e);
    ok = false;
}

process 方法使用布爾變量 ok 來指代在處理過程當中是否發現錯誤,從代碼中能夠看到一旦catch到錯誤,就會設爲false 並使用布爾變量finishResponse 來指代 Response 接口中的 finishResponse 方法是否應該被調用。

boolean ok = true;

 boolean finishResponse = true;

另外, process 方法也使用了布爾變量 keepAlive,stopped 和 http11。 keepAlive 表示鏈接 是不是持久的, stopped 表示 HttpProcessor 實例是否已經被鏈接器終止來確認 process 是否也應該中止,http11 表示 從 web 客戶端過來的 HTTP 請求是否支持 HTTP 1.1。

而後,有個 while 循環用來保持從輸入流中讀取,直到 HttpProcessor 被中止,一個異常被拋出或者鏈接給關閉爲止。

while (!stopped && ok && keepAlive) {

    //....  
}

在 while 循環的內部,process 方法首先把 finishResponse 設置爲 true,並得到輸出流,並對請求和響應對象作些初始化處理

若是初始化過程都catch到錯誤,解析鏈接和頭部就不用作了,因此拋錯時ok會設爲false

//初始化請求和響應對象
request.setStream(input);
request.setResponse(response);
output = socket.getOutputStream();
response.setStream(output);
response.setRequest(request);
((HttpServletResponse) response.getResponse()).setHeader
                ("Server", SERVER_INFO);

接着,process 方法經過調用 parseConnection,parseRequest 和 parseHeaders 方法開始解析前來的 HTTP 請求,這些方法將在這節的後面討論。

parseConnection(socket);
parseRequest(input, output);
if (!request.getRequest().getProtocol()   
    .startsWith("HTTP/0"))
   parseHeaders(input);

parseConnection 方法得到協議的值,像 HTTP0.9, HTTP1.0 或 HTTP1.1。

若是協議是 HTTP1.0,keepAlive 設置爲 false,由於 HTTP1.0 不支持持久鏈接。

若是在 HTTP 請求裏邊找到 Expect: 100-continue 的頭部信息,則 parseHeaders 方法將把 sendAck 設置爲 true。

若是協議是 HTTP1.1,而且 web 客戶端發送頭部 Expect: 100-continue 的話,經過調用ackRequest 方法它將響應這個頭部。它將會測試組塊是不是容許的。

getProtocol()獲取的協議值是在parseConnection時設置的

ackRequest 方法測試 sendAck 的值,並在 sendAck 爲 true 的時候發送下面的字符串:HTTP/1.1 100 Continue\r\n\r\n

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

在解析 HTTP 請求的過程當中,有可能會拋出異常。任何異常將會把 ok 或者 finishResponse設置爲 false。

解析事後,process 方法把請求和響應對象傳遞給容器的 invoke 方法

((HttpServletResponse) response).setHeader
   ("Date", FastHttpDateFormat.getCurrentDate());
if (ok) {
   connector.getContainer().invoke(request, response);
}

還記得上一節的ex04.pyrmont.startup.Bootstrap.java嗎?

裏面有這麼一段:

SimpleContainer container = new SimpleContainer();
connector.setContainer(container);

SimpleContainer是什麼? 它實現了Container接口,經過HttpConnector解析出請求和響應後傳遞給容器

容器經過請求和響應對象得到servletName,負責servlet的加載執行,第4節只是簡單實現了invoke方法,其餘方法未實現。

對應前面幾節的ServletProcessor.java

因此爲何要有Container呢,就是將servlet加載執行過程分離出來,固然實際不止那麼簡單。

public class SimpleContainer implements Container{

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

    }
}

接着,若是 finishResponse 仍然是 true響應對象的 finishResponse 方法和請求對象的finishRequest 方法將被調用,而且結束輸出。 (只要可以正常解析就是true,狀態碼是在執行servlet過程當中修改的,默認是200)

finishResponse處理的是判斷響應狀態,若是是正常的,則setHeader("Connection", "close"); 不然會將錯誤寫到到頁面上,能夠本身查看代碼

finishRequest主要就是關閉HttRequest中的流

if (finishResponse) {
  //...
  response.finishResponse();
  //...
  request.finishRequest();
}

while 循環的最後一部分檢查響應的 Connection 頭部是否已經在 servlet 內部設爲 close,或者協議是 HTTP1.0.若是是這種狀況的話,keepAlive 設置爲 false。一樣,請求和響應對象接着會被回收利用。

進入這兩個方法,能夠看到其實就是將HttpRequestImpl.java和它的基類HttpRquestBase等類的實例變量還原到原來的值,這樣在下次請求若是再從處理器池中拿到這個處理器時,保證裏面的請求和響應對象是初始值。

if ( "close".equals(response.getHeader("Connection")) ) {
    keepAlive = false;
}

// End of request processing
status = Constants.PROCESSOR_IDLE;

// Recycling the request and the response objects
request.recycle();
response.recycle();

在這個場景中,若是 keepAlive 是 true 的話,while 循環將會在開頭就啓動。由於在前面的解析過程當中和容器的 invoke 方法中沒有出現錯誤,或者 HttpProcessor 實例沒有被中止。不然,shutdownInput 方法將會調用,而套接字將被關閉.

try {
    shutdownInput(input);
    socket.close();

shutdownInput 方法檢查是否有未讀取的字節。若是有的話,跳過那些字節。

解析鏈接

parseConnection 方法從套接字中獲取到網絡地址並把它賦予 HttpRequestImpl 對象。

它也檢查是否使用代理並把套接字賦予請求對象。

private void parseConnection(Socket socket)
        throws IOException, ServletException {

        ((HttpRequestImpl) request).setInet(socket.getInetAddress());
        if (proxyPort != 0)
            request.setServerPort(proxyPort);
        else
            request.setServerPort(serverPort);
        request.setSocket(socket);

}

解析請求

parseRequest 方法是第 3 節中相似方法的完整版本。若是你閱讀過上一節,你經過閱讀這個方法應該能夠理解這個方法是怎麼運行的。

解析頭部

默認連接器的 parseHeaders 方法使用包 org.apache.catalina.connector.http 裏邊的HttpHeader 和 DefaultHeaders 類。類 HttpHeader 指代一個 HTTP 請求頭部。類 HttpHeader 不是像第3節那樣使用字符串,而是使用字符數據用來避免昂貴的字符串操做。類 DefaultHeaders是一個 final 類,在字符數組中包含了標準的 HTTP 請求頭部

下面是DefaultHeaders.java部分代碼

static final char[] AUTHORIZATION_NAME = "authorization".toCharArray();
static final char[] ACCEPT_LANGUAGE_NAME = "accept-language".toCharArray();
static final char[] COOKIE_NAME = "cookie".toCharArray();
static final char[] CONTENT_LENGTH_NAME = "content-length".toCharArray();

parseHeaders 方法包含一個 while 循環,能夠持續讀取 HTTP 請求直到再也沒有更多的頭部能夠讀取到。

while 循環首先調用請求對象的 allocateHeader 方法來獲取一個空的 HttpHead 實例,若是看這個方法,發現HttpRequestImpl中以HttpHeader數組形式保存,若是,默認規定頭部大小爲10個,若是超過,則經過複製給新數組實現新Header對象的分配。

這個實例被傳遞給SocketInputStream 的 readHeader 方法。

HttpHeader header = request.allocateHeader();

// Read the next header
input.readHeader(header);

假如全部的頭部都被已經被讀取的話, readHeader 方法將不會賦值給 HttpHeader 實例,這個時候 parseHeaders 方法將會返回。

if (header.nameEnd == 0) {
    if (header.valueEnd == 0) {
        return;
    } else {
        throw new ServletException
            (sm.getString("httpProcessor.parseHeaders.colon"));
    }
}

若是存在一個頭部的名稱的話,這裏必須一樣會有一個頭部的值:String value = new String(header.value, 0, header.valueEnd); 接下去,像第 3 節那樣, parseHeaders 方法將會把頭部名稱和 DefaultHeaders 裏邊的名稱作對比。

注意的是,這樣的對比是基於兩個字符數組之間,而不是兩個字符串之間的。

if (header.equals(DefaultHeaders.AUTHORIZATION_NAME)) {
       request.setAuthorization(value);
} else if (header.equals(DefaultHeaders.ACCEPT_LANGUAGE_NAME)) {
       parseAcceptLanguage(value);
//...

第三部分:小結

這一個補充的其實就是爲了將整個默認鏈接器的內容比較完整的過一遍, 理解一下就行。固然,本身以爲有用的也能夠吸取。

下一節開始講容器。

相應代碼能夠在個人github上找到下載,拷貝到eclipse,而後打開對應包的代碼便可。

如發現編譯錯誤,多是因爲jdk不一樣版本對編譯的要求不一樣致使的,能夠無論,供學習研究使用。

相關文章
相關標籤/搜索