Chapter 4: Tomcat Default Connector
一个Tomcat连接器就是一个独立的模块,它可以被当做插件一样安置在servlet容器中。这里已经存在了许多连接器,包括:Coyote, mod_jk, mod_jk2, 和 mod_webapp都是连接器。作为一个Tomcat连接器,它必须符合一下要求:
它必须实现org.apache.catalina.Connector接口
它必须创建实现org.apache.catalina.Request接口的request对象。
它必须创建实现org.apache.catalina.Response接口的response对象。
Tomcat 4的默认连接器跟第三章中的简单连接器很类似。它等来HTTP请求,创建request和response对象,然后把request和response对象传递给容器。一个连接器通过调用org.apache.catalina.Container interface'的invoke方法把request和response对象传递给容器。
public void invoke(
org.apache.catalina.Request request,
org.apache.catalina.Response response);
}
在invoke方法里面,连接器加载servlet类,调用service方法,管理session,记录log错误信息等。
默认的连接器也一些优化策略是与第三章的连接器不一样的。首先提供了不同的对象池来避免创建对象的开销。第二,在很多地方使用字符数组代替字符串。
这章的应用程序的连接器可以和默认连接器联系起来。默认的连接器实现了HTTP1.1到HTTP 0.9和1.0的所有特性。本章会讲解HTTP1.1的新特性。
HTTP 1.1 New Features
Persistent Connections (持久连接)
在HTTP1.1之前,一个浏览器不管什么时候连接到web服务器,这个连接会在服务器响应了请求资源后立马关闭。但是,一个网页可以包含其他资源:图片文件,applet等。当一个页面被请求,浏览器也需要通过这个页面下载相关资源。(创建新的连接请求该网页的其他相关资源)如果一个页面和这个页面相关的所有资源的下载使用不同的链接,这种处理方式会变得很慢。这就是为什么HTTP1.1提出的持久连接。有了持久连接,当一个页面被下载,这个服务器不会立马关闭连接。它会等待web客户端请求所有与该页面相关的资源。这种方式,一个网页和该网页相关的资源可以使用同一个连接下载。这样对于服务器,客户端和网络来所都节省了许多工作和时间。因为建立和拆除连接是开销很大的操作。
持久连接是HTTP1.1默认的连接。也可以明确告诉浏览器发送头信息的connection值:
connection: keep-alive
Chunked Encoding(块编码)
建立一个持久连接的结果是:服务器可以通过多媒体资源发送字节流,客户端可以接收使用同样的链接接收者多媒体资源。结果,发送者必须在每一个request和response的头部信息中发送内容的长度以便接收者知道怎么来解释这些字节。但是,通常情况是发送者不知道究竟要发送多少字节。例如:一个servlet容器在当持有一些可以(available)使用的字节就可以开始发送response,而不是在等到所有的都准备好了之后再一起发送。这种方式,必须有一种方法可以告诉接收者在不能提前知道content-length头部信息的情况下怎么解释字节流。
就算没有多媒体资源请求或许多响应,一个服务器或客户端不必要它要发送知道多少数据。在HTTP1.0。一个服务器可以忽略content-length这个头部信息,保持等待连接。当完成了,它就关闭连接。这种情况下,客户端在读取到以-l作为到达文件结束的标识符之前一直保持读取的状态。
HTTP1.1有一个叫做transfer-encoding的特殊头部信息表明字节流将会以块状形式发送。对于每一个块:长度(16进制数表示)后面紧跟CR/LF且位于要发送的数据之前。一个事务被用一个0(zero)标示的长度块。假设你发送下面的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表明是事务的结束。
Use of the 100 (Continue) Status (使用100状态号)
HTTP1.1客户端可以在发送请求体之前发送Expect:100-continue头部信息到服务器,然后等待服务器的确认。这是很正常的,如果客户端打算发送一个长请求体,但是不确定服务器是否会接受。如果客户端发送长请求体到服务器仅仅只是为了确认服务器是绝接收长请求体本身就是浪费。
在接受到Expect: 100-continue头部信息,服务器如果可以处理请求,就回应一个100-continue再加上两对CRLF字符的头部信息。
HTTP/1.1 100 Continue
这服务器就可以继续读取输入流。
The Connector Interface
一个Tomcat连接器必须实现org.apache.catalina.Connector接口。这个接口中最重要的方法就是:getContainer, setContainer, createRequest和createResponse.
getContainer方法用来把连接器和容器联系起来。GetContainer方法返回被联系的容器。CreateRequest给到来的HTTP请求构建一个request对象,createReponse创建response对象。
org.apache.catalina.connector.http.HttpConnector是一个实现了Connector接口的类,它将在下一节讨论。
一个Connector和一个Container是一对一的关系。
The HttpConnector Class
你在第三章中的简单版本的org.apache.catalina.connector.http.HttpConnector已经知道这个类是怎么工作的。它实现了org.apache.catalina.Connector接口(更合适与Catalina工作),java.lang.Runnable接口(这样它的实例可以工作在自己的线程里面),org.apache.catalina.Lifecycle接口。Lifecycle接口用来维护每一个实现了该接口的Catalina组件的生命周期。
Lifecycle在第六章讲解。现在你只需要了解:通过实现Lifecycle接口,你创建了一个HttpConnector实例后,你可以调用它的initialize核start方法。两个方法在组件的生命期间都只必须调用一次。你可以看到和第三章中HttpConnector的不同的地方:HttpConnector怎么创建一个服务器socket,怎么维护一个HttpProcessor池和为HTTP请求服务。
Creating a Server Socket
HttpConnector的initialize方法调用一个private修饰的的open方法。Open方法返回一个java.net.ServerSocket实例,把它指配给serverSocket。但是,不是调用java.net.ServerSocket的构造函数,open方法从一个服务器socket工厂中获取一个ServerSocket实例。如果你想知道这个工厂的细节,读在org.apache.catalina.net package包中的ServerSocketFactory接口和DefaultServerSocketFactory类。
Maintaining HttpProcessor Instances
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被设置为5maxProcessors设置为20,但是你可以用setMinProcessors和setMaxProcessors方法来修该这些值。
protected int minProcessors = 5;
private int maxProcessors = 20;
起初,HttpConnector对象创建minProcessors个HttpProcessor实例。如果在同一时刻有更多的超过HttpProcessor实例个数请求,HttpConnector创建更多HttpProcessor实例,直到实例数到达了maxProcessors。当到达了maxProcessors后,HttpProcessor实例还是不够请求使用,这些到来的HTTP请求将会被忽略。如果你想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请求行,头部信息和填充request对象。此外,每一个HttpProcessor实例把一个request对象和一个response对象关联起来。HttpProcessor类的构造函数包含了调用HttpConnector类的createRequest方法和createResponse方法。
Serving HTTP Requests
HttpConnector类在它的run方法里有它的主逻辑:包含一个while循环,服务器socket一直等待HTTP请求,知道HttpConnector被停止。
while (!stopped) {
Socket socket = null;
try {
socket = serverSocket.accept();
...
对于每一个到服务器访问的HTTP的请求,它通过调用createProcessor方法获得一个HttpProcessor实例。
HttpProcessor processor = createProcessor();
但是,大多数时间createProcessor方法不会创建一个新的HttpProcessor对象,而是在HttpProcessor池中获得一个HttpProcessor实例。如果任然有一个HttpProcessor实例在栈中,createProcessor从栈中弹出一个HttpProcessor实例。如果栈是空的且还没达到HttpProcessor实例的最大数量,createProcessor创建一个HttpProcessor实例。但是如果到达HttpProcessor实例的最大数量,createProcessor返回null。如果发生了,socket只是简单的关闭,来访的HTTP请求不会被处理。
if (processor == null) {
try {
log(sm.getString("httpConnector.noProcessor"));
socket.close();
}
...
continue;
如果createProcessor不返回null,客户端socket被传递给HttpProcessor类的assign方法:
processor.assign(socket);
现在就该HttpProcessor实例读取socket的输入流和解析HTTP请求。很重要的一点是。assign方法必须马上返回,不能等到HttpProcessor处理完解析工作才返回。这样才能让下一个来访的HTTP请求得到服务。每一个HttpProcessor实例有它自己的线程来解析,这也很容易办到。
The HttpProcessor Class
HttpProcessor类在默认连接器是一个完整的版本。你可以知道它是怎么工作,HttpProcessor类让它的assign方法实现异步,这样就可以让HttpConnector实例在同一时刻服务许多的HTTP请求。
注意:HttpProcessor类另外一个很重要的方法:process方法。将诶西HTTP请求和调用容器的invoke方法。
第三章,HttpConnector运行在自己的线程中。但是,它还必须等待当前处理的HTTP请求完成后才能处理下一个请求。下面是第三章中的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);
}
}
第三章HttpProcessor类的process方法是同步的。所以,它的run方法必须等到process方法处理完成了一个请求后,才能接收下一个请求。
在默认的连接器里,HttpProcessor类实现了java.lang.Runnable接口,每一个HttpProcessor实例运行在自己的被叫做“processor thread”的线程里。下面是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循环一直保持做下面的处理:获得一个socket,处理socket,调用容器的recycle方法把当前的HttpProcessor实例推回到栈中。下面是HttpConenctor类的recycle方法:
void recycle(HttpProcessor processor) {
processors.push(processor);
}
注意到这run方法里的while循环的结束受控于await方法。await方法持有"processor thread"的控制流,直到它从HttpConnector获得一个新的socket。换句话说,直到HttpConnector调用HttpProcessor实例的assign方法。await方法跟assign方法是运行在不同的线程里。assign方法是在HttpConnector 的run方法里面调用的。我们把这个HttpConnector实例run方法里面运行的线程叫做"connector thread"。assign方法怎么通知await方法,告诉它assign方法被调用了呢?使用一个叫做available的boolean变量和使用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);
}
所有方法的程序流在下面总结出来
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
首先,当"processor thread"开始,available值是false,线程在while循环里面等待。直到另外一个线程调用notify或notifyAll结束循环。就是说调用wait方法让"processor thread"暂停,直到"connector thread"的HttpProcessor实例调用notifyAll方法。
现在,当一个新的socket被指配,"connector thread"调用HttpProcessor的assign方法。available的值为faslse,所以从while循环跳出,socket被指配到HttpProcessor实例的socket变量:
this.socket = socket;
然后"connector thread"把available值设置为true,调用notifyAll方法。这是唤醒"processor thread",现在available的值是true,程序跳出while循环:把实例的socket指配给一个局部变量,把available设置为false,调用notifyAll方法,返回这个socket,让这个socket得到处理。
为什么await方法需要使用局部变(socket)而不是返回一个实例的socket变量呢?这是因为实例socket变量可以在当前socket被处理完毕之前,这个实例socket变量可以指配给下一个到来的socket。
为什么await方法需要调用notifyAll?当available变量是true时另外一个socket到达了。在这种情况,"connector thread"的assign方法里面的while循环在"processor thread"中的notifyAll方法得到调用时才停止。