连接器
ps:本篇详细讲述对http协议的解析。
从main方法为入口,顺一下流程:
1.使用启动类进行启动,启动类会创建连接器,并启动连接器的监听线程。
2.连接器的线程在while循环中执行如下操作:
- 等待HTTP请求;
- 为每个请求创建一个HttpProcessor实例;
- 调用HttpProcessor对象的process()方法。
3.HttpProcessor类的process()方法接受来自传入的HTTP请求的套接字。对每个传入的HTTP请求进行一下操作:
- 创建一个HttpRequest对象;
- 创建一个HttpResponse对象;
- 解析HTTP请求的第一行内容和请求头信息,填充HttpRequest对象;
- 将HttpRequest对象和HttpResponse对象传递给ServletProcessor或staticResourceProcessor的process()方法。
大体过程如上所述,接下来解析HTTP请求(本次解析的是tomcat4源码,虽然是个老古董,源码也是费了好大劲才找到,但用来学习,理解HTTP协议足够了)
①读取套接字的输入流
使用SocketInputStream类来进行解析。
SocketInputStream input = new SocketInputStream(socket.getInputStream(), 2048);
②解析请求行
HttpProcessor类的process()方法会调用私有方法parseRequest()方法来解析请求行:
GET /myApp/Modernservlet?userName=tarzan&password=pwd HTTP/1.1
说白了就是parseRequest()方法会从这一串里边解析出有用的东西填充到request对象里边去。
<1>先调用SocketInputStream类的readRequestLine()方法
input.readRequestLine(requestLine);
requestLine变量是HttpRequestLine的实例。
<2>调用该方法,将SocketInputStream对象中的信息填入到HttpRequestLine实例。然后获取请求方法,URI和请求协议和版本信息:
String method = new String(requestLine.method, 0, requestLine.methodEnd);
String uri = null;
String protocol = new String(requestLine.protocol, 0, requestLine.protocolEnd);
<3>URI后边可能还会跟随有查询字符串。若有,则与URI使用一个问号分隔的。因此会先解析该查询字符串并填充request对象。
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);
}
<4>大多数URI都是指向一个相对路径,也有指向绝对路径的,所以会进行如下检查:
if (!uri.startsWith("/")) {
int pos = uri.indexOf("://");
/** 解析协议和主机名 */
if (pos != -1) {
pos = uri.indexOf('/', pos + 3);
if (pos == -1) {
uri = "";
} else {
uri = uri.substring(pos);
}
}
}
<5>然后查询字符串里边可能会包含一个会话标识符,参数名为jsessionid。因此还要检查是否包含jsessionid,如果包含,则说明会话标识符在查询字符串中,而不再cookie中,要获取其值并填充到HttpResquest实例。
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);
}
<6>然后,会将URI传到normalize()方法中进行修正,对不正常的地方进行修正。比如:出现“\”的地方会被替换为"/"。所以我们在输入的时候“\“ ”/"都可以就是在此做了相关处理。如果无法修正,则返回null。parseRequest()方法就会抛出异常。
String normalizedUri = normalize(uri);
<7>最后,parseRequest()方法会设置HttpRequest对象的一些属性。
((HttpRequest) request).setMethod(method);
request.setProtocol(protocol);
if (normalizedUri != null) {
((HttpRequest) request).setRequestURI(normalizedUri);
} else {
((HttpRequest) request).setRequestURI(uri);
}
if (normalizedUri == null) {
throw new ServletException("Invalid URI: " + uri + "'");
}
③解析请求头
parseHeader()方法中有一个while循环,不断从SocketinputStream中读取请求头信息,直到信息全部读完。
<1>可以通过HttpHeader类的无参构造来创建一个请求头实例;
HttpHeader header = new HttpHeader();
<2>创建了之后,将其传递给SocketInputStream类的readHeader()方法。若有请求头信息可以读取,readHeader()方法会相应的填充HttpHeader对象。若没有,则将nameEnd和valueEnd字段都设为0;
input.readHeader(header);
if (header.nameEnd == 0) {
if (header.valueEnd == 0) {
return;
} else {
throw new ServletException(sm.getString("httpProcessor.parseHeaders.colon"));
}
}
<3>如果存在请求头,则获取其名字和值:
String name = new String(header.name, 0, header.nameEnd);
String value = new String(header.value, 0, header.valueEnd);
<4>当获取到了请求头的名称和值之后,就可以调用HttpRequest对象的addHeader()方法,将其添加到HttpRequest对象的hashMap请求头中去:
request.addHeader(name, value);
<5>某些请求头包含一些属性设置信息,例如:当调用ServletRequest类的getContentLength()方法时,会返回请求头”content-length“的值,而请求头”cookies“中是一些Cookie的集合:
if (name.equals("cookie")) {
/** 将value解析成cookie数组 */
Cookie cookies[] = RequestUtil.parseCookieHeader(value);
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("jsessionid")) {
/** 判断cookie名是否为JSESSIONID */
if (!request.isRequestedSessionIdFromCookie()) {
/** 只接受第一个session ID */
request.setRequestedSessionId(cookies[i].getValue());
request.setRequestedSessionCookie(true);
request.setRequestedSessionURL(false);
}
}
/** 添加cookie到request对象 */
request.addCookie(cookies[i]);
}
/** content-length头 */
} 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);
}
④解析Cookie
Cookie是由浏览器作为HTTP请求头的一部分发送的。这样的请求头名称是”cookie“,其对应值是一些键值对。下面是一个Cookie请求头的例子,其中包括两个Cookie:userName和password。
Cookie:userName=budi; password=pwd;
对Cookie的解析是通过RequestUtil类的parseCookieHeader()方法完成的。该方法接受Cookie请求头,返回Cookie类型的一个数组。
public static Cookie[] parseCookieHeader(String header) {
if ((header == null) || (header.length() < 1))
return (new Cookie[0]);
ArrayList cookies = new ArrayList();
while (header.length() > 0) {
int semicolon = header.indexOf(';');
if (semicolon < 0)
semicolon = header.length();
if (semicolon == 0)
break;
String token = header.substring(0, semicolon);
if (semicolon < header.length())
header = header.substring(semicolon + 1);
else
header = "";
try {
int equals = token.indexOf('=');
if (equals > 0) {
String name = token.substring(0, equals).trim();
String value = token.substring(equals+1).trim();
cookies.add(new Cookie(name, value));
}
} catch (Throwable e) {
;
}
}
return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()]));
}
⑤获取参数
在HttpRequest类中,所有与参数有关的方法的实现都会调用parseParameter()方法。参数只会解析一次,因为在请求体中包含参数,解析参数的工作会使SocketInputStream类读完整个字节流。HttpRequest类使用一个名为parsed的布尔值来标识是否已经完成对参数的解析。
参数可以出现在查询字符串或请求体中。若用户使用GET方法请求servlet,则所有的参数都会在查询字符串中;若用户使用POST方法请求servlet,则请求体中可能也会有参数。
这里使用了一个特殊的Map来存储参数名及其值。程序员可以将其作为一个对象获取参数和值,但不能修改它。ParamenterMap,继承了HashMap。其中有个名为locked的布尔值。只有当变量locked值为false时,才可以对ParameterMap对象中的名/值对进行添加,更新或者删除操作。否则会抛出异常。但是可以在任意时刻读取其值。
下面来看一下parseParameters()是如何工作的:
<1>由于参数可以存在于查询字符串或HTTP请求体中,因此parseParameters()方法必须对两者都进行检查。当解析完成后,参数会存储到变量parameters中,所以parseParameters()方法首先会检查布尔值parsed,若该变量为true,则该方法直接返回。
if (parsed)
return;
<2>然后,parseParameters()方法会创建一个名为results的ParameterMap类型的变量,将其指向变量parameters。若变量parameters为null,则parseParameters()方法会new一个ParameterMap对象,然后parseParameters()方法打开parameterMap对象的锁,使其可写。
ParameterMap results = parameters;
if (results == null)
results = new ParameterMap();
results.setLocked(false);
<3>接下来parseParameters()方法检查字符串encoding,若encoding为null,则使用默认编码。
String encoding = getCharacterEncoding();
if (encoding == null)
encoding = "ISO-8859-1";
<4>然后parseParameters()方法会对参数进行解析,解析的工作会调用RequestUtil类的parseParameters()方法完成。
String queryString = getQueryString();
try {
RequestUtil.parseParameters(results, queryString, encoding);
} catch (UnsupportedEncodingException e) {
;
}
<5>接着,parseParameters()方法会检查HTTP请求体是否包含请求参数。若用户使用POST方法提交请求时,请求体会包含参数,则请求头”content-length“的值会大于0,”content-type“的值为”application/x-www-form-urlencoded“。下边的代码用于解释请求体:
String contentType = getContentType();
if (contentType == null)
contentType = "";
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("POST".equals(getMethod()) && (getContentLength() > 0)
&& (this.stream == null) && "application/x-www-form-urlencoded".equals(contentType)) {
try {
int max = getContentLength();
int len = 0;
byte buf[] = new byte[getContentLength()];
ServletInputStream is = getInputStream();
while (len < max) {
int next = is.read(buf, len, max - len);
if (next < 0 ) {
break;
}
len += next;
}
is.close();
if (len < max) {
throw new RuntimeException("Content length mismatch");
}
RequestUtil.parseParameters(results, buf, encoding);
} catch (UnsupportedEncodingException ue) {
;
} catch (IOException e) {
throw new RuntimeException(sm.getString("httpRequestBase.contentReadFail") +
e.getMessage());
}
}
<6>最后,parseParameters()方法会锁定ParameterMap对象,将布尔值locked设置为true将变量results赋值给变量parameters:
results.setLocked(true);
parsed = true;
parameters = results;