Tomcat中的servlet容器叫做Catalina,Catalina有两个主要模块:连接器与容器。在本章,将会建立一个连接器来增强第二章中应用程序的功能,用一种更好的方式来创建request与response对象。
截止文章编写日期,servlet规范已经出到了6.0版本,但是连接器的基本功能没变,都是需要创建javax.servlet.http.HttpServletRequest实例与javax.servlet.http.HttpServletResponse实例,并将它们作为servlet#service方法的参数传入。
本章内容将我们这个Web容器拆分成三个模块:启动模块,连接器模块,servlet容器模块,包的规划如下图
servlet容器模块本章不做扩展,仍然使用前一章的ServletProcessor与StaticResourceProcessor。本章主要聚焦连接器模块,即connector包下的内容。
从本章开始,每章的应用程序中都会有一个启动类来启动整个应用程序,但是目前还没有一种机制来关闭应用程序,这个到指定章节再做实现。目前只能通过杀进程的方式来关闭应用。
在正式介绍本章的程序设计之前,先来看看Tomcat中的一个处理错误消息的类org.apache.catalina.util.StringManager
StringManager类
这个类需要搭配一个文件来运作:LocalStrings.properties。来看这个文件中放的什么格式的内容
啧,全是key,value的形式,它的目的就是针对某一类错误,定义了一个统一的报错文案,如果要改文案的话直接改这个文件中的就可以,避免写的太分散不好改。
另外这种单提出来文件的形式,也方便做国际化的设计,例如Tomcat为了支持西班牙语与日语,创建了以 _es与_ja 为后缀的文件,三个文件内容保持key相同,value值定为指定语言的文案即可。
LocalStrings.properties的生效范围为当前包,也就是说它仅针对它所在包中的错误做定义,所以不可避免的在Tomcat源码中,有很多包下都存在LocalStrings.properties文件。
再回来看StringManager这个类,这个类就是要利用起来这些LocalStrings.properties文件。由于LocalStrings.properties文件是按包划分的,StringManager对象也按包划分,每个包用一个StringManager对象。
StringManager中用一个HashTable来保存各个包下的StringManager对象
private static Hashtable managers = new Hashtable();/*** 获取特定包的StringManager。如果managers中已经存在,它将被重用,否则将创建并返回一个新的StringManager。*/
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;
}
使用StringManager的方法如下
如果在ex03.hml.connector.http包下,获取其StringManager的方法为
StringManager sm = StringManager.getManager("ex03.hml.connector.http");
使用方法为
sm.getString("httpProcessor.parseHeaders.colon")
这样就拿到了指定包下LocalStrings.properties文件中定义的错误信息。
下面正式开始介绍本章的程序设计
本章程序设计
上面讲到了,本章程序将由三个模块组成(启动模块,连接器模块,servlet容器模块),接下来分别看下各自模块的设计
第二章的HttpServer类既做了服务的启动又做了http请求的连接功能,本章将HttpServer拆成两块内容,启动模块与连接器模块。
启动模块
启动模块只有一个类Bootstrap,负责启动整个应用程序。
连接器模块
连接器涉及的类比较多,可以分为以下5个类型
- 连接器及其支持类(HttpConnector与HttpProcessor),HttpConnector负责接收http请求,HttpProcessor负责将http请求解析为HttpRequest与HttpResponse对象。
- 表示HTTP请求的类(HttpRequest)及其支持类
- 表示HTTP响应的类(HttpResponse)及其支持类
- 外观类(HttpRequestFacade与HttpResponseFacade)
- 常量类
servlet容器模块
servlet容器模块包含ServletProcessor与StaticResourceProcessor两个类,这两个类与第二章的代码并无太大区别。
本章应用程序的UML图如下
接下来看具体的程序代码
启动类-Bootstrap
此类很简单,就是一个main方法,用来启动一个连接器
package ex03.hml.startup;import ex03.hml.connector.http.HttpConnector;/*** 启动器,用于启动一个Web应用*/
public final class Bootstrap {public static void main(String[] args) {HttpConnector connector = new HttpConnector();connector.start();}
}
连接器类-HttpConnector
连接器实现了Runnable接口,以一个独立线程的方式来启动。HttpConnector只负责不断地接收Socket连接,具体对Scoket连接的处理交给HttpProcessor来完成。
package ex03.hml.connector.http;import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;/*** 连接器,用于接收socket连接(一次http请求建立一次连接,http返回后销毁连接)* 此连接器是以一个独立线程的方式启动起来的*/
public class HttpConnector implements Runnable {boolean stopped;// scheme这个属性在本章暂时没地方用到private String scheme = "http";public String getScheme() {return scheme;}public void run() {// 创建一个ServerSocket用来接收客户端的Socket连接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);}// 建立循环,不停的等待并处理socket连接,这里虽然设了停止标记(stopped),但是暂时没用到,停止服务仍然采用终止进程的方式,// 如何优雅的停止服务在后面的章节会有设计while (!stopped) {Socket socket;try {// 阻塞等待下一个 Socket 连接socket = serverSocket.accept();} catch (Exception e) {continue;}// 新建一个HttpProcessor来处理此 Socket 请求HttpProcessor processor = new HttpProcessor(this);processor.process(socket);}}/*** 启动连接器的线程*/public void start() {Thread thread = new Thread(this);thread.start();}
}
接下来其实应该看HttpProcessor具体处理HTTP请求的过程,不过,由于HttpProcessor类依赖了好几个其他类,所以在介绍HttpProcessor之前,先介绍一下它依赖的几个类:HttpRequestLine、HttpHeader、SocketInputStream、HttpRequest、HttpResponse、HttpRequestFacade、HttpResponseFacade。
HttpRequestLine类
一个处理HTTP请求的过程中间类,保存HTTP请求行的信息,便于转化为HttpRequest类中的 method、uri、protocol、queryString字段
HttpRequestLine对象中的这些属性值的填充将被 SocketInputStream 的 readRequestLine 方法实现。
源码大概看一下就行
package ex03.hml.connector.http;/*** HTTP request line enum type.** @author Remy Maucherat* @version $Revision: 1.6 $ $Date: 2002/03/18 07:15:40 $* @deprecated*/final class HttpRequestLine {// -------------------------------------------------------------- Constantspublic static final int INITIAL_METHOD_SIZE = 8;public static final int INITIAL_URI_SIZE = 64;public static final int INITIAL_PROTOCOL_SIZE = 8;public static final int MAX_METHOD_SIZE = 1024;public static final int MAX_URI_SIZE = 32768;public static final int MAX_PROTOCOL_SIZE = 1024;// ----------------------------------------------------------- Constructorspublic HttpRequestLine() {this(new char[INITIAL_METHOD_SIZE], 0, new char[INITIAL_URI_SIZE], 0,new char[INITIAL_PROTOCOL_SIZE], 0);}public HttpRequestLine(char[] method, int methodEnd,char[] uri, int uriEnd,char[] protocol, int protocolEnd) {this.method = method;this.methodEnd = methodEnd;this.uri = uri;this.uriEnd = uriEnd;this.protocol = protocol;this.protocolEnd = protocolEnd;}// ----------------------------------------------------- Instance Variablespublic char[] method;public int methodEnd;public char[] uri;public int uriEnd;public char[] protocol;public int protocolEnd;// ------------------------------------------------------------- Properties// --------------------------------------------------------- Public Methods/*** 释放所有对象引用,并初始化实例变量为重用该对象做准备。*/public void recycle() {methodEnd = 0;uriEnd = 0;protocolEnd = 0;}/*** Test if the uri includes the given char array.*/public int indexOf(char[] buf) {return indexOf(buf, buf.length);}/*** Test if the value of the header includes the given char array.*/public int indexOf(char[] buf, int end) {char firstChar = buf[0];int pos = 0;while (pos < uriEnd) {pos = indexOf(firstChar, pos);if (pos == -1)return -1;if ((uriEnd - pos) < end)return -1;for (int i = 0; i < end; i++) {if (uri[i + pos] != buf[i])break;if (i == (end - 1))return pos;}pos++;}return -1;}/*** Test if the value of the header includes the given string.*/public int indexOf(String str) {return indexOf(str.toCharArray(), str.length());}/*** Returns the index of a character in the value.*/public int indexOf(char c, int start) {for (int i = start; i < uriEnd; i++) {if (uri[i] == c)return i;}return -1;}// --------------------------------------------------------- Object Methodspublic int hashCode() {// FIXMEreturn 0;}public boolean equals(Object obj) {return false;}}
HttpHeader类
一个处理HTTP请求的过程中间类,保存HTTP请求中请求头的信息,注意一个HttpHeader对象只对应一个请求头,通常情况下一个HTTP请求中会包含多个请求头,解析出来后就是一个 HttpHeader的对象集合。
HttpHeader对象中的这些属性值的填充将被 SocketInputStream 的 readHeader 方法实现。
HttpHeader最终会被转化为 name、value(String类型),放入HttpRequest的 protected HashMap headers = new HashMap(); 属性中。
源码大概看一下就行
package ex03.hml.connector.http;/*** HTTP header enum type.** @author Remy Maucherat* @version $Revision: 1.4 $ $Date: 2002/03/18 07:15:40 $* @deprecated*/final class HttpHeader {// -------------------------------------------------------------- Constantspublic static final int INITIAL_NAME_SIZE = 32;public static final int INITIAL_VALUE_SIZE = 64;public static final int MAX_NAME_SIZE = 128;public static final int MAX_VALUE_SIZE = 4096;// ----------------------------------------------------------- Constructorspublic HttpHeader() {this(new char[INITIAL_NAME_SIZE], 0, new char[INITIAL_VALUE_SIZE], 0);}public HttpHeader(char[] name, int nameEnd, char[] value, int valueEnd) {this.name = name;this.nameEnd = nameEnd;this.value = value;this.valueEnd = valueEnd;}public HttpHeader(String name, String value) {this.name = name.toLowerCase().toCharArray();this.nameEnd = name.length();this.value = value.toCharArray();this.valueEnd = value.length();}// ----------------------------------------------------- Instance Variablespublic char[] name;public int nameEnd;public char[] value;public int valueEnd;protected int hashCode = 0;// ------------------------------------------------------------- Properties// --------------------------------------------------------- Public Methods/*** Release all object references, and initialize instance variables, in* preparation for reuse of this object.*/public void recycle() {nameEnd = 0;valueEnd = 0;hashCode = 0;}/*** Test if the name of the header is equal to the given char array.* All the characters must already be lower case.*/public boolean equals(char[] buf) {return equals(buf, buf.length);}/*** Test if the name of the header is equal to the given char array.* All the characters must already be lower case.*/public boolean equals(char[] buf, int end) {if (end != nameEnd)return false;for (int i=0; i<end; i++) {if (buf[i] != name[i])return false;}return true;}/*** Test if the name of the header is equal to the given string.* The String given must be made of lower case characters.*/public boolean equals(String str) {return equals(str.toCharArray(), str.length());}/*** Test if the value of the header is equal to the given char array.*/public boolean valueEquals(char[] buf) {return valueEquals(buf, buf.length);}/*** Test if the value of the header is equal to the given char array.*/public boolean valueEquals(char[] buf, int end) {if (end != valueEnd)return false;for (int i=0; i<end; i++) {if (buf[i] != value[i])return false;}return true;}/*** Test if the value of the header is equal to the given string.*/public boolean valueEquals(String str) {return valueEquals(str.toCharArray(), str.length());}/*** Test if the value of the header includes the given char array.*/public boolean valueIncludes(char[] buf) {return valueIncludes(buf, buf.length);}/*** Test if the value of the header includes the given char array.*/public boolean valueIncludes(char[] buf, int end) {char firstChar = buf[0];int pos = 0;while (pos < valueEnd) {pos = valueIndexOf(firstChar, pos);if (pos == -1)return false;if ((valueEnd - pos) < end)return false;for (int i = 0; i < end; i++) {if (value[i + pos] != buf[i])break;if (i == (end-1))return true;}pos++;}return false;}/*** Test if the value of the header includes the given string.*/public boolean valueIncludes(String str) {return valueIncludes(str.toCharArray(), str.length());}/*** Returns the index of a character in the value.*/public int valueIndexOf(char c, int start) {for (int i=start; i<valueEnd; i++) {if (value[i] == c)return i;}return -1;}/*** Test if the name of the header is equal to the given header.* All the characters in the name must already be lower case.*/public boolean equals(HttpHeader header) {return (equals(header.name, header.nameEnd));}/*** Test if the name and value of the header is equal to the given header.* All the characters in the name must already be lower case.*/public boolean headerEquals(HttpHeader header) {return (equals(header.name, header.nameEnd))&& (valueEquals(header.value, header.valueEnd));}// --------------------------------------------------------- Object Methods/*** Return hash code. The hash code of the HttpHeader object is the same* as returned by new String(name, 0, nameEnd).hashCode().*/public int hashCode() {int h = hashCode;if (h == 0) {int off = 0;char val[] = name;int len = nameEnd;for (int i = 0; i < len; i++)h = 31*h + val[off++];hashCode = h;}return h;}public boolean equals(Object obj) {if (obj instanceof String) {return equals(((String) obj).toLowerCase());} else if (obj instanceof HttpHeader) {return equals((HttpHeader) obj);}return false;}}
SocketInputStream类
引入此类主要就是为了使用 readRequestLine 与 readHeader 两个方法,其实现逻辑比较晦涩,你且知道这两个方法是干啥的就行
- public void readRequestLine(HttpRequestLine requestLine):解析InputStream,填充requestLine对象的属性值。
- public void readHeader(HttpHeader header):解析InputStream,读取到下一个请求头的信息,填充header对象的属性值。
另外有一点需要注意InputStream流的读取过程应该是从头至尾按顺序读的,所以应该先获取请求行,再获取请求头,最后获取body体。
这里说的InputStream就是Socket的InputStream,本章接下来提到的InputStream如果没有特别声明的话,都是Socket的InputStream。
package ex03.hml.connector.http;import org.apache.catalina.util.StringManager;import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;/*** Extends InputStream to be more efficient reading lines during HTTP* header processing.** @author <a href="mailto:remm@apache.org">Remy Maucherat</a>* @deprecated*/
public class SocketInputStream extends InputStream {// -------------------------------------------------------------- Constants/*** CR.*/private static final byte CR = (byte) '\r';/*** LF.*/private static final byte LF = (byte) '\n';/*** SP.*/private static final byte SP = (byte) ' ';/*** HT.*/private static final byte HT = (byte) '\t';/*** COLON.*/private static final byte COLON = (byte) ':';/*** Lower case offset.*/private static final int LC_OFFSET = 'A' - 'a';/*** Internal buffer.*/protected byte buf[];/*** Last valid byte.*/protected int count;/*** Position in the buffer.*/protected int pos;/*** Underlying input stream.*/protected InputStream is;// ----------------------------------------------------------- Constructors/*** Construct a servlet input stream associated with the specified socket* input.** @param is socket input stream* @param bufferSize size of the internal buffer*/public SocketInputStream(InputStream is, int bufferSize) {this.is = is;buf = new byte[bufferSize];}// -------------------------------------------------------------- Variables/*** The string manager for this package.*/protected static StringManager sm = StringManager.getManager(Constants.Package);// ----------------------------------------------------- Instance Variables// --------------------------------------------------------- Public Methods/*** 读取请求行,并将其复制到给定的缓冲区。其实就是解析InputStream,填充HttpRequestLine对象的属性值* 这函数是在HTTP请求头解析期间使用的。不要试图使用它来读取请求体。** @param requestLine HttpRequestLine 对象* @throws IOException 如果在底层套接字期间发生异常读取操作,或者如果给定的缓冲区不够大来容纳整个请求行。*/public void readRequestLine(HttpRequestLine requestLine) throws IOException {// Recycling checkif (requestLine.methodEnd != 0) requestLine.recycle();// Checking for a blank lineint chr;// Skipping CR or LFdo {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 nameint maxRead = requestLine.method.length;int readStart = pos;int readCount = 0;boolean space = false;while (!space) {// if the buffer is full, extend itif (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 bufferif (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 URImaxRead = requestLine.uri.length;readStart = pos;readCount = 0;space = false;boolean eol = false;while (!space) {// if the buffer is full, extend itif (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 bufferif (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 requesteol = true;space = true;}requestLine.uri[readCount] = (char) buf[pos];readCount++;pos++;}requestLine.uriEnd = readCount - 1;// Reading protocolmaxRead = requestLine.protocol.length;readStart = pos;readCount = 0;while (!eol) {// if the buffer is full, extend itif (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 bufferif (pos >= count) {// Copying part (or all) of the internal buffer to the line// bufferint 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;}/*** 读取header,并将其复制到给定的缓冲区。其实就是从InputStream中解析出下一个请求头的信息,填充进HttpHeader对象* 该函数将在HTTP请求头解析期间使用。不要试图使用它来读取请求体。** @param header HttpHeader 对象* @throws IOException 如果在底层套接字读取操作期间发生异常,或者给定的缓冲区不够大,无法容纳整行。*/public void readHeader(HttpHeader header) throws IOException {// Recycling checkif (header.nameEnd != 0) header.recycle();// Checking for a blank lineint chr = read();if ((chr == CR) || (chr == LF)) { // Skipping CRif (chr == CR) read(); // Skipping LFheader.nameEnd = 0;header.valueEnd = 0;return;} else {pos--;}// Reading the header nameint maxRead = header.name.length;int readStart = pos;int readCount = 0;boolean colon = false;while (!colon) {// if the buffer is full, extend itif (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 bufferif (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 bufferif (pos >= count) {// Copying part (or all) of the internal buffer to the line// bufferint 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 itif (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 bufferif (pos >= count) {// Copying part (or all) of the internal buffer to the line// bufferint 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 fineint 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 itif (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;}/*** Read byte.*/public int read() throws IOException {if (pos >= count) {fill();if (pos >= count) return -1;}return buf[pos++] & 0xff;}/****//*public int read(byte b[], int off, int len)throws IOException {}*//****//*public long skip(long n)throws IOException {}*//*** Returns the number of bytes that can be read from this input* stream without blocking.*/public int available() throws IOException {return (count - pos) + is.available();}/*** Close the input stream.*/public void close() throws IOException {if (is == null) return;is.close();is = null;buf = null;}// ------------------------------------------------------ Protected Methods/*** Fill the internal buffer using data from the undelying input stream.*/protected void fill() throws IOException {pos = 0;count = 0;int nRead = is.read(buf, 0, buf.length);if (nRead > 0) {count = nRead;}}}
HttpRequest类
HttpRequest实现了HttpServletRequest接口,不过大多数接口方法都未具体实现。但是经过HttpProcessor处理后,servlet程序员已经可以从中获取HTTP请求的请求头,Cookie和请求参数的信息了。这三类数据分别存在以下三个变量中
protected HashMap headers = new HashMap();
protected ArrayList cookies = new ArrayList();
protected ParameterMap parameters = null;
这样servlet中就可以调用HttpRequest的 getHeader()、getCookies()、getParameter()等一系列相关的方法了。
该类中也提供了addCookie()、addHeader()方法来给HttpRequest填充对应属性。填充parameters属性的方法单独说一下
HttpRequest中持有一个InputStream对象的引用,并对外提供了parseParameters()方法,以便在合适的时机去解析请求参数。
为什么说“合适的时机”呢? 因为并不是所有servlet都需要获取请求参数的,而解析请求参数又是一个耗时耗费资源的过程,所以在需要时调用会更合理。
什么时候“需要”呢?当servlet调用HttpRequest中获取请求参数的方法时就是需要的时候,如getParameter()、getParameterMap()、getParameterNames()、getParameterValues()等方法。当然parseParameters()也会只保证执行一次(执行完后给parsed标记设为true),不会重复执行做无用功。
另外parameters这个Map的类型是ParameterMap,它继承了HashMap,并持有一个boolean locked字段,字段为true时才可对parameters进行修改操作,字段为false时不允许操作,防止其他程序篡改HTTP消息。
HttpRequest类代码如下,很多留空的方法,大概看一下上面提到的属性和方法就行
package ex03.hml.connector.http;/** this class copies methods from org.apache.catalina.connector.HttpRequestBase* and org.apache.catalina.connector.http.HttpRequestImpl.* The HttpRequestImpl class employs a pool of HttpHeader objects for performance* These two classes will be explained in Chapter 4.*/import ex03.hml.connector.RequestStream;
import org.apache.catalina.util.Enumerator;
import org.apache.catalina.util.ParameterMap;
import org.apache.catalina.util.RequestUtil;import javax.servlet.RequestDispatcher;
import javax.servlet.ServletInputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.security.Principal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;public class HttpRequest implements HttpServletRequest {private String contentType;private int contentLength;private InetAddress inetAddress;private InputStream input;private String method;private String protocol;private String queryString;private String requestURI;private String serverName;private int serverPort;private Socket socket;private boolean requestedSessionCookie; // session在cookie中声明private String requestedSessionId;private boolean requestedSessionURL; // session在URL中声明/*** The request attributes for this request.*/protected HashMap attributes = new HashMap();/*** The authorization credentials sent with this Request.*/protected String authorization = null;/*** The context path for this request.*/protected String contextPath = "";/*** The set of cookies associated with this Request.*/protected ArrayList cookies = new ArrayList();/*** An empty collection to use for returning empty Enumerations. Do not* add any elements to this collection!*/protected static ArrayList empty = new ArrayList();/*** The set of SimpleDateFormat formats to use in getDateHeader().*/protected SimpleDateFormat formats[] = {new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)};/*** The HTTP headers associated with this Request, keyed by name. The* values are ArrayLists of the corresponding header values.*/protected HashMap headers = new HashMap();/*** The parsed parameters for this request. This is populated only if* parameter information is requested via one of the* <code>getParameter()</code> family of method calls. The key is the* parameter name, while the value is a String array of values for this* parameter.* <p>* <strong>IMPLEMENTATION NOTE</strong> - Once the parameters for a* particular request are parsed and stored here, they are not modified.* Therefore, application level access to the parameters need not be* synchronized.*/protected ParameterMap parameters = null;/*** Have the parameters for this request been parsed yet?*/protected boolean parsed = false;protected String pathInfo = null;/*** The reader that has been returned by <code>getReader</code>, if any.*/protected BufferedReader reader = null;/*** The ServletInputStream that has been returned by* <code>getInputStream()</code>, if any.*/protected ServletInputStream stream = null;public HttpRequest(InputStream input) {this.input = input;}public void addHeader(String name, String value) {name = name.toLowerCase();synchronized (headers) {ArrayList values = (ArrayList) headers.get(name);if (values == null) {values = new ArrayList();headers.put(name, values);}values.add(value);}}/*** 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 stringString queryString = getQueryString();try {RequestUtil.parseParameters(results, queryString, encoding);}catch (UnsupportedEncodingException e) {;}// Parse any parameters specified in the input streamString 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");}}// Store the final resultsresults.setLocked(true);parsed = true;parameters = results;}public void addCookie(Cookie cookie) {synchronized (cookies) {cookies.add(cookie);}}/*** Create and return a ServletInputStream to read the content* associated with this Request. The default implementation creates an* instance of RequestStream associated with this request, but this can* be overridden if necessary.** @exception IOException if an input/output error occurs*/public ServletInputStream createInputStream() throws IOException {return (new RequestStream(this));}public InputStream getStream() {return input;}public void setContentLength(int length) {this.contentLength = length;}public void setContentType(String type) {this.contentType = type;}public void setInet(InetAddress inetAddress) {this.inetAddress = inetAddress;}public void setContextPath(String path) {if (path == null)this.contextPath = "";elsethis.contextPath = path;}public void setMethod(String method) {this.method = method;}public void setPathInfo(String path) {this.pathInfo = path;}public void setProtocol(String protocol) {this.protocol = protocol;}public void setQueryString(String queryString) {this.queryString = queryString;}public void setRequestURI(String requestURI) {this.requestURI = requestURI;}/*** Set the name of the server (virtual host) to process this request.** @param name The server name*/public void setServerName(String name) {this.serverName = name;}/*** Set the port number of the server to process this request.** @param port The server port*/public void setServerPort(int port) {this.serverPort = port;}public void setSocket(Socket socket) {this.socket = socket;}/*** Set a flag indicating whether or not the requested session ID for this* request came in through a cookie. This is normally called by the* HTTP Connector, when it parses the request headers.** @param flag The new flag*/public void setRequestedSessionCookie(boolean flag) {this.requestedSessionCookie = flag;}public void setRequestedSessionId(String requestedSessionId) {this.requestedSessionId = requestedSessionId;}public void setRequestedSessionURL(boolean flag) {requestedSessionURL = flag;}/* implementation of the HttpServletRequest*/public Object getAttribute(String name) {synchronized (attributes) {return (attributes.get(name));}}public Enumeration getAttributeNames() {synchronized (attributes) {return (new Enumerator(attributes.keySet()));}}public String getAuthType() {return null;}public String getCharacterEncoding() {return null;}public int getContentLength() {return contentLength ;}public String getContentType() {return contentType;}public String getContextPath() {return contextPath;}public Cookie[] getCookies() {synchronized (cookies) {if (cookies.size() < 1)return (null);Cookie results[] = new Cookie[cookies.size()];return ((Cookie[]) cookies.toArray(results));}}public long getDateHeader(String name) {String value = getHeader(name);if (value == null)return (-1L);// Work around a bug in SimpleDateFormat in pre-JDK1.2b4// (Bug Parade bug #4106807)value += " ";// Attempt to convert the date header in a variety of formatsfor (int i = 0; i < formats.length; i++) {try {Date date = formats[i].parse(value);return (date.getTime());}catch (ParseException e) {;}}throw new IllegalArgumentException(value);}public String getHeader(String name) {name = name.toLowerCase();synchronized (headers) {ArrayList values = (ArrayList) headers.get(name);if (values != null)return ((String) values.get(0));elsereturn null;}}public Enumeration getHeaderNames() {synchronized (headers) {return (new Enumerator(headers.keySet()));}}public Enumeration getHeaders(String name) {name = name.toLowerCase();synchronized (headers) {ArrayList values = (ArrayList) headers.get(name);if (values != null)return (new Enumerator(values));elsereturn (new Enumerator(empty));}}public ServletInputStream getInputStream() throws IOException {if (reader != null)throw new IllegalStateException("getInputStream has been called");if (stream == null)stream = createInputStream();return (stream);}public int getIntHeader(String name) {String value = getHeader(name);if (value == null)return (-1);elsereturn (Integer.parseInt(value));}public Locale getLocale() {return null;}public Enumeration getLocales() {return null;}public String getMethod() {return method;}public String getParameter(String name) {parseParameters();String values[] = (String[]) parameters.get(name);if (values != null)return (values[0]);elsereturn (null);}public Map getParameterMap() {parseParameters();return (this.parameters);}public Enumeration getParameterNames() {parseParameters();return (new Enumerator(parameters.keySet()));}public String[] getParameterValues(String name) {parseParameters();String values[] = (String[]) parameters.get(name);if (values != null)return (values);elsereturn null;}public String getPathInfo() {return pathInfo;}public String getPathTranslated() {return null;}public String getProtocol() {return protocol;}public String getQueryString() {return queryString;}public BufferedReader getReader() throws IOException {if (stream != null)throw new IllegalStateException("getInputStream has been called.");if (reader == null) {String encoding = getCharacterEncoding();if (encoding == null)encoding = "ISO-8859-1";InputStreamReader isr =new InputStreamReader(createInputStream(), encoding);reader = new BufferedReader(isr);}return (reader);}public String getRealPath(String path) {return null;}public String getRemoteAddr() {return null;}public String getRemoteHost() {return null;}public String getRemoteUser() {return null;}public RequestDispatcher getRequestDispatcher(String path) {return null;}public String getScheme() {return null;}public String getServerName() {return null;}public int getServerPort() {return 0;}public String getRequestedSessionId() {return null;}public String getRequestURI() {return requestURI;}public StringBuffer getRequestURL() {return null;}public HttpSession getSession() {return null;}public HttpSession getSession(boolean create) {return null;}public String getServletPath() {return null;}public Principal getUserPrincipal() {return null;}public boolean isRequestedSessionIdFromCookie() {return false;}public boolean isRequestedSessionIdFromUrl() {return isRequestedSessionIdFromURL();}public boolean isRequestedSessionIdFromURL() {return false;}public boolean isRequestedSessionIdValid() {return false;}public boolean isSecure() {return false;}public boolean isUserInRole(String role) {return false;}public void removeAttribute(String attribute) {}public void setAttribute(String key, Object value) {}/*** Set the authorization credentials sent with this request.** @param authorization The new authorization credentials*/public void setAuthorization(String authorization) {this.authorization = authorization;}public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException {}
}
HttpResponse类
HttpResponse类实现了HttpServletResponse接口,对部分接口做了具体实现。相比于第二章的Response类,HttpResponse类拥有更多的属性,例如针对HTTP相应信息的属性:contentType、contentLength、cookies、headers 等等。
HttpResponse提供给servlet往输出流中写数据的方法仍然是提供PrintWriter。但是本章的PrintWriter会使用一个它的子类:ResponseWriter。下面是getWriter的代码
一个往OutputStream中写数据的方法被封装了好几层。OutputStreamWriter可以指定输出内容的字符集;ReponseStream继承自ServletOutputStream,所以它也是当做一个数据流来编码,它持有一个HttpResponse对象,它的write方法是调用的HttpResponse的write方法,使用HttpResponse持有的OutputStream对象将数据写入Socket的输出流中。
ResponseWriter在每个写数据的方法都额外做了一件事,就是调用了OutputStreamWriter的flush方法,其实最终调用的是HttpResponse中持有的OutputStream对象的flush方法。解决了第二章中使用原生PrintWriter的 print 方法时不会刷新输出流的弊端。
读取静态资源的方法仍然保留 sendStaticResource()。
package ex03.hml.connector.http;import ex03.hml.connector.ResponseStream;
import ex03.hml.connector.ResponseWriter;
import org.apache.catalina.util.CookieTools;import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;public class HttpResponse implements HttpServletResponse {// the default buffer sizeprivate static final int BUFFER_SIZE = 1024;HttpRequest request;OutputStream output;PrintWriter writer;protected byte[] buffer = new byte[BUFFER_SIZE];protected int bufferCount = 0;/*** Has this response been committed yet?*/protected boolean committed = false;/*** The actual number of bytes written to this Response.*/protected int contentCount = 0;/*** The content length associated with this Response.*/protected int contentLength = -1;/*** The content type associated with this Response.*/protected String contentType = null;/*** The character encoding associated with this Response.*/protected String encoding = null;/*** The set of Cookies associated with this Response.*/protected ArrayList cookies = new ArrayList();/*** The HTTP headers explicitly added via addHeader(), but not including* those to be added with setContentLength(), setContentType(), and so on.* This collection is keyed by the header name, and the elements are* ArrayLists containing the associated values that have been set.*/protected HashMap headers = new HashMap();/*** The date format we will use for creating date headers.*/protected final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);/*** The error message set by <code>sendError()</code>.*/protected String message = getStatusMessage(HttpServletResponse.SC_OK);/*** The HTTP status code associated with this Response.*/protected int status = HttpServletResponse.SC_OK;public HttpResponse(OutputStream output) {this.output = output;}/*** call this method to send headers and response to the output*/public void finishResponse() {// sendHeaders();// Flush and close the appropriate output mechanismif (writer != null) {writer.flush();writer.close();}}public int getContentLength() {return contentLength;}public String getContentType() {return contentType;}protected String getProtocol() {return request.getProtocol();}/*** Returns a default status message for the specified HTTP status code.** @param status The status code for which a message is desired*/protected String getStatusMessage(int status) {switch (status) {case SC_OK:return ("OK");case SC_ACCEPTED:return ("Accepted");case SC_BAD_GATEWAY:return ("Bad Gateway");case SC_BAD_REQUEST:return ("Bad Request");case SC_CONFLICT:return ("Conflict");case SC_CONTINUE:return ("Continue");case SC_CREATED:return ("Created");case SC_EXPECTATION_FAILED:return ("Expectation Failed");case SC_FORBIDDEN:return ("Forbidden");case SC_GATEWAY_TIMEOUT:return ("Gateway Timeout");case SC_GONE:return ("Gone");case SC_HTTP_VERSION_NOT_SUPPORTED:return ("HTTP Version Not Supported");case SC_INTERNAL_SERVER_ERROR:return ("Internal Server Error");case SC_LENGTH_REQUIRED:return ("Length Required");case SC_METHOD_NOT_ALLOWED:return ("Method Not Allowed");case SC_MOVED_PERMANENTLY:return ("Moved Permanently");case SC_MOVED_TEMPORARILY:return ("Moved Temporarily");case SC_MULTIPLE_CHOICES:return ("Multiple Choices");case SC_NO_CONTENT:return ("No Content");case SC_NON_AUTHORITATIVE_INFORMATION:return ("Non-Authoritative Information");case SC_NOT_ACCEPTABLE:return ("Not Acceptable");case SC_NOT_FOUND:return ("Not Found");case SC_NOT_IMPLEMENTED:return ("Not Implemented");case SC_NOT_MODIFIED:return ("Not Modified");case SC_PARTIAL_CONTENT:return ("Partial Content");case SC_PAYMENT_REQUIRED:return ("Payment Required");case SC_PRECONDITION_FAILED:return ("Precondition Failed");case SC_PROXY_AUTHENTICATION_REQUIRED:return ("Proxy Authentication Required");case SC_REQUEST_ENTITY_TOO_LARGE:return ("Request Entity Too Large");case SC_REQUEST_TIMEOUT:return ("Request Timeout");case SC_REQUEST_URI_TOO_LONG:return ("Request URI Too Long");case SC_REQUESTED_RANGE_NOT_SATISFIABLE:return ("Requested Range Not Satisfiable");case SC_RESET_CONTENT:return ("Reset Content");case SC_SEE_OTHER:return ("See Other");case SC_SERVICE_UNAVAILABLE:return ("Service Unavailable");case SC_SWITCHING_PROTOCOLS:return ("Switching Protocols");case SC_UNAUTHORIZED:return ("Unauthorized");case SC_UNSUPPORTED_MEDIA_TYPE:return ("Unsupported Media Type");case SC_USE_PROXY:return ("Use Proxy");case 207: // WebDAVreturn ("Multi-Status");case 422: // WebDAVreturn ("Unprocessable Entity");case 423: // WebDAVreturn ("Locked");case 507: // WebDAVreturn ("Insufficient Storage");default:return ("HTTP Response Status " + status);}}public OutputStream getStream() {return this.output;}/*** Send the HTTP response headers, if this has not already occurred.*/protected void sendHeaders() throws IOException {if (isCommitted()) return;// Prepare a suitable output writerOutputStreamWriter osr = null;try {osr = new OutputStreamWriter(getStream(), getCharacterEncoding());} catch (UnsupportedEncodingException e) {osr = new OutputStreamWriter(getStream());}final PrintWriter outputWriter = new PrintWriter(osr);// Send the "Status:" headeroutputWriter.print(this.getProtocol());outputWriter.print(" ");outputWriter.print(status);if (message != null) {outputWriter.print(" ");outputWriter.print(message);}outputWriter.print("\r\n");// Send the content-length and content-type headers (if any)if (getContentType() != null) {outputWriter.print("Content-Type: " + getContentType() + "\r\n");}if (getContentLength() >= 0) {outputWriter.print("Content-Length: " + getContentLength() + "\r\n");}// Send all specified headers (if any)synchronized (headers) {Iterator names = headers.keySet().iterator();while (names.hasNext()) {String name = (String) names.next();ArrayList values = (ArrayList) headers.get(name);Iterator items = values.iterator();while (items.hasNext()) {String value = (String) items.next();outputWriter.print(name);outputWriter.print(": ");outputWriter.print(value);outputWriter.print("\r\n");}}}// Add the session ID cookie if necessary
/* HttpServletRequest hreq = (HttpServletRequest) request.getRequest();HttpSession session = hreq.getSession(false);if ((session != null) && session.isNew() && (getContext() != null)&& getContext().getCookies()) {Cookie cookie = new Cookie("JSESSIONID", session.getId());cookie.setMaxAge(-1);String contextPath = null;if (context != null)contextPath = context.getPath();if ((contextPath != null) && (contextPath.length() > 0))cookie.setPath(contextPath);elsecookie.setPath("/");if (hreq.isSecure())cookie.setSecure(true);addCookie(cookie);}
*/// Send all specified cookies (if any)synchronized (cookies) {Iterator items = cookies.iterator();while (items.hasNext()) {Cookie cookie = (Cookie) items.next();outputWriter.print(CookieTools.getCookieHeaderName(cookie));outputWriter.print(": ");outputWriter.print(CookieTools.getCookieHeaderValue(cookie));outputWriter.print("\r\n");}}// Send a terminating blank line to mark the end of the headersoutputWriter.print("\r\n");outputWriter.flush();committed = true;}public void setRequest(HttpRequest request) {this.request = request;}/* This method is used to serve a static page */public void sendStaticResource() {try {if (request.getRequestURI().equals("/shutdown")) {String msg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Content-Length: 32\r\n" +"\r\n" +"<h1>server already shutdown</h1>";output.write(msg.getBytes());return;}File file = new File(Constants.WEB_ROOT, request.getRequestURI());if (file.exists()) {FileInputStream fileInputStream = new FileInputStream(file);byte[] bytes = new byte[fileInputStream.available()];fileInputStream.read(bytes);String successMsg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Content-Length: " + bytes.length + "\r\n" +"\r\n";output.write(successMsg.getBytes());output.write(bytes);fileInputStream.close();} else {String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +"Content-Type: text/html\r\n" +"Content-Length: 23\r\n" +"\r\n" +"<h1>File Not Found</h1>";output.write(errorMessage.getBytes());}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (output != null) {try {output.flush();output.close();} catch (IOException e) {e.printStackTrace();}}}}public void write(int b) throws IOException {if (bufferCount >= buffer.length) flushBuffer();buffer[bufferCount++] = (byte) b;contentCount++;}public void write(byte b[]) throws IOException {write(b, 0, b.length);}public void write(byte b[], int off, int len) throws IOException {// If the whole thing fits in the buffer, just put it thereif (len == 0) return;if (len <= (buffer.length - bufferCount)) {System.arraycopy(b, off, buffer, bufferCount, len);bufferCount += len;contentCount += len;return;}// Flush the buffer and start writing full-buffer-size chunksflushBuffer();int iterations = len / buffer.length;int leftoverStart = iterations * buffer.length;int leftoverLen = len - leftoverStart;for (int i = 0; i < iterations; i++)write(b, off + (i * buffer.length), buffer.length);// Write the remainder (guaranteed to fit in the buffer)if (leftoverLen > 0) write(b, off + leftoverStart, leftoverLen);}/*** implementation of HttpServletResponse*/public void addCookie(Cookie cookie) {if (isCommitted()) return;// if (included)// return; // Ignore any call from an included servletsynchronized (cookies) {cookies.add(cookie);}}public void addDateHeader(String name, long value) {if (isCommitted()) return;
// if (included)// return; // Ignore any call from an included servletaddHeader(name, format.format(new Date(value)));}public void addHeader(String name, String value) {if (isCommitted()) return;
// if (included)// return; // Ignore any call from an included servletsynchronized (headers) {ArrayList values = (ArrayList) headers.get(name);if (values == null) {values = new ArrayList();headers.put(name, values);}values.add(value);}}public void addIntHeader(String name, int value) {if (isCommitted()) return;
// if (included)// return; // Ignore any call from an included servletaddHeader(name, "" + value);}public boolean containsHeader(String name) {synchronized (headers) {return (headers.get(name) != null);}}public String encodeRedirectURL(String url) {return null;}public String encodeRedirectUrl(String url) {return encodeRedirectURL(url);}public String encodeUrl(String url) {return encodeURL(url);}public String encodeURL(String url) {return null;}public void flushBuffer() throws IOException {//committed = true;if (bufferCount > 0) {try {output.write(buffer, 0, bufferCount);} finally {bufferCount = 0;}}}public int getBufferSize() {return 0;}public String getCharacterEncoding() {if (encoding == null) return ("ISO-8859-1");else return (encoding);}public Locale getLocale() {return null;}public ServletOutputStream getOutputStream() throws IOException {return null;}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;}/*** Has the output of this response already been committed?*/public boolean isCommitted() {return (committed);}public void reset() {}public void resetBuffer() {}public void sendError(int sc) throws IOException {}public void sendError(int sc, String message) throws IOException {}public void sendRedirect(String location) throws IOException {}public void setBufferSize(int size) {}public void setContentLength(int length) {if (isCommitted()) return;
// if (included)// return; // Ignore any call from an included servletthis.contentLength = length;}public void setContentType(String type) {}public void setDateHeader(String name, long value) {if (isCommitted()) return;
// if (included)// return; // Ignore any call from an included servletsetHeader(name, format.format(new Date(value)));}public void setHeader(String name, String value) {if (isCommitted()) return;
// if (included)// return; // Ignore any call from an included servletArrayList values = new ArrayList();values.add(value);synchronized (headers) {headers.put(name, values);}String match = name.toLowerCase();if (match.equals("content-length")) {int contentLength = -1;try {contentLength = Integer.parseInt(value);} catch (NumberFormatException e) {;}if (contentLength >= 0) setContentLength(contentLength);} else if (match.equals("content-type")) {setContentType(value);}}public void setIntHeader(String name, int value) {if (isCommitted()) return;//if (included)//return; // Ignore any call from an included servletsetHeader(name, "" + value);}public void setLocale(Locale locale) {if (isCommitted()) return;//if (included)//return; // Ignore any call from an included servlet// super.setLocale(locale);String language = locale.getLanguage();if ((language != null) && (language.length() > 0)) {String country = locale.getCountry();StringBuffer value = new StringBuffer(language);if ((country != null) && (country.length() > 0)) {value.append('-');value.append(country);}setHeader("Content-Language", value.toString());}}public void setStatus(int sc) {}public void setStatus(int sc, String message) {}
}
HttpRequestFacade与HttpResponseFacade
两个外观类,
HttpRequestFacade是HttpRequest的外观类,同样实现了HttpServletRequest接口,负责给servlet暴露HttpServletRequest接口方法的实现。
HttpResponseFacade是HttpResponse的外观类,同样实现了HttpServletResponse接口,负责给servlet暴露HttpServletResponse接口方法的实现。
HTTP连接处理类-HttpProcessor
讲了好几个HTTP请求与相应相关的类,终于轮到HttpProcessor了,前面讲了:HttpConnector只负责接收http请求的消息,具体的处理流程交给HttpProcessor来做。所以这个类的职责是:将http请求的请求行与请求头解析出来,并封装成HttpRequest与HttpResponse对象,然后交给serlvet容器。
这个类的主要复杂点在于这两行内容
parseRequest方法负责解析请求行的内容,将method、uri、protocol、queryString解析出来,如果uri中包含jsessionid的话,将jsessionid也解析出来。
带jsessionid的请求url大概长这个样子http://localhost:8080/user/login.jsp;jsessionid=CA0CA7E455535994E523B01357B42214?xxxx=xxx
parseHeaders方法负责将HTTP请求中的请求头解析出来,放到 protected HashMap headers = new HashMap(); 这个属性里。如果检测到了请求头中有cookie信息,将其取出来往 protected ArrayList cookies = new ArrayList(); 这个属性里放一份。另外 content-length、content-type请求头的值也单独取出来放到了HttpRequest的 contentLength、contentType字段里。
由于InputStream流只能从头读到尾,所以 parseRequest、parseHeaders 的先后顺序不能反。而body体是否读取,就看servlet中是否需要了。
HttpProcessor代码如下
package ex03.hml.connector.http;import ex03.hml.ServletProcessor;
import ex03.hml.StaticResourceProcessor;
import org.apache.catalina.util.RequestUtil;
import org.apache.catalina.util.StringManager;import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;/*** 这个类被用来处理具体的某个http请求*/
public class HttpProcessor {public HttpProcessor(HttpConnector connector) {this.connector = connector;}/*** 与调用它的 HttpConnector 做一个关联,但是这个属性暂时没用*/private HttpConnector connector = null;private HttpRequest request;private HttpRequestLine requestLine = new HttpRequestLine();private HttpResponse response;// 下面这两个属性也暂时没用protected String method = null;protected String queryString = null;/*** 这是当前包的 StringManager*/protected StringManager sm = StringManager.getManager("ex03.hml.connector.http");/*** 处理http请求*/public void process(Socket socket) {SocketInputStream input;OutputStream output;try {input = new SocketInputStream(socket.getInputStream(), 2048);output = socket.getOutputStream();// 构建 HttpRequest,HttpResponse对象request = new HttpRequest(input);response = new HttpResponse(output);response.setRequest(request);response.setHeader("Server", "hml Servlet Container");// 解析请求行内容(HTTP请求的第一行内容),填充进request对象parseRequest(input, output);// 解析请求头,填充进request对象parseHeaders(input);//判断请求的是静态资源还是servlet,servlet请求格式为 /servlet/servletNameif (request.getRequestURI().startsWith("/servlet/")) {ServletProcessor processor = new ServletProcessor();processor.process(request, response);} else {StaticResourceProcessor processor = new StaticResourceProcessor();processor.process(request, response);}// 关闭 socketsocket.close();} catch (Exception e) {// 此http请求处理如果出现了问题,进行异常捕获,不影响下一个http请求的处理e.printStackTrace();}}/*** 本方法是org.apache.catalina.connector.http.HttpProcessor中类似方法的简化版。* 但是,此方法只解析一些“简单”的头文件,例如* "cookie"、"content-length"和"content-type",忽略其他报头*/private void parseHeaders(SocketInputStream input) throws IOException, ServletException {while (true) {HttpHeader header = new HttpHeader();// 读取下一个headerinput.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);// do something for some headers, ignore others.if (name.equals("cookie")) {// 解析出所有cookieCookie cookies[] = RequestUtil.parseCookieHeader(value);for (int i = 0; i < cookies.length; i++) {if (cookies[i].getName().equals("jsessionid")) {// Override anything requested in the URLif (!request.isRequestedSessionIdFromCookie()) {// Accept only the first session id cookierequest.setRequestedSessionId(cookies[i].getValue());request.setRequestedSessionCookie(true);request.setRequestedSessionURL(false);}}request.addCookie(cookies[i]);}} 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);}} //end while}/*** 这个方法解析SocketInputStream获取请求行内容(即HTTP请求第一行)* 包括:queryString、method、protocol、uri。如果uri中包含jsessionid的话,同时也罢jsessionid解析出来*/private void parseRequest(SocketInputStream input, OutputStream output)throws IOException, ServletException {// 从input流中解析出请求行input.readRequestLine(requestLine);String method = new String(requestLine.method, 0, requestLine.methodEnd);String uri = null;String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);// 校验 request lineif (method.length() < 1) {throw new ServletException("Missing HTTP request method");} else if (requestLine.uriEnd < 1) {throw new ServletException("Missing HTTP request URI");}// 判断URI中存不存在query parameters,并解析出真正的URIint 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);}// 判断URI是不是绝对路径中的值 (带HTTP协议头的,例如:http://www.brainysoftware.com/index.html?name=Tarzan)if (!uri.startsWith("/")) {int pos = uri.indexOf("://");// 将协议和 host name 移除出去if (pos != -1) {pos = uri.indexOf('/', pos + 3);if (pos == -1) {uri = "";} else {uri = uri.substring(pos);}}}// 如果URI中包含jsessionid则将其解析出来,例如:http://localhost:8080/user/login.jsp;jsessionid=CA0CA7E455535994E523B01357B42214?xxxx=xxxString 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);}// 标准化 URI,对非正常的URI进行修正String normalizedUri = normalize(uri);// Set 正确的请求参数request.setMethod(method);request.setProtocol(protocol);if (normalizedUri != null) {request.setRequestURI(normalizedUri);} else {request.setRequestURI(uri);}if (normalizedUri == null) {throw new ServletException("Invalid URI: " + uri + "'");}}/*** Return a context-relative path, beginning with a "/", that represents* the canonical version of the specified path after ".." and "." elements* are resolved out. If the specified path attempts to go outside the* boundaries of the current context (i.e. too many ".." path elements* are present), return <code>null</code> instead.** @param path Path to be normalized*/protected String normalize(String path) {if (path == null)return null;// Create a place for the normalized pathString normalized = path;// Normalize "/%7E" and "/%7e" at the beginning to "/~"if (normalized.startsWith("/%7E") || normalized.startsWith("/%7e"))normalized = "/~" + normalized.substring(4);// Prevent encoding '%', '/', '.' and '\', which are special reserved// charactersif ((normalized.indexOf("%25") >= 0)|| (normalized.indexOf("%2F") >= 0)|| (normalized.indexOf("%2E") >= 0)|| (normalized.indexOf("%5C") >= 0)|| (normalized.indexOf("%2f") >= 0)|| (normalized.indexOf("%2e") >= 0)|| (normalized.indexOf("%5c") >= 0)) {return null;}if (normalized.equals("/."))return "/";// Normalize the slashes and add leading slash if necessaryif (normalized.indexOf('\\') >= 0)normalized = normalized.replace('\\', '/');if (!normalized.startsWith("/"))normalized = "/" + normalized;// Resolve occurrences of "//" in the normalized pathwhile (true) {int index = normalized.indexOf("//");if (index < 0)break;normalized = normalized.substring(0, index) + normalized.substring(index + 1);}// Resolve occurrences of "/./" in the normalized pathwhile (true) {int index = normalized.indexOf("/./");if (index < 0)break;normalized = normalized.substring(0, index) + normalized.substring(index + 2);}// Resolve occurrences of "/../" in the normalized pathwhile (true) {int index = normalized.indexOf("/../");if (index < 0)break;if (index == 0)return (null); // Trying to go outside our contextint index2 = normalized.lastIndexOf('/', index - 1);normalized = normalized.substring(0, index2) + normalized.substring(index + 3);}// Declare occurrences of "/..." (three or more dots) to be invalid// (on some Windows platforms this walks the directory tree!!!)if (normalized.indexOf("/...") >= 0)return (null);// Return the normalized path that we have completedreturn (normalized);}}
servlet容器类-ServletProcessor
ServletProcessor的方法逻辑没有变化,仍然是先获取类加载器,然后加载servlet类,反射创建指定的servlet对象,创建HttpRequest与HttpResponse的门面类作为参数,调用servlet的service方法。
package ex03.hml;import ex03.hml.connector.http.*;import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;public class ServletProcessor {public void process(HttpRequest request, HttpResponse response) {try {String uri = request.getRequestURI();String servletName = uri.substring(uri.lastIndexOf("/") + 1);//首先获取类加载器File file = new File(Constants.WEB_ROOT);String repository = (new URL("file", null, file.getCanonicalPath() + File.separator)).toString();URL[] urls = new URL[1];urls[0] = new URL(null, repository);URLClassLoader urlClassLoader = new URLClassLoader(urls);//加载servlet对应的类Class<?> aClass = urlClassLoader.loadClass(servletName);Servlet servlet = (Servlet) aClass.newInstance();HttpRequestFacade requestFacade = new HttpRequestFacade(request);HttpResponseFacade responseFacade = new HttpResponseFacade(response);servlet.service(requestFacade, responseFacade);response.finishResponse();} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException |ServletException e) {e.printStackTrace();}}}
StaticResourceProcessor类
静态资源处理类,一如既往的简单,处理静态资源的逻辑仍然放到了HttpResponse类中实现
package ex03.hml;import ex03.hml.connector.http.HttpRequest;
import ex03.hml.connector.http.HttpResponse;public class StaticResourceProcessor {public void process(HttpRequest request, HttpResponse response) {response.sendStaticResource();}}
Servlet具体实现类
除了上一章讲到的PrimitiveServlet外,本章引入一个新的servlet:ModernServlet,这个servlet中以html形式,将Http请求的一些信息展现了出来。
原书中的ModernServlet有一个坑点,那就是HTTP响应内容,使用了 Transfer-Encoding: chunked 分块传输的形式,但是却没有返回数据块的长度,导致返回结果无法解析,这里我将一并将它修复了。
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;public class ModernServlet extends HttpServlet {public void init(ServletConfig config) {System.out.println("ModernServlet -- init");}public void doGet(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException {response.setContentType("text/html");PrintWriter out = response.getWriter();//先输出HTTP的头部信息 String msg = "HTTP/1.1 200 OK\r\n" +"Content-Type: text/html\r\n" +"Transfer-Encoding: chunked\r\n" +"\r\n";out.print(msg);StringBuilder builder = new StringBuilder();//再输出HTTP的消息体builder.append("<html>");builder.append("<head>");builder.append("<title>Modern Servlet</title>");builder.append("</head>");builder.append("<body>");builder.append("<h2>Headers</h2>");Enumeration headers = request.getHeaderNames();while (headers.hasMoreElements()) {String header = (String) headers.nextElement();builder.append("<br>" + header + " : " + request.getHeader(header));}builder.append("<br><h2>Method</h2>");builder.append("<br>" + request.getMethod());builder.append("<br><h2>Parameters</h2>");Enumeration parameters = request.getParameterNames();while (parameters.hasMoreElements()) {String parameter = (String) parameters.nextElement();builder.append("<br>" + parameter + " : " + request.getParameter(parameter));}builder.append("<br><h2>Query String</h2>");builder.append("<br>" + request.getQueryString());builder.append("<br><h2>Request URI</h2>");builder.append("<br>" + request.getRequestURI());builder.append("</body>");builder.append("</html>");// 这里是与原书中代码不一样的地方,原代码没有加chunked块的长度,浏览器不能正常解析out.print(Integer.toHexString(builder.length()) + "\r\n");out.print(builder.toString() + "\r\n");out.print("0\r\n\r\n");out.flush();out.close();}
}
运行结果展示
请求动态资源
请求静态资源
OK,以上就是本章的程序设计。截止到这章的内容,我们的Web容器仍然是运行在单线程模式下,只能挨个按顺序处理客户端的HTTP请求。什么时候开始支持并发呢?敬请期待下一章
源码分享
https://gitee.com/huo-ming-lu/HowTomcatWorks
本章代码在ex03包下