[Tomcat原始碼系列] Tomcat Connector

potato123發表於2010-03-20

      Connector是Tomcat最核心的元件之一,負責處理一個WebServer最核心的連線管理、Net IO、執行緒(可選)、協議解析和處理的工作。
一、聯結器介紹
在開始Connector探索之路之前,先看看Connector幾個關鍵字

  • NIO:Tomcat可以利用Java比較新的NIO技術,提升高併發下的Socket效能
  • AJP:Apache JServ Protocol,AJP的提出當然還是為了解決java亙古不變的問題——效能,AJP協議是基於包的長連線協議,以減少前端Proxy與Tomcat連線Socket連線建立的代價,目前Apache通過JK和AJP_ROXY的方式支援AJP協議,需要注意的是,雖然Nginx作為代理伺服器效能強勁,但其只能通過HTTP PROXY的方式與後端的Tomcat聯絡,因此如果從作為代理伺服器的角度上講,在這種情況下Nginx未必會比Apache體現出更優的效能
  • APR/Native:Apache Portable Runtime,還是一個詞,效能。APR的提出利用Native程式碼更好地解決效能問題,更好地與本地伺服器(linux)打交道。讓我們看看Tomcat文件對APR的介紹
Tomcat6文件 寫道
Tomcat can use the Apache Portable Runtime to provide superior scalability, performance, and better integration with native server technologies. The Apache Portable Runtime is a highly portable library that is at the heart of Apache HTTP Server 2.x. APR has many uses, including access to advanced IO functionality (such as sendfile, epoll and OpenSSL), OS level functionality (random number generation, system status, etc), and native process handling (shared memory, NT pipes and Unix sockets).
These features allows making Tomcat a general purpose webserver, will enable much better integration with other native web technologies, and overall make Java much more viable as a full fledged webserver platform rather than simply a backend focused technology.

通過對如上名詞的組合,Tomcat組成了如下的Connector系列:

  • Http11Protocol:支援HTTP1.1協議的聯結器
  • Http11NioProtocol:支援HTTP1.1 協議+ NIO的聯結器
  • Http11AprProtocol:使用APR技術處理連線的聯結器
  • AjpProtocol:支援AJP協議的聯結器
  • AjpAprProtocol:使用APR技術處理連線的聯結器

二、範例
      我們以最簡單的Http11Protocol為例,看看從請求進來到處理完畢,聯結器部件是處理處理的。首先我們利用Tomcat元件組成我們一個最簡單的WebServer,其具備如下功能:

  • 監停某個埠,接受客戶端的請求,並將請求分配給處理執行緒
  • 處理執行緒處理請求,分析HTTP1.1請求,封裝Request/Response物件,並將請求由請求處理器處理
  • 實現最簡單的請求處理器,向客戶端列印Hello World

程式碼非常簡單,首先是主功能(這裡,我們利用JDK5.0的執行緒池,聯結器不再管理執行緒功能):

package ray.tomcat.test;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.coyote.http11.Http11Protocol;

public class TomcatMainV2
{
    public static void main(String[] args) throws Exception
    {
        Http11Protocol protocol = new Http11Protocol();
        protocol.setPort(8000);
        ThreadPoolExecutor threadPoolExecutor = createThreadPoolExecutor();
        threadPoolExecutor.prestartCoreThread();
        protocol.setExecutor(threadPoolExecutor);
        protocol.setAdapter(new MyHandler());
        protocol.init();
        protocol.start();
    }

    public static ThreadPoolExecutor createThreadPoolExecutor()
    {
        int corePoolSize = 2;
        int maximumPoolSize = 10;
        long keepAliveTime = 60;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        return threadPoolExecutor;
    }
}
 

請求處理器向客戶端打引Hello World,程式碼如下

package ray.tomcat.test;

import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;

import org.apache.coyote.Adapter;
import org.apache.coyote.Request;
import org.apache.coyote.Response;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.net.SocketStatus;

public class MyHandler implements Adapter
{
    //支援Comet,Servlet3.0將對Comet提供支援,Tomcat6目前是非標準的實現
    public boolean event(Request req, Response res, SocketStatus status)
            throws Exception
    {
        System.out.println("event");
        return false;
    }

    //請求處理
    public void service(Request req, Response res) throws Exception
    {
        System.out.println("service");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos));
        writer.println("Hello World");
        writer.flush();

        ByteChunk byteChunk = new ByteChunk();
        byteChunk.append(baos.toByteArray(), 0, baos.size());
        res.doWrite(byteChunk);
    }   
}
 

     執行主程式,在瀏覽器中輸入http://127.0.0.1:8000,我們可以看到列印”Hello World”
三、分析
     以如上Http11Protocol為例,我們可以看到,Tomcat實現一個最簡單的處理Web請求的程式碼其實非常簡單,其主要包括如下核心處理類:

  • Http11Protocol:Http1.1協議處理入口類,其本身沒有太多邏輯,對請求主要由JIoEndPoint類處理
  • Http11Protocol$Http11ConnectionHandler:連線管理器,管理連線處理佇列,並分配Http11Processor對請求進行處理
  • Http11Processor:請求處理器,負責HTTP1.0協議相關的工作,包括解析請求和處理響應資訊,並呼叫Adapter做實際的處理工作,如上我們看到了我們自定義的Adapter實現響應”Hello World”
  • JIoEndPoint:監停埠,啟動接受執行緒準備接收請求,在請求接受後轉給工作執行緒處理
  • JIoEndPoint$Acceptor:請求接收器,接收後將Socket分配給工作執行緒繼續後續處理
  • JIoEndPoint$Worker:工作執行緒,使用Handler來處理請求,對於我們的HTTP1.1協議來說,其實現是Http11Protocol$Http11ConnectionHandler。這部分不是必須的,也可以選擇JDK的concurrent包的執行緒池

      實際上各種聯結器實現基本大同小異,基本上都是由如上部分組合而成

      1.初始化:首先,還是從入口開始,先看看初始化init

public void init() throws Exception {
        endpoint.setName(getName());
        endpoint.setHandler(cHandler); //請求處理器,對於HTTP1.1協議,是Http11Protocol$Http11ConnectionHandler

       
// 初始化ServerSocket工廠類,如果需SSL/TLS支援,使用JSSESocketFactory/PureTLSSocketFactory
. . . (略)       
              //主要的初始化過程實際是在endpoint(JIoEndpoint)
        endpoint.init();
        . . . (略)
    }

      Http11Protocol的初始化非常簡單,準備好ServerSocket工廠,呼叫JIoEndPoint的初始化。讓我們接下來看看JIoEndPoint的初始化過程

public void init()
        throws Exception {

        if (initialized)
            return;
       
        // Initialize thread count defaults for acceptor
        // 請求接收處理執行緒,這個值實際1已經足夠
        if (acceptorThreadCount == 0) {
            acceptorThreadCount = 1;
        }
        if (serverSocketFactory == null) {
            serverSocketFactory = ServerSocketFactory.getDefault();
        }
       
        //建立監停ServerSocket,port為監聽埠,address為監停地址
        // backlog為連線請求佇列容量(@param backlog how many connections are queued)
        if (serverSocket == null) {
            try {
                if (address == null) {
                    serverSocket = serverSocketFactory.createSocket(port, backlog);
                } else {
                    serverSocket = serverSocketFactory.createSocket(port, backlog, address);
                }
            } catch (BindException be) {
                throw new BindException(be.getMessage() + ":" + port);
            }
        }
        //if( serverTimeout >= 0 )
        //    serverSocket.setSoTimeout( serverTimeout );
       
        initialized = true;  
    }

        可以看到,監停埠在此處準備就緒

   public void start()
        throws Exception {
        // Initialize socket if not done before
        if (!initialized) {
            init();
        }
        if (!running) {
            running = true;
            paused = false;

            // Create worker collection
            // 初始化工作執行緒池,有WorkerStack(Tomcat自實現)和Executor(JDK concurrent包)兩種實現
            if (executor == null) {
                workers = new WorkerStack(maxThreads);
            }

            // Start acceptor threads
            // 啟動請求連線接收處理執行緒
            for (int i = 0; i < acceptorThreadCount; i++) {
                Thread acceptorThread = new Thread(new Acceptor(), getName() + "-Acceptor-" + i);
                acceptorThread.setPriority(threadPriority);
                acceptorThread.setDaemon(daemon); //設定是否daemon引數,預設為true
                acceptorThread.start();
            }
        }
    }
 


      2.準備好連線處理:初始化完畢,準備好連線處理,準備接收連線上來,同樣的,Http11Protocol的start基本沒幹啥事,呼叫一下JIoEndPoint的start,我們來看看JIoEndPoint的start

   public void start()
        throws Exception {
        // Initialize socket if not done before
        if (!initialized) {
            init();
        }
        if (!running) {
            running = true;
            paused = false;

            // Create worker collection
            // 初始化工作執行緒池,有WorkerStack(Tomcat自實現)和Executor(JDK concurrent包)兩種實現
            if (executor == null) {
                workers = new WorkerStack(maxThreads);
            }

            // Start acceptor threads
            // 啟動請求連線接收處理執行緒
            for (int i = 0; i < acceptorThreadCount; i++) {
                Thread acceptorThread = new Thread(new Acceptor(), getName() + "-Acceptor-" + i);
                acceptorThread.setPriority(threadPriority);
                acceptorThread.setDaemon(daemon); //設定是否daemon引數,預設為true
                acceptorThread.start();
            }
        }
    }

      主要處理的事情無非就是準備和工作執行緒(處理具體請求的執行緒度池,可選,也可以使用JDK5.0的執行緒池),連線請求接收處理執行緒(程式碼中,一般acceptorThreadCount=1)

      3.連線請求接收處理:準備就緒,可以連線入請求了。現在工作已經轉到了Acceptor(JIoEndPoint$Acceptor)這裡,我們看看Acceptor到底做了些啥

public void run() {

            // Loop until we receive a shutdown command
            while (running) {
                . . . (略)
                    //阻塞等待客戶端連線
                    Socket socket = serverSocketFactory.acceptSocket(serverSocket);
                    serverSocketFactory.initSocket(socket);
                    // Hand this socket off to an appropriate processor
                    if (!processSocket(socket)) {
                        // Close socket right away
                        try {
                            socket.close();
                        } catch (IOException e) {
                            // Ignore
                        }
                    }
                  . . . (略)
            }
 }
. . . (略)
    protected boolean processSocket(Socket socket) {
        try {
            //由工作執行緒繼續後續的處理
            if (executor == null) {
                getWorkerThread().assign(socket);
            } else {
                executor.execute(new SocketProcessor(socket));
            }
        } catch (Throwable t) {
            . . . (略)           
return false;
        }
        return true;
    }

       實際上也沒有什麼複雜的工作,無非就是有連線上來之後,將連線轉交給工作執行緒(SocketProcessor)去處理

       4.工作執行緒:SocketProcessor

public void run() {
            // Process the request from this socket
            if (!setSocketOptions(socket) || !handler.process(socket)) {
                // Close socket
                try {
                    socket.close();
                } catch (IOException e) {
                }
            }
            // Finish up this request
            socket = null;
        }   

    工作執行緒主要是設定一下Socket引數,然後將請求轉交給handler去處理,需要注意一下如下幾個連線引數的意義:

  • SO_LINGER:若設定了SO_LINGER並確定了非零的超時間隔,則closesocket()呼叫阻塞程式,直到所剩資料傳送完畢或超時。這種關閉稱為“優雅的”關 閉。請注意如果套介面置為非阻塞且SO_LINGER設為非零超時,則closesocket()呼叫將以WSAEWOULDBLOCK錯誤返回。若在一個流類套介面上設定了SO_DONTLINGER,則closesocket()呼叫立即返回。但是,如果可能,排隊的資料將在套介面關閉前傳送。請注意,在這種情況下WINDOWS套介面實現將在 一段不確定的時間內保留套介面以及其他資源(TIME_WAIT),這對於想用所以套介面的應用程式來說有一定影響。預設此引數不開啟
  • TCP_NODELAY:是否開啟Nagle,預設開啟,使用Nagle演算法是為了避免多次傳送小的分組,而是累計到一定程度或者超過一定時間後才一起傳送。對於AJP連線,可能需要關注一下這個選項。
  • SO_TIMEOUT:JDK API註釋如下,With this option set to a non-zero timeout,a read() call on the InputStream associated with this Socket will block for only this amount of time.  If the timeout expires, a java.net.SocketTimeoutException is raised, though the Socket is still valid. The option must be enabled prior to entering the blocking operation to have effect. The timeout must be > 0。預設設定的是60秒

        關於預設的設定,可以參見org.apache.coyote.http11.Constants定義
      5.最終請求終於回到了Handler,此處的Handler實現是org.apache.coyote.http11.Http11Processor,其主要處理一些HTTP協議性細節的東西,此處程式碼不再列出,有興趣可以自行讀程式碼。最終請求終於回到了我們的Adapter物件,一個請求處理完畢,功德圓滿。

相關文章