TOMCAT 源码分析 – 一次请求
前语
在上一篇源码分析《TOMCAT源码分析–启动》中已经知道,Tomcat在启动中,会通过NIO
监听端口,而真正去接收请求的是pollerThread.start()
轮询线程的启动,那么请求的入口应该是到NIO
中,最后被轮询线程发现并被处理,那么自然就要去看Poller
线程的run()
方法,看其是如何处理。(PS:心中要有上一篇里面的模块架构图,很重要!)
端点接收请求
// org.apache.tomcat.util.net.NioEndpoint.Poller#run
@Override
public void run() {
// Loop until destroy() is called
// 一直循环,直到destroy方法被调用
while (true) {
boolean hasEvents = false;
try {
if (!close) {
// 遍历事件队列判断是否有事件(请求)待处理
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// If we are here, means we have other stuff to do
// Do a non blocking select
// 如果在这个分支,说明有其他工作要做,这里就不去做一个阻塞的select
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
} catch (Throwable x) {
}
// ...
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper == null) {
iterator.remove();
} else {
// socketWrapper 包装不为空,将其取出,进行处理
iterator.remove();
processKey(sk, socketWrapper);
}
}
// Process timeouts
timeout(keyCount,hasEvents);
}
getStopLatch().countDown();
}
端点一直进行循环,检测有没有可用的NIO
准备就绪,如果有就拿去processKey
消费掉。那么看看它具体怎么做的
// org.apache.tomcat.util.net.NioEndpoint.Poller#processKey
protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
try {
if (close) {
cancelledKey(sk, socketWrapper);
} else if (sk.isValid() && socketWrapper != null) {
if (sk.isReadable() || sk.isWritable()) {
if (socketWrapper.getSendfileData() != null) {
processSendfile(sk, socketWrapper, false);
} else {
unreg(sk, socketWrapper, sk.readyOps());
boolean closeSocket = false;
// Read goes before write
if (sk.isReadable()) {
if (socketWrapper.readOperation != null) {
if (!socketWrapper.readOperation.process()) {
closeSocket = true;
}
// 最终进入下面这个分支
} else if (!processSocket(socketWrapper, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
}
}
} else {
}
} catch (CancelledKeyException ckx) {
} catch (Throwable t) {
}
}
// org.apache.tomcat.util.net.AbstractEndpoint#processSocket
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = null;
// 想从缓存处理器中取一个出来先
if (processorCache != null) {
sc = processorCache.pop();
}
// 如果没取到则创建一个处理器
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
// 获取线程池处理 -- 这里也就是并发去处理这个处理器了,这个线程就自己回去处理NIO的监听了。
Executor executor = getExecutor();
if (dispatch && executor != null) {
// 可以观察到进入了这个分支
executor.execute(sc);
} else {
// 如果没有获取到线程池就直接调用这个处理器的run方法
sc.run();
}
} catch (RejectedExecutionException ree) {
}
// ...
return true;
}
- 从上面的执行过程可以看到他使用线程池提交了
sc
处理器,那么就去看处理器的run
方法,且从运行中可以看到这个sc
实际为NioEndPoint$SocketProcessor
的实例,发现这个实例中并没有run
方法,但是其超类SocketProcessorBase
中的run
方法实际调用了实例的doRun
方法
// org.apache.tomcat.util.net.NioEndpoint.SocketProcessor#doRun
@Override
protected void doRun() {
NioChannel socket = socketWrapper.getSocket();
Poller poller = NioEndpoint.this.poller;
try {
int handshake = -1;
if (handshake == 0) {
SocketState state = SocketState.OPEN;
// Process the request from this socket
// 很容器看出来,它将在下面获取处理器进行处理这个请求
if (event == null) {
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
// 实际上,进的是这个分支,而event事件刚好也是上面的OPEN_READ
state = getHandler().process(socketWrapper, event);
}
} catch (CancelledKeyException cx) {
} catch (Throwable t) {
} finally {
}
}
- 从上面获取了处理器要进行处理,也就是上一篇中的
coyote
的端点EndPoint
获取Processor
,并且调用他的process
方法进行处理。这个处理方法十分长,它的主要逻辑就是Handler
获取真正的处理器Process
,然后调用它的process
方法处理
// org.apache.coyote.AbstractProtocol.ConnectionHandler#process
@Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
// 获取处理器 -- 实际本次请求当前处理器为Null
Processor processor = (Processor) wrapper.getCurrentProcessor();
try {
if (processor == null) {
// 获取协议,需要通过协议去找对应的协议处理器 -- 这儿实际返回Null
String negotiatedProtocol = wrapper.getNegotiatedProtocol();
// ...
}
if (processor == null) {
// 还是null,就先去已经回收但还未释放的处理器栈中弹出一个来
// 因为已经使用过,所以这里就返回了Http11Processor
processor = recycledProcessors.pop();
}
if (processor == null) {
// 如果还没获取到处理器,就根据协议去创建一个对应的处理器,第一次访问时会如此
processor = getProtocol().createProcessor();
register(processor);
}
do {
// 协议处理器开始处理包装的请求
state = processor.process(wrapper, status);
} while ( state == SocketState.UPGRADING);
}
- 可以发现找到处理器后,调用
process
处理,不过这个方法是超类中的方法,且Http11Processor
未进行覆盖
// org.apache.coyote.AbstractProcessorLight#process
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
throws IOException {
SocketState state = SocketState.CLOSED;
Iterator<DispatchType> dispatches = null;
do {
if (dispatches != null) {
} else if (status == SocketEvent.DISCONNECT) {
// Do nothing here, just wait for it to get recycled
} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
state = dispatch(status);
state = checkForPipelinedData(state, socketWrapper);
} else if (status == SocketEvent.OPEN_WRITE) {
state = SocketState.LONG;
} else if (status == SocketEvent.OPEN_READ) {
// 根据DEBUG之前提到了,event的类型就是OPEN_READ,故进这个分支,进入核心的处理。
state = service(socketWrapper);
} else if (status == SocketEvent.CONNECT_FAIL) {
logAccess(socketWrapper);
} else {
state = SocketState.CLOSED;
}
return state;
}
那么state = service(socketWrapper);
就是Http11Processor
的核心处理了
// org.apache.coyote.http11.Http11Processor#service
@Override
public SocketState service(SocketWrapperBase<?> socketWrapper)
throws IOException {
// ...
// Process the request in the adapter
if (getErrorState().isIoAllowed()) {
try {
rp.setStage(org.apache.coyote.Constants.STAGE_SERVICE);
// 就是这一步去找适配器并进行处理
// 获得CoyoteAdapter的实例
getAdapter().service(request, response);
if(keepAlive && !getErrorState().isError() && !isAsync() &&
statusDropsConnection(response.getStatus())) {
setErrorState(ErrorState.CLOSE_CLEAN, null);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("http11processor.request.process"), t);
// 500 - Internal Server Error
response.setStatus(500);
setErrorState(ErrorState.CLOSE_CLEAN, t);
getAdapter().log(request, response, 0);
}
}
}
- 这一步很关键,他去获得了
Adapter
,也就是模块图中,要通过这个适配器,去进行将Request
进行转换封装,最后调用Catalina
进行处理的位置
// org.apache.catalina.connector.CoyoteAdapter#service
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
// 将Request 进行转换封装
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());
try {
// Parse and set Catalina and configuration specific
// 解析并设置Catalina和特定于配置 -- 什么意思呢,就是在这里去找配置-映射了
// request parameters
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
// 调用容器的管道,进行流水线处理了
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
}
}
- 看到这儿就清晰多了,他两步走,第一步根据配置寻找映射,
- 第二步,调用了
Service
容器的关掉进行处理这个请求,那么根据套娃模式
,他也必将通过一层又一层的相似调用进去,拭目以待~
// org.apache.catalina.connector.CoyoteAdapter#postParseRequest
protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
org.apache.coyote.Response res, Response response) throws IOException, ServletException {
// 获得请求解码后的地址
MessageBytes undecodedURI = req.requestURI();
while (mapRequired) {
// This will map the the latest version by default
// 在容器中寻找映射
connector.getService().getMapper().map(serverName, decodedURI,
version, request.getMappingData());
}
}
// map方法最终进入这个超类
// org.apache.catalina.mapper.Mapper#map(org.apache.tomcat.util.buf.MessageBytes, org.apache.tomcat.util.buf.MessageBytes, java.lang.String, org.apache.catalina.mapper.MappingData)
public void map(MessageBytes host, MessageBytes uri, String version,
MappingData mappingData) throws IOException {
// 将主机名和URI做映射
// 内部映射
internalMap(host.getCharChunk(), uri.getCharChunk(), version, mappingData);
}
// org.apache.catalina.mapper.Mapper#internalMap
private final void internalMap(CharChunk host, CharChunk uri,
String version, MappingData mappingData) throws IOException {
// Context mappinp
// 在这一步执行完之后,在contextList中就已经可以看到配置的具体映射了
ContextList contextList = mappedHost.contextList;
MappedContext[] contexts = contextList.contexts;
// Wrapper mapping
if (!contextVersion.isPaused()) {
// 将映射数据包装
internalMapWrapper(contextVersion, uri, mappingData);
}
}
// org.apache.catalina.mapper.Mapper#internalMapWrapper
private final void internalMapWrapper(ContextVersion contextVersion,
CharChunk path,
MappingData mappingData) throws IOException {
// ...
// 这儿的逻辑很长,但一直调式,发现他最后走了Rule 7
// Rule 7 -- Default servlet
// 默认的servlet
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
if (contextVersion.defaultWrapper != null) {
mappingData.wrapper = contextVersion.defaultWrapper.object;
mappingData.requestPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
mappingData.wrapperPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
mappingData.matchType = MappingMatch.DEFAULT;
}
// Redirection to a folder
char[] buf = path.getBuffer();
if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
String pathStr = path.toString();
// Note: Check redirect first to save unnecessary getResource()
// call. See BZ 62968.
if (contextVersion.object.getMapperDirectoryRedirectEnabled()) {
WebResource file;
// Handle context root
if (pathStr.length() == 0) {
file = contextVersion.resources.getResource("/");
} else {
file = contextVersion.resources.getResource(pathStr);
}
if (file != null && file.isDirectory()) {
// Note: this mutates the path: do not do any processing
// after this (since we set the redirectPath, there
// shouldn't be any)
path.setOffset(pathOffset);
path.append('/');
mappingData.redirectPath.setChars
(path.getBuffer(), path.getStart(), path.getLength());
} else {
mappingData.requestPath.setString(pathStr);
mappingData.wrapperPath.setString(pathStr);
}
} else {
mappingData.requestPath.setString(pathStr);
mappingData.wrapperPath.setString(pathStr);
}
}
}
}
把映射对象包装好后,进入第二步,去套娃中挖掘具体的处理:
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
// org.apache.catalina.core.StandardEngineValve#invoke
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// Select the Host to be used for this Request
Host host = request.getHost();
if (host == null) {
// HTTP 0.9 or HTTP 1.0 request without a host when no default host
// is defined. This is handled by the CoyoteAdapter.
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// Ask this Host to process this request
// 要求主机处理此请求
host.getPipeline().getFirst().invoke(request, response);
}
- 可以看到进入了
StandardEngineValve
类中,那么根据上一篇的模块架构图,以及server.xml
的配置就可以知道,它将一层一层处理管道线,一层一层的进行invoke
。那么中间省略他套娃式的几步invoke
,走到了下面StandardContextValve
的代码
// org.apache.catalina.core.StandardContextValve#invoke
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {
// ...
// 管道处理
// 此时他已经一层一层的走进来,一直走到包装了servlet的wrapper
wrapper.getPipeline().getFirst().invoke(request, response);
}
那么看看这个wrapper
的值是什么:StandardEngine[Catalina].StandardHost[localhost].StandardContext[/webmvc].StandardWrapper[springmvc]
,可以发现引擎是“Catalina”,虚拟主机是“localhost”,上下文是“webmvc”,进行处理的servlet是“springmvc”,那么就没错了。放入tomcat的webapps目录中的样例工程,就是这个上下文,以及spring mvc
的servlet入口。
接下来,有了对应的wrapper之后,它还经过了过滤链进行一定的过滤filter
,最终在下面这个地方进行调用:
// org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// 真正调用servlet对请求做处理的位置,如果是spring mvc 这里就是dispatcherServlet对象
servlet.service(request, response);
}
- DEBUG可以看到这个
servlet
实例的值是DispatcherServlet
,同时也可以看到它的上下文对象是spring
的上下文,如下图:
那么,接下来请求的处理就是调用spring mvc
进行完成了,tomcat对请求的处理分发就完成了!
结语
Tomcat的一次请求走的代码看起来很长,实际上了解其架构之后,逻辑还是十分清晰的。
本次在Tomcat源码的入门学习中,有以下体会:
- Tomcat的架构清晰,多层嵌套,看起来复杂,实际上就是剥洋葱的行为,看源码的过程根据DEBUG来追踪就可以清晰的一层一层的进去。
- 看源码DEBUG真的很重要!
- 了解架构,从宏观出发,不抓细枝末节,是快速看完源码,了解主要思想的重要途径。过分追求每一行代码的行为容易忘了自己前面看过的,以及你到底想看的是什么。
- 了解完请求过程,那么对一次请求最终调用之前打一个Log日志,做统计等等都是可以完成的了~
推广
推广我一分钱也没得赚!!!!
喝水不忘打井人吧,这个是在拉勾教育上看的《Tomcat核心源码剖析》,应癫出品。只有三天的课程,第三天还会推荐你报班(手动滑稽~)。
槽点: 这是个“训练营”课程,18元人民币,有效期18天。到期就没得看了,也督促我三天看完,五天内完成笔记~