Tomcat源码五部曲(三)、 请求处理过程

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

Tomcat处理Http请求又是非常复杂的一步,至少我是这么认为。对于这一流程,个人实在没有精力深入研究了(怕了怕了),所以在这里只总结一下大概,或许有什么地方可能不太正确,请多多指正。

一、主要类简介

处理请求主要涉及到这么几个类,Endpoint、Acceptor、Mapper、CoyoteAdapter、Processor、ProtocolHandler等还有很多。

Endpoint:通信监听的接口,具体Socket的接收处理类,对传输层的抽象,根据I/O方式的不同,提供了NioEndpoint、AprEndpoint、Nio2Endpoint三个实现。Tomcat源码五部曲(三)、 请求处理过程Acceptor:在第一章也提到过,当Connector启动时,也会启动他内部的Endpoint,Endpoint并行运行多个线程,每个线程运行着一个Acceptor实例,Acceptor中监听端口通信,也就是用于接受新连接,并将新连接封装一下,也就是将Socket封装为SocketWrapper,并交给SocketProcessor对象处理(此过程由线程池异步处理)。根据I/O的不同当然有不同处理方式,如NIO采用轮询方式检测SelectKey是否就绪,如果就绪,则获取一个有效的SocketProcessor对象提交到线程池。Tomcat源码五部曲(三)、 请求处理过程

Mapper:维护着映射。

CoyoteAdapter:将Connector和Mapper、Container联系起来。

Processor:负责构造Request、Response对象,并通过Adapter交给Catalina容器处理,对应用层的抽象。Processor是单线程的,Tomcat在同一次链接中复用Processor。并按照协议的不同,分为AjpProcessor(AJP)、Http11Processor(HTTP/1.1)、StreamProcessor(HTTP/2.0)Tomcat源码五部曲(三)、 请求处理过程ProtocolHandler:封装AbstractEndpoint和Processor,实现针对具体协议的处理功能,按照协议和I/O提供了6个实现类。Tomcat源码五部曲(三)、 请求处理过程

二、跟踪调用栈

要想跟踪请求的调用栈,可以直接在HttpServlet的service方法上打断点,调用过程说多不多,说少也不少。Tomcat源码五部曲(三)、 请求处理过程Tomcat源码五部曲(三)、 请求处理过程最上边是WrappingRunnable,自身也是实现了Runnable接口,内部又包装了一个Runnable,自身run被启动时候也会直接调用内部Runnable的run方法,只不过用try包装了一下,避免一些错误。

但是如果从这开始的分析的话,还不是头,还需要往前走。(以下以NIO方式分析)。

三、请求处理过程

首先是NioEndpoint的startInternal方法,主要就是创建线程池、启动Poller轮询、启动Acceptor接收。

public void startInternal() throws Exception {
     ......
        /**
         * 创建线程池
         */

        if (getExecutor() == null) {
            createExecutor();
        }

        /**
         * 开启轮训线程
         */

        poller = new Poller();
        .....
        pollerThread.start();
        /**
         * 开启接收线程
         */

        startAcceptorThread();
    }

Acceptor启动后会阻塞在 ServerSocketChannel.accept()方法处,当有新连接到达时,返回一个 SocketChannel。Tomcat源码五部曲(三)、 请求处理过程然后封装为SocketWrapper实例,并交给SocketProcessor处理(此过程是由线程池异步处理)。

SocketProcessor是一个线程池Worker实例,每一个I/O方式都有自己的实现。

至于SocketProcessor我们要看子类实现的doRun方法,如NioEndpoint.SocketProcessor下的doRun中会调用ConnectionHandler的process方法进行处理。Tomcat源码五部曲(三)、 请求处理过程其中的getProtocol().createProcessor()会创建一个 Http11Processor,它用来解析 Socket,将 Socket 中的内容封装到 Request 中,但这个 Request 是临时使用的一个类,它的全类名是 org.apache.coyote.Request,也就是在Tomcat源码中有两个Request。

protected static class ConnectionHandler<Simplements AbstractEndpoint.Handler<S{
 public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
   if (processor == null) {
    //返回Http11Processor
      processor = getProtocol().createProcessor();
      //调用Http11Processor的process
      state = processor.process(wrapper, status);
   }
 }
}

接着就是Http11Processor的process方法,由于自身没有重写,所以到了父类AbstractProcessorLight中。在父类的process方法中判断如果是SocketEvent.OPEN_READ事件,则交给子类实现的service方法中处理。所以还是回到了Http11Processor的service方法

public abstract class AbstractProcessorLight implements Processor {
    public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
            throws IOException 
{
            
            if (dispatches != null) {
   //.........
            } else if (status == SocketEvent.OPEN_READ) {
             //交给子类实现
                state = service(socketWrapper);
 }
}

在Http11Processor中依靠Http11InputBuffer来读取,也就是读取请求行、请求头,对应的方法依次是parseRequestLine、parseHeaders。

当读取完成后,开始下一步交给CoyoteAdapter处理。

 @Override
public SocketState service(SocketWrapperBase<?> socketWrapper){
 inputBuffer.parseRequestLine();
 
  inputBuffer.parseHeaders();
 //.........
  getAdapter().service(request, response);
 //............
}

四、CoyoteAdapter

请求映射就在这个过程中,具体也分为两步,一步分在postParseRequest中,一步分在Mapper.map中。这个过程也是非常复杂,恕我无能,没有去深究。

 @Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res){
 /**
  *根据参数生成一系列实例,其中包含最重要的wrapper
  */

 postParseSuccess = postParseRequest(req, request, res, response);

   /**
    *  交给容器处理,第一个容器是Engine Valve
    */

   Service service = connector.getService();
   Engine container = service.getContainer();
   Pipeline pipeline = container.getPipeline();
   Valve first = pipeline.getFirst();
   first.invoke(request,response);

 //connector.getService().getContainer().getPipeline().getFirst().invoke(request,response);
}

在postParseRequest中重要的也是这一句,getMapper返回的就是Mapper,Mapper维护着请求链接于Host、Context、Warpper等的映射。

最终将结果放在org.apache.catalina.connector.Request.mappingData对象中,mappingData中的wrapper属性就保存着能处理这个请求的Servlet,还有其他如所属的Host实例、所属的Context实例。

protected boolean postParseRequest(org.apache.coyote.Request req, Request request,
            org.apache.coyote.Response res, Response response)
 throws IOException, ServletException 
{
 
 connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData());
}

但如果没有找到,他就返回DefaultServlet处理,这对于项目路径匹配到了,而项目下没有能处理这个请求路径的Servlet是这样的,但如果项目路径也没匹配到,则都返回null,后续会直接return。

Tomcat源码五部曲(三)、 请求处理过程之后是交给第一个容器处理。

connector.getService().getContainer().getPipeline().getFirst().invoke(request,response);

五、Pipleline、Valve

Tomcat中每个容器通过一个责任链来处理具体的请求,于是定义了Pipeline(管道)和Valve(阀)两个接口,前者用于构造责任链,后者代表责任链上的每个处理器,还有Pipleline中维护这一个基础的Valve,它始终在Pipleline的末端,也就是他会最后执行。

所以,在ContainerBase中有个Pipleline(另外四个容器都会继承ContainerBase),通过pipeline.setBasic可以为这个Pipleline设置一个基本的Valve,他将在最后执行。Tomcat源码五部曲(三)、 请求处理过程在另外四个容器的构造方法中都会调用pipeline.setBasic()来设置基本的Valve,但有的容器还在其他地方调用pipeline.addValve添加其他的Valve,这样的话这些Valve会优先于Basic。并且,这个Valve处理完之后要手动调用getNext().invoke(request,response);交给下一个处理。

下面是这四个容器基本的Valve。

Tomcat源码五部曲(三)、 请求处理过程Tomcat源码五部曲(三)、 请求处理过程Tomcat源码五部曲(三)、 请求处理过程Tomcat源码五部曲(三)、 请求处理过程上述代码中的connector.getService().getContainer().getPipeline()也就是返回就是StandardEngineValve,这也不是绝对,如果这个容器上没有添加过其他Valve的话。

  public StandardEngine() {
      super();
      /**
       * 设置基本处理器
       */

      pipeline.setBasic(new StandardEngineValve());
  }

也就是下面这一部分会跟各种Valve打交道。

六、StandardEngineValve

所以继续跟踪代码来到StandardEngineValve的invoke方法,这里面就是获取到Host,然后获取Host下的Pipeline,如果Host中没有添加其他Valve的话,第一个也是最后一个,也就是StandardHostValve。

(Tomcat中调用下一个时都是写在了一行,不方便调试,你可以把他拆分出来)

@Override
public final void invoke(Request request, Response response)
    throws IOException, ServletException 
{
    /**
     * 获取对应主机
     */

    Host host = request.getHost();
    if (host == null) {
        return;
    }
    /**
     * 获取Host下的Pipeline,并交给第一个Valve处理
     */

    Pipeline pipeline = host.getPipeline();
    Valve first = pipeline.getFirst();
    first.invoke(request,response);
    //host.getPipeline().getFirst().invoke(request,response);
}

注意此时的Host从何而来?点击去可以看到是mappingData的一个字段,而他在上面请求映射的时候被赋值,这里在上面说过了。Tomcat源码五部曲(三)、 请求处理过程但其实StandardHostValve不是第一个,第一个是ErrorReportValve,他在StandardHost的startInternal方法中被添加。

 @Override
 protected synchronized void startInternal() throws LifecycleException {
     String errorValve = getErrorReportValveClass();

     Valve valve =(Valve) Class.forName(errorValve).getConstructor().newInstance();
     getPipeline().addValve(valve);

 }

理所应当我们走到ErrorReportValve的invoke下。

@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
    //交给下一个处理
    getNext().invoke(request, response);

 //如果已经提交
    if (response.isCommitted()) {
        .............
        return;
    }
    report(request, response, throwable);

在此过程中你可以找到这个错误页面的生成过程,也就是report方法。Tomcat源码五部曲(三)、 请求处理过程看到这么多拼接应该能知道干什么吧。Tomcat源码五部曲(三)、 请求处理过程接着是StandardHostValve的invoke方法,同样的重点也是在下面这句。拿到Context的第一个Valve。如果没有做其他配置,第一个是NonLoginAuthenticator,然后才是基本处理者StandardContextValve。

现在可以理解为什么这里要判断为null吧?这是在项目路径也匹配不到时候直接返回。

举个例子,现在有个项目Demo,Demo下有个Servlet,地址为/TestServlet,访问他完整的路径就是http://xxx/Demo/TestServlet,但你不小心打成了http://xxx/Demxx/TestServlet,这时候Context就为空,直接返回。

  Context context = request.getContext();
  if (context == null) {
      return;
  }
   Pipeline pipeline = context.getPipeline();
   Valve first = pipeline.getFirst();
   first.invoke(request,response);
   //context.getPipeline().getFirst().invoke(request, response);

其他就不看了,各个内置的Valve都做了一些不为人知的骚操作。

七、过滤器配置

直接到StandardWrapperValve的invoke方法,这里面就可以看到过滤器的创建以及执行过程,也就是这句,内部先找到所有过滤器,然后根据匹配规则筛选出能为这个Servlet过滤的过滤器。

  ApplicationFilterChain filterChain =
  ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
  //...........
  filterChain.doFilter(request.getRequest(), response.getResponse());

然后执行filterChain.doFilter开始依次过滤,doFilter的核心方法也在internalDoFilter中。通过把自己传递给我们编写的过滤器,我们处理完成后必须调用filterChain.doFilter(servletRequest,servletResponse);继续下一个。

八、service

当我们所有过滤都执行完成后,调用Servlet的service方法,由service调用doGet或者doPost。

这其实也是个递归过程,当pos<n时继续下一个过滤器,n为过滤器的数量,pos是当前过滤器位置。

private void internalDoFilter(ServletRequest request,
                              ServletResponse response)

    throws IOException, ServletException 
{
    if (pos < n) {
     ApplicationFilterConfig filterConfig = filters[pos++];
     Filter filter = filterConfig.getFilter();
     //调用我们的过滤器
         filter.doFilter(request, response, this);
        return;
    }
  //过滤器都执行完成后调用我们servlet的service方法,由service调用doGet或者doPost。
    servlet.service(request, response);

}

还没说这个servlet实例从哪来,其实也是在StandardWrapperValve的invoke下。ValveBase有个字段是container,保存着这个Valve所属的容器。对于StandardWrapperValve自然而然是StandardWrapper,既然拿到了StandardWrapper,那就好办了,在前几章说过内部会保存着这个Servlet的实例和完整类名,可以通过其allocate方法初始化。

  servlet = wrapper.allocate();

这一流程是真的复杂.........end


原文始发于微信公众号(十四个字节):Tomcat源码五部曲(三)、 请求处理过程