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天。到期就没得看了,也督促我三天看完,五天内完成笔记~