zoukankan      html  css  js  c++  java
  • Tomcat源码学习(17)How Tomcat works(转)

    第四章:Tomcat的默认连接器

    概要

        第3章的连接器运行良好,可以完善以获得更好的性能。但是,它只是作为一个教育工具,设计来介绍Tomcat4的默认连接器用的。理解第3章中的连接器是理解Tomcat4的默认连接器的关键所在。现在,在第4章中将通过剖析Tomcat4的默认连接器的代码,讨论需要什么来创建一个真实的Tomcat连接器。
    注意:本章中提及的“默认连接器”是指Tomcat4的默认连接器。即使默认的连机器已经被弃用,被更快的,代号为Coyote的连接器所代替,它仍然是一个很好的学习工具。
        Tomcat连接器是一个可以插入servlet容器的独立模块,已经存在相当多的连接器了,包括Coyote, mod_jk, mod_jk2和mod_webapp。一个Tomcat连接器必须符合以下条件:
    1. 必须实现接口org.apache.catalina.Connector。
    2. 必须创建请求对象,该请求对象的类必须实现接口org.apache.catalina.Request。
    3. 必须创建响应对象,该响应对象的类必须实现接口org.apache.catalina.Response。
        Tomcat4的默认连接器类似于第3章的简单连接器。它等待前来的HTTP请求,创建request和response对象,然后把request和response对象传递给容器。连接器是通过调用接口org.apache.catalina.Container的invoke方法来传递request和response对象的。invoke的方法签名如下所示:

    public void invoke(
       org.apache.catalina.Request request,
       org.apache.catalina.Response response);

        在invoke方法里边,容器加载servlet,调用它的service方法,管理会话,记录出错日志等等。
        默认连接器同样使用了一些第3章中的连接器未使用的优化。首先就是提供一个各种各样对象的对象池用于避免昂贵对象的创建。接着,在很多地方使用字节数组来代替字符串。
        本章中的应用程序是一个和默认连接器管理的简单容器。然而,本章的焦点不是简单容器而是默认连接器。我们将会在第5章中讨论容器。不管怎样,为了展示如何使用默认连接器,将会在接近本章末尾的“简单容器的应用程序”一节中讨论简单容器。
        另一个需要注意的是默认连接器除了提供HTTP0.9和HTTP1.0的支持外,还实现了HTTP1.1的所有新特性。为了理解HTTP1.1中的新特性,你首先需要理解本章首节解释的这些新特性。在这之后,我们将会讨论接口
    org.apache.catalina.Connector和如何创建请求和响应对象。假如你理解第3章中连接器如何工作的话,那么在理解默认连接器的时候你应该不会遇到任何问题。
        本章首先讨论HTTP1.1的三个新特性。理解它们是理解默认连接器内部工作机制的关键所在。然后,介绍所有连接器都会实现的接口 org.apache.catalina.Connector。你会发现第3章中遇到的那些类,例如HttpConnector, HttpProcessor等等。不过,这个时候,它们比第3章那些类似的要高级些。

    HTTP 1.1新特性

        本节解释了HTTP1.1的三个新特性。理解它们是理解默认连接器如何处理HTTP请求的关键。

    持久连接

        在HTTP1.1之前,无论什么时候浏览器连接到一个web服务器,当请求的资源被发送之后,连接就被服务器关闭了。然而,一个互联网网页包括其他资源,例如图片文件,applet等等。因此,当一个页面被请求的时候,浏览器同样需要下载页面所引用到的资源。加入页面和它所引用到的全部资源使用不同连接来下载的话,进程将会非常慢。那就是为什么HTTP1.1引入持久连接的原因了。使用持久连接的时候,当页面下载的时候,服务器并不直接关闭连接。相反,它等待web客户端请求页面所引用的全部资源。这种情况下,页面和所引用的资源使用同一个连接来下载。考虑建立和解除HTTP连接的宝贵操作的话,这就为 web服务器,客户端和网络节省了许多工作和时间。
        持久连接是HTTP1.1的默认连接方式。同样,为了明确这一点,浏览器可以发送一个值为keep-alive的请求头部connection:

    connection: keep-alive

    块编码

        建立持续连接的结果就是,使用同一个连接,服务器可以从不同的资源发送字节流,而客户端可以使用发送多个请求。结果就是,发送方必须为每个请求或响应发送内容长度的头部,以便接收方知道如何解释这些字节。然而,大部分的情况是发送方并不知道将要发送多少个字节。例如,在开头一些字节已经准备好的时候,servlet容器就可以开始发送响应了,而不会等到所有都准备好。这意味着,在content-length头部不能提前知道的情况下,必须有一种方式来告诉接收方如何解释字节流。
        即使不需要发送多个请求或者响应,服务器或者客户端也不需要知道将会发送多少数据。在HTTP1.0中,服务器可以仅仅省略content-length 头部,并保持写入连接。当写入完成的时候,它将简单的关闭连接。在这种情况下,客户端将会保持读取状态,直到获取到-1,表示已经到达文件的尾部。
        HTTP1.1使用一个特别的头部transfer-encoding来表示有多少以块形式的字节流将会被发送。对每块来说,在数据之前,长度(十六进制)后面接着CR/LF将被发送。整个事务通过一个零长度的块来标识。假设你想用2个块发送以下38个字节,第一个长度是29,第二个长度是9。

    I'm as helpless as a kitten up a tree.

        你将这样发送:

    1D\r\n
    I'm as helpless as a kitten u
    9\r\n
    p a tree.
    0\r\n

       1D,是29的十六进制,指示第一块由29个字节组成。0\r\n标识这个事务的结束。

    状态100(持续状态)的使用

        在发送请求内容之前,HTTP 1.1客户端可以发送Expect: 100-continue头部到服务器,并等待服务器的确认。这个一般发生在当客户端需要发送一份长的请求内容而未能确保服务器愿意接受它的时候。如果你发送一份长的请求内容仅仅发现服务器拒绝了它,那将是一种浪费来的。
        当接受到Expect: 100-continue头部的时候,假如乐意或者可以处理请求的话,服务器响应100-continue头部,后边跟着两对CRLF字符。

    HTTP/1.1 100 Continue

        接着,服务器应该会继续读取输入流。

    Connector接口

        Tomcat连接器必须实现org.apache.catalina.Connector接口。在这个接口的众多方法中,最重要的是getContainer,setContainer, createRequest和createResponse。
       setContainer是用来关联连接器和容器用的。getContainer返回关联的容器。createRequest为前来的HTTP请求构造一个请求对象,而createResponse创建一个响应对象。
        类org.apache.catalina.connector.http.HttpConnector是Connector接口的一个实现,将会在下一节“HttpConnector类”中讨论.

    HttpConnector类

        由于在第3章中org.apache.catalina.connector.http.HttpConnector的简化版本已经被解释过了,所以你已经知道这个类是怎样的了。它实现了org.apache.catalina.Connector (为了和Catalina协调),
    java.lang.Runnable (因此它的实例可以运行在自己的线程上)和org.apache.catalina.Lifecycle。接口Lifecycle用来维护每个已经实现它的Catalina组件的生命周期。
       Lifecycle将在第6章中解释,现在你不需要担心它,只要明白通过实现Lifecycle,在你创建HttpConnector实例之后,你应该调用它的initialize和start方法。这两个方法在组件的生命周期里必须只调用一次。我们将看看和第3章的HttpConnector类的那些不同方面:HttpConnector如何创建一个服务器套接字,它如何维护一个HttpProcessor对象池,还有它如何处理HTTP请求。

    创建一个服务器套接字

        HttpConnector的initialize方法调用open这个私有方法,返回一个java.net.ServerSocket实例,并把它赋予 serverSocket。然而,不是调用java.net.ServerSocket的构造方法,open方法是从一个服务端套接字工厂中获得一个 ServerSocket实例。如果你想知道这工厂的详细信息,可以阅读包org.apache.catalina.net里边的接口 ServerSocketFactory和类DefaultServerSocketFactory。它们是很容易理解的。

    维护HttpProcessor实例

        在第3章中,HttpConnector实例一次仅仅拥有一个HttpProcessor实例,所以每次只能处理一个HTTP请求。在默认连接器中,HttpConnector拥有一个HttpProcessor对象池,每个HttpProcessor实例拥有一个独立线程。因此,HttpConnector可以同时处理多个HTTP请求。
        HttpConnector维护一个HttpProcessor的实例池,从而避免每次创建HttpProcessor实例。这些HttpProcessor实例是存放在一个叫processors的java.io.Stack中:

    private Stack processors = new Stack();

        在HttpConnector中,创建的HttpProcessor实例数量是有两个变量决定的:minProcessors和 maxProcessors。默认情况下,minProcessors为5而maxProcessors为20,但是你可以通过 setMinProcessors和setMaxProcessors方法来改变他们的值。

    protected int minProcessors = 5;
    private int maxProcessors = 20;

        开始的时候,HttpConnector对象创建minProcessors个HttpProcessor实例。如果一次有比HtppProcessor 实例更多的请求需要处理时,HttpConnector创建更多的HttpProcessor实例,直到实例数量达到maxProcessors个。在到达这点之后,仍不够HttpProcessor实例的话,请来的请求将会给忽略掉。如果你想让HttpConnector继续创建 HttpProcessor实例的话,把maxProcessors设置为一个负数。还有就是变量curProcessors保存了 HttpProcessor实例的当前数量。
        下面是类HttpConnector的start方法里边关于创建初始数量的HttpProcessor实例的代码:

    while (curProcessors < minProcessors) {
       if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
           break;
       HttpProcessor processor = newProcessor();
       recycle(processor);
    }

        newProcessor方法构造一个HttpProcessor对象并增加curProcessors。recycle方法把HttpProcessor队会栈。
        每个HttpProcessor实例负责解析HTTP请求行和头部,并填充请求对象。因此,每个实例关联着一个请求对象和响应对象。类 HttpProcessor的构造方法包括了类HttpConnector的createRequest和createResponse方法的调用。

    为HTTP请求服务

        就像第3章一样,HttpConnector类在它的run方法中有其主要的逻辑。run方法在一个服务端套接字等待HTTP请求的地方存在一个while循环,一直运行直至HttpConnector被关闭了。

    while (!stopped) {
       Socket socket = null;
       try {
           socket = serverSocket.accept();
           ...

        对每个前来的HTTP请求,会通过调用私有方法createProcessor获得一个HttpProcessor实例。

    HttpProcessor processor = createProcessor();

        然而,大部分时候createProcessor方法并不创建一个新的HttpProcessor对象。相反,它从池子中获取一个。如果在栈中已经存在一个HttpProcessor实例,createProcessor将弹出一个。如果栈是空的并且没有超过HttpProcessor实例的最大数量,createProcessor将会创建一个。然而,如果已经达到最大数量的话,createProcessor将会返回null。出现这样的情况的话,套接字将会简单关闭并且前来的HTTP请求不会被处理。

    if (processor == null) {
       try {
           log(sm.getString("httpConnector.noProcessor"));
           socket.close();
       }
       ...
       continue;

        如果createProcessor不是返回null,客户端套接字会传递给HttpProcessor类的assign方法:

    processor.assign(socket);

        现在就是HttpProcessor实例用于读取套接字的输入流和解析HTTP请求的工作了。重要的一点是,assign方法不会等到 HttpProcessor完成解析工作,而是必须马上返回,以便下一个前来的HTTP请求可以被处理。每个HttpProcessor实例有自己的线程用于解析,所以这点不是很难做到。你将会在下节“HttpProcessor类”中看到是怎么做的。

    HttpProcessor类

        默认连接器中的HttpProcessor类是第3章中有着类似名字的类的全功能版本。你已经学习了它是如何工作的,在本章中,我们很有兴趣知道 HttpProcessor类怎样让assign方法异步化,这样HttpProcessor实例就可以同时间为很多HTTP请求服务了。
       注意: HttpProcessor类的另一个重要方法是私有方法process,它是用于解析HTTP请求和调用容器的invoke方法的。我们将会在本章稍后部分的“处理请求”一节中看到它。
        在第3章中,HttpConnector在它自身的线程中运行。但是,在处理下一个请求之前,它必须等待当前处理的HTTP请求结束。下面是第3章中HttpProcessor类的run方法的部分代码:

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

        第3章中的HttpProcessor类的process方法是同步的。因此,在接受另一个请求之前,它的run方法要等待process方法运行结束。
        在默认连接器中,然而,HttpProcessor类实现了java.lang.Runnable并且每个HttpProcessor实例运行在称作处理器线程(processor thread)的自身线程上。对HttpConnector创建的每个HttpProcessor实例,它的start方法将被调用,有效的启动了 HttpProcessor实例的处理线程。Listing 4.1展示了默认处理器中的HttpProcessor类的run方法:
    Listing 4.1: The HttpProcessor class's run method.

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

        run方法中的while循环按照这样的循序进行:获取一个套接字,处理它,调用连接器的recycle方法把当前的HttpProcessor实例推回栈。这里是HttpConenctor类的recycle方法:

    void recycle(HttpProcessor processor) {
       processors.push(processor);
    }

        需要注意的是,run中的while循环在await方法中结束。await方法持有处理线程的控制流,直到从HttpConnector中获取到一个新的套接字。用另外一种说法就是,直到HttpConnector调用HttpProcessor实例的assign方法。但是,await方法和assign方法运行在不同的线程上。assign方法从HttpConnector的run方法中调用。我们就说这个线程是HttpConnector实例的run方法运行的处理线程。assign方法是如何通知已经被调用的await方法的?就是通过一个布尔变量available并且使用java.lang.Object的wait和notifyAll方法。
        注意:wait方法让当前线程等待直到另一个线程为这个对象调用notify或者notifyAll方法为止。
    这里是HttpProcessor类的assign和await方法:

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

        两个方法的程序流向在Table 4.1中总结。
    Table 4.1: Summary of the await and assign method

    The processor thread (the await method) The connector thread (the assign method)
    while (!available) {                                while (available) {
    wait();                                                wait();
    }                                                        }
    Socket socket = this.socket;                   this.socket = socket;
    available = false;                                   available = true;
    notifyAll();                                            notifyAll();
    return socket; // to the run                     ...
    // method

        刚开始的时候,当处理器线程刚启动的时候,available为false,线程在while循环里边等待(见Table 4.1的第1列)。它将等待另一个线程调用notify或notifyAll。这就是说,调用wait方法让处理器线程暂停,直到连接器线程调用HttpProcessor实例的notifyAll方法。
        现在,看看第2列,当一个新的套接字被分配的时候,连接器线程调用HttpProcessor的assign方法。available的值是false,所以while循环给跳过,并且套接字给赋值给HttpProcessor实例的socket变量:

    this.socket = socket;

       连接器线程把available设置为true并调用notifyAll。这就唤醒了处理器线程,因为available为true,所以程序控制跳出while循环:把实例的socket赋值给一个本地变量,并把available设置为false,调用notifyAll,返回最后需要进行处理的socket。
        为什么await需要使用一个本地变量(socket)而不是返回实例的socket变量呢?因为这样一来,在当前socket被完全处理之前,实例的socket变量可以赋给下一个前来的socket。
        为什么await方法需要调用notifyAll呢? 这是为了防止在available为true的时候另一个socket到来。在这种情况下,连接器线程将会在assign方法的while循环中停止,直到接收到处理器线程的notifyAll调用。

  • 相关阅读:
    [洛谷2397]yyy loves Maths VI
    subs函数
    JSON 对象和字符串
    JSON添加注释的问题
    给定一个数组求里面数字拼在一起组成的最小数字
    为什么集合类没有实现Cloneable和Serializable接口
    Java 接口
    Unicode(统一码、万国码、单一码)
    meta 中的属性viewport
    meta标签
  • 原文地址:https://www.cnblogs.com/macula7/p/1960620.html
Copyright © 2011-2022 走看看