译How Tomcat Works(第三章)

第三章: Connector
概览
简介中介绍过,Catalina中有两个主要的模块:Connector和Container。这章我们将写一个Connector来增强第二章的应用程序,这个Connector可以创建更好的request和response对象。要使Connector遵循Servlet 2.3 /2.4规范必须创建javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse实例传递给被调用的servlet的service方法。在第二章的servlet container中,仅能运行实现javax.servlet.Servlet的servlet。传递给service方法的只能是javax.servlet.ServletRequest和javax.servlet.ServletResponse实例。因为connector是不知道servlet的类型的(比如,是否该servlet实现了javax.servlet.Servlet接口,继承了javax.servlet.GenericServlet,或是javax.servlet.http.HttpServlet),在本章的程序中Connector必须提供HttpServletRequest和HttpServletResponse实例。
在本章的程序中,Connector解析HTTP Request header使servlet获取到header,cookies和parameter的键值对等。我们还将完善第二章中Response类的getWriter方法。有了这些改进后,我们就可以从PrimitiveServlet中获取到完整的响应,也可以运行更复杂的ModernServlet类了。
这章中构建的Connector是Tomcat 4中默认Connector的一个简化版,我们将在第四章继续讨论这个Connector。Tomcat 4的默认connector是不被认同的,但是它仍旧是一个很好的学习工具。在剩下的章节中,应用程序中connector将参与模型的构建。
注:不同于前一章中的程序,这章的应用程序中,connector是与container分离的。
这章的程序在 ex03.pyrmont 包及子包中。从这章开始,每个程序都有一个bootstrap类用来启动该应用程序。但是,现阶段还没有一个机制可以停止该程序,所以一旦运行了,你必须通过关闭控制台来结束该程序(Windows),如果是UNIX/Linux ,就需要杀掉进程的方式了。在解释该应用程序之前,让我们从 org.apache.catalina.util包下的StringManager类开始。该类处理了不同模块以及Catalina本身的错误信息的国际化问题。随后将继续讨论本章的程序。
在解释程序之前,让我们从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是单例模式。它唯一的构造函数是私有的,因此在类外部不能通过new 关键字实例化这个类。要通过调用公有的静态方法getManager来获得一个实例,所需参数是一个包名。每个实例都存储在一个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;
}
例如,要在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用哪个文件取决于运行程序的服务器的locale。打开 LocalStrings.properties 文件,第一个非注释行如下:
httpConnector.alreadyInitialized=HTTP connector has already been initialized
要获取错误信息,可以用StringManager类的getString方法,参数为一个错误代码。
public String getString(String key)

传递httpConnector参数可以调用getString方法。“httpConnector.alreadyInitialized”作为参数,返回值是“HTTP connector has already been initialized”。
应用程序
从这章开始,所附的应用的程序都被分成模块。这章的程序包含三个模块:connector,startup和core。Startup模块仅有一个类Bootstrap,用于启动应用程序。Connector模块的类可以被分成五类:
 Connector及其支持类(HttpConnector 和 HttpProcessor)。
 描绘HTTP request的类(HttpRequest)及其支持类。
 描绘HTTP response的类(HttpResponse)及其支持类。
 Façade类(HttpRequestFacade 和 HttpResponseFacade)。
 Constant类。
核心模块包含两个类:ServletProcessor和StaticResourceProcessor
图3.1展示的是该应用程序的UML图。为了让该图易读,与HttpRequest和HttpResponse关联的类都被忽略了。当我们分别讨论Request和Response时,将会给出。
图 3.1: 应用程序UML 图

与图2.1比较,HttpServer类被分成两个类:HttpConnector和HttpProcessor,Request被HttpRequest取代,Response被HttpResponse取代,这章还要用到更多的类。
第二章中,HttpServer类的任务就是等待HTTP 请求并创建request和response对象。这章的程序中,等待HTTP请求的任务交给了HttpConnector类,创建request和response的任务交给了HttpProcessor类。
这章用httpRequest类展示HTTP request对象,它实现javax.servlet.http.HttpServletRequest接口。一个HttpRequest对象将被强转为HttpServletRequest实例传给servlet的service方法。因此,每个HttpRequest实例必须有其适当的域,以便servlet使用。URI,query string, parameters, cookies 和其他headers等的值都需要赋给HttpRequest对象。因为Connector不知道被调用的servlet需要哪些值。Connector必须从HTTP请求中解析出所有的值。但是,解析一个HTTP请求需要使用字符串操作和其他操作,如果connector只解析Servlet需要的值则能节省很多CPU资源,例如,如果Servlet不需要任何的request 参数 (也就是说不调用javax.servlet.http.HttpServletRequest 的getParameter,getParameterMap,getParameterNames或getParameterValues方法),connector就不用从query String 或者HTTP Request body中解析这些参数。Tomcat的默认Connector(和这章程序中的Connector)在Servlet真正需要时才解析这些参数,这样可以更高效。
Tomcat的默认Connector和我们的Connector都是使用 SocketInputStream类从socket的 InputStream.读取字节流。一个SocketInputStream实例包含由socket的getInputStream 方法返回的java.io.InputStream实例。SocketInputStream提供了两个重要的方法:readHeader和readRequestLine。readRequestLine返回HTTP请求的第一行,也就是包含URI,方法和HTTP版本的行。因为处理Socket的inputStream中的字节流就意味着从第一个字节一直读到最后一个(并且从未向后移动),readRequestLine只能被调用一次,并且只能在调用readHeader之前调用。每次调用readHeader可以获取一个header的键值对,必须反复调用readHeader,直到所有的header都读取完。readRequestLine的返回值是一个HttpRequestLine对象,而readHeader的返回值是HttpHeader的一个对象。接下来的部分,我们将讨论一下HttpRequestLine和HttpHeader类。
HttpProcessor对象创建HttpRequest实例,并封装。HttpProcessor用parse方法解析HTTP请求中的request line和header。解析出来的值将被赋值给HttpProcessor对象的属性。parse方法不解析请求体中的parameter和query string,这个任务留给HttpRequest对象自己。仅当Servlet需要的时候才解析query string 或请求体。
较前一章的另外一个改进bootstrap类的出现,它用于启动应用程序。在以下章节中我们就来解释一下这些程序:
•     启动应用程序
•     Connector
•     创建 HttpRequest 对象  
•     创建 HttpResponse 对象
•     静态资源和Servlet的处理
•     运行程序
启动应用程序
从ex03.pyrmont.startup.Bootstrap类开始该应用程序。这个类的代码在清单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();
    }
}
main方法中实例化了一个HttpConnector类,并调用了start方法。HttpConnector类的代码在清单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 ();
    }
}
Connector
ex03.pyrmont.connector.http.HttpConnector 类说明connector的职责就是创建一个server socket,等待HTTP请求。 清单3.2中将给出该类的代码。
HttpConnector实现了java.lang.Runnable接口,因此它有自己专用的线程。当你启动该程序,一个HttpConnector实例就会被创建,并且它的run方法会被执行。

注:你可以阅读"Working with Threads"这篇文章,来了解java线程是如何创建的。

run方法包含一个while循环,它要做的是:
 等待HTTP请求
 为每个请求创建一个HttpProcessor实例
 调用HttpProcessor的process方法

注:run方法与第二章中HttpServer1 类的await 方法类似。

你可以现在就看一下HttpConnector类与ex02.pyrmont.HttpServer1类,它们是非常相似的,除了在从java.net.ServerSocket 的accept方法获得socket之后,一个HttpProcessor实例被创建并且其process方法被调用以外,process的参数为socket。

注:HttpConnector还有一个方法叫getScheme,返回值是scheme(HTTP)。

HttpConnector类的process方法接收来自HTTP请求的socket,对于每个HTTP请求,它都要做如下工作:
1.  创建一个 HttpRequest 对象 。
2.  创建一个 HttpResponse 对象。
3.  解析HTTP 请求的第一行和headers 并组装成HttpRequest 对象。
4.  将HttpRequest 和HttpResponse 对象传递给ServletProcessor或StaticResourceProcessor。像第二章一样,ServletProcessor调用被请求的Servlet的service方法,StaticResourceProcessor发送一个静态资源的内容。
process 方法在清单3.3中给出

清单 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方法从获取到socket 的input stream 和 output stream开始,注意,在此方法中,我们用的是SocketInputStream类,它继承自java.io.InputStream。
        SocketInputStream input = null;
        OutputStream output = null;
        try {
            input = new SocketInputStream(socket.getInputStream(), 2048);
            output = socket.getOutputStream();  
然后,它创建一个HttpRequest实例和一个HttpResponse实例,并将HttpRequest对象设置这到HttpResponse中。
            // create HttpRequest object and parse
            request = new HttpRequest(input);
             // create HttpResponse object
            response = new HttpResponse(output);
            response.setRequest(request);
本章应用程序中的HttpResponse类比第二章的完美一些。举例来说,你可以调用setHeader方法发送header到客户端。
response.setHeader("Server", "Pyrmont Servlet Container");
接下来,process方法调用HttpProcessor中的两个私有方法来解析请求。
            parseRequest(input, output);
            parseHeaders (input);
然后,交由ServletProcessor或StaticResourceProcessor处理,取决于这个请求的URI的格式
            if (request.getRequestURI().startsWith("/servlet/")) {
                ServletProcessor processor = new ServletProcessor();
                processor.process(request, response);
            }
            else {
                StaticResourceProcessor processor =
                    new StaticResourceProcessor();
                processor.process(request, response);
            }
最后,关闭socket 。
            socket.close();
注:HttpProcessor也使用org.apache.catalina.util.StringManager类来发送错误信息:
protected StringManager sm = StringManager.getManager("ex03.pyrmont.connector.http");
调用HttpRequest类的私有方法parseRequest、parseHeaders和normalize来组装一个HttpRequest对象,这些方法将在下面的章节“创建一个HttpRequest对象”中讨论。
创建 HttpRequest 对象
HttpRequest类实现了javax.servlet.http.HttpServletRequest接口。还有一个façade 类叫HttpRequestFacade。图3.2给出了HttpRequest及其相关联的类的UML图。

图3.2:HttpReques及其相关类

HttpRequest的很多方法都是置空的(第四章会有一个完整的实现),但是servlet程序员已经能从HTTP请求中重新得到header,Cookies和parameters了。这三种类型的值存储在如下变量中:
    protected HashMap headers = new HashMap();
    protected ArrayList cookies = new ArrayList();
    protected ParameterMap parameters = null;

注:  ParameterMap 类将在“获取Parameters”部分解释。
因此,servlet程序员可以从javax.servlet.http.HttpServletRequest的方法中得到正确的返回值:getCookies,getDateHeader,getHeader,getHeaders,getHeaderNames, getParameter,getPrameterMap,getParameterNames和 getParameterValues方法。一旦获取到用正确的值组装好的 headers,cookies,和 parameters,相关方法的实现是非常容易的,就像你在HttpRequest 类中看到的一样。
不用说,现在最大的挑战就是解析HTTP请求并组装成HttpRequest对象。对于header和Cookies来说,HttpRequest类提供了addHeader 和 addCookie方法,它们将在HttpProcessor的parseHeaders方法中被调用。需要时,用HttpRequest 类的parseParameters 方法来解析Parameters。以上所有方法将在这部分给出解释。
因为解析HTTP Request是一个非常复杂的任务,把这部分内容分成以下几个部分:
 读取socket 的输入流
 解析request行
 解析header
 解析cookies
 获取parameters

读取 Socket的 Input Stream
在第一章和第二章中,你在ex01.pyrmont.HttpRequest 和 ex02.pyrmont.HttpRequest类中已经做过一些解析请求的工作。通过调用java.io.InputStream类的read方法,我们获得了包含了方法、URI、HTTP版本等信息的request line。
byte[] buffer = new byte [2048];
        try {
            // input is the InputStream from the socket.
            i = input.read(buffer);
        }
你不用试图为这两个程序进一步解析request。本章应用程序中,ex03.pyrmont.connector.http.SocketInputStream类是org.apache.catalina.connector.http.SocketInputStream的一个拷贝。这个类提供方法既能获取到request line 也能获取到request header。
通过传一个InputStream和一个在实例中用到的Integer型的buffer size作为参数,创建一个SocketInputStream实例。在本程序中,用ex03.pyrmont.connector.http.HttpProcessor类的process方法创建一个SocketInputStream对象。代码片段如下:
        SocketInputStream input = null;
        OutputStream output = null;
        try {
            input = new SocketInputStream(socket.getInputStream(), 2048);
            ...
正如前面提到的,构造一个SocketInputStream对象是为了使用它的两个重要方法:readRequestLine和readHeader。
解析 Request Line
HttpProcessor的process方法调用私有的parseRequest方法解析request line,也就是HTTP请求的第一行。下面是request line 的一个例子:
GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1
request line的第二部分是URI和一个可选的query string。在上面例子中,URI是:
/myApp/ModernServlet
在“?”号后的是query string。因此,该例子中的query string 是:
userName=tarzan&password=pwd
query string 可以含有0个也可以含有很多个parameter。在上面的例子中,有两个参数键值对:userName/tarzan和password/pwd。在Servlet/JSP编程中,jsessionid参数用来储存一个sessions 标识。Session 标识一般是被嵌入到cookies中的,但是程序员可以选择将它嵌入到query string中,例如浏览器关闭了对cookies的支持。
当parseRequest方法在HttpProcessor类的process方法中被调用时,request变量指向一个HttpRequest实例。parseRequest方法解析request line以获取值并赋值给HttpRequest对象。现在,让我们在清单3.4中进距离看一下parseRequest方法吧:
清单 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是HttpRequestLine的一个实例
private HttpRequestLine requestLine = new HttpRequestLine();
调用它的readRequestLine方法告诉SocketInputStream组装HttpRequestLine实例。
然后,parseRequest 方法获得了request line的method ,URI 和协议:
     String method =  new String(requestLine.method, 0, requestLine.methodEnd);
     String uri = null;
     String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);
但是在URI后面也许还有query string。如果存在的话,URI与query string是用“?”分割的,因此,parseRequest方法首先获取query string并调用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);
                }
            }
        }
query string可能还包含session 标识,参数名为jsessionid。因此,parseRequest方法也必须检测session标识。如果在query string 中发现了session标识,该方法获取session标识并调用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,意味着session 标识也被加到 query string中,而不是作为cookie。因此传一个true给request的setRequestedSessionURL方法。否则传false,并且传null给setRequestedSessionId方法。
这样,URI的值就与jsessionid剥离开了。
然后,parseRequest方法将URI传递给normalize方法来纠正一个规范化 URI。例如,任何出现“¥”符号的地方替换成“/”。如果URI格式正确或错误可以被纠正,normalize方法返回相同的URI或纠正过的URI。如果URI不能被纠正,将被认为无效而返回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 + "'");
        }
解析 Headers
HTTP header由HttpHeader类来描述。这个类将在第四章中解释。眼下,只要知道以下知识就足够了:
 可以用HttpHeader类的无参构造函数实例化一个对象。
 创建HttpHeader类的实例后,传给SocketInputStream类的readHeader方法。如果有header,readHeader方法将根据该header组装这个HttpHeader对象。如果没有header,HttpHeader实例的nameEnd 和 valueEnd属性将都为0。
 用如下方法获得header的name 和value
 String name = new String(header.name, 0, header.nameEnd);
 String value = new String(header.value, 0, header.valueEnd);
parseHeaders方法包含一个循环从SocketInputStream中持续读取header信息直到没有header位置。循环从构造一个HttpHeader实例,并传给SocketInputStream类的readHeader方法开始:
          HttpHeader header = new HttpHeader();
          // Read the next header
          input.readHeader(header);
然后,你可以通过检测HttpHeader实例的nameEnd 和valueEnd属性来检测InputStream中是否还有下一个header:
if (header.nameEnd == 0) {
              if (header.valueEnd == 0) {
                  return;
              }
              else {
                  throw new ServletException
                      (sm.getString("httpProcessor.parseHeaders.colon"));
              }
          }
如果还有下一个header,header name 和value就能被获取到:
String name = new String(header.name, 0, header.nameEnd);
          String value = new String(header.value, 0, header.valueEnd);
一旦得到了header的 name和value,就可以调用addHeader方法把它们加到HttpRequest对象中的header的HashMap中:
request.addHeader(name, value);

有些header还需要设置一些properties。例如,当servlet调用javax.servlet.ServletRequest的getContentLength方法时,content-length header的值将被返回,并且包含cookies的cookie header 将被加到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是被作为一个HTTP request header由浏览器发送的,是名为“cookie”且值是cookie键/值对的header。下面是一个cookie header的例子,它包含两个cookies:userName 和password。
Cookie: userName=budi; password=pwd;
Cookie的解析是由org.apache.catalina.util.RequestUtil类的parseCookieHeader方法完成的。这个方法接收cookie header参数并返回一个javax.servlet.http.Cookie数组。该数组中元素数与header中的键/值对的个数相同。parseCookieHeader方法的代码清单在3.5中给出:

清单 3.5:org.apache.catalina.util.RequestUtil 类的 parseCookieHeader 方法
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方法处理cookies的部分代码:
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]);
              }
          }

获取Parameters
直到servlet需要时才调用javax.servlet.http.HttpServletRequest类的getParameter、getParameterMap、getParameterNames或getParameterValues方法来解析query string 或者HTTP request body获取parameters。因此,HttpRequest中这四个方法的实现总是以调用parseParameter方法开始的。
Parameters仅需要被解析一次,或许也仅能解析一次,因为如果在request body 中发现parameters,parameter解析将引起SocketInputStream到达字节流的末端。HttpRequest类有一个boolean类型的变量,用来标记是否已经做过解析操作。
Parameters可能会包含在query string 和request body中,如果用户请求servlet时用的是GET方法,那么所有的parameters都在query string中;如果用的是POST方法,那么也许在request body中也会发现一些parameters。所有的键/值对都被存储在一个HashMap中。Servlet程序可以获取parameters的Map(通过调用HttpServletRequest的getParameterMap)。但是这是被捕获(catch)的,servlet程序员是不允许改变parameters的值的。因此,一个特殊的HashMap就被用到了:org.apache.catalina.util.ParameterMap。
ParameterMap类继承了java.util.HashMap,它有一个boolean类型的属性叫locked。如果locked为false,键值对可以被add,update或remove。否则,将会抛出IllegalStateException异常。然而读取值,可能不需要花费什么时间。ParameterMap类的代码将在清单3.6中给出。它重写了adding、updating和removing方法。这些方法只能在locked为false时调用。

清单 3.6  org.apache.Catalina.util.ParameterMap 类
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方法是如何工作的。
因为parameters可能存在于query string 或HTTP request body中,parseParameters方法要检测query string 和 request body。一旦解析过,就可以在parameters变量中找到parameters,所以方法的开始要检测boolean类型parsed变量,如果为true就说明已经解析过了。
if (parsed)
          return;
下一步,parseParameter方法创建一个叫results的ParameterMap变量并赋值为parameters。如果results为null,则new一个ParameterMap。
ParameterMap results = parameters;
        if (results == null)
           results = new ParameterMap();
下一步,parseParameter方法打开ParameterMap的锁,使其可写。
results.setLocked(false);
下一步,parseParameter方法检测编码集,并指派一个默认的编码集如果编码为空的话。
String encoding = getCharacterEncoding();
      if (encoding == null)
          encoding = "ISO-8859-1";
下一步,parseParameter方法解析query string中的parameters。解析参数是用org.apache.Catalina.util.RequestUtil的parseParameter方法。
// Parse any parameters specified in the query string
      String queryString = getQueryString();
      try {
          RequestUtil.parseParameters(results, queryString, encoding);
      }
      catch (UnsupportedEncodingException e) {
          ;
      }
下一步,这个方法检查是否HTTP request body 包含parameters。如果用户使用POST方法发送request,并且Content的长度大于0,并且content的类型是application/x-www-form-urlencoded时才做检测。下面是解析request body的代码。
// 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,并把result赋值给parameters。
// Store the final results
      results.setLocked(true);
      parsed = true;
      parameters = results;
创建 HttpResponse 对象
HttpResponse类实现了javax.servlet.http.HttpServletResponse接口。下面是一个叫HttpResponseFacade的façade类。图3.3是HttpResponse及其相关类的UML图。
图3.3:HttpResponse及其相关类
在第二章,我们已经和一个HttpResponse类打过交道了,不过那个是功能不完善的。比如说,它的getWriter 方法返回一个java.io.PrintWriter 对象,当调用print方法时,不是自动flush输出的。这章的应用程序将完善这个问题。为了知道是如何完善的,你首先需要知道Writer是什么。
在servlet内部,使用PrintWriter来输出字符,你可以用任何你想用的编码格式,然而字符都是以字节流的形式发送到浏览器的。因此,第二章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发送出去。
这章中,你可以用ex03.pyrmont.connector.ResponseStream类的实例作为OutputStream传递给PrintWriter。注意ResponseStream类直接继承于java.io.OutputStream 类。
你还可以用ex03.pyrmont.connector.ResponseWriter类,它继承了PrintWriter类。ResponseWriter类重写了print和println方法,并且调用这两个方法都使用自动flush的输出方式到底层的OutputStream。因此,我们使用带有ResponseStream对象的ResponseWriter实例。
我们可以通过传递一个ResponseStream对象实例化ResponseWriter类。然而我们使用的是一个java.io.OutputStreamWriter对象作为ResponseWriter对象和ResponseStream对象之间的桥梁。
有了OutputStreamWriter,被写入的字符被使用一种指定的字符集编码成字节。这个字符集或者是通过名字指定的,或者是显示给出的,或者是平台的默认字符集。每次调用write方法都会对给定字符进行编码转换。作为结果的字节在被写入到底层的OutputStream之前存储在buffer中。buffer的大小也许是被指定的,但是默认值就足够大了。注意,传递给write方法的字符不是buffer。
下面的是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类与第二章中的ex02.pyrmont.ServletProcessor类类似。它们都只有一个方法:process。但是第三章的ex03.pyrmont.connector.ServletProcessor类的process方法接收的参数是HttpRequest和HttpResponse,而不是Request和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.
注:运行第二章的PrimitiveServlet没有第二行结果。
你还可以调用ModernServet,它在第二章的servlet Container中不会运行。URL如下:
http://localhost:8080/servlet/ModernServlet 
注:ModernServet类的源代码在webroot路径下。
你可以在URL后附加一个query string来测试这个servlet。图3.4展示了使用如下URL运行ModernServet的结果:
http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd 

图 3.4:行 ModernServlet
总结
这章,你学会了connector是如何工作的。Connector构建是Tomcat4的默认connector的简化版本。正如你所知道的,默认的connector是不被认同的,因为它是不完善的。比如说,所有的HTTP Request header 都被解析,即使他们在servlet中也许不被使用。这导致这个默认的connector很低效,因此它被更高效的connector Coyote所取代,Coyote代码可以在Apache的网站上下载。但是,这个默认的connector仍然是一个很好的学习工具,我们将在第四章具体讨论。

你可能感兴趣的:(apache,tomcat,servlet,socket,REST)