How Tomcat Works中文版

    

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版本。压缩源文件能够从做者的网站 http://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

<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Brainy Software
</body>
</html>
    响应头部的第一行相似于请求头部的第一行。第一行告诉你该协议使用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包里边,由三个类组成:
  • HttpServer
  • Request
  • Response
    这个应用程序的入口点(静态main方法)能够在HttpServer类里边找到。main方法建立了一个HttpServer的实例并调用了它的await方法。await方法,顾名思义就是在一个指定的端口上等待HTTP请求,处理它们并发送响应返回客户端。它一直等待直至接收到shutdown命令。
    应用程序不能作什么,除了发送静态资源,例如放在一个特定目录的HTML文件和图像文件。它也在控制台上显示传入的HTTP请求的字节流。不过,它不给浏览器发送任何的头部例如日期或者cookies。
    如今咱们将在如下各小节中看看这三个类。

HttpServer类

    HttpServer类表明一个web服务器并展现在Listing 1.1中。请注意,await方法放在Listing 1.2中,为了节省空间没有重复放在Listing 1.1中。
        Listing 1.1: HttpServer类
package ex01.pyrmont;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
public class HttpServer {
    /** WEB_ROOT is the directory where our HTML and other files reside.
    * For this package, WEB_ROOT is the "webroot" directory under the
    * working directory.
    * The working directory is the location in the file system
    * from where the java command was invoked.
    */
    public static final String WEB_ROOT =
    System.getProperty("user.dir") + File.separator + "webroot";
    // shutdown command
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    // the shutdown command received
    private boolean shutdown = false;
    public static void main(String[] args) {
        HttpServer server = new HttpServer();
        server.await();
    }
    public void await() {
        ...
    }
}
        Listing 1.2: HttpServer类的await方法
public void await() {
    ServerSocket serverSocket = null;
    int port = 8080;
    try {
        serverSocket = new ServerSocket(port, 1,
        InetAddress.getByName("127.0.0.1"));
    }
    catch (IOException e) {
    e.printStackTrace();
    System.exit(1);
    }
    // Loop waiting for a request
    while (!shutdown) {
        Socket socket = null;
        InputStream input = null;
        OutputStream output = null;
        try {
            socket = serverSocket.accept();
            input = socket.getInputStream();
            output = socket.getOutputStream();
            // create Request object and parse
            Request request = new Request(input);
            request.parse();
            // create Response object
            Response response = new Response(output);
            response.setRequest(request);
            response.sendStaticResource();
            // Close the socket
            socket.close();
            //check if the previous URI is a shutdown command
            shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
        }
        catch (Exception e) {
            e.printStackTrace ();
            continue;
        }
    }
}
     web服务器能提供公共静态final变量WEB_ROOT所在的目录和它下面全部的子目录下的静态资源。以下所示,WEB_ROOT被初始化:
public static final String WEB_ROOT =
System.getProperty("user.dir") + File.separator + "webroot";
    代码列表包括一个叫webroot的目录,包含了一些你能够用来测试这个应用程序的静态资源。你一样能够在相同的目录下找到几个servlet用于测试下一章的应用程序。为了请求一个静态资源,在你的浏览器的地址栏或者网址框里边敲入如下的URL:
http://machineName:port/staticResource
    若是你要从一个不一样的机器上发送请求到你的应用程序正在运行的机器上,machineName应该是正在运行应用程序的机器的名称或者IP地址。假如你的浏览器在同一台机器上,你可使用localhost做为machineName。端口是8080,staticResource是你须要请求的文件的名称,且必须位于WEB_ROOT里边。
    举例来讲,假如你正在使用同一台计算机上测试应用程序,而且你想要调用HttpServer对象去发送一个index.html文件,你可使用一下的URL:
http://localhost:8080/index.html
    要中止服务器,你能够在web浏览器的地址栏或者网址框里边敲入预约义字符串,就在URL的host:port的后面,发送一个shutdown命令。shutdown命令是在HttpServer类的静态final变量SHUTDOWN里边定义的:
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    所以,要中止服务器,使用下面的URL:
http://localhost:8080/SHUTDOWN
    如今咱们来看看Listing 1.2印出来的await方法。
    使用方法名await而不是wait是由于wait方法是与线程相关的java.lang.Object类的一个重要方法。
    await方法首先建立一个ServerSocket实例而后进入一个while循环。
serverSocket = new ServerSocket(port, 1,
    InetAddress.getByName("127.0.0.1"));
...
// Loop waiting for a request
while (!shutdown) {
    ...
}
    while循环里边的代码运行到ServletSocket的accept方法停了下来,只会在8080端口接收到一个HTTP请求的时候才返回:
socket = serverSocket.accept();
    接收到请求以后,await方法从accept方法返回的Socket实例中取得java.io.InputStream和java.io.OutputStream对象。
input = socket.getInputStream();
output = socket.getOutputStream();
    await方法接下去建立一个ex01.pyrmont.Request对象而且调用它的parse方法去解析HTTP请求的原始数据。
// create Request object and parse
Request request = new Request(input);
request.parse ();
    在这以后,await方法建立一个Response对象,把Request对象设置给它,并调用它的sendStaticResource方法。
// create Response object
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
    最后,await关闭套接字并调用Request的getUri来检测HTTP请求的URI是否是一个shutdown命令。假如是的话,shutdown变量将被设置为true且程序会退出while循环。
// Close the socket
socket.close ();
//check if the previous URI is a shutdown command
shutdown = request.getUri().equals(SHUTDOWN_COMMAND);

Request类

    ex01.pyrmont.Request类表明一个HTTP请求。从负责与客户端通讯的Socket中传递过来InputStream对象来构造这个类的一个实例。你调用InputStream对象其中一个read方法来获取HTTP请求的原始数据。
    Request类显示在Listing 1.3。Request对象有parse和getUri两个公共方法,分别在Listings 1.4和1.5列出来。
        Listing 1.3: Request类
package ex01.pyrmont;
import java.io.InputStream;
import java.io.IOException;
public class Request {
    private InputStream input;
    private String uri;
    public Request(InputStream input) {
        this.input = input;
    }
    public void parse() {
        ...
    }
    private String parseUri(String requestString) {
        ...
    }
    public String getUri() {
        return uri;
    }
}
        Listing 1.4: Request类的parse方法
public void parse() {
    // Read a set of characters from the socket
    StringBuffer request = new StringBuffer(2048);
    int i;
    byte[] buffer = new byte[2048];
    try {
        i = input.read(buffer);
    }
    catch (IOException e) {
        e.printStackTrace();
        i = -1;
    }
    for (int j=0; j<i; j++) {
        request.append((char) buffer[j]);
    }
    System.out.print(request.toString());
    uri = parseUri(request.toString());
}
        Listing 1.5: Request类的parseUri方法
private String parseUri(String requestString) {
    int index1, index2;
    index1 = requestString.indexOf(' ');
    if (index1 != -1) {
        index2 = requestString.indexOf(' ', index1 + 1);
        if (index2 > index1)
        return requestString.substring(index1 + 1, index2);
    }
    return null;
}
    parse方法解析HTTP请求里边的原始数据。这个方法没有作不少事情。它惟一可用的信息是经过调用HTTP请求的私有方法parseUri得到的URI。parseUri方法在uri变量里边存储URI。公共方法getUri被调用并返回HTTP请求的URI。
     注意:在第3章和下面各章的附带程序里边,HTTP请求将会对原始数据进行更多的处理。
    为了理解parse和parseUri方法是怎样工做的,你须要知道上一节“超文本传输协议(HTTP)”讨论的HTTP请求的结构。在这一章中,咱们仅仅关注HTTP请求的第一部分,请求行。请求行从一个方法标记开始,接下去是请求的URI和协议版本,最后是用回车换行符(CRLF)结束。请求行里边的元素是经过一个空格来分隔的。例如,使用GET方法来请求index.html文件的请求行以下所示。
GET /index.html HTTP/1.1
    parse方法从传递给Requst对象的套接字的InputStream中读取整个字节流并在一个缓冲区中存储字节数组。而后它使用缓冲区字节数据的字节来填入一个StringBuffer对象,而且把表明StringBuffer的字符串传递给parseUri方法。
    parse方法列在Listing 1.4。
    而后parseUri方法从请求行里边得到URI。Listing 1.5给出了parseUri方法。parseUri方法搜索请求里边的第一个和第二个空格并从中获取URI。

Response类

    ex01.pyrmont.Response类表明一个HTTP响应,在Listing 1.6里边给出。
        Listing 1.6: Response类
package ex01.pyrmont;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.File;
/*
HTTP Response = Status-Line
*(( general-header | response-header | entity-header ) CRLF)
CRLF
[ message-body ]
Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
*/
public class Response {
    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;
    public Response(OutputStream output) {
        this.output = output;
    }
    public void setRequest(Request request) {
        this.request = request;
    }
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputStream fis = null;
        try {
            File file = new File(HttpServer.WEB_ROOT, request.getUri());
            if (file.exists()) {
                fis = new FileInputStream(file);
                int ch = fis.read(bytes, 0, BUFFER_SIZE);
                while (ch!=-1) {
                    output.write(bytes, 0, ch);
                    ch = fis.read(bytes, 0, BUFFER_SIZE);
                }
            }
            else {
            // file not found
            String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                "Content-Type: text/html\r\n" +
                "Content-Length: 23\r\n" +
                "\r\n" +
                "<h1>File Not Found</h1>";
            output.write(errorMessage.getBytes());
            }
        }
        catch (Exception e) {
            // thrown if cannot instantiate a File object
            System.out.println(e.toString() );
        }
        finally {
            if (fis!=null)
            fis.close();
        }
    }
}
    首先注意到它的构造方法接收一个java.io.OutputStream对象,就像以下所示。
public Response(OutputStream output) {
    this.output = output;
}
    响应对象是经过传递由套接字得到的OutputStream对象给HttpServer类的await方法来构造的。Response类有两个公共方法:setRequest和sendStaticResource。setRequest方法用来传递一个Request对象给Response对象。
    sendStaticResource方法是用来发送一个静态资源,例如一个HTML文件。它首先经过传递上一级目录的路径和子路径给File累的构造方法来实例化java.io.File类。
File file = new File(HttpServer.WEB_ROOT, request.getUri());
    而后它检查该文件是否存在。假如存在的话,经过传递File对象让sendStaticResource构造一个java.io.FileInputStream对象。而后,它调用FileInputStream的read方法并把字节数组写入OutputStream对象。请注意,这种状况下,静态资源是做为原始数据发送给浏览器的。
if (file.exists()) {
    fis = new FileInputstream(file);
    int ch = fis.read(bytes, 0, BUFFER_SIZE);
    while (ch!=-1) {
        output.write(bytes, 0, ch);
        ch = fis.read(bytes, 0, BUFFER_SIZE);
    }
}
    假如文件并不存在,sendStaticResource方法发送一个错误信息到浏览器。
String errorMessage =
    "Content-Type: text/html\r\n" +
    "Content-Length: 23\r\n" +
    "\r\n" +
    "<h1>File Not Found</h1>";
output.write(errorMessage.getBytes());

运行应用程序

    为了运行应用程序,能够在工做目录下敲入下面的命令:
java ex01.pyrmont.HttpServer
    为了测试应用程序,能够打开你的浏览器并在地址栏或网址框中敲入下面的命令:
http://localhost:8080/index.html
    正如Figure 1.1所示,你将会在你的浏览器里边看到index.html页面。
Figure 1.1: web服务器的输出
    在控制台中,你能够看到相似于下面的HTTP请求:
GET /index.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
application/vnd.ms-excel, application/msword, application/vnd.ms-
powerpoint, application/x-shockwave-flash, application/pdf, */*
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR
1.1.4322)
Host: localhost:8080
Connection: Keep-Alive

GET /images/logo.gif HTTP/1.1
Accept: */*
Referer: http://localhost:8080/index.html
Accept-Language: en-us
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; .NET CLR
1.1.4322)
Host: localhost:8080
Connection: Keep-Alive

总结

    在这章中你已经看到一个简单的web服务器是如何工做的。这章附带的程序仅仅由三个类组成,并非全功能的。不过,它提供了一个良好的学习工具。下一章将要讨论动态内容的处理过程。

第2章:一个简单的Servlet容器

概要

    本章经过两个程序来讲明你如何开发本身的servlet容器。第一个程序被设计得足够简单使得你能理解一个servlet容器是如何工做的。而后它演变为第二个稍微复杂的servlet容器。
     注意:每个servlet容器的应用程序都是从前一章的应用程序逐渐演变过来的,直至一个全功能的Tomcat servlet容器在第17章被创建起来。
    这两个servlet容器均可以处理简单的servlet和静态资源。你可使用PrimitiveServlet来测试这个容器。PrimitiveServlet在Listing 2.1中列出而且它的类文件能够在webroot目录下找到。更复杂的servlet就超过这些容器的能力了,可是你将会在如下各章中学到如何创建更复杂的servlet容器。
        Listing 2.1: PrimitiveServlet.java
import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;
public class PrimitiveServlet implements Servlet {
    public void init(ServletConfig config) throws ServletException {
    System.out.println("init");
    }
    public void service(ServletRequest request, ServletResponse response)
        throws ServletException, IOException {
        System.out.println("from service");
        PrintWriter out = response.getWriter();
        out.println("Hello. Roses are red.");
        out.print("Violets are blue.");
    }
    public void destroy() {
        System.out.println("destroy");
    }
    public String getServletInfo() {
        return null;
    }
    public ServletConfig getServletConfig() {
        return null;
    }
}
    两个应用程序的类都放在ex02.pyrmont包里边。为了理解应用程序是如何工做的,你须要熟悉javax.servlet.Servlet接口。为了给你复习一下,将会在本章的首节讨论这个接口。在这以后,你将会学习一个servlet容器作了什么工做来为一个servlet提供HTTP请求。

javax.servlet.Servlet接口

    Servlet编程是经过javax.servlet和javax.servlet.http这两个包的类和接口来实现的。其中一个相当重要的就是javax.servlet.Servlet接口了。全部的servlet必须实现实现或者继承实现该接口的类。
    Servlet接口有五个方法,其用法以下。
public void init(ServletConfig config) throws ServletException
public void service(ServletRequest request, ServletResponse response)
    throws ServletException, java.io.IOException
public void destroy()
public ServletConfig getServletConfig()
public java.lang.String getServletInfo()
    在Servlet的五个方法中,init,service和destroy是servlet的生命周期方法。在servlet类已经初始化以后,init方法将会被servlet容器所调用。servlet容器只调用一次,以此代表servlet已经被加载进服务中。init方法必须在servlet能够接受任何请求以前成功运行完毕。一个servlet程序员能够经过覆盖这个方法来写那些仅仅只要运行一次的初始化代码,例如加载数据库驱动,值初始化等等。在其余状况下,这个方法一般是留空的。
    servlet容器为servlet请求调用它的service方法。servlet容器传递一个javax.servlet.ServletRequest对象和javax.servlet.ServletResponse对象。ServletRequest对象包括客户端的HTTP请求信息,而ServletResponse对象封装servlet的响应。在servlet的生命周期中,service方法将会给调用屡次。
    当从服务中移除一个servlet实例的时候,servlet容器调用destroy方法。这一般发生在servlet容器正在被关闭或者servlet容器须要一些空闲内存的时候。仅仅在全部servlet线程的service方法已经退出或者超时淘汰的时候,这个方法才被调用。在servlet容器已经调用完destroy方法以后,在同一个servlet里边将不会再调用service方法。destroy方法提供了一个机会来清理任何已经被占用的资源,例如内存,文件句柄和线程,并确保任何持久化状态和servlet的内存当前状态是同步的。
    Listing 2.1介绍了一个名为PrimitiveServlet的servlet的代码,是一个很是简单的的servlet,你能够用来测试本章里边的servlet容器应用程序。PrimitiveServlet类实现了javax.servlet.Servlet(全部的servlet都必须这样作),并为Servlet的这五个方法都提供了实现。PrimitiveServlet作的事情很是简单。在init,service或者destroy中的任何一个方法每次被调用的时候,servlet把方法名写到标准控制台上面去。另外,service方法从ServletResponse对象得到java.io.PrintWriter实例,并发送字符串到浏览器去。

应用程序1

    如今,让咱们从一个servlet容器的角度来研究一下servlet编程。总的来讲,一个全功能的servlet容器会为servlet的每一个HTTP请求作下面一些工做:
  • 当第一次调用servlet的时候,加载该servlet类并调用servlet的init方法(仅仅一次)。
  • 对每次请求,构造一个javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例。
  • 调用servlet的service方法,同时传递ServletRequest和ServletResponse对象。
  • 当servlet类被关闭的时候,调用servlet的destroy方法并卸载servlet类。
    本章的第一个servlet容器不是全功能的。所以,她不能运行什么除了很是简单的servlet,并且也不调用servlet的init方法和destroy方法。相反它作了下面的事情:
  • 等待HTTP请求。
  • 构造一个ServletRequest对象和一个ServletResponse对象。
  • 假如该请求须要一个静态资源的话,调用StaticResourceProcessor实例的process方法,同时传递ServletRequest和ServletResponse对象。
  • 假如该请求须要一个servlet的话,加载servlet类并调用servlet的service方法,同时传递ServletRequest和ServletResponse对象。
     注意:在这个servlet容器中,每一次servlet被请求的时候,servlet类都会被加载。
    第一个应用程序由6个类组成:
  • HttpServer1
  • Request
  • Response
  • StaticResourceProcessor
  • ServletProcessor1
  • Constants
    Figure 2.1显示了第一个servlet容器的UML图。
    Figure 2.1: 第一个servlet容器的UML图
    这个应用程序的入口点(静态main方法)能够在HttpServer1类里边找到。main方法建立了一个HttpServer1的实例并调用了它的await方法。await方法等待HTTP请求,为每次请求建立一个Request对象和一个Response对象,并把他们分发到一个StaticResourceProcessor实例或者一个ServletProcessor实例中去,这取决于请求一个静态资源仍是一个servlet。
    Constants类包括涉及其余类的静态final变量WEB_ROOT。WEB_ROOT显示了PrimitiveServlet和这个容器能够提供的静态资源的位置。
    HttpServer1实例会一直等待HTTP请求,直到接收到一个shutdown的命令。你科研用第1章的作法发送一个shutdown命令。
    应用程序里边的每一个类都会在如下各节中进行讨论。

HttpServer1类

    这个应用程序里边的HttpServer1类相似于第1章里边的简单服务器应用程序的HttpServer类。不过,在这个应用程序里边HttpServer1类能够同时提供静态资源和servlet。要请求一个静态资源,你能够在你的浏览器地址栏或者网址框里边敲入一个URL:
http://machineName:port/staticResource
    就像是在第1章提到的,你能够请求一个静态资源。
    为了请求一个servlet,你可使用下面的URL:
http://machineName:port/servlet/servletClass
    所以,假如你在本地请求一个名为PrimitiveServlet的servlet,你在浏览器的地址栏或者网址框中敲入:
http://localhost:8080/servlet/PrimitiveServlet
    servlet容器能够就提供PrimitiveServlet了。不过,假如你调用其余servlet,如ModernServlet,servlet容器将会抛出一个异常。在如下各章中,你将会创建能够处理这两个状况的程序。
    HttpServer1类显示在Listing 2.2中。
        Listing 2.2: HttpServer1类的await方法
package ex02.pyrmont;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.InetAddress;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
public class HttpServer1 {
    /** WEB_ROOT is the directory where our HTML and other files reside.
    * For this package, WEB_ROOT is the "webroot" directory under the
    * working directory.
    * The working directory is the location in the file system
    * from where the java command was invoked.
    */
    // shutdown command
    private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
    // the shutdown command received
    private boolean shutdown = false;
    public static void main(String[] args) {
        HttpServer1 server = new HttpServer1();
        server.await();
    }
    public void await() {
        ServerSocket serverSocket = null;
        int port = 8080;
        try {
            serverSocket = new ServerSocket(port, 1,
            InetAddress.getByName("127.0.0.1"));
        }
        catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        // Loop waiting for a request
        while (!shutdown) {
            Socket socket = null;
            InputStream input = null;
            OutputStream output = null;
            try {
                socket = serverSocket.accept();
                input = socket.getInputstream();
                output = socket.getOutputStream();
                // create Request object and parse
                Request request = new Request(input);
                request.parse();
                // create Response object
                Response response = new Response(output);
                response.setRequest(request);
                // check if this is a request for a servlet or
                // a static resource
                // a request for a servlet begins with "/servlet/"
                if (request.getUri().startsWith("/servlet/")) {
                    ServletProcessor1 processor = new ServletProcessor1();
                    processor.process(request, response);
                }
                else {
                    StaticResoureProcessor processor =
                    new StaticResourceProcessor();
                    processor.process(request, response);
                }
                // Close the socket
                socket.close();
                //check if the previous URI is a shutdown command
                shutdown = request.getUri().equals(SHUTDOWN_COMMAND);
            }
            catch (Exception e) {
                e.printStackTrace();
                System.exit(1);
            }
        }
    }
}
    类的await方法等待HTTP请求直到一个shutdown命令给发出,让你想起第1章的await方法。Listing 2.2的await方法和第1章的区别是,在Listing 2.2里边,请求能够分发给一个StaticResourceProcessor或者一个ServletProcessor。假如URI包括字符串/servlet/的话,请求将会转发到后面去。
    否则的话,请求将会传递给StaticResourceProcessor实例 instance. 请注意,这部分在Listing 2.2中灰暗显示。

Request类

    servlet的service方法从servlet容器中接收一个javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例。这就是说对于每个HTTP请求,servlet容器必须构造一个ServletRequest对象和一个ServletResponse对象并把它们传递给正在服务的servlet的service方法。
    ex02.pyrmont.Request类表明一个request对象并被传递给servlet的service方法。就自己而言,它必须实现javax.servlet.ServletRequest接口。这个类必须提供这个接口全部方法的实现。不过,咱们想要让它很是简单而且仅仅提供实现其中一些方法,咱们在如下各章中再实现所有的方法。要编译Request类,你须要把这些方法的实现留空。假如你看过Listing 2.3中的Request类,你将会看到那些须要返回一个对象的方法返回了null
        Listing 2.3: Request类
package ex02.pyrmont;
import java.io.InputStream;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
public class Request implements ServletRequest {
    private InputStream input;
    private String uri;
    public Request(InputStream input){
        this.input = input;
    }
    public String getUri() {
        return uri;
    }
    private String parseUri(String requestString) {
        int index1, index2;
        index1 = requestString.indexOf(' ');
        if (index1 != -1) {
            index2 = requestString.indexOf(' ', index1 + 1);
            if (index2 > index1)
                return requestString.substring(index1 + 1, index2);
        }
        return null;
    }
    public void parse() {
        // Read a set of characters from the socket
        StringBuffer request = new StringBuffer(2048);
        int i;
        byte[] buffer = new byte[2048];
        try {
            i = input.read(buffer);
        }
        catch (IOException e) {
            e.printStackTrace();
            i = -1;
        }
        for (int j=0; j<i; j++) {
            request.append((char) buffer(j));
        }
        System.out.print(request.toString());
        uri = parseUri(request.toString());
    }
    /* implementation of ServletRequest */
    public Object getAttribute(String attribute) {
        return null;
    }
    public Enumeration getAttributeNames() {
        return null;
    }
    public String getRealPath(String path) {
        return null;
    }
    public RequestDispatcher getRequestDispatcher(String path) {
        return null;
    }
    public boolean isSecure() {
        return false;
    }
    public String getCharacterEncoding() {
        return null;
    }
    public int getContentLength() {
        return 0;
    }
    public String getContentType() {
        return null;
    }
    public ServletInputStream getInputStream() throws IOException {
        return null;
    }
    public Locale getLocale() {
        return null;
    }
    public Enumeration getLocales() {
        return null;
    }
    public String getParameter(String name) {
        return null;
    }
    public Map getParameterMap() {
        return null;
    }
    public Enumeration getParameterNames() {
        return null;
    }
    public String[] getParameterValues(String parameter) {
        return null;
    }
    public String getProtocol() {
        return null;
    }
    public BufferedReader getReader() throws IOException {
        return null;
    }
    public String getRemoteAddr() {
        return null;
    }
    public String getRemoteHost() {
        return null;
    }
    public String getScheme() {
        return null;
    }
    public String getServerName() {
        return null;
    }
    public int getServerPort() {
        return 0;
    }
    public void removeAttribute(String attribute) { }
    public void setAttribute(String key, Object value) { }
    public void setCharacterEncoding(String encoding)
        throws UnsupportedEncodingException { }
}
    另外,Request类仍然有在第1章中讨论的parse和getUri方法。

Response类

    在Listing 2.4列出的ex02.pyrmont.Response类,实现了javax.servlet.ServletResponse。就自己而言,这个类必须提供接口里边的全部方法的实现。相似于Request类,咱们把除了getWriter以外的全部方法的实现留空。
        Listing 2.4: Response类
package ex02.pyrmont;
import java.io.OutputStream;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.File;
import java.io.PrintWriter;
import java.util.Locale;
import javax.servlet.ServletResponse;
import javax.servlet.ServletOutputStream;
public class Response implements ServletResponse {
    private static final int BUFFER_SIZE = 1024;
    Request request;
    OutputStream output;
    PrintWriter writer;
    public Response(OutputStream output) {
        this.output = output;
    }
    public void setRequest(Request request) {
        this.request = request;
    }
    /* This method is used to serve static pages */
    public void sendStaticResource() throws IOException {
        byte[] bytes = new byte[BUFFER_SIZE];
        FileInputstream fis = null;
        try {
            /* request.getUri has been replaced by request.getRequestURI */
            File file = new File(Constants.WEB_ROOT, request.getUri());
            fis = new FileInputstream(file);
            /*
            HTTP Response = Status-Line
            *(( general-header | response-header | entity-header ) CRLF)
            CRLF
            [ message-body ]
            Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
            */
            int ch = fis.read(bytes, 0, BUFFER_SIZE);
            while (ch!=-1) {
                output.write(bytes, 0, ch);
                ch = fis.read(bytes, 0, BUFFER_SIZE);
            }
        }
        catch (FileNotFoundException e) {
            String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
                "Content-Type: text/html\r\n" +
                "Content-Length: 23\r\n" +
                "\r\n" +
                "<h1>File Not Found</h1>";
            output.write(errorMessage.getBytes());
        }
        finally {
            if (fis!=null)
            fis.close();
        }
    }
    /** implementation of ServletResponse */
    public void flushBuffer() throws IOException ( }
    public int getBufferSize() {
        return 0;
    }
    public String getCharacterEncoding() {
        return null;
    }
    public Locale getLocale() {
        return null;
    }
    public ServletOutputStream getOutputStream() throws IOException {
        return null;
    }
    public PrintWriter getWriter() throws IOException {
        // autoflush is true, println() will flush,
        // but print() will not.
        writer = new PrintWriter(output, true);
        return writer;
    }
    public boolean isCommitted() {
        return false;
    }
    public void reset() { }
    public void resetBuffer() { }
    public void setBufferSize(int size) { }
    public void setContentLength(int length) { }
    public void setContentType(String type) { }
    public void setLocale(Locale locale) { }
}
    在getWriter方法中,PrintWriter类的构造方法的第二个参数是一个布尔值代表是否容许自动刷新。传递true做为第二个参数将会使任何println方法的调用都会刷新输出(output)。不过,print方法不会刷新输出。
    所以,任何print方法的调用都会发生在servlet的service方法的最后一行,输出将不会被发送到浏览器。这个缺点将会在下一个应用程序中修复。
    Response类还拥有在第1章中谈到的sendStaticResource方法。

StaticResourceProcessor类

    ex02.pyrmont.StaticResourceProcessor类用来提供静态资源请求。惟一的方法是process方法。Listing 2.5给出了StaticResourceProcessor类。
        Listing 2.5: StaticResourceProcessor类
package ex02.pyrmont;
import java.io.IOException;
public class StaticResourceProcessor {
    public void process(Request request, Response response) {
    try {
        response.sendStaticResource();
    }
    catch (IOException e) {
        e.printStackTrace();
    }
    }
}
    process方法接收两个参数:一个ex02.pyrmont.Request实例和一个ex02.pyrmont.Response实例。这个方法只是简单的呼叫Response对象的sendStaticResource方法。

ServletProcessor1类

    Listing 2.6中的ex02.pyrmont.ServletProcessor1类用于处理servlet的HTTP请求。
        Listing 2.6: ServletProcessor1类
package ex02.pyrmont;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandler;
import java.io.File;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class ServletProcessor1 {
    public void process(Request request, Response response) {
        String uri = request.getUri();
        String servletName = uri.substring(uri.lastIndexOf("/") + 1);
        URLClassLoader loader = null;
        try {
            // create a URLClassLoader
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(Constants.WEB_ROOT);
            // the forming of repository is taken from the
            // createClassLoader method in
            // org.apache.catalina.startup.ClassLoaderFactory
            String repository =(new URL("file", null, classPath.getCanonicalPath() +
                File.separator)).toString() ;
            // the code for forming the URL is taken from
            // the addRepository method in
            // org.apache.catalina.loader.StandardClassLoader.
            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((ServletRequest) request,
            (ServletResponse) response);
        }
        catch (Exception e) {
            System.out.println(e.toString());
        }
        catch (Throwable e) {
            System.out.println(e.toString());
        }
    }
}
    ServletProcessor1类出奇的简单,仅仅由一个方法组成:process。这个方法接受两个参数:一个
javax.servlet.ServletRequest实例和一个javax.servlet.ServletResponse实例。该方法从ServletRequest中经过调用getRequestUri方法得到URI:
String uri = request.getUri();
    请记住URI是如下形式的:
/servlet/servletName
    在这里servletName是servlet类的名字。
    要加载servlet类,咱们须要从URI中知道servlet的名称。咱们可使用process方法的下一行来得到servlet的名字:
String servletName = uri.substring(uri.lastIndexOf("/") + 1);
    接下去,process方法加载servlet。要完成这个,你须要建立一个类加载器并告诉这个类加载器要加载的类的位置。对于这个servlet容器,类加载器直接在Constants指向的目录里边查找。WEB_ROOT就是指向工做目录下面的webroot目录。
     注意: 类加载器将在第8章详细讨论。
    要加载servlet,你可使用java.net.URLClassLoader类,它是java.lang.ClassLoader类的一个直接子类。一旦你拥有一个URLClassLoader实例,你使用它的loadClass方法去加载一个servlet类。如今举例说明URLClassLoader类是straightforward直接转发的。这个类有三个构造方法,其中最简单的是:
public URLClassLoader(URL[] urls);
    这里urls是一个java.net.URL的对象数组,这些对象指向了加载类时候查找的位置。任何以/结尾的URL都假设是一个目录。不然,URL会Otherwise, the URL假定是一个将被下载并在须要的时候打开的JAR文件。
     注意:在一个servlet容器里边,一个类加载器能够找到servlet的地方被称为资源库(repository)。
    在咱们的应用程序里边,类加载器必须查找的地方只有一个,如工做目录下面的webroot目录。所以,咱们首先建立一个单个URL组成的数组。URL类提供了一系列的构造方法,因此有不少中构造一个URL对象的方式。对于这个应用程序来讲,咱们使用Tomcat中的另外一个类的相同的构造方法。这个构造方法以下所示。
public URL(URL context, java.lang.String spec, URLStreamHandler hander)
throws MalformedURLException
    你可使用这个构造方法,并为第二个参数传递一个说明,为第一个和第三个参数都传递null。不过,这里有另一个接受三个参数的构造方法:
public URL(java.lang.String protocol, java.lang.String host,
    java.lang.String file) throws MalformedURLException
    所以,假如你使用下面的代码时,编译器将不会知道你指的是那个构造方法:
new URL(null, aString, null);
    你能够经过告诉编译器第三个参数的类型来避开这个问题,例如。
URLStreamHandler streamHandler = null;
new URL(null, aString, streamHandler);
    你可使用下面的代码在组成一个包含资源库(servlet类能够被找到的地方)的字符串,并做为第二个参数,
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator)).toString() ;
    把全部的片断组合在一块儿,这就是用来构造适当的URLClassLoader实例的process方法中的一部分:
// create a URLClassLoader
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(Constants.WEB_ROOT);
String repository = (new URL("file", null,
classPath.getCanonicalPath() + File.separator)).toString() ;
urls[0] = new URL(null, repository, streamHandler);
loader = new URLClassLoader(urls);
     注意: 用来生成资源库的代码是从org.apache.catalina.startup.ClassLoaderFactory的createClassLoader方法来的,而生成URL的代码是从org.apache.catalina.loader.StandardClassLoader的addRepository方法来的。不过,在如下各章以前你不须要担忧这些类。
    当有了一个类加载器,你可使用loadClass方法加载一个servlet:
Class myClass = null;
try {
    myClass = loader.loadClass(servletName);
}
catch (ClassNotFoundException e) {
    System.out.println(e.toString());
}
    而后,process方法建立一个servlet类加载器的实例, 把它向下转换(downcast)为javax.servlet.Servlet, 并调用servlet的service方法:
Servlet servlet = null;
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) request,(ServletResponse) response);
}
catch (Exception e) {
    System.out.println(e.toString());
}
catch (Throwable e) {
    System.out.println(e.toString());
}

运行应用程序

    要在Windows上运行该应用程序,在工做目录下面敲入如下命令:
java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer1
    在Linux下,你使用一个冒号来分隔两个库:
java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer1
    要测试该应用程序,在浏览器的地址栏或者网址框中敲入:
http://localhost:8080/index.html
    或者
http://localhost:8080/servlet/PrimitiveServlet
    当调用PrimitiveServlet的时候,你将会在你的浏览器看到下面的文本:
Hello. Roses are red.
    请注意,由于只是第一个字符串被刷新到浏览器,因此你不能看到第二个字符串Violets are blue。咱们将在第3章修复这个问题。

应用程序2

    第一个应用程序有一个严重的问题。在ServletProcessor1类的process方法,你向上转换ex02.pyrmont.Request实例为javax.servlet.ServletRequest,并做为第一个参数传递给servlet的service方法。你也向下转换ex02.pyrmont.Response实例为javax.servlet.ServletResponse,并做为第二个参数传递给servlet的service方法。
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) request,(ServletResponse) response);
}
    这会危害安全性。知道这个servlet容器的内部运做的Servlet程序员能够分别把ServletRequest和ServletResponse实例向下转换为ex02.pyrmont.Request和ex02.pyrmont.Response,并调用他们的公共方法。拥有一个Request实例,它们就能够调用parse方法。拥有一个Response实例,就能够调用sendStaticResource方法。
    你不能够把parse和sendStaticResource方法设置为私有的,由于它们将会被其余的类调用。不过,这两个方法是在个servlet内部是不可见的。其中一个解决办法就是让Request和Response类拥有默认访问修饰,因此它们不能在ex02.pyrmont包的外部使用。不过,这里有一个更优雅的解决办法:经过使用facade类。请看Figure 2.2中的UML图。
                            Figure 2.2: Façade classes
    在这第二个应用程序中,咱们增长了两个façade类: RequestFacade和ResponseFacade。RequestFacade实现了ServletRequest接口并经过在构造方法中传递一个引用了ServletRequest对象的Request实例做为参数来实例化。ServletRequest接口中每一个方法的实现都调用了Request对象的相应方法。然而ServletRequest对象自己是私有的,并不能在类的外部访问。咱们构造了一个RequestFacade对象并把它传递给service方法,而不是向下转换Request对象为ServletRequest对象并传递给service方法。Servlet程序员仍然能够向下转换ServletRequest实例为RequestFacade,不过它们只能够访问ServletRequest接口里边的公共方法。如今parseUri方法就是安全的了。
    Listing 2.7 显示了一个不完整的RequestFacade类
        Listing 2.7: RequestFacade类
package ex02.pyrmont;
public class RequestFacade implements ServletRequest {
    private ServleLRequest request = null;
    public RequestFacade(Request request) {
        this.request = request;
    }
    /* implementation of the ServletRequest*/
    public Object getAttribute(String attribute) {
        return request.getAttribute(attribute);
    }
    public Enumeration getAttributeNames() {
        return request.getAttributeNames();
    }
    ...
}
    请注意RequestFacade的构造方法。它接受一个Request对象并立刻赋值给私有的servletRequest对象。还请注意,RequestFacade类的每一个方法调用ServletRequest对象的相应的方法。
    这一样使用于ResponseFacade类。
    这里是应用程序2中使用的类:
  • HttpServer2
  • Request
  • Response
  • StaticResourceProcessor
  • ServletProcessor2
  • Constants
    HttpServer2类相似于HttpServer1,除了它在await方法中使用ServletProcessor2而不是ServletProcessor1:
if (request.getUri().startWith("/servlet/")) {
    servletProcessor2 processor = new ServletProcessor2();
    processor.process(request, response);
}
else {
    ...
}
    ServletProcessor2类相似于ServletProcessor1,除了process方法中的如下部分:
Servlet servlet = null;
RequestFacade requestFacade = new RequestFacade(request);
ResponseFacade responseFacade = new ResponseFacade(response);
try {
    servlet = (Servlet) myClass.newInstance();
    servlet.service((ServletRequest) requestFacade,(ServletResponse)responseFacade);
}

运行应用程序

    要在Windows上运行该应用程序,在工做目录下面敲入如下命令:
java -classpath ./lib/servlet.jar;./ ex02.pyrmont.HttpServer2
    在Linux下,你使用一个冒号来分隔两个库:
java -classpath ./lib/servlet.jar:./ ex02.pyrmont.HttpServer2
    你可使用与应用程序1同样的地址,并获得相同的结果。

总结

    本章讨论了两个简单的能够用来提供静态资源和处理像PrimitiveServlet这么简单的servlet的servlet容器。一样也提供了关于javax.servlet.Servlet接口和相关类型的背景信息。

第3章:链接器

概要

    在介绍中提到,Catalina中有两个主要的模块:链接器和容器。本章中你将会写一个能够建立更好的请求和响应对象的链接器,用来改进第2章中的程序。一个符合Servlet 2.3和2.4规范的链接器必须建立javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse,并传递给被调用的servlet的service方法。在第2章 中,servlet容器只能够运行实现了javax.servlet.Servlet的servlet,并传递 javax.servlet.ServletRequest和javax.servlet.ServletResponse实例给service方法。由于链接器并不知道servlet的类型(例如它是否实现了javax.servlet.Servlet,继承了javax.servlet.GenericServlet,或者继承了javax.servlet.http.HttpServlet),因此链接器必须始终提供HttpServletRequest和HttpServletResponse的实例。
    在本章的应用程序中,链接器解析HTTP请求头部并让servlet能够得到头部, cookies, 参数名/值等等。你将会完善第2章中Response类的getWriter方法,让它可以正确运行。因为这些改进,你将会从 PrimitiveServlet中获取一个完整的响应,并可以运行更加复杂的ModernServlet。
    本章你创建的链接器是将在第4章详细讨论的Tomcat4的默认链接器的一个简化版本。Tomcat的默认链接器在Tomcat4中是不推荐使用的,但它仍然能够做为一个很是棒的学习工具。在这章的剩余部分,"connector"指的是内置在咱们应用程序的模块。
     注意:和上一章的应用程序不一样的是,本章的应用程序中,链接器和容器是分离的。
    本章的应用程序能够在包ex03.pyrmont和它的子包中找到。组成链接器的这些类是包
ex03.pyrmont.connector 和ex03.pyrmont.connector.http的一部分。在本章的开头,每一个附带的程序都有个bootstrap类用来启动应用程序。不过,在这个阶段,还没有有一个机制来中止这个应用程序。一旦运行,你必须经过关闭控制台(Windows)或者杀死进程(UNIX/Linux)的方法来鲁 莽的关闭应用程序。
    在咱们解释该应用程序以前,让咱们先来讲说包org.apache.catalina.util里边的StringManager类。这个类用来处理这个程序中不一样模块和Catalina自身的错误信息的国际化。以后会讨论附带的应用程序。

StringManager类

    一个像Tomcat这样的大型应用须要仔细的处理错误信息。在Tomcat中,错误信息对于系统管理员和servlet程序员都是有用的。例 如,Tomcat记录错误信息,让系统管理员能够定位发生的任何异常。对servlet程序员来讲,Tomcat会在抛出的任何一个 javax.servlet.ServletException中发送一个错误信息,这样程序员能够知道他/她的servlet究竟发送什么错误了。
    Tomcat所采用的方法是在一个属性文件里边存储错误信息,这样,能够容易的修改这些信息。不过,Tomcat中有数以百计的类。把全部类使用的错误信 息存储到一个大的属性文件里边将会容易产生维护的噩梦。为了不这一状况,Tomcat为每一个包都分配一个属性文件。例如,在包 org.apache.catalina.connector里边的属性文件包含了该包全部的类抛出的全部错误信息。每一个属性文件都会被一个 org.apache.catalina.util.StringManager类的实例所处理。当Tomcat运行时,将会有许多 StringManager实例,每一个实例会读取包对应的一个属性文件。此外,因为Tomcat的受欢迎程度,提供多种语言的错误信息也是有意义的。目前,有三种语言是被支持的。英语的错误信息属性文件名为LocalStrings.properties。另外两个是西班牙语和日语,分别放在 LocalStrings_es.properties和LocalStrings_ja.properties里边。
    当包里边的一个类须要查找放在该包属性文件的一个错误信息时,它首先会得到一个StringManager实例。不过,相同包里边的许多类可能也须要 StringManager,为每一个对象建立一个StringManager实例是一种资源浪费。所以,StringManager类被设计成一个StringManager实例能够被包里边的全部类共享。假如你熟悉设计模式,你将会正确的猜到StringManager是一个单例 (singleton)类。仅有的一个构造方法是私有的,全部你不能在类的外部使用new关键字来实例化。你经过传递一个包名来调用它的公共静态方法 getManager来得到一个实例。每一个实例存储在一个以包名为键(key)的Hashtable中。
private static Hashtable managers = new Hashtable();
public synchronized static StringManager
getManager(String packageName) {
    StringManager mgr = (StringManager)managers.get(packageName);
    if (mgr == null) {
        mgr = new StringManager(packageName);
        managers.put(packageName, mgr);
    }
    return mgr;
}
     注意:一篇关于单例模式的题为"The Singleton Pattern"的文章能够在附带的ZIP文件中找到。
    例如,要在包ex03.pyrmont.connector.http的一个类中使用StringManager,能够传递包名给StringManager类的getManager方法:
StringManager sm =
    StringManager.getManager("ex03.pyrmont.connector.http");
    在包ex03.pyrmont.connector.http中,你会找到三个属性文件:LocalStrings.properties, LocalStrings_es.properties和LocalStrings_ja.properties。StringManager实例是根据运行程序的服务器的区域设置来决定使用哪一个文件的。假如你打开LocalStrings.properties,非注释的第一行是这样的:
httpConnector.alreadyInitialized=HTTP connector has already been initialized
    要得到一个错误信息,可使用StringManager类的getString,并传递一个错误代号。这是其中一个重载方法:
public String getString(String key)
    经过传递httpConnector.alreadyInitialized做为getString的参数,将会返回"HTTP connector has already been initialized"。

应用程序

    从本章开始,每章附带的应用程序都会分红模块。这章的应用程序由三个模块组成:connector,
startup和core。
    startup模块只有一个类,Bootstrap,用来启动应用的。connector模块的类能够分为五组:
  • 链接器和它的支撑类(HttpConnector和HttpProcessor)。
  • 指代HTTP请求的类(HttpRequest)和它的辅助类。
  • 指代HTTP响应的类(HttpResponse)和它的辅助类。
  • Facade类(HttpRequestFacade和HttpResponseFacade)。
  • Constant类
    core模块由两个类组成:ServletProcessor和StaticResourceProcessor。
    Figure 3.1显示了这个应用的类的UML图。为了让图更具可读性,HttpRequest和HttpResponse相关的类给省略了。你能够在咱们讨论Request和Response对象的时候分别找到UML图。
            Figure 3.1: 应用程序的UML图
    和Figure 2.1的UML图相比,第2章中的HttpServer类被分离为两个类:HttpConnector和HttpProcessor,Request被 HttpRequest所取代,而Response被HttpResponse所取代。一样,本章的应用使用了更多的类。
    第2章中的HttpServer类的职责是等待HTTP请求并建立请求和响应对象。在本章的应用中,等待HTTP请求的工做交给HttpConnector实例,而建立请求和响应对象的工做交给了HttpProcessor实例。
    本章中,HTTP请求对象由实现了javax.servlet.http.HttpServletRequest的HttpRequest类来表明。一个 HttpRequest对象将会给转换为一个HttpServletRequest实例并传递给被调用的servlet的service方法。所以,每一个 HttpRequest实例必须适当增长字段,以便servlet可使用它们。值须要赋给HttpRequest对象,包括URI,查询字符串,参数,cookies和其余的头部等等。由于链接器并不知道被调用的servlet须要哪一个值,因此链接器必须从HTTP请求中解析全部可得到的值。不过,解析一个HTTP请求牵涉昂贵的字符串和其余操做,假如只是解析servlet须要的值的话,链接器就能节省许多CPU周期。例如,假如servlet不 解析任何一个请求参数(例如不调用javax.servlet.http.HttpServletRequest的getParameter, getParameterMap,getParameterNames或者getParameterValues方法),链接器就不须要从查询字符串或者 HTTP请求内容中解析这些参数。Tomcat的默认链接器(和本章应用程序的链接器)试图不解析参数直到servlet真正须要它的时候,经过这样来得到更高效率。
    Tomcat的默认链接器和咱们的链接器使用SocketInputStream类来从套接字的InputStream中读取字节流。一个 SocketInputStream实例对从套接字的getInputStream方法中返回的java.io.InputStream实例进行包装。 SocketInputStream类提供了两个重要的方法:readRequestLine和readHeader。readRequestLine返回一个HTTP请求的第一行。例如,这行包括了URI,方法和HTTP版本。由于从套接字的输入流中处理字节流意味着只读取一次,从第一个字节到最后一个字节(而且不回退),所以readHeader被调用以前,readRequestLine必须只被调用一次。readHeader每次被调用来得到一个头部的名/值对,而且应该被重复的调用知道全部的头部被读取到。readRequestLine的返回值是一个HttpRequestLine的实例,而 readHeader的返回值是一个HttpHeader对象。咱们将在下节中讨论类HttpRequestLine和HttpHeader。
    HttpProcessor对象建立了HttpRequest的实例,所以必须在它们当中增长字段。HttpProcessor类使用它的parse方法 来解析一个HTTP请求中的请求行和头部。解析出来并把值赋给HttpProcessor对象的这些字段。不过,parse方法并不解析请求内容或者请求 字符串里边的参数。这个任务留给了HttpRequest对象它们。只是当servlet须要一个参数时,查询字符串或者请求内容才会被解析。
    另外一个跟上一个应用程序比较的改进是用来启动应用程序的bootstrap类ex03.pyrmont.startup.Bootstrap的出现。
    咱们将会在下面的子节里边详细说明该应用程序:
  • 启动应用程序
  • 链接器
  • 建立一个HttpRequest对象
  • 建立一个HttpResponse对象
  • 静态资源处理器和servlet处理器
  • 运行应用程序

启动应用程序

    你能够从ex03.pyrmont.startup.Bootstrap类来启动应用程序。这个类在Listing 3.1中给出。
        Listing 3.1: Bootstrap类
package ex03.pyrmont.startup;
import ex03.pyrmont.connector.http.HttpConnector;
public final class Bootstrap {
    public static void main(String[] args) {
        HttpConnector connector = new HttpConnector();
        connector.start();
    }
}
    Bootstrap类中的main方法实例化HttpConnector类并调用它的start方法。HttpConnector类在Listing 3.2给出。
        Listing 3.2: HttpConnector类的start方法
package ex03.pyrmont.connector.http;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpConnector implements Runnable {
    boolean stopped;
    private String scheme = "http";
    public String getScheme() {
        return scheme;
    }
    public void run() {
        ServerSocket serverSocket = null;
        int port = 8080;
        try {
            serverSocket = new
            ServerSocket(port, 1, InetAddress.getByName("127.0.0.1"));
        }
        catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }
        while (!stopped) {
            // Accept the next incoming connection from the server socket
            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 start() {
        Thread thread = new Thread(this);
        thread.start ();
    }
}

链接器

    ex03.pyrmont.connector.http.HttpConnector类指代一个链接器,职责是建立一个服务器套接字用来等待前来的HTTP请求。这个类在Listing 3.2中出现。
    HttpConnector类实现了java.lang.Runnable,因此它能被它本身的线程专用。当你启动应用程序,一个HttpConnector的实例被建立,而且它的run方法被执行。
     注意: 你能够经过读"Working with Threads"这篇文章来提醒你本身怎样建立Java线程。
    run方法包括一个while循环,用来作下面的事情:
  • 等待HTTP请求
  • 为每一个请求建立个HttpProcessor实例
  • 调用HttpProcessor的process方法
      注意:run方法相似于第2章中HttpServer1类的await方法。
    立刻你就会看到HttpConnector类和ex02.pyrmont.HttpServer1类很是相像,除了从 java.net.ServerSocket类的accept方法中得到一个套接字以后,一个HttpProcessor实例会被建立,而且经过传递该套 接字给它的process方法调用。
     注意:HttpConnector类有另外一个方法叫getScheme,用来返回一个scheme(HTTP)。
    HttpProcessor类的process方法接受前来的HTTP请求的套接字,会作下面的事情:
1. 建立一个HttpRequest对象。
2. 建立一个HttpResponse对象。
3. 解析HTTP请求的第一行和头部,并放到HttpRequest对象。
4. 解析HttpRequest和HttpResponse对象到一个ServletProcessor或者 StaticResourceProcessor。像第2章里边说的,ServletProcessor调用被请求的servlet的service方 法,而StaticResourceProcessor发送一个静态资源的内容。
    process方法在Listing 3.3给出.
Listing 3.3: HttpProcessor类process方法
public void process(Socket socket) {
    SocketInputStream input = null;
    OutputStream output = null;
    try {
        input = new SocketInputStream(socket.getInputStream(), 2048);
        output = socket.getOutputStream();
        // create HttpRequest object and parse
        request = new HttpRequest(input);
        // create HttpResponse object
        response = new HttpResponse(output);
        response.setRequest(request);
        response.setHeader("Server", "Pyrmont Servlet Container");
        parseRequest(input, output);
        parseHeaders(input);
        //check if this is a request for a servlet or a static resource
        //a request for a servlet begins with "/servlet/"
        if (request.getRequestURI().startsWith("/servlet/")) {
            ServletProcessor processor = new ServletProcessor();
            processor.process(request, response);
        }
        else {
            StaticResourceProcessor processor = new
            StaticResourceProcessor();
            processor.process(request, response);
        }
        // Close the socket
        socket.close();
        // no shutdown for this application
    }
    catch (Exception e) {
        e.printStackTrace ();
    }
}
    process首先得到套接字的输入流和输出流。请注意,在这个方法中,咱们适合继承了java.io.InputStream的SocketInputStream类。
SocketInputStream input = null;
OutputStream output = null;
try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    output = socket.getOutputStream();
    而后,它建立一个HttpRequest实例和一个 instance and an HttpResponse instance and assigns
the HttpRequest to the HttpResponse.
// create HttpRequest object and parse
request = new HttpRequest(input);
// create HttpResponse object
response = new HttpResponse(output);
response.setRequest(request);
    本章应用程序的HttpResponse类要比第2章中的Response类复杂得多。举例来讲,你能够经过调用他的setHeader方法来发送头部到一个客户端。
response.setHeader("Server", "Pyrmont Servlet Container");
    接下去,process方法调用HttpProcessor类中的两个私有方法来解析请求。
parseRequest(input, output);
parseHeaders (input);
    而后,它根据请求URI的形式把HttpRequest和HttpResponse对象传给ServletProcessor或者StaticResourceProcessor进行处理。
if (request.getRequestURI().startsWith("/servlet/")) {
    ServletProcessor processor = new ServletProcessor();
    processor.process(request, response);
}
else {
    StaticResourceProcessor processor =
        new StaticResourceProcessor();
    processor.process(request, response);
}
    最后,它关闭套接字。
socket.close();
    也要注意的是,HttpProcessor类使用org.apache.catalina.util.StringManager类来发送错误信息:
protected StringManager sm =
    StringManager.getManager("ex03.pyrmont.connector.http");
    HttpProcessor类中的私有方法--parseRequest,parseHeaders和normalize,是用来帮助填充HttpRequest的。这些方法将会在下节"建立一个HttpRequest对象"中进行讨论。

建立一个HttpRequest对象

    HttpRequest类实现了javax.servlet.http.HttpServletRequest。跟随它的是一个叫作 HttpRequestFacade的facade类。Figure 3.2显示了HttpRequest类和它的相关类的UML图。
        Figure 3.2: HttpRequest类和它的相关类
    HttpRequest类的不少方法都留空(你须要等到第4章才会彻底实现),可是servlet程序员已经能够从到来的HTTP请求中得到头部,cookies和参数。这三种类型的值被存储在下面几个引用变量中:
protected HashMap headers = new HashMap();
protected ArrayList cookies = new ArrayList();
protected ParameterMap parameters = null;
     注意:ParameterMap类将会在“获取参数”这节中解释。
    所以,一个servlet程序员能够从javax.servlet.http.HttpServletRequest中的下列方法中取得正确的返回 值:getCookies,getDateHeader,getHeader, getHeaderNames, getHeaders, getParameter, getPrameterMap,getParameterNames和getParameterValues。就像你在HttpRequest类中看到的同样,一旦你取得了头部,cookies和填充了正确的值的参数,相关的方法的实现是很简单的。
    不用说,这里主要的挑战是解析HTTP请求和填充HttpRequest类。对于头部和cookies,HttpRequest类提供了addHeader和addCookie方法用于HttpProcessor的parseHeaders方法调用。当须要的时候,会使用 HttpRequest类的parseParameters方法来解析参数。在本节中全部的方法都会被讨论。
    由于HTTP请求的解析是一项至关复杂的任务,因此本节会分为如下几个小节:
  • 读取套接字的输入流
  • 解析请求行
  • 解析头部
  • 解析cookies
  • 获取参数

读取套接字的输入流

    在第1章和第2章中,你在ex01.pyrmont.HttpRequest和ex02.pyrmont.HttpRequest类中作了一点请求解析。 你经过调用java.io.InputStream类的read方法获取了请求行,包括方法,URI和HTTP版本:
byte[] buffer = new byte [2048];
try {
    // input is the InputStream from the socket.
    i = input.read(buffer);
}
    你没有试图为那两个应用程序去进一步解析请求。不过,在本章的应用程序中,你拥有 ex03.pyrmont.connector.http.SocketInputStream类,这是 org.apache.catalina.connector.http.SocketInputStream的一个拷贝。这个类提供了方法不只用来获取请求行,还有请求头部。
    你经过传递一个InputStream和一个指代实例使用的缓冲区大小的整数,来构建一个SocketInputStream实例。在本章中,你在 ex03.pyrmont.connector.http.HttpProcessor的process方法中建立了一个 SocketInputStream对象,就像下面的代码片断同样:
SocketInputStream input = null;
OutputStream output = null;
try {
    input = new SocketInputStream(socket.getInputStream(), 2048);
    ...
    就像前面提到的同样,拥有一个SocketInputStream是为了两个重要方法:readRequestLine和readHeader。请继续往下阅读。

解析请求行

    HttpProcessor的process方法调用私有方法parseRequest用来解析请求行,例如一个HTTP请求的第一行。这里是一个请求行的例子:
GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1
    请求行的第二部分是URI加上一个查询字符串。在上面的例子中,URI是这样的:
/myApp/ModernServlet
    另外,在问好后面的任何东西都是查询字符串。所以,查询字符串是这样的:
userName=tarzan&password=pwd
    查询字符串能够包括零个或多个参数。在上面的例子中,有两个参数名/值对,userName/tarzan和password/pwd。在servlet/JSP编程中,参数名jsessionid是用来携带一个会话标识符。会话标识符常常被做为cookie来嵌入,可是程序员能够选择把它嵌入到查询字符串去,例如,当浏览器的cookie被禁用的时候。
    当parseRequest方法被HttpProcessor类的process方法调用的时候,request变量指向一个HttpRequest实例。parseRequest方法解析请求行用来得到几个值并把这些值赋给HttpRequest对象。如今,让咱们来关注一下在Listing 3.4中的parseRequest方法。
        Listing 3.4:HttpProcessor类中的parseRequest方法
private void parseRequest(SocketInputStream input, OutputStream output)
throws IOException, ServletException {
    // Parse the incoming request line
    input.readRequestLine(requestLine);
    String method =
        new String(requestLine.method, 0, requestLine.methodEnd);
    String uri = null;
    String protocol = new String(requestLine.protocol, 0,
    requestLine.protocolEnd);
    // Validate the incoming request line
    if (method, length () < 1) {
        throw new ServletException("Missing HTTP request method");
    }
    else if (requestLine.uriEnd < 1) {
        throw new ServletException("Missing HTTP request URI");
    }
    // Parse any query parameters out of the request URI
    int question = requestLine.indexOf("?");
    if (question >= 0) {
        request.setQueryString(new String(requestLine.uri, question + 1,
        requestLine.uriEnd - question - 1));
        uri = new String(requestLine.uri, 0, question);
    }
    else {
        request.setQueryString(null);
        uri = new String(requestLine.uri, 0, requestLine.uriEnd);
    }
    // Checking for an absolute URI (with the HTTP protocol)
    if (!uri.startsWith("/")) {
        int pos = uri.indexOf("://");
        // Parsing out protocol and host name
        if (pos != -1) {
            pos = uri.indexOf('/', pos + 3);
            if (pos == -1) {
                uri = "";
            }
            else {
                uri = uri.substring(pos);
            }
        }
    }
    // Parse any requested session ID out of the request URI
    String match = ";jsessionid=";
    int semicolon = uri.indexOf(match);
    if (semicolon >= 0) {
        String rest = uri.substring(semicolon + match,length());
        int semicolon2 = rest.indexOf(';');
        if (semicolon2 >= 0) {
            request.setRequestedSessionId(rest.substring(0, semicolon2));
            rest = rest.substring(semicolon2);
        }
        else {
            request.setRequestedSessionId(rest);
            rest = "";
        }
        request.setRequestedSessionURL(true);
        uri = uri.substring(0, semicolon) + rest;
    }
    else {
        request.setRequestedSessionId(null);
        request.setRequestedSessionURL(false);
    }
    // Normalize URI (using String operations at the moment)
    String normalizedUri = normalize(uri);
    // Set the corresponding request properties
    ((HttpRequest) request).setMethod(method);
    request.setProtocol(protocol);
    if (normalizedUri != null) {
        ((HttpRequest) request).setRequestURI(normalizedUri);
    }
    else {
        ((HttpRequest) request).setRequestURI(uri);
    }
    if (normalizedUri == null) {
        throw new ServletException("Invalid URI: " + uri + "'");
    }
}
    parseRequest方法首先调用SocketInputStream类的readRequestLine方法:
input.readRequestLine(requestLine);
    在这里requestLine是HttpProcessor里边的HttpRequestLine的一个实例:
private HttpRequestLine requestLine = new HttpRequestLine();
    调用它的readRequestLine方法来告诉SocketInputStream去填入HttpRequestLine实例。
    接下去,parseRequest方法得到请求行的方法,URI和协议:
String method =
    new String(requestLine.method, 0, requestLine.methodEnd);
String uri = null;
String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);
    不过,在URI后面能够有查询字符串,假如存在的话,查询字符串会被一个问好分隔开来。所以,parseRequest方法试图首先获取查询字符串。并调用setQueryString方法来填充HttpRequest对象:
// Parse any query parameters out of the request URI
int question = requestLine.indexOf("?");
if (question >= 0) { // there is a query string.
    request.setQueryString(new String(requestLine.uri, question + 1,
    requestLine.uriEnd - question - 1));
    uri = new String(requestLine.uri, 0, question);
}
else {
    request.setQueryString (null);
    uri = new String(requestLine.uri, 0, requestLine.uriEnd);
}
    不过,大多数状况下,URI指向一个相对资源,URI还能够是一个绝对值,就像下面所示:
http://www.brainysoftware.com/index.html?name=Tarzan
    parseRequest方法一样也检查这种状况:
// Checking for an absolute URI (with the HTTP protocol)
if (!uri.startsWith("/")) {
    // not starting with /, this is an absolute URI
    int pos = uri.indexOf("://");
    // Parsing out protocol and host name
    if (pos != -1) {
        pos = uri.indexOf('/', pos + 3);
        if (pos == -1) {
            uri = "";
        }
        else {
            uri = uri.substring(pos);
        }
    }
}
    而后,查询字符串也能够包含一个会话标识符,用jsessionid参数名来指代。所以,parseRequest方法也检查一个会话标识符。假如在查询字符串里边找到jessionid,方法就取得会话标识符,并经过调用setRequestedSessionId方法把值交给HttpRequest实例:
// Parse any requested session ID out of the request URI
String match = ";jsessionid=";
int semicolon = uri.indexOf(match);
if (semicolon >= 0) {
    String rest = uri.substring(semicolon + match.length());
    int semicolon2 = rest.indexOf(';');
    if (semicolon2 >= 0) {
        request.setRequestedSessionId(rest.substring(0, semicolon2));
        rest = rest.substring(semicolon2);
    }
    else {
        request.setRequestedSessionId(rest);
        rest = "";
    }
    request.setRequestedSessionURL (true);
    uri = uri.substring(0, semicolon) + rest;
}
else {
    request.setRequestedSessionId(null);
    request.setRequestedSessionURL(false);
}
    当jsessionid被找到,也意味着会话标识符是携带在查询字符串里边,而不是在cookie里边。所以,传递true给request的 setRequestSessionURL方法。不然,传递false给setRequestSessionURL方法并传递null给 setRequestedSessionURL方法。
    到这个时候,uri的值已经被去掉了jsessionid。
    接下去,parseRequest方法传递uri给normalize方法,用于纠正“异常”的URI。例如,任何\的出现都会给/替代。假如uri是正确的格式或者异常能够给纠正的话,normalize将会返回相同的或者被纠正后的URI。假如URI不能纠正的话,它将会给认为是非法的而且一般会返回null。在这种状况下(一般返回null),parseRequest将会在方法的最后抛出一个异常。
    最后,parseRequest方法设置了HttpRequest的一些属性:
((HttpRequest) request).setMethod(method);
request.setProtocol(protocol);
if (normalizedUri != null) {
    ((HttpRequest) request).setRequestURI(normalizedUri);
}
else {
    ((HttpRequest) request).setRequestURI(uri);
}
    还有,假如normalize方法的返回值是null的话,方法将会抛出一个异常:
if (normalizedUri == null) {
    throw new ServletException("Invalid URI: " + uri + "'");
}

解析头部

    一个HTTP头部是用类HttpHeader来表明的。这个类将会在第4章详细解释,而如今知道下面的内容就足够了:
  • 你能够经过使用类的无参数构造方法构造一个HttpHeader实例。
  • 一旦你拥有一个HttpHeader实例,你能够把它传递给SocketInputStream的readHeader方法。假如这里有头部须要读取,readHeader方法将会相应的填充HttpHeader对象。假如再也没有头部须要读取了,HttpHeader实例的nameEnd和valueEnd字段将会置零。
  • 为了获取头部的名称和值,使用下面的方法:
  • String name = new String(header.name, 0, header.nameEnd);
  • String value = new String(header.value, 0, header.valueEnd);
    parseHeaders方法包括一个while循环用于持续的从SocketInputStream中读取头部,直到再也没有头部出现为止。循环从构建一个HttpHeader对象开始,并把它传递给类SocketInputStream的readHeader方法:
HttpHeader header = new HttpHeader();
// Read the next header
input.readHeader(header);
    而后,你能够经过检测HttpHeader实例的nameEnd和valueEnd字段来测试是否能够从输入流中读取下一个头部信息:
if (header.nameEnd == 0) {
    if (header.valueEnd == 0) {
        return;
    }
    else {
        throw new ServletException             (sm.getString("httpProcessor.parseHeaders.colon"));
    }
}
      假如存在下一个头部,那么头部的名称和值能够经过下面方法进行检索:
String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
    一旦你获取到头部的名称和值,你经过调用HttpRequest对象的addHeader方法来把它加入headers这个HashMap中:
request.addHeader(name, value);
    一些头部也须要某些属性的设置。例如,当servlet调用javax.servlet.ServletRequest的getContentLength方法的时候,content-length头部的值将被返回。而包含cookies的cookie头部将会给添加到cookie集合中。就这样,下面是其中一些过程:
if (name.equals("cookie")) {
    ... // process cookies here
}
else if (name.equals("content-length")) {
    int n = -1;
    try {
        n = Integer.parseInt (value);
    }
    catch (Exception e) {
        throw new ServletException(sm.getString(
            "httpProcessor.parseHeaders.contentLength"));
    }
    request.setContentLength(n);
}
else if (name.equals("content-type")) {
    request.setContentType(value);
}
    Cookie的解析将会在下一节“解析Cookies”中讨论。

解析Cookies

    Cookies是做为一个Http请求头部经过浏览器来发送的。这样一个头部名为"cookie"而且它的值是一些cookie名/值对。这里是一个包括两个cookie:username和password的cookie头部的例子。
Cookie: userName=budi; password=pwd;
    Cookie的解析是经过类org.apache.catalina.util.RequestUtil的parseCookieHeader方法来处理的。这个方法接受cookie头部并返回一个javax.servlet.http.Cookie数组。数组内的元素数量和头部里边的cookie名/值对个数是同样的。parseCookieHeader方法在Listing 3.5中列出。
Listing 3.5: The org.apache.catalina.util.RequestUtil class's parseCookieHeader method
public static Cookie[] parseCookieHeader(String header) {
    if ((header == null) || (header.length 0 < 1) )
        return (new Cookie[0]);
    ArrayList cookies = new ArrayList();
    while (header.length() > 0) {
        int semicolon = header.indexOf(';');
        if (semicolon < 0)
            semicolon = header.length();
        if (semicolon == 0)
            break;
        String token = header.substring(0, semicolon);
        if (semicolon < header.length())
            header = header.substring(semicolon + 1);
        else
            header = "";
        try {
            int equals = token.indexOf('=');
            if (equals > 0) {
                String name = token.substring(0, equals).trim();
                String value = token.substring(equals+1).trim();
                cookies.add(new Cookie(name, value));
            }
        }
        catch (Throwable e) {
            ;
        }
    }
    return ((Cookie[]) cookies.toArray (new Cookie [cookies.size ()]));
}
    还有,这里是HttpProcessor类的parseHeader方法中用于处理cookie的部分代码:
else if (header.equals(DefaultHeaders.COOKIE_NAME)) {
    Cookie cookies[] = RequestUtil.ParseCookieHeader (value);
    for (int i = 0; i < cookies.length; i++) {
        if (cookies[i].getName().equals("jsessionid")) {
            // Override anything requested in the URL
            if (!request.isRequestedSessionIdFromCookie()) {
                // Accept only the first session id cookie
                request.setRequestedSessionId(cookies[i].getValue());
                request.setRequestedSessionCookie(true);
                request.setRequestedSessionURL(false);
            }
        }
        request.addCookie(cookies[i]);
    }
}

获取参数

    你不须要立刻解析查询字符串或者HTTP请求内容,直到servlet须要经过调用javax.servlet.http.HttpServletRequest的getParameter,
getParameterMap, getParameterNames或者getParameterValues方法来读取参数。所以,HttpRequest的这四个方法开头调用了parseParameter方法。
    这些参数只须要解析一次就够了,由于假如参数在请求内容里边被找到的话,参数解析将会使得SocketInputStream到达字节流的尾部。类HttpRequest使用一个布尔变量parsed来指示是否已经解析过了。
    参数能够在查询字符串或者请求内容里边找到。假如用户使用GET方法来请求servlet的话,全部的参数将在查询字符串里边出现。假如使用POST方法的话,你也能够在请求内容中找到一些。全部的名/值对将会存储在一个HashMap里边。Servlet程序员能够以Map的形式得到参数(经过调用HttpServletRequest的getParameterMap方法)和参数名/值。There is a catch, though. Servlet程序员不被容许修改参数值。所以,将使用一个特殊的HashMap:org.apache.catalina.util.ParameterMap。
    类ParameterMap继承java.util.HashMap,并使用了一个布尔变量locked。当locked是false的时候,名/值对仅仅能够添加,更新或者移除。不然,异常IllegalStateException会抛出。而随时均可以读取参数值。
类ParameterMap将会在Listing 3.6中列出。它覆盖了方法用于增长,更新和移除值。那些方法仅仅在locked为false的时候能够调用。
Listing 3.6: The org.apache.Catalina.util.ParameterMap class.
package org.apache.catalina.util;
import java.util.HashMap;
import java.util.Map;
public final class ParameterMap extends HashMap {
    public ParameterMap() {
        super ();
    }
    public ParameterMap(int initialCapacity) {
        super(initialCapacity);
    }
    public ParameterMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
    }
    public ParameterMap(Map map) {
        super(map);
    }
    private boolean locked = false;
    public boolean isLocked() {
        return (this.locked);
    }
    public void setLocked(boolean locked) {
        this.locked = locked;
    }
    private static final StringManager sm =
        StringManager.getManager("org.apache.catalina.util");
    public void clear() {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        super.clear();
    }
    public Object put(Object key, Object value) {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        return (super.put(key, value));
    }
    public void putAll(Map map) {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        super.putAll(map);
    }
    public Object remove(Object key) {
        if (locked)
            throw new IllegalStateException
                (sm.getString("parameterMap.locked"));
        return (super.remove(key));
    }
}
    如今,让咱们来看parseParameters方法是怎么工做的。
    由于参数能够存在于查询字符串或者HTTP请求内容中,因此parseParameters方法会检查查询字符串和请求内容。一旦解析事后,参数将会在对象变量parameters中找到,因此方法的开头会检查parsed布尔变量,假如已经解析过的话,parsed将会返回true。
if (parsed)
return;
    而后,parseParameters方法建立一个名为results的ParameterMap变量,并指向parameters。假如
parameters为null的话,它将建立一个新的ParameterMap。
ParameterMap results = parameters;
if (results == null)
    results = new ParameterMap();
    而后,parseParameters方法打开parameterMap的锁以便写值。
results.setLocked(false);
    下一步,parseParameters方法检查字符编码,并在字符编码为null的时候赋予默认字符编码。
String encoding = getCharacterEncoding();
if (encoding == null)
    encoding = "ISO-8859-1";
    而后,parseParameters方法尝试解析查询字符串。解析参数是使用org.apache.Catalina.util.RequestUtil的parseParameters方法来处理的。
// Parse any parameters specified in the query string
String queryString = getQueryString();
try {
    RequestUtil.parseParameters(results, queryString, encoding);
}
catch (UnsupportedEncodingException e) {
    ;
}
    接下来,方法尝试查看HTTP请求内容是否包含参数。这种状况发生在当用户使用POST方法发送请求的时候,内容长度大于零,而且内容类型是application/x-www-form-urlencoded的时候。因此,这里是解析请求内容的代码:
// Parse any parameters specified in the input stream
String contentType = getContentType();
if (contentType == null)
    contentType = "";
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
    contentType = contentType.substring (0, semicolon).trim();
}
else {
    contentType = contentType.trim();
}
if ("POST".equals(getMethod()) && (getContentLength() > 0)
    && "application/x-www-form-urlencoded".equals(contentType)) {
    try {
        int max = getContentLength();
        int len = 0;
        byte buf[] = new byte[getContentLength()];
        ServletInputStream is = getInputStream();
        while (len < max) {
            int next = is.read(buf, len, max - len);
            if (next < 0 ) {
                break;
            }
            len += next;
        }
        is.close();
        if (len < max) {
            throw new RuntimeException("Content length mismatch");
        }
        RequestUtil.parseParameters(results, buf, encoding);
    }
    catch (UnsupportedEncodingException ue) {
        ;
    }
    catch (IOException e) {
        throw new RuntimeException("Content read fail");
    }
}
    最后,parseParameters方法锁定ParameterMap,设置parsed为true,并把results赋予parameters。
// Store the final results
results.setLocked(true);
parsed = true;
parameters = results;

建立一个HttpResponse对象

    HttpResponse类实现了javax.servlet.http.HttpServletResponse。跟随它的是一个叫作HttpResponseFacade的façade类。Figure 3.3显示了HttpResponse类和它的相关类的UML图。
    在第2章中,你使用的是一个部分实现的HttpResponse类。例如,它的getWriter方法,在它的其中一个print方法被调用的时候,返回一个不会自动清除的java.io.PrintWriter对象。在本章中应用程序将会修复这个问题。为了理解它是如何修复的,你须要知道Writer是什么东西来的。
    在一个servlet里边,你使用PrintWriter来写字节。你可使用任何你但愿的编码,可是这些字节将会以字节流的形式发送到浏览器去。所以,第2章中ex02.pyrmont.HttpResponse类的getWriter方法就不奇怪了:
public PrintWriter getWriter() {
// if autoflush is true, println() will flush,
// but print() will not.
// the output argument is an OutputStream
    writer = new PrintWriter(output, true);
    return writer;
}
    请看,咱们是如何构造一个PrintWriter对象的?就是经过传递一个java.io.OutputStream实例来实现的。你传递给PrintWriter的print或println方法的任何东西都是经过底下的OutputStream进行发送的。
    在本章中,你为PrintWriter使用ex03.pyrmont.connector.ResponseStream类的一个实例来替代
OutputStream。须要注意的是,类ResponseStream是间接的从类java.io.OutputStream传递过去的。
    一样的你使用了继承于PrintWriter的类ex03.pyrmont.connector.ResponseWriter。
类ResponseWriter覆盖了全部的print和println方法,而且让这些方法的任何调用把输出自动清除到底下的
OutputStream去。所以,咱们使用一个带底层ResponseStream对象的ResponseWriter实例。
    咱们能够经过传递一个ResponseStream对象实例来初始化类ResponseWriter。然而,咱们使用一个java.io.OutputStreamWriter对象充当ResponseWriter对象和ResponseStream对象之间的桥梁。
    经过OutputStreamWriter,写进去的字符经过一种特定的字符集被编码成字节。这种字符集可使用名字来设定,或者明确给出,或者使用平台可接受的默认字符集。write方法的每次调用都会致使在给定的字符上编码转换器的调用。在写入底层的输出流以前,生成的字节都会累积到一个缓冲区中。缓冲区的大小能够本身设定,可是对大多数场景来讲,默认的就足够大了。注意的是,传递给write方法的字符是没有被缓冲的。
    所以,getWriter方法以下所示:
public PrintWriter getWriter() throws IOException {
    ResponseStream newStream = new ResponseStream(this);
    newStream.setCommit(false);
    OutputStreamWriter osr =
        new OutputStreamWriter(newStream, getCharacterEncoding());
    writer = new ResponseWriter(osr);
    return writer;
}

静态资源处理器和Servlet处理器

    类ServletProcessor相似于第2章中的类ex02.pyrmont.ServletProcessor。它们都只有一个方法:process。然而ex03.pyrmont.connector.ServletProcessor中的process方法接受一个HttpRequest和
HttpResponse,代替了Requese和Response实例。下面是本章中process的方法签名:
public void process(HttpRequest request, HttpResponse response) {
    另外,process方法使用HttpRequestFacade和HttpResponseFacade做为
request和response的facade类。另外,在调用了servlet的service方法以后,它调用了类HttpResponse的
finishResponse方法。
servlet = (Servlet) myClass.newInstance();
HttpRequestFacade requestPacade = new HttpRequestFacade(request);
HttpResponseFacade responseFacade = new HttpResponseFacade(response);
servlet.service(requestFacade, responseFacade);
((HttpResponse) response).finishResponse();
    类StaticResourceProcessor几乎等同于类ex02.pyrmont.StaticResourceProcessor。

运行应用程序

    要在Windows上运行该应用程序,在工做目录下面敲入如下命令:
java -classpath ./lib/servlet.jar;./ ex03.pyrmont.startup.Bootstrap
    在Linux下,你使用一个冒号来分隔两个库:
java -classpath ./lib/servlet.jar:./ ex03.pyrmont.startup.Bootstrap
    要显示index.html,使用下面的URL:
http://localhost:808O/index.html
    要调用PrimitiveServlet,让浏览器指向下面的URL:
http://localhost:8080/servlet/PrimitiveServlet
    在你的浏览器中将会看到下面的内容:
Hello. Roses are red.
Violets are blue.
     注意:在第2章中运行PrimitiveServlet不会看到第二行。
    你也能够调用ModernServet,在第2章中它不能运行在servlet容器中。下面是相应的URL:
http://localhost:8080/servlet/ModernServlet
     注意:ModernServlet的源代码在工做目录的webroot文件夹能够找到。
    你能够加上一个查询字符串到URL中去测试servlet。加入你使用下面的URL来运行ModernServlet的话,将显示Figure 3.4中的运行结果。
http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd
Figure 3.4: Running ModernServlet

总结

    在本章中,你已经知道了链接器是如何工做的。创建起来的链接器是Tomcat4的默认链接器的简化版本。正如你所知道的,由于默认链接器并不高效,因此已经被弃用了。例如,全部的HTTP请求头部都被解析了,即便它们没有在servlet中使用过。所以,默认链接器很慢,而且已经被Coyote所代替了。Coyote是一个更快的链接器,它的源代码能够在Apache软件基金会的网站中下载。无论怎样,默认链接器做为一个优秀的学习工具,将会在第4章中详细讨论。

第四章:Tomcat的默认链接器

概要

    第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章中的链接器未使用的优化。首先就是提供一个各类各样对象的对象池用于避免昂贵对象的建立。接着,在不少地方使用字节数组来代替字符串。
    本章中的应用程序是一个和默认链接器管理的简单容器。然而,本章的焦点不是简单容器而是默认链接器。咱们将会在第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新特性

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

持久链接

    在HTTP1.1以前,不管何时浏览器链接到一个web服务器,当请求的资源被发送以后,链接就被服务器关闭了。然而,一个互联网网页包括其余资源,例如图片文件,applet等等。所以,当一个页面被请求的时候,浏览器一样须要下载页面所引用到的资源。加入页面和它所引用到的所有资源使用不一样链接来下载的话,进程将会很是慢。那就是为何HTTP1.1引入持久链接的缘由了。使用持久链接的时候,当页面下载的时候,服务器并不直接关闭链接。相反,它等待web客户端请求页面所引用的所有资源。这种状况下,页面和所引用的资源使用同一个链接来下载。考虑创建和解除HTTP链接的宝贵操做的话,这就为 web服务器,客户端和网络节省了许多工做和时间。
    持久链接是HTTP1.1的默认链接方式。一样,为了明确这一点,浏览器能够发送一个值为keep-alive的请求头部connection:
connection: keep-alive

块编码

    创建持续链接的结果就是,使用同一个链接,服务器能够从不一样的资源发送字节流,而客户端可使用发送多个请求。结果就是,发送方必须为每一个请求或响应发送内容长度的头部,以便接收方知道如何解释这些字节。然而,大部分的状况是发送方并不知道将要发送多少个字节。例如,在开头一些字节已经准备好的时候,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标识这个事务的结束。

状态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建立一个响应对象。
    类org.apache.catalina.connector.http.HttpConnector是Connector接口的一个实现,将会在下一节“HttpConnector类”中讨论。如今,仔细看一下Figure 4.1中的默认链接器的UML类图。注意的是,为了保持图的简单化,Request和Response接口的实现被省略了。除了 SimpleContainer类,org.apache.catalina前缀也一样从类型名中被省略了。
Figure 4.1: The default connector class diagram
    所以,Connector须要被org.apache.catalina.Connector,util.StringManager
org.apache.catalina.util.StringManager等等访问到。
    一个Connector和Container是一对一的关系。箭头的方向显示出Connector知道Container但反过来就不成立了。一样须要注意的是,不像第3章的是,HttpConnector和HttpProcessor是一对多的关系。

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

建立一个服务器套接字

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

维护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方法来改变他们的值。
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队会栈。
    每一个HttpProcessor实例负责解析HTTP请求行和头部,并填充请求对象。所以,每一个实例关联着一个请求对象和响应对象。类 HttpProcessor的构造方法包括了类HttpConnector的createRequest和createResponse方法的调用。

为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类”中看到是怎么作的。

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: 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循环按照这样的循序进行:获取一个套接字,处理它,调用链接器的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);
}
    两个方法的程序流向在Table 4.1中总结。
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。这就是说,调用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调用。

请求对象

    默认链接器哩变得HTTP请求对象指代org.apache.catalina.Request接口。这个接口被类RequestBase直接实现了,也是HttpRequest的父接口。最终的实现是继承于HttpRequest的HttpRequestImpl。像第3章同样,有几个facade类:RequestFacade和HttpRequestFacade。Request接口和它的实现类的UML图在Figure 4.2中给出。注意的是,除了属于javax.servlet和javax.servlet.http包的类,前缀org.apache.catalina已经被省略了。
Figure 4.2: The Request interface and related types
    若是你理解第3章的请求对象,理解这个结构图你应该不会遇到什么困难。

响应对象

    Response接口和它的实现类的UML图在Figure 4.3中给出。
    Figure 4.3: The Response interface and its implementation classes

处理请求

    到这个时候,你已经理解了请求和响应对象,而且知道HttpConnector对象是如何建立它们的。如今是这个过程的最后一点东西了。在这节中咱们关注HttpProcessor类的process方法,它是一个套接字赋给它以后,在HttpProcessor类的run方法中调用的。process方法会作下面这些工做:
  • 解析链接
  • 解析请求
  • 解析头部
    在解释完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 need
try {
    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);
    output = socket.getOutputStream();
    response.setStream(output);
    response.setRequest(request);
    ((HttpServletResponse) response.getResponse()).setHeader("Server", SERVER_INFO);
}
catch (Exception e) {
    log("process.create", e); //logging is discussed in Chapter 7
    ok = false;
}
    接着,process方法经过调用parseConnection,parseRequest和parseHeaders方法开始解析前来的HTTP请求,这些方法将在这节的小节中讨论。
try {
    if (ok) {
        parseConnection(socket);
        parseRequest(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,响应对象的finishResponse方法和请求对象的finishRequest方法将被调用,而且结束输出。
if (finishResponse) {
    ...
    response.finishResponse();
    ...
    request.finishRequest();
    ...
    output.flush();
    while循环的最后一部分检查响应的Connection头部是否已经在servlet内部设为close,或者协议是HTTP1.0.若是是这种状况的话,keepAlive设置为false。一样,请求和响应对象接着会被回收利用。
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();
}
    在这个场景中,若是哦keepAlive是true的话,while循环将会在开头就启动。由于在前面的解析过程当中和容器的invoke方法中没有出现错误,或者HttpProcessor实例没有被中止。不然,shutdownInput方法将会调用,而套接字将被关闭。
try {
    shutdownInput(input);
    socket.close();
}
...
    shutdownInput方法检查是否有未读取的字节。若是有的话,跳过那些字节。

解析链接

    parseConnection方法从套接字中获取到网络地址并把它赋予HttpRequestImpl对象。它也检查是否使用代理并把套接字赋予请求对象。parseConnection方法在Listing4.2中列出。
Listing 4.2: The parseConnection method
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);
}

解析请求

    parseRequest方法是第3章中相似方法的完整版本。若是你很好的理解第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 header
input.readHeader(header);
    假如全部的头部都被已经被读取的话,readHeader方法将不会赋值给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
}
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();

简单容器的应用程序

    本章的应用程序的主要目的是展现默认链接器是怎样工做的。它包括两个类:
    ex04.pyrmont.core.SimpleContainer和ex04 pyrmont.startup.Bootstrap。类
SimpleContainer实现了org.apache.catalina.container接口,因此它能够和链接器关联。类Bootstrap是用来启动应用程序的,咱们已经移除了第3章带的应用程序中的链接器模块,类ServletProcessor和
StaticResourceProcessor,因此你不能请求一个静态页面。
    类SimpleContainer展现在Listing 4.3.
Listing 4.3: The SimpleContainer class
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) {
}
}
    我只是提供了SimpleContainer类的invoke方法的实现,由于默认链接器将会调用这个方法。invoke方法建立了一个类加载器,加载servlet类,并调用它的service方法。这个方法和第3章的ServletProcessor类在哦个的process方法很是相似。
    Bootstrap类在Listing 4.4在列出.
    Listing 4.4: The ex04.pyrmont.startup.Bootstrap class
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端口上的任何请求作好了准备。
    你能够经过在控制台中输入一个按键来终止这个应用程序。

运行应用程序

    要在Windows中运行这个程序的话,在工做目录下输入如下内容: html

java -classpath ./lib/servlet.jar;./ ex04.pyrmont.startup.Bootstrap
    在Linux的话,你可使用分号来分隔两个库。
java -classpath ./lib/servlet.jar:./ ex04.pyrmont.startup.Bootstrap
    你能够和第三章那样调用PrimitiveServlet和ModernServlet。
    注意的是你不能请求index.html,由于没有静态资源的处理器。

总结

    本章展现了如何构建一个能和Catalina工做的Tomcat链接器。剖析了Tomcat4的默认链接器的代码并用这个链接器构建了一个小应用程序。接下来的章节的全部应用程序都会使用默认链接器。
相关文章
相关标签/搜索