How Tomcat Works 中文版
介绍
概要
欢迎阅读《How Tomcat Works》这本书。这本书解剖了 Tomcat4.1.12 和 5.0.18 版本,解释
了它的 servlet 容器的内部运行机制,那是一个免费的,开源的,最受欢迎的 servlet 容器,代
号为 Catalina。Tomcat 是一个复杂的系统,由许多不同的组件构成。那些想要学习 Tomcat 运行
机制的朋友大部分知道从何入手。这本书会提供一个蓝图,然后为每一个组件构造一个简化版本,
使得可以更加容易的理解这些组件。在这之后才会对真实的组件进行解释。
你应该从这份简介开始阅读,因为它解释了这本书的结构,同时给你勾画了这个项目构造的
简洁轮廓。“准备前提软件”这一节会给你一些指示,例如你需要下载什么样的软件,如何为你
的代码创建目录结构等等。
本书为谁而作
这本书是为任何一个使用 Java 技术进行工作的人而准备的。
假如你是一个 servlet/jsp 程序员或者一个 Tomcat 用户,而且对一个 servlet 容器是
如何工作这个问题你感兴趣的话,这本书就是为你准备的。
假如你想加入 Tomcat 的开发团队的话,这本书就是为你准备的,因为你首先需要学习
那些已存在的代码是如何工作的。
假如你从未涉及 web 开发,但你对一般意义上的软件开发感兴趣的话,你可以在这本书
学到一个像 Tomcat 一样的大型项目是如何进行设计和开发的。
假如你想配置和自定义 Tomcat,你也应该读读这本书。
为了理解书中的讨论,你需要了解 Java 面向对象编程技术以及 servlet 编程。假如你对这
些不熟悉的话,这里有很多书籍可以参考,包括 Budi 的《Java for the Web with Servlets, JSP,
and EJB》。为了让这些材料更容易理解,每一章开始都会有便于理解所讨论主题的必要的背景资
料介绍。
Servlet 容器是如何工作的
servlet 容器是一个复杂的系统。不过,一个 servlet 容器要为一个 servlet 的请求提供服
务,基本上有三件事要做:
创建一个 request 对象并填充那些有可能被所引用的 servlet 使用的信息,如参数、头
部、cookies、查询字符串、URI 等等。一个 request 对象是
javax.servlet.ServletRequest 或 javax.servlet.http.ServletRequest 接口的一个实
例。
创建一个 response 对象,所引用的 servlet 使用它来给客户端发送响应。一个 response
对象 javax.servlet.ServletResponse 或 javax.servlet.http.ServletResponse 接口
的一个实例。
调用 servlet 的 service 方法,并传入 request 和 response 对象。在这里 servlet 会
从 request 对象取值,给 response 写值。
当你读这些章节的时候,你将会找到关于 catalina servlet 容器的详细讨论。
Catalina 架构图
Catalina 是一个非常复杂的,并优雅的设计开发出来的软件,同时它也是模块化的。基于
“Servlet 容器是如何工作的”这一节中提到的任务,你可以把 Catalina 看成是由两个主要模
块所组成的:连接器(connector)和容器(container)。在 Figure I.1 中的架构图,当然是简化
了。在稍后的章节里边,你将会一个个的揭开所有更小的组件的神秘面纱。
现在重新回到 Figure I.1,连接器是用来“连接”容器里边的请求的。它的工作是为接收
到每一个 HTTP 请求构造一个 request 和 response 对象。然后它把流程传递给容器。容器从连接
器接收到 requset 和 response 对象之后调用 servlet 的 service 方法用于响应。谨记,这个描
述仅仅是冰山一角而已。这里容器做了相当多事情。例如,在它调用 servlet 的 service 方法之
前,它必须加载这个 servlet,验证用户(假如需要的话),更新用户会话等等。一个容器为了处
理这个进程使用了很多不同的模块,这也并不奇怪。例如,管理模块是用来处理用户会话,而加
载器是用来加载 servlet 类等等。
Tomcat 4 和 5
这本书涵盖了 Tomcat4 和 5.这两者有一些不同之处:
Tomcat 5 支持 Servlet 2.4 和 JSP 2.0 规范,而 Tomcat 4 支持 Servlet 2.3 和 JSP 1.2。
比起 Tomcat 4,Tomcat 5 有一些更有效率的默认连接器。
Tomcat 5 共享一个后台处理线程,而 Tomcat 4 的组件都有属于自己的后台处理线程。
因此,就这一点而言,Tomcat 5 消耗较少的资源。
Tomcat 5 并不需要一个映射组件(mapper component)用于查找子组件,因此简化了代码。
各章概述
这本书共 20 章,其中前面两章作为导言。
第 1 章说明一个 HTTP 服务器是如何工作的,第 2 章突出介绍了一个简单的 servlet 容器。
接下来的两章关注连接器,第 5 章到第 20 章涵盖容器里边的每一个组件。以下是各章节的摘要。
注意:对于每个章节,会有一个附带程序,类似于正在被解释的组件。
第 1 章从这本书一开始就介绍了一个简单的 HTTP 服务器。要建立一个可工作的 HTTP 服务器,
你需要知道在 java.net 包里边的 2 个类的内部运作:Socket 和 ServerSocket。这里有关于这 2
个类足够的背景资料,使得你能够理解附带程序是如何工作的。
第 2 章说明简单的 servlet 容器是如何工作的。这一章带有 2 个 servlet 容器应用,可以处
理静态资源和简单的 servlet 请求。尤其是你将会学到如何创建 request 和 response 对象,然
后把它们传递给被请求的 servlet 的 service 方法。在 servlet 容器里边还有一个 servlet,你
可以从一个 web 浏览器中调用它。
第 3 章介绍了一个简化版本的 Tomcat 4 默认连接器。这章里边的程序提供了一个学习工具,
用于理解第 4 章里边的讨论的连接器。
第 4 章介绍了 Tomcat 4 的默认连接器。这个连接器已经不推荐使用,推荐使用一个更快的
连接器,Coyote。不过,默认的连接器更简单,更易于理解。
第 5 章讨论 container 模块。container 指的是 org.apache.catalina.Container 接口,有
4 种类型的 container:engine, host, context 和 wrapper。这章提供了两个工作于 context 和
wrapper 的程序。
第 6 章解释了 Lifecycle 接口。这个接口定义了一个 Catalina 组件的生命周期,并提供了
一个优雅的方式,用来把在该组件发生的事件通知其他组件。另外,Lifecycle 接口提供了一个
优雅的机制,用于在 Catalina 通过单一的 start/stop 来启动和停止组件
第 7 章包括日志,该组件是用来记录错误信息和其他信息的。
第 8 章解释了加载器(loader)。加载器是一个重要的 Catalina 模块,负责加载 servlet 和
一个 web 应用所需的其他类。这章还展示了如何实现应用的重新加载。
第 9 章讨论了管理器(manager)。这个组件用来管理会话管理中的会话信息。它解释了各式
各样类型的管理器,管理器是如何把会话对象持久化的。在章末,你将会学到如何创建一个的应
用,该应用使用 StandardManager 实例来运行一个使用会话对象进行储值的 servlet。
第 10 章包括 web 应用程序安全性的限制,用来限制进入某些内容。你将会学习与安全相关
的实体,例如
主角(principals),角色(roles),登陆配置,认证等等。你也将会写两个程序,它们在
StandardContext 对象中安装一个身份验证阀(authenticator valve)并且使用了基本的认证来
对用户进行认证。
第 11 章 详 细 解 释 了 在 一 个 web 应 用 中 代 表 一 个 servlet 的
org.apache.catalina.core.StandardWrapper 类。特别的是,这章解释了过滤器(filter)和一
个 servlet 的 service 方法是怎样给调用的。这章的附带程序使用 StandardWrapper 实例来代表
servlet。
第 12 章 包 括 了 在 一 个 web 应 用 中 代 表 一 个 servlet 的
org.apache.catalina.core.StandardContext 类。特别是这章讨论了一个 StandardContext 对
象是如何给配置的,对于每个传入的 HTTP 请求在它里面会发生什么,是怎样支持自动重新加载
的,还有就是,在一个在其相关的组件中执行定期任务的线程中,Tomcat 5 是如何共享的。
第 13 章介绍了另外两个容器:host 和 engine。你也同样可以找到这两个容器的标准实
现:org.apache.catalina.core.StandardHost 和 org.apache.catalina.core.StandardEngine。
第 14 章提供了服务器和服务组件的部分。服务器为整个 servlet 容器提供了一个优雅的启
动和停止机制,而服务为容器和一个或多个连接器提供了一个支架。这章附带的程序说明了如何
使用服务器和服务。
第 15 章解释了通过 Digester 来配置 web 应用。Digester 是来源于 Apache 软件基金会的一
个令人振奋的开源项目。对那些尚未初步了解的人,这章通过一节略微介绍了 Digester 库以及
XML 文件中如何使用它来把节点转换为 Java 对象。然后解释了用来配置一个 StandardContext
实例的 ContextConfig 对象。
第 16 章解释了 shutdown 钩子,Tomcat 使用它总能获得一个机会用于 clean-up,而无论用
户是怎样停止它的(即适当的发送一个 shutdown 命令或者不适当的简单关闭控制台)。
第 17 章讨论了通过批处理文件和 shell 脚本对 Tomcat 进行启动和停止。
第 18 章介绍了部署工具(deployer),这个组件是负责部署和安装 web 应用的。
第 19 章讨论了一个特殊的接口,ContainerServlet,能够让 servlet 访问 Catalina 的内部
对象。特别是,它讨论了 Manager 应用,你可以通过它来部署应用程序。
第 20 章讨论了 JMX 以及 Tomcat 是如何通过为其内部对象创建 MBeans 使得这些对象可管理
的。
各章的程序
每一章附带了一个或者多个程序,侧重于 Catalina 的一个特定的组件。通常你可以找到这
些简化版本,无论是正在被解释的组件或者解释如何使用 Catalina 组件的代码。各章节的程序
的所有的类和接口都放在 ex[章节号].pyrmont 包或者它的子包。例如第 1 章的程序的类就是放
在 ex01.pyrmont 包中。
准备的前提软件
这 本 书 附 带 的 程 序 运 行 于 J2SE1.4 版 本 。 压 缩 源 文 件 可 以 从 作 者 的 网 站
www.brainysoftware.com 中下载。它包括 Tomcat 4.1.12 和这本书所使用的程序的源代码。假
设你已经安装了 J2SE 1.4 并且你的 path 环境变量中已经包括了 JDK 的安装目录,请按照下列步
骤:
1. 解压缩 ZIP 文件。所有的解压缩文件将放在一个新的目录 howtomcatworks 中。
howtomcatworks 将是你的工作目录。在 howtomcatworks 目录下面将会有数个子目录,
包括 lib (包括所有所需的库),src (包括所有的源文件),webroot (包括一个 HTML 文
件和三个 servlet 样本),和 webapps (包括示例应用程序)。
2. 改变目录到工作目录下并编译 java 文件。加入你使用的是 Windows,运行
win-compile.bat 文件。假如你的计算机是 Linux 机器,敲入以下内容:(如有必要的话
不用忘记使用 chmod 更改文件属性)
./linux-compile.sh
注意:你可以在 ZIP 文件中的 Readme.txt 文件找到更多信息。
第一章:一个简单的 Web 服务器
本章说明 java web 服务器是如何工作的。Web 服务器也成为超文本传输协议(HTTP)服务器,
因为它使用 HTTP 来跟客户端进行通信的,这通常是个 web 浏览器。一个基于 java 的 web 服务器
使用两个重要的类:java.net.Socket 和 java.net.ServerSocket,并通过 HTTP 消息进行通信。
因此这章就自然是从 HTTP 和这两个类的讨论开始的。接下去,解释这章附带的一个简单的 web
服务器。
超文本传输协议(HTTP)
HTTP 是一种协议,允许 web 服务器和浏览器通过互联网进行来发送和接受数据。它是一种
请求和响应协议。客户端请求一个文件而服务器响应请求。HTTP 使用可靠的 TCP 连接--TCP 默认
使用 80 端口。第一个 HTTP 版是 HTTP/0.9,然后被 HTTP/1.0 所替代。正在取代 HTTP/1.0 的是
当 前 版 本 HTTP/1.1 , 它 定 义 于 征 求 意 见 文 档 (RFC) 2616 , 可 以 从
http://www.w3.org/Protocols/HTTP/1.1/rfc2616.pdf 下载。
注意:本节涵盖的 HTTP 1.1 只是简略的帮助你理解 web 服务器应用发送的消息。假如你对
更多详细信息感兴趣,请阅读 RFC 2616。
在 HTTP 中,始终都是客户端通过建立连接和发送一个 HTTP 请求从而开启一个事务。web 服
务器不需要联系客户端或者对客户端做一个回调连接。无论是客户端或者服务器都可以提前终止
连接。举例来说,当你正在使用一个 web 浏览器的时候,可以通过点击浏览器上的停止按钮来停
止一个文件的下载进程,从而有效的关闭与 web 服务器的 HTTP 连接。
HTTP 请求
一个 HTTP 请求包括三个组成部分:
方法—统一资源标识符(URI)—协议/版本
请求的头部
主体内容
下面是一个 HTTP 请求的例子:
POST /examples/default.jsp HTTP/1.1
Accept: text/plain; text/html
Accept-Language: en-gb
Connection: Keep-Alive
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 4.01; Windows 98)
Content-Length: 33
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
lastName=Franks&firstName=Michael
方法—统一资源标识符(URI)—协议/版本出现在请求的第一行。
POST /examples/default.jsp HTTP/1.1
这里 POST 是请求方法,/examples/default.jsp 是 URI,而 HTTP/1.1 是协议/版本部分。
每个 HTTP 请求可以使用 HTTP 标准里边提到的多种方法之一。HTTP 1.1 支持 7 种类型的请
求:GET, POST,
HEAD, OPTIONS, PUT, DELETE 和 TRACE。GET 和 POST 在互联网应用里边最普遍使用的。
URI 完全指明了一个互联网资源。URI 通常是相对服务器的根目录解释的。因此,始终一斜
线/开头。统一资源定位器(URL)其实是一种 URI(查看 http://www.ietf.org/rfc/rfc2396.txt)
来的。该协议版本代表了正在使用的 HTTP 协议的版本。
请求的头部包含了关于客户端环境和请求的主体内容的有用信息。例如它可能包括浏览器设
置的语言,主体内容的长度等等。每个头部通过一个回车换行符(CRLF)来分隔的。
对于 HTTP 请求格式来说,头部和主体内容之间有一个回车换行符(CRLF)是相当重要的。CRLF
告诉 HTTP 服务器主体内容是在什么地方开始的。在一些互联网编程书籍中,CRLF 还被认为是 HTTP
请求的第四部分。
在前面一个 HTTP 请求中,主体内容只不过是下面一行:
lastName=Franks&firstName=Michael
实体内容在一个典型的 HTTP 请求中可以很容易的变得更长。
HTTP 响应
类似于 HTTP 请求,一个 HTTP 响应也包括三个组成部分:
方法—统一资源标识符(URI)—协议/版本
响应的头部
主体内容
下面是一个 HTTP 响应的例子:
HTTP/1.1 200 OK
Server: Microsoft-IIS/4.0
Date: Mon, 5 Jan 2004 13:13:33 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 13:13:12 GMT
Content-Length: 112
HTTP Response Example
Welcome to Brainy Software
响应头部的第一行类似于请求头部的第一行。第一行告诉你该协议使用 HTTP 1.1,请求成
功(200=成功),表示一切都运行良好。
响应头部和请求头部类似,也包括很多有用的信息。响应的主体内容是响应本身的 HTML 内
容。头部和主体内容通过 CRLF 分隔开来。
Socket 类
套接字是网络连接的一个端点。套接字使得一个应用可以从网络中读取和写入数据。放在两
个不同计算机上的两个应用可以通过连接发送和接受字节流。为了从你的应用发送一条信息到另
一个应用,你需要知道另一个应用的 IP 地址和套接字端口。在 Java 里边,套接字指的是
java.net.Socket 类。
要创建一个套接字,你可以使用 Socket 类众多构造方法中的一个。其中一个接收主机名称
和端口号:
public Socket (java.lang.String host, int port)
在这里主机是指远程机器名称或者 IP 地址,端口是指远程应用的端口号。例如,要连接
yahoo.com 的 80 端口,你需要构造以下的 Socket 对象:
new Socket ("yahoo.com", 80);
一旦你成功创建了一个 Socket 类的实例,你可以使用它来发送和接受字节流。要发送字节
流,你首先必须调用 Socket 类的 getOutputStream 方法来获取一个 java.io.OutputStream 对象。
要 发 送 文 本 到 一 个 远 程 应 用 , 你 经 常 要 从 返 回 的 OutputStream 对 象 中 构 造 一 个
java.io.PrintWriter 对 象 。 要 从 连 接 的 另 一 端 接 受 字 节 流 , 你 可 以 调 用 Socket 类 的
getInputStream 方法用来返回一个 java.io.InputStream 对象。
以下的代码片段创建了一个套接字,可以和本地 HTTP 服务器(127.0.0.1 是指本地主机)进
行通讯,发送一个 HTTP 请求,并从服务器接受响应。它创建了一个 StringBuffer 对象来保存响
应并在控制台上打印出来。
Socket socket = new Socket("127.0.0.1", "8080");
OutputStream os = socket.getOutputStream();
boolean autoflush = true;
PrintWriter out = new PrintWriter(
socket.getOutputStream(), autoflush);
BufferedReader in = new BufferedReader(
new InputStreamReader( socket.getInputstream() ));
// send an HTTP request to the web server
out.println("GET /index.jsp HTTP/1.1");
out.println("Host: localhost:8080");
out.println("Connection: Close");
out.println();
// read the response
boolean loop = true;
StringBuffer sb = new StringBuffer(8096);
while (loop) {
if ( in.ready() ) {
int i=0;
while (i!=-1) {
i = in.read();
sb.append((char) i);
}
loop = false;
}
Thread.currentThread().sleep(50);
}
// display the response to the out console
System.out.println(sb.toString());
socket.close();
请注意,为了从 web 服务器获取适当的响应,你需要发送一个遵守 HTTP 协议的 HTTP 请求。
假如你已经阅读了前面一节超文本传输协议(HTTP),你应该能够理解上面代码提到的 HTTP 请求。
注意:你可以本书附带的 com.brainysoftware.pyrmont.util.HttpSniffer 类来发送一个
HTTP 请求并显示响应。要使用这个 Java 程序,你必须连接到互联网上。虽然它有可能并不会起
作用,假如你有设置防火墙的话。
ServerSocket 类
Socket 类代表一个客户端套接字,即任何时候你想连接到一个远程服务器应用的时候你构
造的套接字,现在,假如你想实施一个服务器应用,例如一个 HTTP 服务器或者 FTP 服务器,你
需要一种不同的做法。这是因为你的服务器必须随时待命,因为它不知道一个客户端应用什么时
候会尝试去连接它。为了让你的应用能随时待命,你需要使用 java.net.ServerSocket 类。这是
服务器套接字的实现。
ServerSocket 和 Socket 不同,服务器套接字的角色是等待来自客户端的连接请求。一旦服
务器套接字获得一个连接请求,它创建一个 Socket 实例来与客户端进行通信。
要创建一个服务器套接字,你需要使用 ServerSocket 类提供的四个构造方法中的一个。你
需要指定 IP 地址和服务器套接字将要进行监听的端口号。通常,IP 地址将会是 127.0.0.1,也
就是说,服务器套接字将会监听本地机器。服务器套接字正在监听的 IP 地址被称为是绑定地址。
服务器套接字的另一个重要的属性是 backlog,这是服务器套接字开始拒绝传入的请求之前,传
入的连接请求的最大队列长度。
其中一个 ServerSocket 类的构造方法如下所示:
public ServerSocket(int port, int backLog, InetAddress bindingAddress);
对于这个构造方法,绑定地址必须是 java.net.InetAddress 的一个实例。一种构造
InetAddress 对象的简单的方法是调用它的静态方法 getByName,传入一个包含主机名称的字符
串,就像下面的代码一样。
InetAddress.getByName("127.0.0.1");
下面一行代码构造了一个监听的本地机器 8080 端口的 ServerSocket,它的 backlog 为 1。
new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"));
一旦你有一个 ServerSocket 实例,你可以让它在绑定地址和服务器套接字正在监听的端口
上等待传入的连接请求。你可以通过调用 ServerSocket 类的 accept 方法做到这点。这个方法只
会在有连接请求时才会返回,并且返回值是一个 Socket 类的实例。Socket 对象接下去可以发送
字节流并从客户端应用中接受字节流,就像前一节"Socket 类"解释的那样。实际上,这章附带
的程序中,accept 方法是唯一用到的方法。
应用程序
我们的 web 服务器应用程序放在 ex01.pyrmont 包里边,由三个类组成: