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 {
// -------------------------------------------------------------- Constants
public 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;
// ----------------------------------------------------------- Constructors
public 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 Variables
public 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 Methods
public int hashCode() {
// FIXME
return 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 {
// -------------------------------------------------------------- Constants
public 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;
// ----------------------------------------------------------- Constructors
public 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 Variables
public 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 check
if (requestLine.methodEnd != 0) requestLine.recycle();
// Checking for a blank line
int chr;
// Skipping CR or LF
do {
try {
chr = read();
} catch (IOException e) {
chr = -1;
}
} while ((chr == CR) || (chr == LF));
if (chr == -1) throw new EOFException(sm.getString("requestStream.readline.error"));
pos--;
// Reading the method name
int maxRead = requestLine.method.length;
int readStart = pos;
int readCount = 0;
boolean space = false;
while (!space) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpRequestLine.MAX_METHOD_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(requestLine.method, 0, newBuffer, 0, maxRead);
requestLine.method = newBuffer;
maxRead = requestLine.method.length;
} else {
throw new IOException(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
int val = read();
if (val == -1) {
throw new IOException(sm.getString("requestStream.readline.error"));
}
pos = 0;
readStart = 0;
}
if (buf[pos] == SP) {
space = true;
}
requestLine.method[readCount] = (char) buf[pos];
readCount++;
pos++;
}
requestLine.methodEnd = readCount - 1;
// Reading URI
maxRead = requestLine.uri.length;
readStart = pos;
readCount = 0;
space = false;
boolean eol = false;
while (!space) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpRequestLine.MAX_URI_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(requestLine.uri, 0, newBuffer, 0, maxRead);
requestLine.uri = newBuffer;
maxRead = requestLine.uri.length;
} else {
throw new IOException(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
int val = read();
if (val == -1) throw new IOException(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if (buf[pos] == SP) {
space = true;
} else if ((buf[pos] == CR) || (buf[pos] == LF)) {
// HTTP/0.9 style request
eol = true;
space = true;
}
requestLine.uri[readCount] = (char) buf[pos];
readCount++;
pos++;
}
requestLine.uriEnd = readCount - 1;
// Reading protocol
maxRead = requestLine.protocol.length;
readStart = pos;
readCount = 0;
while (!eol) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpRequestLine.MAX_PROTOCOL_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(requestLine.protocol, 0, newBuffer, 0, maxRead);
requestLine.protocol = newBuffer;
maxRead = requestLine.protocol.length;
} else {
throw new IOException(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
// Copying part (or all) of the internal buffer to the line
// buffer
int val = read();
if (val == -1) throw new IOException(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if (buf[pos] == CR) {
// Skip CR.
} else if (buf[pos] == LF) {
eol = true;
} else {
requestLine.protocol[readCount] = (char) buf[pos];
readCount++;
}
pos++;
}
requestLine.protocolEnd = readCount;
}
/**
* 读取header,并将其复制到给定的缓冲区。其实就是从InputStream中解析出下一个请求头的信息,填充进HttpHeader对象
* 该函数将在HTTP请求头解析期间使用。不要试图使用它来读取请求体。
*
* @param header HttpHeader 对象
* @throws IOException 如果在底层套接字读取操作期间发生异常,或者给定的缓冲区不够大,无法容纳整行。
*/
public void readHeader(HttpHeader header) throws IOException {
// Recycling check
if (header.nameEnd != 0) header.recycle();
// Checking for a blank line
int chr = read();
if ((chr == CR) || (chr == LF)) { // Skipping CR
if (chr == CR) read(); // Skipping LF
header.nameEnd = 0;
header.valueEnd = 0;
return;
} else {
pos--;
}
// Reading the header name
int maxRead = header.name.length;
int readStart = pos;
int readCount = 0;
boolean colon = false;
while (!colon) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpHeader.MAX_NAME_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(header.name, 0, newBuffer, 0, maxRead);
header.name = newBuffer;
maxRead = header.name.length;
} else {
throw new IOException(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
int val = read();
if (val == -1) {
throw new IOException(sm.getString("requestStream.readline.error"));
}
pos = 0;
readStart = 0;
}
if (buf[pos] == COLON) {
colon = true;
}
char val = (char) buf[pos];
if ((val >= 'A') && (val <= 'Z')) {
val = (char) (val - LC_OFFSET);
}
header.name[readCount] = val;
readCount++;
pos++;
}
header.nameEnd = readCount - 1;
// Reading the header value (which can be spanned over multiple lines)
maxRead = header.value.length;
readStart = pos;
readCount = 0;
int crPos = -2;
boolean eol = false;
boolean validLine = true;
while (validLine) {
boolean space = true;
// Skipping spaces
// Note : Only leading white spaces are removed. Trailing white
// spaces are not.
while (space) {
// We're at the end of the internal buffer
if (pos >= count) {
// Copying part (or all) of the internal buffer to the line
// buffer
int val = read();
if (val == -1) throw new IOException(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if ((buf[pos] == SP) || (buf[pos] == HT)) {
pos++;
} else {
space = false;
}
}
while (!eol) {
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpHeader.MAX_VALUE_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(header.value, 0, newBuffer, 0, maxRead);
header.value = newBuffer;
maxRead = header.value.length;
} else {
throw new IOException(sm.getString("requestStream.readline.toolong"));
}
}
// We're at the end of the internal buffer
if (pos >= count) {
// Copying part (or all) of the internal buffer to the line
// buffer
int val = read();
if (val == -1) throw new IOException(sm.getString("requestStream.readline.error"));
pos = 0;
readStart = 0;
}
if (buf[pos] == CR) {
} else if (buf[pos] == LF) {
eol = true;
} else {
// FIXME : Check if binary conversion is working fine
int ch = buf[pos] & 0xff;
header.value[readCount] = (char) ch;
readCount++;
}
pos++;
}
int nextChr = read();
if ((nextChr != SP) && (nextChr != HT)) {
pos--;
validLine = false;
} else {
eol = false;
// if the buffer is full, extend it
if (readCount >= maxRead) {
if ((2 * maxRead) <= HttpHeader.MAX_VALUE_SIZE) {
char[] newBuffer = new char[2 * maxRead];
System.arraycopy(header.value, 0, newBuffer, 0, maxRead);
header.value = newBuffer;
maxRead = header.value.length;
} else {
throw new IOException(sm.getString("requestStream.readline.toolong"));
}
}
header.value[readCount] = ' ';
readCount++;
}
}
header.valueEnd = readCount;
}
/**
* 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 string
String queryString = getQueryString();
try {
RequestUtil.parseParameters(results, queryString, encoding);
}
catch (UnsupportedEncodingException e) {
;
}
// Parse any parameters specified in the input stream
String contentType = getContentType();
if (contentType == null)
contentType = "";
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
}
else {
contentType = contentType.trim();
}
if ("POST".equals(getMethod()) && (getContentLength() > 0)
&& "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 results
results.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 = "";
else
this.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 formats
for (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));
else
return 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));
else
return (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);
else
return (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]);
else
return (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);
else
return 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 size
private 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 mechanism
if (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: // WebDAV
return ("Multi-Status");
case 422: // WebDAV
return ("Unprocessable Entity");
case 423: // WebDAV
return ("Locked");
case 507: // WebDAV
return ("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 writer
OutputStreamWriter osr = null;
try {
osr = new OutputStreamWriter(getStream(), getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
osr = new OutputStreamWriter(getStream());
}
final PrintWriter outputWriter = new PrintWriter(osr);
// Send the "Status:" header
outputWriter.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);
else
cookie.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 headers
outputWriter.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 there
if (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 chunks
flushBuffer();
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 servlet
synchronized (cookies) {
cookies.add(cookie);
}
}
public void addDateHeader(String name, long value) {
if (isCommitted()) return;
// if (included)
// return; // Ignore any call from an included servlet
addHeader(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 servlet
synchronized (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 servlet
addHeader(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 servlet
this.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 servlet
setHeader(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 servlet
ArrayList 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 servlet
setHeader(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/servletName
if (request.getRequestURI().startsWith("/servlet/")) {
ServletProcessor processor = new ServletProcessor();
processor.process(request, response);
} else {
StaticResourceProcessor processor = new StaticResourceProcessor();
processor.process(request, response);
}
// 关闭 socket
socket.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();
// 读取下一个header
input.readHeader(header);
if (header.nameEnd == 0) {
if (header.valueEnd == 0) {
return;
} else {
throw new ServletException(sm.getString("httpProcessor.parseHeaders.colon"));
}
}
String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
request.addHeader(name, value);
// do something for some headers, ignore others.
if (name.equals("cookie")) {
// 解析出所有cookie
Cookie cookies[] = RequestUtil.parseCookieHeader(value);
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("jsessionid")) {
// Override anything requested in the URL
if (!request.isRequestedSessionIdFromCookie()) {
// Accept only the first session id cookie
request.setRequestedSessionId(cookies[i].getValue());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
}
}
request.addCookie(cookies[i]);
}
} 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 line
if (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,并解析出真正的URI
int question = requestLine.indexOf("?");
if (question >= 0) {
request.setQueryString(new String(requestLine.uri, question + 1, requestLine.uriEnd - question - 1));
uri = new String(requestLine.uri, 0, question);
} else {
request.setQueryString(null);
uri = new String(requestLine.uri, 0, requestLine.uriEnd);
}
// 判断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=xxx
String match = ";jsessionid=";
int semicolon = uri.indexOf(match);
if (semicolon >= 0) {
String rest = uri.substring(semicolon + match.length());
int semicolon2 = rest.indexOf(';');
if (semicolon2 >= 0) {
request.setRequestedSessionId(rest.substring(0, semicolon2));
rest = rest.substring(semicolon2);
} else {
request.setRequestedSessionId(rest);
rest = "";
}
request.setRequestedSessionURL(true);
uri = uri.substring(0, semicolon) + rest;
} else {
request.setRequestedSessionId(null);
request.setRequestedSessionURL(false);
}
// 标准化 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 path
String 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
// characters
if ((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 necessary
if (normalized.indexOf('\\') >= 0)
normalized = normalized.replace('\\', '/');
if (!normalized.startsWith("/"))
normalized = "/" + normalized;
// Resolve occurrences of "//" in the normalized path
while (true) {
int index = normalized.indexOf("//");
if (index < 0)
break;
normalized = normalized.substring(0, index) + normalized.substring(index + 1);
}
// Resolve occurrences of "/./" in the normalized path
while (true) {
int index = normalized.indexOf("/./");
if (index < 0)
break;
normalized = normalized.substring(0, index) + normalized.substring(index + 2);
}
// Resolve occurrences of "/../" in the normalized path
while (true) {
int index = normalized.indexOf("/../");
if (index < 0)
break;
if (index == 0)
return (null); // Trying to go outside our context
int 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 completed
return (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包下