Tomcat作为Servlet容器,其最重要的应用仍然是处理web请求。
这里就沿着Web请求处理这条线,继续探究Tomcat。
当Connector接收到请求后,会调用CoyoteAdapter.service(),这里选取重要的部分:
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
...
try {
// Parse and set Catalina and configuration specific
// request parameters
postParseSuccess = postParseRequest(req, request, res, response);
...
}
}
}
根据请求org.apache.coyote.Request,创建了org.apache.catalina.connector.Request;
根据响应org.apache.coyote.Response,创建了org.apache.catalina.connector.Response。
然后是postParseRequest方法。该方法是对请求解析的重要方法,非常冗长,我们选取重要的部分分析。
首先会对请求的URL进行合法性校验,若请求不合法,直接返回400错误,这里可以说的不多,大多数的工作都在做字符的匹配。
然后定义了三个重要的局部变量:
// Version for the second mapping loop and
// Context that we expect to get for that version
String version = null;
Context versionContext = null;
boolean mapRequired = true;
- version:需要匹配的二次循环的版本号,初始化为null
- versionContext:用于暂存按照会话ID匹配的Context,初始化为null
- mapRequired:是否需要映射,控制循环,初始化为true
之后以while(mapRequired)开启循环。
// This will map the the latest version by default
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
这里调用Mapper.map()方法对URI进行解析。具体的解析过程在后面分析。
初次调用时,由于version为空,这里会匹配到所有的版本,并在MappingData.contexts中存放所有结果,并在MappingData.context里存放最新版本。
如果没有匹配到任何结果,返回404错误:
// If there is no context at this point, either this is a 404
// because no ROOT context has been deployed or the URI was invalid
// so no context could be mapped.
if (request.getContext() == null) {
// Don't overwrite an existing error
if (!response.isError()) {
response.sendError(404, "Not found");
}
// Allow processing to continue.
// If present, the error reporting valve will provide a response
// body.
return true;
}
如果有匹配到结果,则继续。尝试从URL、Cookie、SSL会话中获取会话ID,即sessionID。接着使用sessionID寻找匹配的最新版本。
Context[] contexts = request.getMappingData().contexts;
// Single contextVersion means no need to remap
// No session ID means no possibility of remap
if (contexts != null && sessionID != null) {
// Find the context associated with the session
for (int i = contexts.length; i > 0; i--) {
Context ctxt = contexts[i - 1];//递减,查找最新版本
if (ctxt.getManager().findSession(sessionID) != null) {
// We found a context. Is it the one that has
// already been mapped?
if (!ctxt.equals(request.getMappingData().context)) {
// Set version so second time through mapping
// the correct context is found
version = ctxt.getWebappVersion();
versionContext = ctxt;
// Reset mapping
request.getMappingData().recycle();
mapRequired = true;
// Recycle cookies and session info in case the
// correct context is configured with different
// settings
request.recycleSessionInfo();
request.recycleCookieInfo(true);
}
break;
}
}
}
初次匹配时version和versionContext为空。
若sessionID为空,则MappingData.context即为匹配结果。
若sessionID不为空,则从MappingData.contexts中查找包含sessionID的最新版本。
- 从MappingData.contexts找到sessionID,且与从URL、Cookie、SSL会话中获取会话ID相同,则使用此结果;
- 从MappingData.contexts找到sessionID,且与从URL、Cookie、SSL会话中获取会话ID不相同,则将version设置为查询版本,versionContext设置为查询结果,再次开启循环。
Tomcat会确保找到的Context符合以下要求:
- 匹配请求路径
- 如果有有效会话,包含有效会话的最新版本
- 如果没有有效会话,则为所有匹配请求的最新版本
- Context必须有效(非暂停)
继续探讨Mapper.map()方法对URI进行解析的过程。该方法会调用Mapper.internalMap()方法作为解析主方法。
Mapper类对Host、Context、Wrapper进行了封装,匹配的返回结果放置于MappingData中。
解析开始,首先是查找Host。
这里我以http://localhost:8080/hello,http://127.0.0.1:8080/hello 为例分保进行解析,发现:
- 以localhost为请求域名的,会直接找到localhost的对应host;
- 以127.0.0.1为请求地址的,会找不到对应的host,并使用默认的defaultHost。
找到Host之后,更新mappingData:
mappingData.host = mappedHost.object;
接着从host中取出Context集合:
// Context mapping
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
要注意的是,取出的Context集合contexts是有序的。资料上显示是按照名称的ASCII正序排列。然后查找uri所对应的context的位置:
int pos = find(contexts, uri);
find(map, name, name.getStart(), name.getEnd());
Mapper.find()方法如下:
/**
* Find a map element given its name in a sorted array of map elements.
* This will return the index for the closest inferior or equal item in the
* given array.
*/
private static final <T> int find(MapElement<T>[] map, CharChunk name,
int start, int end) {
int a = 0;
int b = map.length - 1;
// Special cases: -1 and 0
if (b == -1) {
return -1;
}
if (compare(name, start, end, map[0].name) < 0 ) {
return -1;
}
if (b == 0) {
return 0;
}
int i = 0;
while (true) {
i = (b + a) >>> 1;
int result = compare(name, start, end, map[i].name);
if (result == 1) {
a = i;
} else if (result == 0) {
return i;
} else {
b = i;
}
if ((b - a) == 1) {
int result2 = compare(name, start, end, map[b].name);
if (result2 < 0) {
return a;
} else {
return b;
}
}
}
}
这里实现了一个简单的二分查找,试图找到一个精准的匹配结果。若没有最精准的结果,则返回比url小的最大值的位置。返回的位置存入pos这个int变量中。