how tomcat works[3] 连接器(源码级http协议解析)

连接器

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;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值