一、概述

Tomcat或者称之为Catalina(开发名称),可以简化为两个主要的模块,如下图:

Chapter 3: Connector(连接器)-LMLPHP

多个Connector关联一个Container。之所以需要多个Connector,是为了处理多种协议,如HTTP(细分为1.1版本和1.0版本),HTTPS,AJP等。后面会学习到,Container也是包含多层级的。

要满足Servlet2.3和Servlet2.4的规范,connector必须创建实现HttpServletRequest和HttpServletReponse接口的实例,用于传递到处理该请求的servlet的service方法中。

这一章介绍的connector是Tomcat4中默认的Connector的简化版本。虽然Tomcat4默认的Connector已经不推荐使用了,推荐使用一个称为Coyote的Connector,但是,是一个很好的学习资料。本章,"connector"代指我们程序中的模块。

这一章的包结构如下:

Chapter 3: Connector(连接器)-LMLPHP

connector包及其子包是重点,就是该章要介绍的connector模块 ,startup包是启动应用程序的类的集合,剩下的类ServletProcessor和StaticResourceProcessor类归为core,也就是核心处理响应的类。

在我们开始介绍这一章的示例程序之前,我们先来看一下org.apache.catalina.util包下面的StringManager这个类。这个类用于处理Tocmat的错误消息的国际化。

二、StringManager类

一个大型的应用程序,比如说tomcat,必须仔细处理好错误消息。对于Tomcat,错误消息对于系统管理员,还是servlet开发人员来说,都是很有价值的。举例来说,Tomcat的错误日志消息可以帮助系统管理员快速定位到异常出现的地方。对于servlet程序员,对于每一个抛出的javax.servlet.ServletException包含一个特定的错误消息,他/她能判断该servlet出了什么样的问题。

Tomcat中用于存储错误消息的方法是使用properties文件,这样便于编辑。但是,Tomcat有数百个类组成,如果把所有的类的出错消息都放在一个配置文件中,将难以维护。为了避免这样的问题,Tomcat采用的方法是对于每一个包分配一个配置文件。举例来说,org.apache.catalina.connector这个包下面的配置文件包含所有这个包下面的类可能会用到的错误信息。当Tomcat运行的时候,可能会有多个StringManager的示例在运行,每一个都和特定的包名相关联。并且,随着Tomcat的流行,提供多种语言的错误信息是有意义的。Tomcat4.1.12支持三种语言。对于英语,属性文件名称为LocalStrings.properties,对于西班牙语,文件名称为LocalStrings_es.properties,对于日语,名称为LocalStrings_ja.properties。

当一个类需要在这个包下面查找一条错误信息的时候,它会首先创建一个StringManager的实例。但是,对于同一个包下面的所有类,在需要查找错误消息的时候,都创建StringManager的实例,是一种资源的浪费。所以,StringManager被设计为如下:所有同一个包下面的类共享一个StringManager实例。如果熟悉设计模式的话,就是说StringManager是一个单例:构造方法私有使得不能从类的外面创建新的实例。通过传入一个包名,调用静态的getManager方法来获得StringManager实例,对应于多个包名的多个实例存储在一个Hashtable的数据结构中。

私有构造方法:

    private StringManager(String packageName) {
String bundleName = packageName + ".LocalStrings";
bundle = ResourceBundle.getBundle(bundleName);
}

每一个StringManager实例关联一个bundle对象,用于从配置文件中读取对应的消息:

private ResourceBundle bundle;

通过调用getManager方法,获取该类所在的包对应的StringManager实例:

    /**
* Get the StringManager for a particular package. If a manager for
* a package already exists, it will be reused, else a new
* StringManager will be created and returned.
*
* @param packageName
*/ 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;
}

通过调用getString方法,传入该条消息对应的key,来读取该条消息对应的value:

为了处理各种各样的情况,重载了很多getString方法,这里看个最简单的:

    /**
* Get a string from the underlying resource bundle.
*
* @param key
*/ public String getString(String key) {
if (key == null) {
String msg = "key is null"; throw new NullPointerException(msg);
} String str = null; try {
str = bundle.getString(key);
} catch (MissingResourceException mre) {
str = "Cannot find message associated with key '" + key + "'";
} return str;
}

举例来说,一个类A在包ex03.pyrmont.connector.http下面要使用StringManager来获取key为"httpConnector.alreadyInitialized"对应的错误消息,并且假设ex03.pyrmont.connector.http包下面要一个属性文件名为LocalStrings.properties,里面内容如下:

httpConnector.alreadyInitialized=HTTP connector has already been initialized

可以这样做,

StringManager sm = StringManager.getManager("ex03.pyrmont.connector.http");

getString("httpConnector.alreadyInitialized");

三、模拟Tomcat4.1.12的程序

从这一章起,程序会分为3个部分,connector,core,startup。

startup部分包含一个类:Bootstrap

connector部分包含的类可以划分为以下5类:

  • connector类本身HttpConnector以及它的支持类(HttpProcessor)
  • 代表Http请求的类HttpRequest以及它的支持类(HttpRequestFacade,RequestStream,HttpRequestLine,HttpHeader)
  • 代表Http响应的类HttpResponse以及它的支持类(HttpResponseFacade,ResponseStream,ResponseWriter)
  • Facade类,可以单独划分为一类或者也可以认为它是支持类
  • Constant类

core部分包含两个类:StaticResourceProcessor and ServletProcessor

该程序的类图如下所示,为了简化便于阅读,和HttpRequest以及HttpResponse相关的类被忽略。关于Request和Reponse的详细类图在介绍Request和Reponse部分可见。

Chapter 3: Connector(连接器)-LMLPHP

从pdf上截取的图不是很清晰,但是结合代码来看,程序的入口HttpConnector等待请求的到来,HttpConnector和HttpProcessor关联,HttpProcessor创建(create)HttpRequest对象和HttpResponse对象。HttpProcessor在process请求的过程中,还使用(uses)到StringManager/SocketInputStream/HttpHeader/HttpRequestLine类。HttpProcessor根据请求的类型会分别调用ServletProcessor或者StaticResourceProcessor。ServletProcessor和StaticResourceProcessor使用(users)到HttpConnector创建的HttpRequest和HttpResponse对象。

和第二章的UML类图比较起来,第二章的HttpServer类被拆分为两个类:HttpConnector,HttpProcessor,功能分别为等待请求以及处理请求。Request类被替换为HttpRequest,Response类被替换为HttpResponse。当然,在这一章中使用到了更多的类。

在这一章中,用HttpRequest代表Http请求对象,它实现了javax.servlet.HttpServletRequest接口。HttpRequest中封装的数据包括了请求URI,查询字符串,请求参数,cookies,以及其他的请求头信息。因为connector并不知道哪些值会被要调用的servlet(the invoked servlet)使用到,connector要从http请求中解析得到所有这些值。但是,解析http请求包含了大量的字符串操作等,如果只解析会被请求servlet使用到的值(比如请求头)而不解析哪些用不到的值(比如请求参数)则会节省大量的cpu周期。举例来说,如果servlet不需要知道请求参数的值(i.e. it does not call the getParameter, getParameterMap, getParameterNames, or getParameterValues methods of javax.servlet.http.HttpServletRequest),则connector就不需要从query string或者request body中解析参数。Tomcat4默认的Connector(虽然已经不推荐使用了)和本章模拟的connector组件都是在servlet需要parameter值时才会从查询字符串或者请求实体中解析得到参数值。(^_^可以分析下代码,看看是如何实现的)。

我们使用SocketInputStream读取socket输入的字节流。SocketInputStream实例wrap了java.io.InputSteam(从socket中获取)并提供了两个重要的方法:readRequestLine和readHeader。readRequestLine returns the first line in an HTTP request, i.e. the line containing the URI, method and HTTP version. Because processing byte stream from the socket's input stream means reading from the first byte to the last (and never moves backwards), readRequestLine must be called only once and must be called before readHeader is called. readHeader is called to obtain a header name/value pair each time it is called and should be called repeatedly until all headers are read. The return value of readRequestLine is an instance of HttpRequestLine and the return value of readHeader is an HttpHeader object. We will discuss the HttpRequestLine and HttpHeader classes in the sections to come.

接下来,HttpProcessor创建HttpRequest和HttpResponse对象并填充字段,通过调用parse方法,解析请求行和请求头。但是,parse方法并不解析在request body and query string中的参数。这个任务留给HttpRequest对象本身,只有在servlet需要一个参数值时request body or query string才会被解析。

我们将分为以下小节来讨论具体的代码。

  • Starting the Application
  • The Connector
  • Creating an HttpRequest Object
  • Creating an HttpResponse Object
  • Static resource processor and servlet processor
  • Running the Application

1、如何启动

通过ex03.pyrmont.startup.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();
}
}

启动了HttpConnector线程。

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();
}
}

2、Connector

从上面的代码可以看到,HttpConnector实现了Runnable接口,所以可以启动为单独的一个线程。run方法中while循环做了以下的事情:等待Http请求/对于每个请求创建HttpProcessor实例/调用HttpProcessor的process方法。

HttpProcessor的process方法得到代表请求的socket对象后,做了以下的事情:创建HttpRequest/创建HttpResponse/解析请求行和请求头并填充HttpRequest对象(这一步比较复杂,下面会分析到)/根据请求资源类型调用ServletProcessor或者StaticResourceProcessor的process方法,关闭socket。

代码如下:

  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();
}
}

3、创建HttpRequest对象

首先HttpRequest的详细UML类图如下:

Chapter 3: Connector(连接器)-LMLPHP

从图中可以看出,HttpRequestFacade和HttpRequest都实现了javax.servlet.http.HttpServletRequest接口,所以根据servlet2.3规范,可以作为参数之一传入要调用的servlet的service方法。另外,HttpRequest组合(Composite)了RequestStream。这个很容易理解,只有得到了socket的输入流,才能得到uri/method/parameters/headers/cookies这些数据,才能填充HttpRequest对象。这些数据分别保存在以下的引用变量中:

protected HashMap headers = new HashMap();

protected ArrayList cookies = new ArrayList();

protected ParameterMap parameters = null;

所以,一个servlet程序员才能从javax.servlet.http.HttpServletRequest中通过调用getCookeis/getHeader/getHeaderNames/getParameter/getParameterMap/getParameterNames这些方法得到正确的值。(^_^这个解释的好,以前写servlet时,在doGet方法中调用request的对象的方法得到参数值时,并没有主要到这个request的类型是什么,现在想来应该是HttpRequestFacade这个类。已通过request.getClass().getName()验证)。

不用说,这里的主要挑战是解析http请求,并填充HttpRequest对象。For headers and cookies, the HttpRequest class provides the addHeader and addCookie methods that are called from the parseHeaders method of HttpProcessor. Parameters are parsed when they are needed, using the HttpRequest class's parseParameters method. All methods are discussed in this section.

因为解析http请求是一个比较复杂的工作,这小节分为以下子小节:

  • Reading the socket's input stream
  • Parsing the request line
  • Parsing headers
  • Parsing cookies
  • Obtaining Parameters

  1、读取socket输入流

在chapter 1以及chapter 2我们做了一点request的解析来获取请求行信息,是通过直接调用java.io.InputStream的read方法,如下:

byte[] buffer = new byte [2048]; try { // input is the InputStream from the socket. i = input.read(buffer); }

在前2章节,并没有更深地解析request。这一章,however, you have the ex03.pyrmont.connector.http.SocketInputStream class, a copy of org.apache.catalina.connector.http.SocketInputStream. This class provides methods for obtaining not only the request line, but also the request headers.我们通过下面的代码片段来构建SocketInputStream。

SocketInputStream input = null; OutputStream output = null; try { input = new SocketInputStream(socket.getInputStream(), 2048); ...

我们要使用SocketInputStream类,主要是因为这两个方法,public void readRequestLine(HttpRequestLine requestLine)和public void readHeader(HttpHeader header),这两个方法做的事情很简单,但是实现起来挺复杂的啊。Read On.

  2、解析请求行

下面是一个请求行的示例:

GET /myApp/ModernServlet?userName=tarzan&password=pwd HTTP/1.1

以空格分为3部分,第二部分就是URI加上可选的查询字符串(?后面的),如上例,

URI:             /myApp/ModernServlet

query string:userName=tarzan&password=pwd

以下为SocketInputstream类中解析请求行的方法:

    /**
* Read the request line, and copies it to the given buffer. This
* function is meant to be used during the HTTP request header parsing.
* Do NOT attempt to read the request body using it.
*
* @param requestLine Request line object
* @throws IOException If an exception occurs during the underlying socket
* read operations, or if the given buffer is not big enough to accomodate
* the whole line.
*/
public void readRequestLine(HttpRequestLine requestLine)
throws IOException { // Recycling check
if (requestLine.methodEnd != 0)
requestLine.recycle(); // Checking for a blank line
int chr = 0;
do { // Skipping CR or LF
try {
chr = read();
} catch (IOException e) {
chr = -1;
}
} while ((chr == CR) || (chr == LF));
if (chr == -1)
throw new EOFException
(sm.getString("requestStream.readline.error"));
pos--; // Reading the method name int maxRead = requestLine.method.length;
int readStart = pos;
int readCount = 0; boolean space = false; while (!space) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpRequestLine.MAX_METHOD_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(requestLine.method, 0, newBuffer, 0,
maxRead);
requestLine.method = newBuffer;
maxRead = requestLine.method.length;
} else {
throw new IOException
(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
int val = read();
if (val == -1) {
throw new IOException
(sm.getString("requestStream.readline.error"));
}
pos = 0;
readStart = 0;
}
if (buf[pos] == SP) {
space = true;
}
requestLine.method[readCount] = (char) buf[pos];
readCount++;
pos++;
} requestLine.methodEnd = readCount - 1; // Reading URI maxRead = requestLine.uri.length;
readStart = pos;
readCount = 0; space = false; boolean eol = false; while (!space) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpRequestLine.MAX_URI_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(requestLine.uri, 0, newBuffer, 0,
maxRead);
requestLine.uri = newBuffer;
maxRead = requestLine.uri.length;
} else {
throw new IOException
(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
int val = read();
if (val == -1)
throw new IOException
(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if (buf[pos] == SP) {
space = true;
} else if ((buf[pos] == CR) || (buf[pos] == LF)) {
// HTTP/0.9 style request
eol = true;
space = true;
}
requestLine.uri[readCount] = (char) buf[pos];
readCount++;
pos++;
} requestLine.uriEnd = readCount - 1; // Reading protocol maxRead = requestLine.protocol.length;
readStart = pos;
readCount = 0; while (!eol) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpRequestLine.MAX_PROTOCOL_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(requestLine.protocol, 0, newBuffer, 0,
maxRead);
requestLine.protocol = newBuffer;
maxRead = requestLine.protocol.length;
} else {
throw new IOException
(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
// Copying part (or all) of the internal buffer to the line
// buffer
int val = read();
if (val == -1)
throw new IOException
(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if (buf[pos] == CR) {
// Skip CR.
} else if (buf[pos] == LF) {
eol = true;
} else {
requestLine.protocol[readCount] = (char) buf[pos];
readCount++;
}
pos++;
} requestLine.protocolEnd = readCount; }

  3、解析请求头

请求头用类HttpHeader表示,可以使用无参构造器构造HttpHeader,调用SocketInputStream的readHeader(httpHeader实例),

如果还有请求头需要读取,readHeader方法会填充httpHeader实例,如果没有,httpHeader对象的nameEnd和valueEnd字段设值为0。

获取请求头的名字和值,可以通过下面的代码:

String name = new String(header.name, 0, header.nameEnd);

String value = new String(header.value, 0, header.valueEnd);

    /**
* Read a header, and copies it to the given buffer. This
* function is meant to be used during the HTTP request header parsing.
* Do NOT attempt to read the request body using it.
*
* @param requestLine Request line object
* @throws IOException If an exception occurs during the underlying socket
* read operations, or if the given buffer is not big enough to accomodate
* the whole line.
*/
public void readHeader(HttpHeader header)
throws IOException { // Recycling check
if (header.nameEnd != 0)
header.recycle(); // Checking for a blank line
int chr = read();
if ((chr == CR) || (chr == LF)) { // Skipping CR
if (chr == CR)
read(); // Skipping LF
header.nameEnd = 0;
header.valueEnd = 0;
return;
} else {
pos--;
} // Reading the header name int maxRead = header.name.length;
int readStart = pos;
int readCount = 0; boolean colon = false; while (!colon) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpHeader.MAX_NAME_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(header.name, 0, newBuffer, 0, maxRead);
header.name = newBuffer;
maxRead = header.name.length;
} else {
throw new IOException
(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
int val = read();
if (val == -1) {
throw new IOException
(sm.getString("requestStream.readline.error"));
}
pos = 0;
readStart = 0;
}
if (buf[pos] == COLON) {
colon = true;
}
char val = (char) buf[pos];
if ((val >= 'A') && (val <= 'Z')) {
val = (char) (val - LC_OFFSET);
}
header.name[readCount] = val;
readCount++;
pos++;
} header.nameEnd = readCount - 1; // Reading the header value (which can be spanned over multiple lines) maxRead = header.value.length;
readStart = pos;
readCount = 0; int crPos = -2; boolean eol = false;
boolean validLine = true; while (validLine) { boolean space = true; // Skipping spaces
// Note : Only leading white spaces are removed. Trailing white
// spaces are not.
while (space) {
// We're at the end of the internal buffer
if (pos >= count) {
// Copying part (or all) of the internal buffer to the line
// buffer
int val = read();
if (val == -1)
throw new IOException
(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if ((buf[pos] == SP) || (buf[pos] == HT)) {
pos++;
} else {
space = false;
}
} while (!eol) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpHeader.MAX_VALUE_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(header.value, 0, newBuffer, 0,
maxRead);
header.value = newBuffer;
maxRead = header.value.length;
} else {
throw new IOException
(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
// Copying part (or all) of the internal buffer to the line
// buffer
int val = read();
if (val == -1)
throw new IOException
(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if (buf[pos] == CR) {
} else if (buf[pos] == LF) {
eol = true;
} else {
// FIXME : Check if binary conversion is working fine
int ch = buf[pos] & 0xff;
header.value[readCount] = (char) ch;
readCount++;
}
pos++;
} int nextChr = read(); if ((nextChr != SP) && (nextChr != HT)) {
pos--;
validLine = false;
} else {
eol = false;
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpHeader.MAX_VALUE_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(header.value, 0, newBuffer, 0,
maxRead);
header.value = newBuffer;
maxRead = header.value.length;
} else {
throw new IOException
(sm.getString("requestStream.readline.toolong"));
}
}
header.value[readCount] = ' ';
readCount++;
} } header.valueEnd = readCount; }

  4、解析cookies信息

Cookie: userName=budi; password=pwd;

这是一个cookie请求头的例子,包含两个cookie,userName 和 password

            } else if (header.equals(DefaultHeaders.COOKIE_NAME)) {
Cookie cookies[] = c(value);

通过RequestUtil.parseCookieHeader解析,传入参数为cookie请求头的值。

    /**
* Parse a cookie header into an array of cookies according to RFC 2109.
*
* @param header Value of an HTTP "Cookie" header
*/
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()])); }

  5、获取参数值(在需要的时候)

在javax.servlet.HttpServletRequest调用getParameter/getParameterMap/getParameterNames/getParameterValues/这些方法之前,我们不需要解析查询字符串和请求体来获取请求参数。

请求参数只需要解析一次并且只能被解析一次,因为如果在请求体中发现了请求参数,解析参数会导致SocketInputStream到达输入流的末尾。HttpRequest中有一个字段名为parsed来指示参数解析是否已经完成。

请求参数可能在查询字符串或者是请求体中进行传递。如果是GET请求,那么请求参数只能在查询字符串中传递。如果是POST请求,那么不仅仅是查询字符串,也可能在请求体中传递。所有的name/value都存储在一个HashMap对象中。Servlet程序员能够获取到请求参数(by calling getParameterMap of HttpServletRequest),但是,不能改变请求参数的值,所以,使用了org.apache.catalina.util.ParameterMap这个特殊的HashMap。

/**
* Extended implementation of <strong>HashMap</strong> that includes a
* <code>locked</code> property. This class can be used to safely expose
* Catalina internal parameter map objects to user classes without having
* to clone them in order to avoid modifications. When first created, a
* <code>ParmaeterMap</code> instance is not locked.
*
* @author Craig R. McClanahan
* @version $Revision: 1.2 $ $Date: 2001/07/22 20:25:13 $
*/ public final class ParameterMap extends HashMap { // ----------------------------------------------------------- Constructors /**
* Construct a new, empty map with the default initial capacity and
* load factor.
*/
public ParameterMap() { super(); } /**
* Construct a new, empty map with the specified initial capacity and
* default load factor.
*
* @param initialCapacity The initial capacity of this map
*/
public ParameterMap(int initialCapacity) { super(initialCapacity); } /**
* Construct a new, empty map with the specified initial capacity and
* load factor.
*
* @param initialCapacity The initial capacity of this map
* @param loadFactor The load factor of this map
*/
public ParameterMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); } /**
* Construct a new map with the same mappings as the given map.
*
* @param map Map whose contents are dupliated in the new map
*/
public ParameterMap(Map map) { super(map); } // ------------------------------------------------------------- Properties /**
* The current lock state of this parameter map.
*/
private boolean locked = false; /**
* Return the locked state of this parameter map.
*/
public boolean isLocked() { return (this.locked); } /**
* Set the locked state of this parameter map.
*
* @param locked The new locked state
*/
public void setLocked(boolean locked) { this.locked = locked; } /**
* The string manager for this package.
*/
private static final StringManager sm =
StringManager.getManager("org.apache.catalina.util"); // --------------------------------------------------------- Public Methods /**
* Remove all mappings from this map.
*
* @exception IllegalStateException if this map is currently locked
*/
public void clear() { if (locked)
throw new IllegalStateException
(sm.getString("parameterMap.locked"));
super.clear(); } /**
* Associate the specified value with the specified key in this map. If
* the map previously contained a mapping for this key, the old value is
* replaced.
*
* @param key Key with which the specified value is to be associated
* @param value Value to be associated with the specified key
*
* @return The previous value associated with the specified key, or
* <code>null</code> if there was no mapping for key
*
* @exception IllegalStateException if this map is currently locked
*/
public Object put(Object key, Object value) { if (locked)
throw new IllegalStateException
(sm.getString("parameterMap.locked"));
return (super.put(key, value)); } /**
* Copy all of the mappings from the specified map to this one. These
* mappings replace any mappings that this map had for any of the keys
* currently in the specified Map.
*
* @param map Mappings to be stored into this map
*
* @exception IllegalStateException if this map is currently locked
*/
public void putAll(Map map) { if (locked)
throw new IllegalStateException
(sm.getString("parameterMap.locked"));
super.putAll(map); } /**
* Remove the mapping for this key from the map if present.
*
* @param key Key whose mapping is to be removed from the map
*
* @return The previous value associated with the specified key, or
* <code>null</code> if there was no mapping for that key
*
* @exception IllegalStateException if this map is currently locked
*/
public Object remove(Object key) { if (locked)
throw new IllegalStateException
(sm.getString("parameterMap.locked"));
return (super.remove(key)); } }

ParameterMap重写了add/update/remove这类方法,这些方法只有在locked变量为false时才可以被调用。

HttpRequestBase的parseParameters方法代码:

    /**
* Parse the parameters of this request, if it has not already occurred.
* If parameters are present in both the query string and the request
* content, they are merged.
*/
protected void parseParameters() { if (parsed)
return; ParameterMap results = parameters;
if (results == null)
results = new ParameterMap();
results.setLocked(false); String encoding = getCharacterEncoding();
if (encoding == null)
encoding = "ISO-8859-1"; // Parse any parameters specified in the query string
String queryString = getQueryString();
try {
RequestUtil.parseParameters(results, queryString, encoding);
} catch (UnsupportedEncodingException e) {
;
} // 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)
&& (this.stream == null)
&& "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) {
// FIX ME, mod_jk when sending an HTTP POST will sometimes
// have an actual content length received < content length.
// Checking for a read of -1 above prevents this code from
// going into an infinite loop. But the bug must be in mod_jk.
// Log additional data when this occurs to help debug mod_jk
StringBuffer msg = new StringBuffer();
msg.append("HttpRequestBase.parseParameters content length mismatch\n");
msg.append(" URL: ");
msg.append(getRequestURL());
msg.append(" Content Length: ");
msg.append(max);
msg.append(" Read: ");
msg.append(len);
msg.append("\n Bytes Read: ");
if ( len > 0 ) {
msg.append(new String(buf,0,len));
}
log(msg.toString());
throw new RuntimeException
(sm.getString("httpRequestBase.contentLengthMismatch"));
}
RequestUtil.parseParameters(results, buf, encoding);
} catch (UnsupportedEncodingException ue) {
;
} catch (IOException e) {
throw new RuntimeException
(sm.getString("httpRequestBase.contentReadFail") +
e.getMessage());
}
} // Store the final results
results.setLocked(true);
parsed = true;
parameters = results; }

并且这个方法虽然会被

getParameter(String)
getParameterMap()
getParameterNames()
getParameterValues(String)

以下方法调用,但是该方法只会执行一次。

如:

    /**
* Return the value of the specified request parameter, if any; otherwise,
* return <code>null</code>. If there is more than one value defined,
* return only the first one.
*
* @param name Name of the desired request parameter
*/
public String getParameter(String name) { parseParameters();
String values[] = (String[]) parameters.get(name);
if (values != null)
return (values[0]);
else
return (null); }

4、创建HttpResponse对象

The HttpResponse class implements javax.servlet.http.HttpServletResponse. Accompanying it is a façade class named HttpResponseFacade. Figure 3.3 shows the UML diagram of HttpResponse and its related classes.

Chapter 3: Connector(连接器)-LMLPHP

With an OutputStreamWriter, characters written to it are encoded into bytes using a specified charset. The charset that it uses may be specified by name or may be given explicitly, or the platform's default charset may be accepted. Each invocation of a write method causes the encoding converter to be invoked on the given character(s). The resulting bytes are accumulated in a buffer before being written to the underlying output stream. The size of this buffer may be specified, but by default it is large enough for most purposes. Note that the characters passed to the write methods are not buffered. Therefore, here is the getWriter method:

    /**
* Return the writer associated with this Response.
*
* @exception IllegalStateException if <code>getOutputStream</code> has
* already been called for this response
* @exception IOException if an input/output error occurs
*/
public PrintWriter getWriter() throws IOException { if (writer != null)
return (writer); if (stream != null)
throw new IllegalStateException
(sm.getString("responseBase.getWriter.ise")); ResponseStream newStream = (ResponseStream) createOutputStream();
newStream.setCommit(false);
OutputStreamWriter osr =
new OutputStreamWriter(newStream, getCharacterEncoding());
writer = new ResponseWriter(osr, newStream);
stream = newStream;
return (writer); }

5、StaticResourceProcessor和ServletProcessor这两个类

ServletProcessor这个类和第二章的ex02.pyrmont.ServletProcessor类似,他们都只有一个方法:process。However, the process method in ex03.pyrmont.connector.ServletProcessor accepts an HttpRequest and an HttpResponse, instead of instances of Request and Response. Here is the signature of the process method in this chapter's application:

public void process(HttpRequest request, HttpResponse response) {

In addition, the process method uses HttpRequestFacade and HttpResponseFacade as facade classes for the request and the response. Also, it calls the HttpResponse class's finishResponse method after calling the servlet's service method.

The StaticResourceProcessor class is almost identical to the ex02.pyrmont.StaticResourceProcessor class.

6、运行

To run the application in Windows, from the working directory, type the following:

java -classpath ./lib/servlet.jar;./ ex03.pyrmont.startup.Bootstrap

In Linux, you use a colon to separate two libraries.

java -classpath ./lib/servlet.jar:./ ex03.pyrmont.startup.Bootstrap

To display index.html, use the following URL:

http://localhost:8080/index.html

To invoke PrimitiveServlet, direct your browser to the following URL: http://localhost:8080/servlet/PrimitiveServlet You'll see the following on your browser: Hello. Roses are red. Violets are blue.
Note
Running PrimitiveServlet in Chapter 2 did not give you the second line.

You can also call ModernServet, which would not run in the servlet containers in Chapter 2.

Here is the URL:

http://localhost:8080/servlet/ModernServlet

You can append a query string to the URL to test the servlet.

http://localhost:8080/servlet/ModernServlet?userName=tarzan&password=pwd

四、总结

在这一章中,你学习到了如何使Connector(连接器)工作。这一章的Connector是Tomcat4默认的连接器的简化版本。因为效率问题(比如说,所有的headers都会被解析,即使他们没有在servlet中被使用到),Tomcat4默认的Connector已经废弃了,被一个新的称为Coyote,一个快速的Connector所代替了。但是,Tomcat4默认的Connector仍然是一个好的学习工具,我们会在Chapter4 中详细介绍。

05-11 11:33