06 规范化:引入HttpRequest与HttpResponse
你好,我是郭屹。今天我们继续手写MiniTomcat。
在前面的学习结束之后,我们引入了池化技术,还实现Processor的异步化。前者复用对象,减少构造新对象的时间开销;后者使Connector能同时服务于多个Processor。这些都是提升性能和吞吐量的常用技术手段。
这节课我们继续研究Servlet规范,对我们现有的代码进行改造,使之适配这个规范。同时我们还要解析HTTP协议中的请求信息,并把它存储到服务器内存之中。
下面就让我们一起来动手实现。
项目结构
这节课因为需要进行Servlet规范适配工作,还要解析头部信息,因此会新增几个类,整体结构如下:
MiniTomcat
├─ src
│ ├─ main
│ │ ├─ java
│ │ │ ├─ server
│ │ │ │ ├─ DefaultHeaders.java
│ │ │ │ ├─ HttpConnector.java
│ │ │ │ ├─ HttpHeader.java
│ │ │ │ ├─ HttpProcessor.java
│ │ │ │ ├─ HttpRequest.java
│ │ │ │ ├─ HttpRequestLine.java
│ │ │ │ ├─ HttpResponse.java
│ │ │ │ ├─ HttpServer.java
│ │ │ │ ├─ Request.java
│ │ │ │ ├─ Response.java
│ │ │ │ ├─ ServletProcessor.java
│ │ │ │ ├─ SocketInputStream.java
│ │ │ │ ├─ StatisResourceProcessor.java
│ │ ├─ resources
│ ├─ test
│ │ ├─ java
│ │ │ ├─ test
│ │ │ │ ├─ HelloServlet.java
│ │ ├─ resources
├─ webroot
│ ├─ test
│ │ ├─ HelloServlet.class
│ ├─ hello.txt
├─ pom.xml
Servlet规范适配
在Servlet规范中,对Request请求和Response返回提供了对应的HTTP协议接口定义,分别是javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse。接下来我们实现定义的接口,并逐步开始解析工作。
首先,定义HttpRequest与HttpResponse,分别实现HttpServletRequest与HttpServletResponse接口。实现了这个接口之后,就会要求实现很多接口里面的方法,绝大部分我们都没有用到,所以这里我只列举出几个基本的,不过后面我也给出了完整的代码链接,你可以参考。
package server;
public class HttpRequest implements HttpServletRequest {
public Object getAttribute(String arg0) {
return null;
}
public Enumeration<String> getAttributeNames() {
return null;
}
public String getCharacterEncoding() {
return null;
}
public int getContentLength() {
return 0;
}
public String getContentType() {
return null;
}
public ServletInputStream getInputStream() throws IOException {
return null;
}
public String getParameter(String arg0) {
return null;
}
public Map<String, String[]> getParameterMap() {
return null;
}
public Enumeration<String> getParameterNames() {
return null;
}
public String[] getParameterValues(String arg0) {
return null;
}
public RequestDispatcher getRequestDispatcher(String arg0) {
return null;
}
public ServletContext getServletContext() {
return null;
}
public void setAttribute(String arg0, Object arg1) {
}
public void setCharacterEncoding(String arg0) throws UnsupportedEncodingException {
}
public Cookie[] getCookies() {
return null;
}
public String getQueryString() {
return null;
}
public String getRequestURI() {
return null;
}
public StringBuffer getRequestURL() {
return null;
}
public String getServletPath() {
return null;
}
public HttpSession getSession() {
return null;
}
public HttpSession getSession(boolean arg0) {
return null;
}
}
同样的,HttpServletResponse接口也会有很多实现方法。我们先定义,然后再开始慢慢实现,并不是每个方法都需要用到,绝大多数方法暂时可以不理会。下面是HttpResponse的定义,和前面一样,也只列出几个基本的方法。
package server;
public class HttpResponse implements HttpServletResponse {
public String getCharacterEncoding() {
return null;
}
public String getContentType() {
return null;
}
public ServletOutputStream getOutputStream() throws IOException {
return null;
}
public PrintWriter getWriter() throws IOException {
return null;
}
public void setCharacterEncoding(String arg0) {
}
public void setContentLength(int arg0) {
}
public void setContentType(String arg0) {
}
public void addCookie(Cookie arg0) {
}
public String getHeader(String arg0) {
return null;
}
public Collection<String> getHeaderNames() {
return null;
}
public Collection<String> getHeaders(String arg0) {
return null;
}
}
Request信息解析
我们期望从Servlet的Request请求中能方便地获取到Header头部的信息,所以我们只需要先关注一下定义里的几个方法:getHeader、getHeaderNames、getHeaders、getParameter、getPrameterMap、getParameterNames、getParameterValues。
在HttpRequest的实现中,我们可以用下面这个结构保存这些头信息。
protected HashMap<String, String> headers = new HashMap<>();
protected Map<String, String> parameters = new ConcurrentHashMap<>();
结构中,headers用来存储头的信息,而parameters存储参数信息,采用ConcurrentHashMap结构也是考虑到允许Servlet并发进行增加删除修改操作。
在正式开始解析这些参数信息之前,我们先考虑一个问题:什么时候解析?目前可以考虑两种方案:一是在Connector中预先全部解析完毕,但这种方式性价比并不高,因为Header信息有数十个,甚至上百个,而一个Servlet中实际用到的可能只有寥寥数个;另外一种是在Servlet实际读取Request的时候,根据Request传入的Header信息每次都进行解析。
因此我们可以考虑做出选择:在Connector启动后,解析头部信息,而参数信息则在Servlet需要使用时再解析。放在不同的时间点解析头部和参数信息,主要是为了提高效率。这个选择是参考了Tomcat的做法。
下面我们就开始着手 解析Request。
在此之前我们都是用InputStream进行解析,以字节为单位依次读取,效率不高。这里我们自己准备一个类——SocketInputStream,直接按行读取,获取Request line与Header。
首先定义HttpRequestLine类。这个类是Http Request头行的抽象,格式是method uri protocol,如 GET /hello.txt HTTP/1.1
。
package server;
public class HttpRequestLine {
public static final int INITIAL_METHOD_SIZE = 8;
public static final int INITIAL_URI_SIZE = 128;
public static final int INITIAL_PROTOCOL_SIZE = 8;
public static final int MAX_METHOD_SIZE = 32;
public static final int MAX_URI_SIZE = 2048;
public static final int MAX_PROTOCOL_SIZE = 32;
//下面的属性对应于Http Request规范,即头行格式method uri protocol
//如:GET /hello.txt HTTP/1.1
//char[] 存储每段的字符串,对应的int值存储的是每段的结束位置
public char[] method;
public int methodEnd;
public char[] uri;
public int uriEnd;
public char[] protocol;
public int protocolEnd;
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;
}
public void recycle() {
methodEnd = 0;
uriEnd = 0;
protocolEnd = 0;
}
public int indexOf(char[] buf) {
return indexOf(buf, buf.length);
}
//这是主要的方法
//在uri[]中查找字符串buf的出现位置
public int indexOf(char[] buf, int end) {
char firstChar = buf[0];
int pos = 0; //pos是查找字符串buf在uri[]中的开始位置
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;
}
public int indexOf(String str) {
return indexOf(str.toCharArray(), str.length());
}
//在uri[]中查找字符c的出现位置
public int indexOf(char c, int start) {
for (int i = start; i < uriEnd; i++) {
if (uri[i] == c) {
return i;
}
}
return -1;
}
}
HttpRequestLine里,包含method、uri和protocol等信息,这是根据HTTP协议来定义的。在实现的过程中,用的数据结构是一个char数组,这样可以从inputstream中直接读取下来,不用经过String的转换,提高了效率,但也因此要提供xxxEnd几个属性来标识最后一个字符的位置,程序的复杂度提高了一些。
这个工具类里面提供了 indexOf()
方法来匹配uri,实现方法是char数组的双重循环匹配。具体可以看上面代码里的注释。
接下来再定义HttpHeader类。
package server;
public class HttpHeader {
public static final int INITIAL_NAME_SIZE = 64;
public static final int INITIAL_VALUE_SIZE = 512;
public static final int MAX_NAME_SIZE = 128;
public static final int MAX_VALUE_SIZE = 1024;
public char[] name;
public int nameEnd;
public char[] value;
public int valueEnd;
protected int hashCode = 0;
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();
}
public void recycle() {
nameEnd = 0;
valueEnd = 0;
hashCode = 0;
}
}
根据上面的实现可以看到,HttpRequestLine里包含method、uri与protocol,HttpHeader中包含name与value。实现的类里面还包含xxxEnd类型的域定义,用来标识某一属性最后的位置。
在HttpRequestLine与HttpHeader类定义完毕之后,我们可以在SocketInputStream中定义两个方法,直接获取Request line与Headers。
public void readRequestLine(HttpRequestLine requestLine){}
public void readHeader(HttpHeader header){}
可以回忆一下我们之前解析Header首行的经过:按行读取成一个字符串,依据空格分隔,获取method、uri和protocol。现在我们参考Tomcat,在SocketInputStream里提供一个更专业、更高效的实现。
protected void fill() throws IOException {
pos = 0;
count = 0;
int nRead = is.read(buf, 0, buf.length);
if (nRead > 0) {
count = nRead;
}
}
上述代码显示,fill方法会从InputStream里读取一批字节,存储到byte数组的buf属性中。
而 fill()
被调用到 read()
方法内,每次从buf中读到当前的字节返回,如果 pos >= count
表示当前的byte已获取完毕,内部就调用fill方法获取新的字节流。因此,对上层程序员来说,使用 read()
就相当于可以连续读取缓存中的数据。
read方法实现如下:
@Override
public int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count) {
return -1;
}
}
return buf[pos++] & 0xff;
}
其中这个方法返回-1表示读取出错。
有了这些铺垫,我们再来看SocketInputStream的完整实现。简单地说,这个类的作用就是从输入流中读出request line和header信息来。
package server;
public class SocketInputStream extends InputStream {
private static final byte CR = (byte) '\r';
private static final byte LF = (byte) '\n';
private static final byte SP = (byte) ' ';
private static final byte HT = (byte) '\t';
private static final byte COLON = (byte) ':';
private static final int LC_OFFSET = 'A' - 'a';
protected byte buf[];
protected int count;
protected int pos;
protected InputStream is;
public SocketInputStream(InputStream is, int bufferSize) {
this.is = is;
buf = new byte[bufferSize];
}
//从输入流中解析出request line
public void readRequestLine(HttpRequestLine requestLine)
throws IOException {
int chr = 0;
//跳过空行
do {
try {
chr = read();
} catch (IOException e) {
}
} while ((chr == CR) || (chr == LF));
//第一个非空位置
pos--;
int maxRead = requestLine.method.length;
int readStart = pos;
int readCount = 0;
boolean space = false;
//解析第一段method,以空格结束
while (!space) {
if (pos >= count) {
int val = read();
if (val == -1) {
throw new IOException("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; //method段的结束位置
maxRead = requestLine.uri.length;
readStart = pos;
readCount = 0;
space = false;
boolean eol = false;
//解析第二段uri,以空格结束
while (!space) {
if (pos >= count) {
int val = read();
if (val == -1)
throw new IOException("requestStream.readline.error");
pos = 0;
readStart = 0;
}
if (buf[pos] == SP) {
space = true;
}
requestLine.uri[readCount] = (char) buf[pos];
readCount++;
pos++;
}
requestLine.uriEnd = readCount - 1; //uri结束位置
maxRead = requestLine.protocol.length;
readStart = pos;
readCount = 0;
//解析第三段protocol,以eol结尾
while (!eol) {
if (pos >= count) {
int val = read();
if (val == -1)
throw new IOException("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;
}
public void readHeader(HttpHeader header)
throws IOException {
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--;
}
// 正在读取 header name
int maxRead = header.name.length;
int readStart = pos;
int readCount = 0;
boolean colon = false;
while (!colon) {
// 我们处于内部缓冲区的末尾
if (pos >= count) {
int val = read();
if (val == -1) {
throw new IOException("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;
// 读取 header 值(可以跨越多行)
maxRead = header.value.length;
readStart = pos;
readCount = 0;
int crPos = -2;
boolean eol = false;
boolean validLine = true;
while (validLine) {
boolean space = true;
// 跳过空格
// 注意:仅删除前面的空格,后面的不删。
while (space) {
// 我们已经到了内部缓冲区的尽头
if (pos >= count) {
// 将内部缓冲区的一部分(或全部)复制到行缓冲区
int val = read();
if (val == -1)
throw new IOException("requestStream.readline.error");
pos = 0;
readStart = 0;
}
if ((buf[pos] == SP) || (buf[pos] == HT)) {
pos++;
} else {
space = false;
}
}
while (!eol) {
// 我们已经到了内部缓冲区的尽头
if (pos >= count) {
// 将内部缓冲区的一部分(或全部)复制到行缓冲区
int val = read();
if (val == -1)
throw new IOException("requestStream.readline.error");
pos = 0;
readStart = 0;
}
if (buf[pos] == CR) {
} else if (buf[pos] == LF) {
eol = true;
} else {
// FIXME:检查二进制转换是否正常
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;
header.value[readCount] = ' ';
readCount++;
}
}
header.valueEnd = readCount;
}
@Override
public int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count) {
return -1;
}
}
return buf[pos++] & 0xff;
}
public int available() throws IOException {
return (count - pos) + is.available();
}
public void close() throws IOException {
if (is == null) {
return;
}
is.close();
is = null;
buf = null;
}
protected void fill() throws IOException {
pos = 0;
count = 0;
int nRead = is.read(buf, 0, buf.length);
if (nRead > 0) {
count = nRead;
}
}
}
从代码可以看出,Tomcat在这里就是用的最简单的扫描方法,从inputstream里读一个个的字节,放到buf里,buf有长度限制,读到尾后就从头再来。然后从buf里一个字节一个字节地判断,pos变量代表当前读取的位置,根据协议规定的分隔符解析到requestline和header里。
拥有了工具类后,Request内获取Request line就比较容易了,在HttpRequest中定义parse方法,直接用SocketInputStream解析。
public void parse(Socket socket) {
this.sis.readRequestLine(requestLine);
this.uri = new String(requestLine.uri, 0, requestLine.uriEnd);
}
这个时候HttpProcessor中就不用Request类构建对象了,改用HttpRequest。
所以ServletProcessor和StaticResourceProcessor里的process方法,传入的Request类型参数都改成HttpRequest。
同理,我们来解析Header。和前面一样,在SocketInputStream里已定义了 readHeader(HttpHeader header)
方法,将头信息读入header属性中。我们先定义DefaultHeaders常量类,假设支持下面这些头信息。
package server;
public class DefaultHeaders {
static final String HOST_NAME = "host";
static final String CONNECTION_NAME = "connection";
static final String ACCEPT_LANGUAGE_NAME = "accept-language";
static final String CONTENT_LENGTH_NAME = "content-length";
static final String CONTENT_TYPE_NAME = "content-type";
static final String TRANSFER_ENCODING_NAME = "transfer-encoding";
}
接下来在HttpRequest中可定义parseHeaders方法解析头信息。
private void parseHeaders() throws IOException, ServletException {
while (true) {
HttpHeader header = new HttpHeader();
sis.readHeader(header);
if (header.nameEnd == 0) {
if (header.valueEnd == 0) {
return;
} else {
throw new ServletException("httpProcessor.parseHeaders.colon");
}
}
String name = new String(header.name,0,header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
// Set the corresponding request headers
if (name.equals(DefaultHeaders.ACCEPT_LANGUAGE_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.CONTENT_LENGTH_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.CONTENT_TYPE_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.HOST_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.CONNECTION_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.TRANSFER_ENCODING_NAME)) {
headers.put(name, value);
} else {
headers.put(name, value);
}
}
目前实现也比较简单,判断是否是我们支持的头信息,支持后直接存入headers这个map数据结构里。parse方法则支持解析Request Line与Header。
综合以上内容,我们来重新实现HttpRequest类,HttpServletRequest接口的那些现在没有用到的方法暂时不列出来。
package server;
public class HttpRequest implements HttpServletRequest {
private InputStream input;
private SocketInputStream sis;
private String uri;
InetAddress address;
int port;
protected HashMap<String, String> headers = new HashMap<>();
protected Map<String, String> parameters = new ConcurrentHashMap<>();
HttpRequestLine requestLine = new HttpRequestLine();
public HttpRequest(InputStream input) {
this.input = input;
this.sis = new SocketInputStream(this.input, 2048);
}
public void parse(Socket socket) {
try {
parseConnection(socket);
this.sis.readRequestLine(requestLine);
parseHeaders();
} catch (IOException e) {
e.printStackTrace();
} catch (ServletException e) {
e.printStackTrace();
}
this.uri = new String(requestLine.uri, 0, requestLine.uriEnd);
}
private void parseConnection(Socket socket) {
address = socket.getInetAddress();
port = socket.getPort();
}
private void parseHeaders() throws IOException, ServletException {
while (true) {
HttpHeader header = new HttpHeader();
sis.readHeader(header);
if (header.nameEnd == 0) {
if (header.valueEnd == 0) {
return;
} else {
throw new ServletException("httpProcessor.parseHeaders.colon");
}
}
String name = new String(header.name,0,header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
// 设置相应的请求头
if (name.equals(DefaultHeaders.ACCEPT_LANGUAGE_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.CONTENT_LENGTH_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.CONTENT_TYPE_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.HOST_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.CONNECTION_NAME)) {
headers.put(name, value);
} else if (name.equals(DefaultHeaders.TRANSFER_ENCODING_NAME)) {
headers.put(name, value);
} else {
headers.put(name, value);
}
}
}
public String getUri() {
return this.uri;
}
}
可以看到这个HttpRequest已经相对简单了。
测试
我们这一节课的实现只是内部的程序结构变动,并不影响用户的Servlet和客户端的访问。所以还是可以沿用上一节课的测试办法进行验证,这里我就不重复说了。
小结
这节课我们主要做了两件事:一是进一步适配Servlet规范,实现HttpServletRequest和HttpServletResponse两个接口。我们看到接口中有很多方法,目前我们绝大部分都没有实现,是留空状态,只是先实现最基本的几个方法就可以让程序运行起来了。
二是引入SocketInputStream,按行读取Request信息,解析Request line与header信息,提高效率和性能,摒弃了我们原有的解析模式。我们可以看到这是一种更加高效的实现办法。
这两件事情本身并没有扩展我们服务器的功能,目的是符合规范并且高效,这一点我们要向Tomcat学习。
本节课完整代码参见: https://gitee.com/yaleguo1/minit-learning-demo/tree/geek_chapter06
思考题
学完了这节课的内容,我们来思考一个问题:我们在 SocketInputStream的read()
方法中,用到了ketInputStream的read()方法中,用到了 buf[pos++] & 0xff
这个操作,它的作用是什么?
欢迎你把你思考后的结果分享到评论区,也欢迎你把这节课的内容分享给其他朋友,我们下节课再见!