就像《简介》中介绍的,Catalina 中有两个主要模 块:Connector (连接器)和Container (容器)。本章,你将编写一个连接 器 来增强第2章的应用,该连接器 能够创建更好的Request 和Response 对 象。符合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 实例给servlet 的service 方 法。连接器 并不知道servlet 的类型(例如,是否实现了javax.servlet.Servlet 接 口, 继承了javax.servlet.GenericServlet ,或继承了javax.servlet.http.HttpServlet ), 因此它必须始终提供HttpServletRequest 实例和HttpServletResponse 实 例。
本章的应用程序中,连接器 解析HTTP 请求的headers , 使得servlet 可以获得headers 、cookies 、参数名/值, 等等。我们也会完善第2章中Response 类的getWriter 方法,修正它的行为(译者注:第2 章中的实现是有问题的)。由于这些改进,我们可以从PrimitiveServlet 得 到完整的响应,同时也能够运行更加复杂的ModernServlet 。本章构建的连接器 是Tomcat 4 默认连接器 的一个简化版本,我们会在第4 章详细讨论Tomcat 4 的默认Connecotr 。虽然Tomcat 默 认连接器 在Tomcat 4 中已经不推荐使用了,但是它仍是一个很好的学习工具。在本章接下来的讨论中,凡是提到的连接器 ,都是指本章构建的连接器 ,而不是Tomcat 的默认连接器 。
提示:与上一章的 应用不同,本章应用中连接器 和容器是分离的。
本章应用的代码在ex03.pyrmont 包及其子包中。构成连接器 的类是ex03.pyrmont.connector 包 和ex03.pyrmont.connector.http 包的一部分。从本章开始, 每个应用都有一个bootstrap 类,用于启动整个应用。不过,目前还没有 停止应用的机制。应用一旦运行起来,你必须通过关闭控制台(Windows 平 台)或杀死进程(UNIX/Linux 平台)的粗鲁的方式来停止应用。
在解释应用之前,请允许我先介绍org.apache.catalina.util 包 中的StringManager 类。该类负责处理本应用及Catalina 自身各模块的错误消息的国际化。然后,我们会讨论整个应用。
像Tomcat 这样的大型程序,都需要仔细地处理错误消息。在Tomcat 中, 错误消息对系统管理员和Servlet 程序员都很重要。例如,通过Tomcat 的错误日志,系统管理员可以轻松定位任何异常。Tomcat 为内部抛出的每个javax.servlet.ServletException 打 印出一条特定错误日志,这样Servlet 程序员就可以知道自己写的servlet 哪里出了问题。
Tomcat 采用的方法是将错误消息存储在一个属性(properties )文件中,这样就可以方便地编辑错误消息。但是,Tomcat 有数百个类,如果将所有类的错误消息都存储在一个巨大的属性文件中,那么维护这些错误消息就是一个恶 梦。为了避免这个问题,Tomcat 为每个包定义了一个属性文件。例如,org.apache.catalina.connector 包中的属性文件包括了该包所有类抛 出的错误消息。每个属性文件都会被一个特定的org.apache.catalina.util.StringManager 实 例处理。Tomcat 运行的时侯,会有很多StringManager 实例,每个实例都会读取对应包中的属性文件。而且,由于Tomcat 十分流行,提供多语言版本的错误消息是很有意义的。目前,Tomcat 共支持三种语言。英语的属性文件名都是LocalStrings.properties 。其他两种语言是西班牙语和日语,其属性文件分别 为LocalStrings_es.properties 和LocalStrings_ja.properties 。
当类需要在属性文件中查找错误消息时,它首先获取一个StringManager 实 例。但是,同一个包中很多类都可能需要一个StringManager 实例,如果为每 个需要错误消息的类对象创建一个StringManager 实例,则是对资源的浪费。 因此StringManager 类被设计成,同一个包中所有类对象可以共享一个StringManager 实例。如果熟悉设计模式,你可能会猜到StringManager 是一个单例类(singleton class )。StringManager 类 唯一的构造函数是私有的(private ),因此你不能使用new 关键字在类外部创建该类的实例。以包名为参数,调用StringManager 类的公开静态方法getManager , 就可以获得一个StringManager 实例。每个实例被存储在一个Hashtable 中,key 就是包的名称。
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; }
提示:在附带的zip 文件中,可以找到一篇题为“The Singleton Pattern ”、关于单例模式的文章。
举个例子,为了使用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 实例根据应用程序运行时所在机器的区域(local ) 来决定使用哪个文件。如果你打开LocalStrings.properties ,非 注释的第一行应该是这样的:
httpConnector.alreadyInitialized=HTTP connector has already been initialized
要得到一条错误消息,你需要以错误码(error code )为参数调用StringManager 类的getString 方 法。下面是该方法的多个重载之一:
public String getString(String key)
以“httpConnector.alreadyInitialized ” 为参数调用getString 方法,就会返回“HTTP connector has already been initialized ”。
从本章开始,每章附带的应用程序被化分成模块。本章的应用包括三个模块:connector 、startup 和core 。
startup 模块只包括一个类:Bootstrap , 其作用是启动整个应用。connector 模块的类可以分成5个类别:
core 模块包括两个类:ServletProcessor 和StaticResourceProcessor 。
Figure 3.1 是本应用的类图。为了让类图更具可读性,HttpRequest 和HttpResponse 相关的类都被省略了。我们后面讨论Request 和Response 对 象时,会给出更加详细的类图。
我们把Figure 3.1 和Figure 2.1 做个比较。第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 、query string 、参数、cookies 和 其他headers 等等。因为连接器 不知道servlet 需要哪些值,所以它 必须解析所有能够从HTTP 请 求获得的值。但是,解析HTTP 请求会带来昂贵(开销巨大)的字符串操作和其 他操作。如果只解析servlet 需要的值,那么就可能节省大量的CPU 周期。例如,如果servlet 不 需要任何请求(也就是,不调用javax.servlet.http.HttpServletRequest 的getParameter 、getParameterMap 、 getParameterNames或getParameterValues 方法),连接器 就不需要从query string 或HTTP request body 中解析出 请求参数。Tomcat 的默认连接器 (包括本章应用中的连接器 )尝试通过“直 到真正需要时才解析请求参数”的方式来提高效率。Tomcat 的默认连接器 和我们的连接器 使 用SocketInputStream 类从Socket 的InputStream 中读取字节流。SocketInputStream 实例包装了Socket 的getInputStream 返回的java.io.InputStream 实例。SocketInputStream 类提供了两个重要方法:readRequestLine 和readHeader 。readRequestLine 返 回HTTP 请求的第一行,即包括URI 、HTTP 方法(method )和HTTP 版 本的那一行。处理套接字输入流中的字节流就意味着,从第一个字节读取到最后一个字节(从不回退),因此readRequestLine 必 须只能被调用一次,而且必须在readHeader 方法之前调用。每调用一次readHeader 就可以读取一个header 名 /值对,而且应该重复调用直到所有的headers 都被读取。readRequestLine 的返回值是一个HttpRequestLine 实 例,readHeader 的返回值是一个HttpHeader 对象。我们将在下面讨论HttpRequestLine 和HttpHeader 。
HttpProcessor 对象负责创建HttpRequest 实 例,因此必须填充HttpRequest 实例的每个成员变量。HttpProcess 类使用它的parse 方 法来解析HTTP 请求的request line 和headers 。parse 方法的返回被赋值给HttpProcessor 对 象的成员变量。但是,parse 方法并不解析query string 和request body 中 的请求参数。这个任务留给了HttpRequest 对象自己(译者注:这就是延迟解 析)。只有servlet 需要一个参数时,query stirng 或request body 才会被解析。
在前一章基础上的另一个改进,就是引入了启动类ex03.pyrmont.startup.Bootstrap 来启动整个应用。
我们将在下面这些小节中,详细解释本章的应用:
我们从ex03.pyrmont.startup.Bootstrap 类启动整个应用。Listing 3.1 列出了该类的代码。
Listing 3.1: The Bootstrap class
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 方法。Listing 3.2 列出了HttpConnector 类的代码。
Listing 3.2: The HttpConnector class's start method
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 循 环,用来做下面的事情:
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 (); } }
SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(), 2048); output = socket.getOutputStream(); // Then, it creates an 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);
response.setHeader("Server", "Pyrmont Servlet Container");
parseRequest(input, output); parseHeaders (input);
if (request.getRequestURI().startsWith("/servlet/")) { ServletProcessor processor = new ServletProcessor(); processor.process(request, response); } else { StaticResourceProcessor processor = new StaticResourceProcessor(); processor.process(request, response); }
socket.close();
protected StringManager sm = StringManager.getManager("ex03.pyrmont.connector.http");
protected HashMap headers = new HashMap(); protected ArrayList cookies = new ArrayList(); protected ParameterMap parameters = null;
在第1 、2 章中,ex01.pyrmont.HttpRequest 类和ex02.pyrmont.HttpRequest 类已经做了一部分解析HTTP 请求的工作。通过调用java.io.InputStream 类 的read 方法,我们可以从请求行获得HTTP 方法、URI 和HTTP 版本:
byte[] buffer = new byte [2048]; try { // input is the InputStream from the socket. i = input.read(buffer); }
第1、2章的应用中,我们没有尝试进一步解析HTTP请求。但是在本章的应用中,我们有了 ex03.pyrmont.connector.http.SocketInputStream 类——org.apache.catalina.connector.http.SocketInputStream 类 的一个拷贝。该类提供了一些方法,这些方法不但可以获得请求行,还可以获得headers 。
要构造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 和可选的query string 。在上面的例子中,URI是:
/myApp/ModernServlet
然后,问号之后的部分都是query stirng 。因此,query string 就是:
userName=tarzan&password=pwd
query string 可 以包含0 或多个参数。在上面的例子中,有两个参数名/值对:username/tarzan 和password/pwd 。在Servlet/JSP 编 程中,jsessionid 参数用来携带会话标识(session identity)。会话标识通常嵌入在cookies 中,但是程序员可以选 择将会话标识嵌入在query string 中,例如在浏览器禁止cookie 的情况下。
当parseRequest 方法被HttpProcessor 类 的process 方法调用时,变量request 已 经指向了一个HttpRequest 实例。parseRequest 方法解析了请求行,获得了几个值,并将它们赋给HttpRequest 对 象。现在,我们来看看Listing 3.4 中parseRequest 方法的代码。
Listing 3.4: The parseRequest method in the HttpProcessor class
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); }
如果找到jessionid ,也意味着会话标识由query string 来承载,而不在cookie 中。 因此,传递true 给request 对 象的setRequestSessionURL 方法。否则,传递false 给setRequestSessionURL 方 法,传递null 给setRequestSessionId 方 法。
这时,uri 的值已经不包含jsessionid 。接着,parseRequest 方 法传递uri 给normalize 方 法,以纠正“异常(abnormal )”的URI。例如,任何\将被替换成 /。如果uri 的格式是正确的,或者异常已被纠正,normalize 方法就返回原来的uri , 或者被纠正的URI 。如果uri 不 能被纠正,normalize 方法会认为uri 不合法,并返回null 。在这种情况下(normalize 方法返回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 ,parseRequest 方法就抛出一个异常:
if (normalizedUri == null) { throw new ServletException("Invalid URI: " + uri + "'"); }
HttpHeader 类描述了HTTP 头部。第4 章 将详细解释该类,现在我们只要知道下面几点就足够了:
String name = new String(header.name, 0, header.nameEnd); String value = new String(header.value, 0, header.valueEnd);
HttpHeader header = new HttpHeader(); // Read the next header input.readHeader(header);
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);
request.addHeader(name, value);
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: userName=budi; password=pwd;
public static Cookie[] parseCookieHeader(String header) { if ((header == null) || (header.length() < 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 ()])); }
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]); } }