Tomcat剖析(四):Tomcat默認連接器(1)


Tomcat剖析(四):Tomcat默認連接器(1)

第一部分:概述

這一節大家可以學到很多東西,不過如果不懂的話可能會很困惑。

本節對應《深入剖析 Tomcat》第四章:Tomcat 默認連接器。

大家一定要先去下載 Tomcat4 相關代碼,否則肯定會不知所雲。 代碼可以在我的github上下載

在第 3 節中的連接器已經能較好的運行,既使用了線程啟動,也完成了對大部分請求解析,但是仍然后很多不足。

一個 Tomcat 連接器必須符合以下條件

  1. 必須實現接口 org.apache.catalina.Connector
  2. 必須創建請求對象,該請求對象的類必須實現接口 org.apache.catalina.Request
  3. 必須創建響應對象,該響應對象的類必須實現接口 org.apache.catalina.Response

可以看到,上節的簡單連接器基本的條件都沒有實現。

Tomcat4 的默認連接器類似於上節的簡單連接器。它等待前來的 HTTP 請求,創建 request和 response 對象,然后把 request 和 response 對象傳遞給容器(上節只是交給響應的處理器Processor處理)。連接器是通過調用接口org.apache.catalina.Container 的 invoke 方法來傳遞 request 和 response 對象的。

寫了一半才發現要寫完全部知識點需要用灰常灰常大的篇幅,所以有些內容留到下一節再講.

首先需要牢記連接器Connector的功能是什么:創建ServerSocket,等待請求,解析請求並傳遞給容器(由容器處理請求)。

這一節講解:創建ServerSocket和等待請求部分,其中重點提到通過實現多個請求的處理,下一節的補充內容為解析請求部分和其他一些瑣碎的問題。

總體來說,處理用戶請求有以下幾個步驟

  1. 先創建serverSocket實例
  2. 建立HttpProcessor處理器池,等待請求
  3. 請求到來時從處理器池中獲取HttpProcessor實例,HttpProcessor實例負責解析請求
  4. 完成請求后將實例返回到池中。實現對多請求的處理。

核心類:

  • HttpConnector.java:連接器類,負責整個流程
  • HttpProcessor.java:處理器類,封裝請求需要的信息,包括請求對象和響應對象

第二部分:代碼講解

1. 啟動

Bootstrap.java作為啟動類

為什么是先從啟動類開始講呢?因為可以通過啟動過程比較完整的分析代碼。

對於什么是SimpleContainer,為什么一個連接器要setContainer(container)將指定容器設置到連接器中,大家可以先不管,在下一節對描述處理請求過程會用到Container,所以下一節還會再回頭看這個類。 還有需要先提前注意一點,這里的HttpConnector已經是Tomcat4中核心包中的代碼,不再是屬於ex**中的類,有點高級,哈哈。

package ex04.pyrmont.startup;

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

public final class Bootstrap {

    public static void main(String[] args) {

        HttpConnector connector = new HttpConnector();
        SimpleContainer container = new SimpleContainer();
        connector.setContainer(container);
        try {
            connector.initialize();
            connector.start();

            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接下來大家看看HttpConnector.java,可以發現實現除了Connector接口外,還實現了Runnable接口和Lifecycle接口。

為什么要實現Runnable接口,因為它的實例可以運行在自己的線程上,也就是需要啟動線程。

Lifecycle 將以后解釋,現在你不需要擔心它,只要明白通過實現 Lifecycle,在你創建 HttpConnector 實例之后,你應該調用它的 initialize 和 start 方法。這兩個方法在組件的 生命周期里必須只調用一次。

2. 連接器初始化

功能:完成ServerSocket創建

對應connector.initialize()

可以看到HttpConnector.java的initialize方法的作用是完成服務器端的ServerSocket的創建,並賦值給HttpConnector的實例變量serversocket中,里面調用了這個類的私有方法open();

serverSocket = open();

看看open(),open()方法負責創建SeverSocket這個方法有下面這些關鍵語句

private ServerSocket open(){
    ServerSocketFactory factory = getFactory();

    try {
            return (factory.createSocket(port, acceptCount, is));
     } catch (BindException be) {
            throw new BindException(be.getMessage() + ":" + address +
                                        ":" + port);
     }
}

getFactory()是用單例模式返回具體工廠,即代碼中的DefaultServerSocketFactory實例。DefaultServerSocketFactory對象負責創建ServerSocket對象,也就factory.createSocket(...)。

public ServerSocketFactory getFactory() {

        if (this.factory == null) {
            synchronized (this) {
                this.factory = new DefaultServerSocketFactory();
            }
        }
        return (this.factory);

}

通過對比前一節或者ex03包下的HttpConnector類,可以發現一個簡單的創建ServerSocket對象變得復雜許多,唯一不變的就是創建的地點都是在HttpConnector.java中,只是不再是在啟動HttpConnector線程時創建,而是在之前創建,不依賴於HttpConnector線程的啟動。

下面是上一節創建 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);
}

3. 啟動連接器

對應 connector.start();

下面是這個方法關鍵代碼。

public void start(){

    threadStart();//啟動HttpConnector線程
    while (curProcessors < minProcessors) {
            if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
                break;
            HttpProcessor processor = newProcessor();//創建HttpProcessor池
            recycle(processor);
    }      
}

啟動 HttpConnector 線程 threadStart() 放在本小節后半部分說,先介紹處理器池。

HttpConnector 維護一個 HttpProcessor 的實例池,從而避免每次創建 HttpProcessor 實例。

這些 HttpProcessor 對象是存放在 HttpConnector.java 中一個叫 processors 實例的 java.io.Stack 中:

private Stack processors = new Stack();

池的實現一般都是享元模式單例模式的綜合使用哦。

啟動連接器概括1:啟動 HttpConnector 線程,每一趟 while 循環 new 出 HttpProcessor 對象,並調用 recyle 方法將對象放入代表池的 processors 棧中。

void recycle(HttpProcessor processor) {

    processors.push(processor);//將創建的實例壓入棧中

}

如何創建 HttpProcessor 實例的: newProcessor() 方法

這個方法獲取到的信息是:創建出一個 HttpProcessor 對象后就立即啟動這個線程

啟動連接器概括2:啟動 HttpConnector 線程,一個 while 循環過后,創建了一些 HttpProcessor 實例,然后啟動它們的線程,最后通過 recyle() 方法放入 processors 實例中

啟動 Processor 線程后發生了什么,稍后再說。

private HttpProcessor newProcessor() {

        //創建實例
        HttpProcessor processor = new HttpProcessor(this, curProcessors++);
        if (processor instanceof Lifecycle) {
            try {
                ((Lifecycle) processor).start();//啟動處理器線程
            } catch (LifecycleException e) {
                log("newProcessor", e);
                return (null);
            }
        }
        created.addElement(processor);
        return (processor);
    }

看看 HttpProcesor.jav a的構造方法

構造方法中,從 connector 中獲取端口,包括代理端口

同時創建HttpRequestImpl 和 HttpResponseImpl 實例,以便於請求到來時不用再創建這些對象,只需要再放入相應的請求信息就行。

同時 HttpConnector.java 通過創建處理器實例,在這個過程綁定了請求和 connector 之間的關系,即 request.setConnector(this);

好了,現在對啟動連接器過程發生了什么可以概括為:

啟動連接器概括3:啟動 HttpConnector 線程,一個 while 循環過后,創建了一些 HttpProcessor 實例;創建每一個 HttpProcessor 實例過程中,都會創建請求和響應對象,避免請求到來時再創建,並綁定連接器與請求之間的關系;然后啟動處理器線程,最后通過 recyle() 方法放入 processors 實例中。

public HttpProcessor(HttpConnector connector, int id) {

        super();
        this.connector = connector;
        this.debug = connector.getDebug();
        this.id = id;
        this.proxyName = connector.getProxyName();
        this.proxyPort = connector.getProxyPort();
        this.request = (HttpRequestImpl) connector.createRequest();
        this.response = (HttpResponseImpl) connector.createResponse();
        this.serverPort = connector.getPort();
        this.threadName =
          "HttpProcessor[" + connector.getPort() + "][" + id + "]";

    }

public Request createRequest() {

    HttpRequestImpl request = new HttpRequestImpl();
    request.setConnector(this);
    return (request);

}

接下來看看 HttpProcessor 線程啟動后發生了什么事,當然就是它的run方法。

現在比如有10個處理器線程,那么從注釋中可以看到,啟動一個處理器線程后,這個線程通過await就一直處於等待請求狀態了,請求到來后就可以調用 process方法處理

啟動連接器概括4:啟動 HttpConnector 線程,一個 while 循環過后,創建了一些 HttpProcessor 實例,這些實例中沒有用戶的 socket 信息;創建每一個 HttpProcessor 實例過程中,都會創建請求和響應對象,避免請求到來時再創建,並綁定連接器與請求之間的關系;然后啟動處理器線程,這個線程啟動后 await 方法進入阻塞狀態等待請求;通過 recyle() 方法放入 processors 實例中;如果請求到來,獲取一個處理器,處理這個請求。

public void run() {

        while (!stopped) {

            Socket socket = await(); //阻塞,直到用戶請求到來獲取到這個處理器才被喚醒
            if (socket == null)
                continue;

            try {
                process(socket); //處理用戶請求
            } catch (Throwable t) {
                log("process.invoke", t);
            }

            connector.recycle(this);  //連接器回收處理器
        }
    }

聽起來可能有些迷糊,因為還沒將連接器線程的啟動。

我們知道 HttpConnector 實現了 Runnable 接口,Bootstrap.java 中調用 connector.start(),最后通過 threadStart方法 啟動 HttpConnector 線程

threadStart方法就是創建創建了 HttpConnector 線程,所以會調用 HttpConnector 類的run方法。

private void threadStart() {

        log(sm.getString("httpConnector.starting"));

        thread = new Thread(this, threadName);
        thread.setDaemon(true);
        thread.start();

 }

HttpConnector 的 run 方法究竟做了什么呢?連接器的用途是什么還記得嗎?就是創建ServerSocket,等待請求,解析請求,傳遞請求響應對象給容器(當然也可以認為是 Processor 做的,但是一般認為Tomcat的組件核心是 Connector 和 Container,可以將 Processor 當作 Connector 的一部分)。

前面講了創建 ServerSocket,那剩下的功能當然是等待請求部分。(解析請求下一節講)

還記得上一節中是如何等待客戶端請求的嗎?當然就是調用 serverSocket 的 accept 方法

經過默認連接器改進了,但是在啟動 HttpConnector 線程后,run 方法依然是使用同樣的方式等待用戶請求。只是更加具體。

下面貼上 theadStart 方法中的關鍵代碼。

public void run() {
        while (!stopped) {
            Socket socket = null;
            try {
                socket = serverSocket.accept();//等待用戶請求,阻塞
            } catch (AccessControlException ace) {
                continue;
            } catch (IOException e) {
                //省略
                continue;
            }
            HttpProcessor processor = createProcessor(); //從池中獲取處理器實例
            if (processor == null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    ;
                }
                continue;
            }
            processor.assign(socket); //將請求的socket交給得到的處理器實例中
        }
    }

簡單說說 createProcessor 方法:

  • 在 HttpConnector 中,創建的 HttpProcessor 實例數量是有兩個變量決定的: minProcessors和 maxProcessors。默認情況下, minProcessors 為 5 而 maxProcessors 為 20,但是你可以通過setMinProcessors 和 setMaxProcessors 方法來改變他們的值。   

     protected int minProcessors = 5;   
    
     private int maxProcessors = 20;
    
    1. 開始的時候, HttpConnector 對象創建 minProcessors 個 HttpProcessor 實例。

    2. 如果一次有比 HtppProcessor 實例更多的請求需要處理時, HttpConnector 創建更多的 HttpProcessor 實例,直到實例數量達到 maxProcessors 個。

    3. 在到達這點之后,仍不夠 HttpProcessor 實例的話,請來的請求將會給忽略掉。

    4. 如果你想讓 HttpConnector 繼續創建 HttpProcessor 實例的話,把maxProcessors 設置為一個負數。還有就是變量 curProcessors 保存了 HttpProcessor 實例的當前數量。

      private HttpProcessor createProcessor() {
      
      synchronized (processors) {
          if (processors.size() > 0) {
              return ((HttpProcessor) processors.pop());  
              //從池中拿處理器對象
          }
          if ((maxProcessors > 0) && (curProcessors < maxProcessors)) {
              return (newProcessor()); //沒有超過上界,創建新的處理器
          } else {
              if (maxProcessors < 0) {
                  return (newProcessor());
                  //如果沒有上界,可以隨意創建處理器實例
              } else {
                  return (null);
              }
          }
      }
      

      }

從上面的遞推的說明中。可以知道整個流程是這樣子的:

首先 connector.initialize 方法創建 ServerSocket 實例。connector.start() 方法中啟動 HttpConnector 線程,這個線程通過 serverSocket.accept 方法進入等待用戶請求狀態。

-隨后 connector.start 方法創建了若干個 HttpProcessor 處理器實例,同時啟動了處理器線程,由於這些處理器實例中沒有用戶的 socket 信息,無法處理請求,所以全部進入阻塞狀態,即 await 方法。當用戶請求到來時,通過 assign 方法將 socket 放入處理器實例中,以便讓處理器處理用戶請求。

那Tomcat4是如何實現同時處理多個請求的呢?

就要看assgin和await方法了。

用戶請求到來,得到了用戶的 socket,調用 assign 方法,因為 avaiable 默認是 false,所以跳過 while 需要,將 socket 放入獲取到的處理器實例中,同時將 avaiable 設為 true,喚醒線程,此時 await 方法中的 wait 方法被喚醒了,同時因為 avaliable 為 true,跳出循環,將 avaiable 設為 false 重新進入阻塞,得到用戶的返回用戶的 socket,最后就能夠通過 process 處理請求了。

synchronized void assign(Socket socket) {

    while (available) { //avaiable默認是false,,第一次執行時跳過while
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }

    this.socket = socket; //將從池中獲取到的HttpProcessor實例中的socket變量賦值
    available = true;
    notifyAll();//喚醒線程。
}

private synchronized Socket await() {

    while (!available) {//默認是false,所以進入循環阻塞,因為處理器實例沒有socket信息,
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }

    Socket socket = this.socket;  //得到了socket
    available = false;  //重新進入阻塞
    notifyAll();

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

    return (socket);

}

疑問:

為什么 await 需要使用一個本地變量(socket)而不是返回實例的 socket 變量呢?

  • 因為這樣一來,在當前 socket 被完全處理之前,實例的 socket 變量可以賦給下一個前來的 socket。

為什么 await 方法需要調用 notifyAll 呢?

  • 這是為了防止在 available 為 true 的時候另一個 socket 到來。在這種情況下,連接器線程將會在 assign 方法的 while 循環中停止,直到接收到處理器線程的 notifyAll 調用。

4. 回收處理器

process方法處理請求后,最后通過connecotr.recyle()方法回收處理器,即將處理器壓回處理器池中

void recycle(HttpProcessor processor) {

    processors.push(processor);//將創建的實例壓入棧中

}

第三部分:小結

本節講解了Tomcat默認連接器是如何實現多個請求處理

並沒有涉及process方法具體是如何處理請求的,在下一節補充。

希望大家看過這篇博客之后有所幫助。

如果覺得還不錯,可以推薦一下或加關注喲,覺得寫得不好的也體諒一下。

相應代碼可以在我的github上找到下載,拷貝到eclipse,然后打開對應包的代碼即可。

如發現編譯錯誤,可能是由於jdk不同版本對編譯的要求不同導致的,可以不管,供學習研究使用。


免責聲明!

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



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