深入剖析Tomcat(三) 实现一个简易连接器

news/2024/10/18 14:17:55/

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对象

java">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方法,用来启动一个连接器

java">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来完成。

java">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 方法实现。

源码大概看一下就行

java">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(); 属性中。

源码大概看一下就行

java">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。

java">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和请求参数的信息了。这三类数据分别存在以下三个变量中

java">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类代码如下,很多留空的方法,大概看一下上面提到的属性和方法就行

java">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()。

java">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代码如下

java">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方法。

java">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类中实现

java">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 分块传输的形式,但是却没有返回数据块的长度,导致返回结果无法解析,这里我将一并将它修复了。

java">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包下


http://www.ppmy.cn/news/1432240.html

相关文章

海康智能相机FTP本地存图流程

背景&#xff1a;近期一个新项目需要使用到智能相机&#xff0c;借助智能相机算法直接输出检测结果并将相机图像进行本地化保存和展示。由于申购目标智能相机未到&#xff0c;暂时使用测试智能相机。 目标智能相机型号&#xff1a;海康智能相机MV-SC3050XC 当前测试相机型号…

Spark面试整理-如何配置和管理Spark集群的资源,包括内存、CPU和存储

配置和管理Apache Spark集群的资源是确保高效运行Spark应用的关键步骤。合理的资源配置可以提高性能,避免资源浪费,并确保任务顺利执行。以下是配置和管理Spark集群资源(包括内存、CPU和存储)的一些指导原则: 内存管理 Executor内存设置:通过spark.executor.memory配置每…

加密、解密、签名、验签、数字证书、CA浅析

一、加密和解密 加密和解密应用的很广&#xff0c;主要作用就是防止数据或者明文被泄露。 加解密算法主要有两大类&#xff0c;对称加密和非对称加密。对称加密就是加密和解密的密钥都是一个&#xff0c;典型的有AES算法。非对称加密就是有公钥和私钥&#xff0c;公钥可以发布…

处理JavaScript中浮点数精度丢失的问题

如何处理JavaScript中浮点数精度丢失的问题 在使用JavaScript进行数学计算时&#xff0c;尤其是涉及浮点数时&#xff0c;经常会遇到精度丢失的问题。这是由于JavaScript使用IEEE 754标准的双精度浮点格式表示数字&#xff0c;这种表示方式在处理特别大或特别小的数时会不够精…

day24 java IO流 四个节点流

目录 FileInputStream FileOutputStream FileOutputStream类 FileOutputStream续写 FileInputStream类 FileWriter FileReader FileWriter写 FileWriter续写 FileWriter换行 FileWriter关闭和刷新 FileReader读 FileReader读取指定长度 当你创建一个流对象时&#x…

LeetCode 34在排序数组中查找元素的第一个和最后一个位置

LeetCode 34在排序数组中查找元素的第一个和最后一个位置 给你一个按照非递减顺序排列的整数数组nums&#xff0c;和一个目标值target。请你找出给定目标值在数组中的开始位置和结束位置。 如果数组中不存在目标值target&#xff0c;返回 [-1, -1]。 你必须设计并实现时间复…

总结一下背包里的顺序和是否逆序

1.对于01背包而言&#xff0c;一维压缩态只能物品到背包且需要逆序 2.对应多重背包而言&#xff0c;组合数物品到背包&#xff0c;排列数背包到物品&#xff0c;且都需要正序

MySQL 服务器权限与对象权限

MySQL服务器权限&#xff08;全局权限&#xff09;和对象权限&#xff08;数据库权限和表权限&#xff09;是MySQL权限体系中的两个重要组成部分&#xff0c;它们共同构成了MySQL的安全管理机制。 服务器权限&#xff08;全局权限&#xff09; 服务器权限&#xff0c;也称为全…