How tomcat works——4 Tomcat中默认连接器

来源:互联网 发布:工业设计 比赛软件 编辑:程序博客网 时间:2024/05/21 17:19

概述

第 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 章中还未使用到的优化。首先提供对象池用于避免昂贵对象的创建。其次,在很多地方使用字节数组来代替字符串。

本章应用Demo中使用一简单容器(container)和默认连接器关联协作。然而,本章重点不是此简单容器而是默认连接器。我们将会在第 5 章中讨论容器。尽管如此,为了展示如何使用默认连接器,我们将会在接近本章末尾时“简单容器的应用程序”一节中讨论此简单容器。

另一点需要注意的是默认连接器除了提供 HTTP0.9 和 HTTP1.0 的支持外,还实现了 HTTP1.1的所有新特性。为了理解 HTTP1.1 新特性,我们首先需要理解本章首节解释的这些新特性。在这之后,我们将会讨论org.apache.catalina.Connector接口 和如何创建请求和响应对象。如果已理解第 3 章中连接器工作机制的话,那么再理解默认连接器应该不会遇到什么问题。

本章首先讨论 HTTP1.1 的三个新特性。理解它们是理解默认连接器内部工作机制的关键所在。然后,介绍所有连接器都会实现的org.apache.catalina.Connector接口。我们会发现第 3 章中遇到的那些类,如 HttpConnector, HttpProcessor 等。不过,此时它们比第 3 章那些相似的类要高级些。

4.1 HTTP1.1新特性

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

4.1.1 HTTP1.1持久连接

在 HTTP1.1 之前,无论什么时候浏览器连接到一个 web 服务器,当请求的资源被发送之后,连接就被服务器关闭了。然而,一个网页包括其他资源, 例如图片,applet 等。因此,当一个页面被请求的时候,浏览器同样需要下载页面所引用到的其它资源。如果加载页面和它所引用到的全部资源使用不同连接来下载的话,那么进程将会非常慢。这就是为什么 HTTP1.1 引入持久连接的原因。使用持久连接时,当页面下载时,服务器并不直接关闭连接。相反,它等待 web 客户端请求页面所引用的全部资源。此时,页面和所引用资源使用同一个连接来下载。鉴于建立和解除 HTTP 连接是非常昂贵的操作,这就为服务器,客户端和网络节省了许多工作和时间。

持久连接是 HTTP1.1 的默认连接方式。同样,为了明确这一点,浏览器可以发送一个值为keep-alive 的请求头部:

connection: keep-alive

4.1.2块编码

建立持续连接的效果就是:服务器使用同一个连接可以从不同的资源发送字节流;客户端可以使用同一个连接发送多个请求。结果就是,发送方必须为每个请求或响应发送含内容长度的头部信息,以便接收方知道如何解释这些字节。然而,大部分的情况是发送方并不知道将要发送多少字节。例如,在开头一些字节已经准备好时,servlet 容器就可以开始发送响应了,而不会等到所有都准备好。这意味着,在 content-length 头部不能提前知道的情况下,必须有一种方式来告诉接收方如何解释字节流。

即使不需要发送多个请求或者响应,服务器或者客户端也不需要知道将会发送多少数据。在
HTTP1.0 中,服务器可以仅仅省略 content-length 头部,并保持写入连接。当写入完成时,它将简单的关闭连接。在这种情况下,客户端将会保持读取状态,直到获取到-1,表示已经到达文件尾部。

HTTP 1.1使用一称为传输编码的特殊报头(transfer-encoding)来指示字节流将以块形式发送。对每块来说,在数据之前,长度(十六进 制)后面接着 CR/LF 将被发送(For every chunk, the length (in hexadecimal) followed by CR/LF is sent prior to the data)。整个事务通过一个零长度的块来标识(A transaction is marked with a zero length chunk)。假设我们想用 2 个块发送以下 38 个字节,第一个长度是 29,第二个长度是 9。

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

我们将这样发送:

1D\r\nI'm as helpless as a kitten u9\r\np a tree.0\r\n

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

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

在发送请求内容之前,HTTP 1.1 客户端可以发送 Expect: 100-continue 报头到服务器,并
等待服务器确认。这个一般发生在当客户端需要发送一份长的请求内容而又未能确保服务器愿意接受它的时候。如果发送一份长的请求内容后却发现遭到服务器拒绝,这将是一种浪费。

当接收到 Expect: 100-continue 报头时,假如乐意或者可以处理请求的话,服务器则响应 100-continue 报头,后边跟着两对 CRLF 字符。

HTTP/1.1 100 Continue

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

4.2 Connector接口

Tomcat连接器必须实现org.apache.catalina.Connector接口。在这个接口众多方法中,最重要的是 getContainer(),setContainer(), createRequest() 和 createResponse()。

setContainer() 用来关联连接器和容器;getContainer() 返回关联的容器;createRequest() 为HTTP 请求构造一请求对象; createResponse() 创建一响应对象。

类 org.apache.catalina.connector.http.HttpConnector 是 Connector 接口的一个实现,将会在下一 节“HttpConnector 类”中讨论。现在,仔细看一下 图4.1 中默认连接器的UML 图。注意的是,为了保持图的简化,Request 和 Response 接口的实现被省略了。除了SimpleContainer 类,org.apache.catalina 前缀也同样从类型名中省略了。
这里写图片描述
图 4.1: 默认连接器类图

因此,Connector 需被读作 org.apache.catalina.Connector;util.StringManager应被读作org.apache.catalina.util.StringManager ,等。

一个Connector和Container是一对一关系。箭头的方向显示出Connector知道Container
但反过来就不成立了(The navigability of the arrow representing the relationship reveals that the Connector knows about the Container but not the other way around)。同样需要注意的是:不像第 3 章那样了,现HttpConnector 和 HttpProcessor是一对多关系(回头看看图3.1那会是一对一关系)。

4.3 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 请求。

4.3.1 创建服务器套接字

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

4.3.2 维护HttpProcessor 实例

在第 3 章中,HttpConnector 实例一次仅仅拥有一个 HttpProcessor 实例,所以每次只能处
理一个 HTTP 请求。在默认连接器中,HttpConnector 拥有一个 HttpProcessor 对象池,并且每个HttpProcessor 实例拥有一个独立线程。因此,HttpConnector 可以同时处理多个 HTTP 请求。

HttpConnector维护一个 HttpProcessor 实例池,从而避免每次创建 HttpProcessor 实例。这些 HttpProcessor 实例是存放在类型为java.io.Stack的processors变量中:

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实例 push到栈中。

每个 HttpProcessor 实例负责解析 HTTP 请求行和头部,并填充请求对象。因此,每个实例关联着一个请求对象和一个响应对象。HttpProcessor类的构造方法调用了HttpConnector 类的createRequest() 和 createResponse() 方法。

4.3.3 处理 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 类”中看到是如何实现的。

4.4 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:

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

两个方法的程序流向在 表4.1 中总结。
这里写图片描述

刚开始,当处理器线程刚启动时,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()。

4.5 Request对象

默认连接器里HTTP 请求对象通过org.apache.catalina.Request 接口指代表现。这个接口被类RequestBase 直接实现,同时org.apache.catalina.Request也是 HttpRequest 的父接口。最终实现是继承于 HttpRequest 的HttpRequestImpl类。像第 3 章一样,有几个 facade 类:RequestFacade 和 HttpRequestFacade。Request 接口和它的实现类的 UML 图在图4.2 中给出。注意的是,除了属于 javax.servlet和 javax.servlet.http 包的类,前缀 org.apache.catalina 已经被省略(已补上)。
这里写图片描述

图 4.2: Request接口和相关

如果理解了第 3 章的请求对象,理解这个结构图应该不会有什么困难。

4.6 Response对象

Response接口及它的实现类的UML图如图4.3
这里写图片描述

图 4.3: Response接口和相关

4.7 处理请求

本章到此,我们已经理解了请求和响应对象,并且知道 HttpConnector 对象是如何创建它们。现在是这个过程的最后一点东西了。在这节中我们关注 HttpProcessor 类的 process()方法,它是当一个套接字赋给它之后,在 HttpProcessor 类的 run() 方法中调用的。process()方法会做如下工作:
• 1》解析连接
• 2》解析请求
• 3》解析头部

在解释完 process() 方法之后,在本节的各个小节中将讨论每个操作。

process()方法使用布尔变量 ok 来指代在处理过程中是否发现错误,并使用布尔变量
finishResponse 来指代是否应该调用Response 接口中的 finishResponse() 方法。

boolean ok = true;boolean finishResponse = true;

另外,process() 方法也使用了布尔变量 keepAlive,stopped 和 http11。keepAlive 表示连接是否是持久连接;stopped 表示 HttpProcessor 实例是否已经被连接器终止以此来确认 process()是否也应该停止;http11 表示从web客户端过来的 HTTP 请求是否支持 HTTP 1.1。

像第 3 章那样,有一个 SocketInputStream 实例用来包装套接字的输入流。注意,SocketInputStream 的构造方法同样传递了从连接器获得的缓冲区大小,而不是从HttpProcessor 的本地变量获得。这是因为对于默认连接器的用户而言,HttpProcessor 是不可访问的。通过Connector 接口传递缓冲区大小,这就使得使用连接器的任何人都可以设置缓冲大小。

SocketInputStream input = null;OutputStream output = null;// Construct and initialize the objects we will needtry {    input = new SocketInputStream(socket.getInputstream(),    connector.getBufferSize());}catch (Exception e) {    ok = false;}

然后,有个 while 循环用来保持从输入流中读取,直到 HttpProcessor 被停止,或有异常被
抛出或连接被关闭为止。

keepAlive = true;while (!stopped && ok && keepAlive) {    ...}

在 while 循环内部,process ()方法首先把 finishResponse 设置为 true,并获得输出流,并对请求和响应对象做些初始化处理。

finishResponse = true;try {    request.setStream(input);    request.setResponse(response);    utput = socket.getOutputStream();    esponse.setStream(output);    esponse.setRequest(request);    (HttpServletResponse) response.getResponse()).setHeader("Server", SERVER_INFO);}catch (Exception e) {    og("process.create", e); //logging is discussed in Chapter 7    k = false;}

接着,process()方法通过调用 parseConnection(),parseRequest() 和 parseHeaders ()方法开始解析 HTTP 请求,这些方法将在这节的小节中讨论。

try {    f (ok) {        rseConnection(socket);        rseRequest(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()方法它将响应这个头部。它将会测试组块是否是允许的。

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

ackRequest()方法测试 sendAck 的值,并在 sendAck 为 true时发送下面的字符串:

HTTP/1.1 100 Continue\r\n\r\n

在解析 HTTP 请求过程中,有可能会抛出异常。任何异常都将会把 ok 或 finishResponse
设置为 false。在解析过后,process()方法把请求和响应对象传递给容器的 invoke()方法:

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

接着,如果 finishResponse 仍然是 true,Response对象的 finishResponse() 方法和Request对象的finishRequest()方法将被调用,并且结束输出。

if (finishResponse) {    ...    response.finishResponse();    ...    request.finishRequest();    ...    output.flush();

while 循环的最后一部分检查响应的 Connection 头部是否已经在 servlet 内部设为 close,
或者协议是 HTTP1.0.如果是这种情况的话,keepAlive 设置为 false。同样,Request和Response对象接着会被回收。

if ( "close".equals(response.getHeader("Connection")) ) {    keepAlive = false;}// End of request processingstatus = Constants.PROCESSOR_IDLE;// Recycling the request and the response objectsrequest.recycle();response.recycle();}

在这个场景中,如果 keepAlive 是 true 的话,while 循环将会在开头就启动。因为在前
面的解析过程中和容器的 invoke()方法中没出现错误,或HttpProcessor 实例没有被停止。
否则,shutdownInput() 方法将会调用,而套接字将被关闭。

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

shutdownInput()方法检查是否有未读取的字节。如果有的话,跳过那些字节。

4.7.1 解析连接

parseConnection()方法从套接字中获取到网络地址并把它赋予 HttpRequestImpl 对象。它也
检查是否使用代理并把套接字赋予请求对象。parseConnection()方法在 Listing4.2 中列出。

Listing 4.2: parseConnection()方法

private void parseConnection(Socket socket)        throws IOException, ServletException {        if (debug >= 2)            log("  parseConnection: address=" + socket.getInetAddress() +                ", port=" + connector.getPort());        ((HttpRequestImpl) request).setInet(socket.getInetAddress());        if (proxyPort != 0)            request.setServerPort(proxyPort);        else            request.setServerPort(serverPort);        request.setSocket(socket);}

4.7.2 解析请求

parseRequest()方法是第 3 章中类似方法的完整版本。如果已很好的理解第 3 章的话,通过阅读这个方法应该可以理解这个方法是如何运行的。

4.7.3 解析头部

默认连接器的 parseHeaders()方法使用包 org.apache.catalina.connector.http 里边的HttpHeader 和 DefaultHeaders 类。类 HttpHeader 指代一个 HTTP 请求头部。类 HttpHeader 不是像第 3 章那样使用字符串,而是使用字符数组用来避免昂贵的字符串操作。类 DefaultHeaders是一个 final 类,在字符数组中包含了标准的 HTTP 请求头部:

standard HTTP request headers in character arrays:static final char[] AUTHORIZATION_NAME = "authorization".toCharArray();static final char[] ACCEPT_LANGUAGE_NAME = "accept-language".toCharArray();static final char[] COOKIE_NAME = "cookie".toCharArray();...

parseHeaders()方法包含一个 while 循环,可以持续读取 HTTP 请求直到再也没有更多的头部可以读取到。while 循环首先调用请求对象的 allocateHeader()方法来获取一个空的 HttpHead 实例。这个实例被传递给SocketInputStream 的 readHeader() 方法。

HttpHeader header = request.allocateHeader();// Read the next headerinput.readHeader(header);

假如所有的头部都被已经被读取的话,readHeader() 方法将没有名称(报头Name)赋值给 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);}else if (header.equals(DefaultHeaders.COOKIE_NAME)) {    // parse cookie}else if (header.equals(DefaultHeaders.CONTENT_LENGTH_NAME)) {// get content length}.....request.nextHeader();

4.8 简单Container应用

本章应用Demo主要目的是展示默认连接器是怎样工作的。它包括两个类:ex04.pyrmont.core.SimpleContainer 和 ex04 pyrmont.startup.Bootstrap。

SimpleContainer类实现了 org.apache.catalina.container 接口,所以它可以和连接器关联。Bootstrap类用来启动应用,我们已经移除了第 3 章带的应用程序中的连接器模块,类ServletProcessor 和StaticResourceProcessor,所以不能请求一个静态页面。

类 SimpleContainer 展示在 Listing 4.3:

Listing 4.3: SimpleContainer类

package ex04.pyrmont.core;import java.beans.PropertyChangeListener;import java.net.URL;import java.net.URLClassLoader;import java.net.URLStreamHandler;import java.io.File;import .....public class SimpleContainer implements Container {public static final String WEB_ROOT =System.getProperty("user.dir") + File.separator + "webroot";public SimpleContainer() { }public String getInfo() {return null;}public Loader getLoader() {return null;}public void setLoader(Loader loader) { }public Logger getLogger() {return null;}public void setLogger(Logger logger) { }public Manager getManager() {return null;}public void setManager(Manager manager) { }public Cluster getCluster() {return null;}public void setCluster(Cluster cluster) { }public String getName() {return null;}public void setName(String name) { }public Container getParent() {return null;}public void setParent(Container container) { }public ClassLoader getParentClassLoader() {return null;}public void setParentClassLoader(ClassLoader parent) { }public Realm getRealm() {return null;}public void setRealm(Realm realm) { }public DirContext getResources() {return null;}public void setResources(DirContext resources) { }public void addChild(Container child) { }public void addContainerListener(ContainerListener listener) { }public void addMapper(Mapper mapper) { }public void addPropertyChangeListener(PropertyChangeListener listener) { }public Container findchild(String name) {return null;}public Container[] findChildren() {return null;}public ContainerListener[] findContainerListeners() {return null;}public Mapper findMapper(String protocol) {return null;}public Mapper[] findMappers() {return null;}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());}}public Container map(Request request, boolean update) {return null;}public void removeChild(Container child) { }public void removeContainerListener(ContainerListener listener) { }public void removeMapper(Mapper mapper) { }public void removoPropertyChangeListener(PropertyChangeListener listener) {}}

在SimpleContainer 类中我们只提供实现了invoke() 方法,因为默认连接器将会调用这个方
法。invoke()方法创建了一类加载器,加载 servlet 类,并调其service() 方法。这个方法和第 3 章的 ServletProcessor 类中process()方法非常类似。

Bootstrap 类在 Listing 4.4 在列出.

Listing 4.4: ex04.pyrmont.startup.Bootstrap类

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();            // make the application wait until we press any key.            System in.read();        }catch (Exception e) {            e.printStackTrace();        }    }}

Bootstrap 类中main()方 法 构 造 了 一 个org.apache.catalina.connector.http.HttpConnector 实例和一个 SimpleContainer 实例。它接下去调用 conncetor 的 setContainer()方法传递 container,让 connector 和 container 关联起来。下一步,它调用 connector 的 initialize() 和 start() 方法。这将会使得 connector 为处理8080 端口上的任何请求做好了准备。

可以通过在控制台中输入一个按键来终止这个应用程序。

4.8.1运行应用程序

Windows 中运行,在工作目录下输入以下内容:

java -classpath ./lib/servlet.jar;./ ex04.pyrmont.startup.Bootstrap

Linux 的话,可以使用分号来分隔两个库:

java -classpath ./lib/servlet.jar:./ ex04.pyrmont.startup.Bootstrap

我们可以和第三章那样调用 PrimitiveServlet 和 ModernServlet。注意,不能请求 index.html,因为没有静态资源的处理器。

4.9 小结

本章展示构建了一个可以和Catalina一起工作的Tomcat连接器。 解析了Tomcat 4默认连接器代码并构建了一个小应用程序使用连接器。在下面的章节所有应用程序中使用该默认连接器。

0 0
原创粉丝点击