攻城狮在路上(伍)How tomcat works(四)Tomcat的默认连接器

时间:2021-08-29 01:02:14
 在第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新特性
1、持久连接
 
    在HTTP1.1之前,无论什么时候浏览器连接到一个web服务器,当请求的资源被发送之后,连接就被服务器关闭了。然而,一个互联网网页包括其他资源,例如图片文件,applet等等。因此,当一个页面被请求的时候,浏览器同样需要下载页面所引用到的资源。加入页面和它所引用到的全部资源使用不同连接来下载的话,进程将会非常慢。那就是为什么HTTP1.1引入持久连接的原因了。使用持久连接的时候,当页面下载的时候,服务器并不直接关闭连接。相反,它等待web客户端请求页面所引用的全部资源。这种情况下,页面和所引用的资源使用同一个连接来下载。考虑建立和解除HTTP连接的宝贵操作的话,这就为 web服务器,客户端和网络节省了许多工作和时间。
    持久连接是HTTP1.1的默认连接方式。同样,为了明确这一点,浏览器可以发送一个值为keep-alive的请求头部connection:
connection: keep-alive
2、块编码
 
    建立持续连接的结果就是,使用同一个连接,服务器可以从不同的资源发送字节流,而客户端可以使用发送多个请求。结果就是,发送方必须为每个请求或响应发送内容长度的头部,以便接收方知道如何解释这些字节。然而,大部分的情况是发送方并不知道将要发送多少个字节。例如,在开头一些字节已经准备好的时候,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标识这个事务的结束。
3、状态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创建一个响应对象。
    一个Connector和Container是一对一的关系。箭头的方向显示出Connector知道Container但反过来就不成立了。同样需要注意的是,不像第3章的是,HttpConnector和HttpProcessor是一对多的关系。
 
三、HttpConnector类:org.apache.catalina.connector.http.HttpConnector
    它实现了org.apache.catalina.Connector (为了和Catalina协调), java.lang.Runnable (因此它的实例可以运行在自己的线程上)和org.apache.catalina.Lifecycle。接口Lifecycle用来维护每个已经实现它的Catalina组件的生命周期。
    我们将看看和第3章的HttpConnector类的那些不同方面:HttpConnector如何创建一个服务器套接字,它如何维护一个HttpProcessor对象池,还有它如何处理HTTP请求。
1、创建一个服务器套接字:
    HttpConnector的initialize方法调用open这个私有方法,返回一个java.net.ServerSocket实例,并把它赋予 serverSocket。然而,不是调用java.net.ServerSocket的构造方法,open方法是从一个服务端套接字工厂中获得一个 ServerSocket实例。如果你想知道这工厂的详细信息,可以阅读包org.apache.catalina.net里边的接口 ServerSocketFactory和类DefaultServerSocketFactory。它们是很容易理解的。
2、维护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方法来改变他们的值。

    开始的时候,HttpConnector对象创建minProcessors个HttpProcessor实例。如果一次有比HtppProcessor 实例更多的请求需要处理时,HttpConnector创建更多的HttpProcessor实例,直到实例数量达到maxProcessors个。在到达这点之后,仍不够HttpProcessor实例的话,请来的请求将会给忽略掉。如果你想让HttpConnector继续创建 HttpProcessor实例的话,把maxProcessors设置为一个负数。还有就是变量curProcessors保存了 HttpProcessor实例的当前数量。
    每个HttpProcessor实例负责解析HTTP请求行和头部,并填充请求对象。因此,每个实例关联着一个请求对象和响应对象。类 HttpProcessor的构造方法包括了类HttpConnector的createRequest和createResponse方法的调用。
3、为HTTP请求服务:
     就像第3章一样,HttpConnector类在它的run方法中有其主要的逻辑。run方法在一个服务端套接字等待HTTP请求的地方存在一个while循环,一直运行直至HttpConnector被关闭了。
    对每个前来的HTTP请求,会通过调用私有方法createProcessor获得一个HttpProcessor实例。
    大部分时候createProcessor方法并不创建一个新的HttpProcessor对象。相反,它从池子中获取一个。如果在栈中已经存在一个HttpProcessor实例,createProcessor将弹出一个。如果栈是空的并且没有超过HttpProcessor实例的最大数量,createProcessor将会创建一个。然而,如果已经达到最大数量的话,createProcessor将会返回null。出现这样的情况的话,套接字将会简单关闭并且前来的HTTP请求不会被处理。
    如果createProcessor不是返回null,客户端套接字会传递给HttpProcessor类的assign方法:assign方法不会等到 HttpProcessor完成解析工作,而是必须马上返回,以便下一个前来的HTTP请求可以被处理。
4、HttpProcessor类:
    在本章中,我们很有兴趣知道 HttpProcessor类怎样让assign方法异步化,这样HttpProcessor实例就可以同时间为很多HTTP请求服务了。
    注意: HttpProcessor类的另一个重要方法是私有方法process,它是用于解析HTTP请求和调用容器的invoke方法的。我们将会在本章稍后部分的“处理请求”一节中看到它。
    第3章中的HttpProcessor类的process方法是同步的。因此,在接受另一个请求之前,它的run方法要等待process方法运行结束。
    在默认连接器中,然而,HttpProcessor类实现了java.lang.Runnable并且每个HttpProcessor实例运行在称作处理器线程(processor thread)的自身线程上。对HttpConnector创建的每个HttpProcessor实例,它的start方法将被调用,有效的启动了 HttpProcessor实例的处理线程。Listing 4.1展示了默认处理器中的HttpProcessor类的run方法。