??? 就像《简介》中介绍的,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
类
{wy}的构造函数是私有的(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
,非
注释的{dy}行应该是这样的:
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个类别:
- connector 和它的支持(supporting )类(HttpConnector 和HttpProcessor )
- 代表HTTP 请求的类(HttpRequest )及 其支持类
- 代表HTTP 响应的类(HttpResponse )及其支持类
- 门面(Facade )类(HttpRequestFacade 和HttpResponseFacade )
- Constant 类
?
??? 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
请求的{dy}行,即包括URI
、HTTP
方法(method
)和HTTP
版
本的那一行。处理套接字输入流中的字节流就意味着,从{dy}个字节读取到{zh1}一个字节(从不回退),因此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
来启动整个应用。
???
我们将在下面这些小节中,详细解释本章的应用:
- 启动应用
- 连接器
- 创建HttpRequest 对象
- 创建HttpResponse 对象
- 静态资源处理器和serlvet 处 理器
- 运行应用
启动应用
?? 我们从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
循
环,用来做下面的事情:
- 等待HTTP 请求
- 为 每个请求创建HttpProcessor 实例
- 调用HttpProcessor 的process 方 法
?
??? 马上你就能看到,HttpConnector 类和ex02.pyrmont.HttpServer1 类非常相似。从java.net.ServerSocket 类的accept 方法获得一个socket 之后发生了变 化,HttpConnector 类创建了一个HttpProcessor 实例,并以socket 为 参数调用其process 方法。
??? 提示: HttpConnector 类拥有另一个名为 getSchema 的方法,该方法返回网络请求的 schema ( HTTP )。
??? HttpProcessor 类 的process 方法接受HTTP 请 求的socket 为参数。对于每个HTTP 请求,process 方法会做如下处 理:
- 创建一个HttpRequest
对象
- 创 建一个HttpResponse 对象
- 解析HTTP 请求的{dy}行和headers , 并填充HttpRequest 对象
- 传递HttpRequest 对象和HttpResponse 对 象给ServletProcessor 或StaticResourceProcessor
? ? 就像第2 章里那样,ServletProcessor 调 用了被请求的servlet 的service 方法,StaticResourceProcessor 发 送静态资源的内容(给客户端)。
??? Listing 3.3 列 出了process 方法的代码。
Listing 3.3: The HttpProcessor class's process method.? ?
?
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 的输入流和输出流。注意,该方法使用的SocketInputStream 继 承自java.io.InputStream 。
???? 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);
??? 本章应用的HttpResponse 类比第2 章 的Response 类要复杂很多。举例来说,你可以通过调用HttpResponse 类的setHeader 方 法向客户端发送headers 。
?? ?? response.setHeader("Server", "Pyrmont Servlet Container");
??? 接下来,process 方 法调用HttpProcessor 类的两个私有方法来解析请求。
????? parseRequest(input, output);
????? parseHeaders (input);
??? 然后,process方法根据请求URI的模式(pattern),将HttpRequest对象和HttpResponse对像甩给(hand off ... to)一个 ServletProcessor对象和一个 StaticResourceProcessor对象。
?????? if (request.getRequestURI().startsWith("/servlet/")) {
???????? ServletProcessor processor = new ServletProcessor();
???????? processor.process(request, response);
?????? }
?????? else {
???????? StaticResourceProcessor processor =
??????????? new StaticResourceProcessor();
???????? processor.process(request, response);
?????? }
??? {zh1},process 方法关闭socket 。
????? 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 的门面类。Figure 3.2 展现了HttpRequest 和 相关类的类图。??? HttpRequest 类 的许多方法都是留空的(等待第4章才会全部实现),但是servlet 程序员 已经可以从HTTP 请求中获得headers 、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类中看到的,一旦获得headers 、cookies 和 请求参数,相关方法的实现就很简单了。
??? 不用说,这里主要的挑战就是解析HTTP请求和填充HttpRequest 对象。对于headers 和cookies ,HttpRequest 类 提供了addHeader 和addCookie 方 法,HttpProcessor 类的prseHeaders 就调用了这两个方法。 请求参数是在需要时才被HttpRequest 类 的parseParameters 方法解析的。本节所有的方法都会被讨论到。
??? 由于解析HTTP 请求是一个非常复杂的任务,因此本节被分成下面几个小节:
- 读 取套接字 的输入流
- 解析请求行(request line )
- 解析headers
- 解析cookies
- 获
取请求参数
读取套接字的输入流
??? 在第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
请求的{dy}行。这里给出请求行的一个例子:
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 = "";