第3 章的连接器工作得很好,而且本可以设计地更好。但是,我们只是将它设计成教学工具,来介 绍Tomcat 4 的默认连接器。理解第3 章的连接器,是理解Tomcat 4 默认连 接器的关键。第4 章将会通过解剖(dissect )Tomcat 4 默认 连接器的代码,来讨论如何构建真正的Tomcat 连接器。
提示:本章的“默认连接器” 就是指Tomcat 4 默认连接器。尽管默认连接器已经废弃(deprecated ),被更快的Coyote 连接器代替了,但是它仍然是一个很好的学习工具。
Tomcat 连接器是一个可以插入Servlet 容 器的独立模块。现在已经有很多连接器,例如Coyote 、mod_jk 、mod_jk2 和mod_webapp 。一个Tomcat 连 接器 满足下面的需求:
Tomcat 4 默认连接器和第3 章中简单连接器的工作原理类似。它等待接受HTTP 请 求,创建请求对象和响应对象,然后将这两个对象传给容器。连接器通过调用org.apache.catalina.Container 接 口的invoke 方法,将请求对象和响应对象传递给容器。invoke 方法的原型如下:
public void invoke( org.apache.catalina.Request request, org.apache.catalina.Response response);
在invoke 方法内部,容器加载servlet 类, 调用其service 方法,管理会话,打印错误日志等等。默认连接器还利用了一些第3 章连接器没有的优化措施。首先,默认连接器提供了对象池来避免昂贵的对象创建。第二, 默认连接器在很多地方使用字符数组来代替字符串。
本章的应用,是一个与默认连接器关联的、简单的容器。不过,本章的焦点不是这个容器,而是默认连接器。容器将在第5 章被讨论。不管怎么样,我们还是在本章的最后一节“简单的容器程序”讨论该容器,以演示如何使用默认连接器。
另一个需要注意的地方是,默认连接器实现了那些在HTTP 1.1 中新加 的、同样可以服务HTTP 0.9 和HTTP 1.0 客户的特性。为了理解HTTP 1.1 的新特性,你首先需要理解这些特性,我们将在本章第一节解释它们。在此之后,我们讨论org.apache.catalina.Connector ,如何创建请求对象和响应对象。如果你理解第3 章的连接器是如何工作的,你也不难理解默认连接器。
本章以HTTP 1.1 的3 个 新特性作为开始。理解它们是理解默认连接器内部原理的关键。然后,本章介绍了所有连接器都必须是实现的org.apache.catalina.Connector 接 口。你会发现第3章中已经遇到的一些类,像HttpConnector 、HttpProcessor 等等。不过,现在这些类比第3 章要高级的多。
本节介绍HTTP 1.1 的三个新特性。理解这些特性,对于理解默认连接器如何处理HTTP 请求,是非常关键的。
在HTTP 1.1 之 前,无论浏览器什么时候连接上Web 服务器,服务器在发送完被请求资源之后立 刻关闭连接。但是,一个网页可以包含其他资源,例如图片文件、applets 等。 因此,当一个页面被请求时,浏览器也需要下载该页面引用的资源。如果页面及其引用的所有资源都通过不同的连接下载,那么整个处理过程会很慢。这就是HTTP 1.1 引入持久化连接的原因。对于持久化连接,页面下载完成后,服务器不会直 接关闭连接,而是等待客户端请求该页面引用的所有资源。这样,页面及其引用的所有资源均使用同一个连接下载。考虑到建立和关闭HTTP 连接都是昂贵的操作,这种方式将大大节省了Web 服务器、客户端及网络的负载和时间。
持久化连接是HTTP 1.1 的默认连接。浏览器也可以通过发送下面的connection 头部,显式地告诉服务器使用持久化连接:
connection: keep-alive
建立持久化连接的一个结果就是,服务器可以在同一个连接上发送多个资源的字节流,客户端也可以在同一个连接上发送多个请求。因此,发送者必须发送每个请求 或响应的content-length 头部,这样接收者才知道如何解析接收到字节流。 但是通常情况下,发送者并不知道要发送多少字节。举个例子,servlet 容 器可以在部分字节准备好时就开始发送响应,而不要等到所有字节都准备好。这意味着,必须有一种方法告诉接收者:在不能提前知道content- length头部的情况下,如何解析字节流。
即使没有发送多个请求或响应,服务器或客户端也没有必要知道,究竟多少数据将被发送。在HTTP 1.0 中,服务器可以省略content-length 头部,直接向 连接写如数据。当写入完成时,服务器会简单地关闭连接。在这种情况下,客户端持续读取,直到返回标识字节流结束的-1 。
HTTP 1.1 利 用了一个名为transfer-encoding 的特殊头部,来指示字节流将按照chunk 的形式被发送。每个chunk 的格式是:首先是十六进制的长度,后面跟着一个CR/LF , 然后是数据。零长度的chunk 标识了一个传输单元(transaction )。假设在某个传输中,你想以2 个chunk 的形式发送下面的38 个字节,第一个chunk 长 度为29 ,第二个chunk 长 度为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 的 十六进制形式,指示第一个chunk 包括29 个字节。0/r/n 标识该传输单元的结 束。
如果HTTP 1.1 客户端打算发送很长的请 求,但是不确定服务器是否愿意接收,那么,它可以在发送请求体之前先发送Expect: 100-continue 头部给服务器,然后等待服务器的确认。不这么做的话,如果客户端发送了很长的请求体,最终发现被服务器拒绝了,那 么这就太浪费了。
接收到Expect: 100-continue 头 部之后,如果服务器愿意(will to )或能够(can )处理请求,那么服务器会返回下面的100-continue 头部,头部后面再跟两对CRLF 。
HTTP/1.1 100 Continue
接着,服务器继续读取输入流。
Tomcat 连接器必须实现org.apache.catalina.Connector 接 口。该接口的众多方法中,最重要的是getContainer 、setContainer 、createRequest 和createResponse 。
setContainer 方法用来将连接器和容器关联起来。getContainer 方法返回关联的容器。createRequest 方 法为进来的HTTP 请求创建一个请求对象,createResponse 方法创建一个响应对象。
org.apache.catalina.connector.http.HttpConnector 是Connector 接口的一个实现类,下一节“The HttpConnector Class ”将讨论它。现在,我们看看默认连接其的类图Figure 4.1 。注意,为了简化类图,Request 和Response 接口的实现类被省略了。除 了Simplecontainer 类,其他都省略了前缀“org.apache.catalina ”,只保留类型名。
因此,连接器...(原文找不到,省略部分文字)
连接器和容器是一对一的关联关系。关联关系的箭头方向表明,连接器知道容器,而容器却不知道连接器。同时需要注意,与第3 章不同,HttpConnector 与HttpProcessor 的关系变成了一对多。
第3 章已经介绍了org.apache.catalina.connector.http.HttpConnector 的 一个简化版,因此你其实已经知道了HttpConnector 的工作原理。HttpConnector 实现了org.apache.catalina.Connector 接 口 (为了和Catalina 协调),java.lang.Runnable 接口 (这样它的实例就可以运行在自己的线程中),以及
private Stack processors = new Stack();
protected int minProcessors = 5;
private int maxProcessors = 20;
while (curProcessors < minProcessors) {
if ((maxProcessors > 0) && (curProcessors >= maxProcessors))
break;
HttpProcessor processor = newProcessor();
recycle(processor);
}
while (!stopped) {
Socket socket = null;
try {
socket = serverSocket.accept();
...
HttpProcessor processor = createProcessor();
if (processor == null) {
try {
log(sm.getString("httpConnector.noProcessor"));
socket.close();
}
...
continue;
processor.assign(socket);
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);
}
}
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();
}
}
void recycle(HttpProcessor processor) {
processors.push(processor);
}
提示: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总结了这两个方法的程序流(program flow)。
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方法为止。这就是说,调用await方法导致“处理器线程”暂停,直 到“连接器线程”调用HttpProcessor实例的notifyAll方法。
现在,看看第2列。当一个新套接字被分配(assign)时,“连接器线程”调用HttpProcessor的assign方法。available的值 为false,因此while循环被跳过去,套接字被赋值给HttpProcessor实例的socket变量:
this.socket = socket;
接着,“连接器线程”将available设置成true,并调用notifyAll方法。这会唤醒“处理器线程”,而且现在available的值为 true,“处理器线程”从而离开while循环:将实例变量socket赋值给本地变量socket,设置available为false,调用 notifyAll方法,返回本地变量socket,最终套接字将被处理。
为什么await方法需要使用本地变量(socket),而不是返回实例变量socket呢?这样做,当前套接字被处理完之前,下一个套接字就可以赋值给 实例变量socket。
为什么await方法需要调用notifyAll呢?就是为了解决这个问题:available值为true时另一个套接字到达。在这种情况下,“连接器 线程”将停在assign方法while循环中,直到“处理器线程”调用notifyAll。
org.apache.catalina.Request接口代表了默认连接器的HTTP请求对象。该接口被HttpRequest的父类 RequestBase直接继承。最终的实现是HttpRequest的子类HttpRequestImpl。就像第3章一样,这里也有几个门面 (facade)类:RequestFacade和HttpRequestFacade。Figure 4.2给出了Request接口及其实现类的UML图。注意该图不包括javax.servlet和javax.servlet.http包中的类型,前 缀org.apache.catalina被省略。
如果你理解第3章中的请求对象,那么你应该能够理解上面这张图。
Figure 4.3 给出了Response接口及其实现类的UML图。
到这里,你已经理解了HttpConnector是如何创建请求对象和响应对象的。现在是整个处理过程的最后一步。本节我们重点关注 HttpProcess的process方法。HttpProcess得到套接字之后,其run方法就会调用process方法。process方法会执 行以下操作:
解释完process方法后,我们会分子章节讨论上述每个操作。
boolean ok = true;
boolean finishResponse = true;
SocketInputStream input = null;
OutputStream output = null;
// Construct and initialize the objects we will need
try {
input = new SocketInputStream(socket.getInputstream(),
connector.getBufferSize());
}
catch (Exception e) {
ok = false;
}
然后,有一个while循环,不断从输入流中读取数据,直到HttpProcessor停止,或抛出异常,或连接关闭。
keepAlive = true;
while (!stopped && ok && keepAlive) {
...
}
finishResponse = true;
try {
request.setStream(input);
request.setResponse(response);
output = socket.getOutputStream();
response.setStream(output);
response.setRequest(request);
((HttpServletResponse) response.getResponse()).setHeader
("Server", SERVER_INFO);
}
catch (Exception e) {
log("process.create", e); //第7章将讨论日志
ok = false;
}
try {
if (ok) {
parseConnection(socket);
parseRequest(input, output);
if (!request.getRequest().getProtocol()
.startsWith("HTTP/0"))
parseHeaders(input);
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);
}
try {
((HttpServletResponse) response).setHeader
("Date", FastHttpDateFormat.getCurrentDate());
if (ok) {
connector.getContainer().invoke(request, response);
}
}
if (finishResponse) {
...
response.finishResponse();
...
request.finishRequest();
...
output.flush();
if ( "close".equals(response.getHeader("Connection")) ) {
keepAlive = false;
}
// End of request processing
status = Constants.PROCESSOR_IDLE;
// Recycling the request and the response objects
request.recycle();
response.recycle();
}
try {
shutdownInput(input);
socket.close();
}
...
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);
}
static final char[] AUTHORIZATION_NAME =
"authorization".toCharArray();
static final char[] ACCEPT_LANGUAGE_NAME =
"accept-language".toCharArray();
static final char[] COOKIE_NAME = "cookie".toCharArray();
...
HttpHeader header = request.allocateHeader();
// Read the next header
input.readHeader(header);
//If all headers have been read, the readHeader method will assign no name to the
//HttpHeader instance, and this is time for the parseHeaders method to return.
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);
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
}
else if (header.equals(DefaultHeaders.CONTENT_TYPE_NAME)) {
request.setContentType(value);
}
else if (header.equals(DefaultHeaders.HOST_NAME)) {
// get host name
}
else if (header.equals(DefaultHeaders.CONNECTION_NAME)) {
if (header.valueEquals(DefaultHeaders.CONNECTION_CLOSE_VALUE)) {
keepAlive = false;
response.setHeader("Connection", "close");
} }
else if (header.equals(DefaultHeaders.EXPECT_NAME)) {
if (header.valueEquals(DefaultHeaders.EXPECT_100_VALUE))
sendAck = true;
else
throw new ServletException(sm.getstring
("httpProcessor.parseHeaders.unknownExpectation"));
}
else if (header.equals(DefaultHeaders.TRANSFER_ENCODING_NAME)) {
//request.setTransferEncoding(header);
}
request.nextHeader();
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 java.io.IOException;
import javax.naming.directory.DirContext;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.Cluster; import org.apache.catalina.Container;
import org.apache.catalina.ContainerListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Logger;
import org.apache.catalina.Manager;
import org.apache.catalina.Mapper;
import org.apache.catalina.Realm;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
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) {
}
}
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();
}
}
}