Tomcat架构与源码分析(转载)

0 背景

7.25Tomcat设计模式☞反应器模式(终)_哔哩哔哩_bilibili

appleyk-CSDN博客

tomact9-markdown: tomcat9源码解读文档 - Gitee.com

        本文架构图及类图均来自上述引用。

        整理之前的学习笔记,学习的目的是为Tomcat回显和内存马做准备,重点关注response对象、listener相关操作和对象、valve相关操作和对象、filter相关操作和对象、servletfilter相关操作和对象的走向。

1 环境搭建

1.1 Tomcat源码部署

         这里选用Tomcat版本8.5.98,源码地址:Release 8.5.98 · apache/tomcat · GitHub

        Tomcat源码部署可参考:【源码系列】Tomcat环境搭建_哔哩哔哩_bilibili

        (1)下载后解压项目,并创建一个pom.xml,将下面内容复制到pom.xml中

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
      http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.apache.tomcat</groupId>
  <artifactId>apache-tomcat</artifactId>
  <version>8.5.98</version>

  <dependencies>
    <dependency>
      <groupId>org.apache.ant</groupId>
      <artifactId>ant</artifactId>
      <version>1.10.4</version>
    </dependency>
    <dependency>
      <groupId>wsdl4j</groupId>
      <artifactId>wsdl4j</artifactId>
      <version>1.6.2</version>
    </dependency>
    <dependency>
      <groupId>javax.xml</groupId>
      <artifactId>jaxrpc-api</artifactId>
      <version>1.1</version>
    </dependency>
    <dependency>
      <groupId>org.eclipse.jdt.core.compiler</groupId>
      <artifactId>ecj</artifactId>
      <version>4.5.1</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>apache-tomcat</finalName>
    <sourceDirectory>java</sourceDirectory>
    <resources>
      <resource>
        <directory>java</directory>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

        (2)IDEA导入项目,并加载pom.xml,变为Maven项目

        (3)定位到org.apache.catalina.startup.Bootstrap启动类的main函数,启动运行。

        (4)可能遇到的问题

        中文乱码解决:修改如下代码

  • org/apache/jasper/compiler/Localizer

  • org/apache/tomcat/util/res/StringManager

        JSP解析报错:修改org/apache/catalina/startup/ContextConfig类的代码

        (5)项目基本目录架构

  • bin文件夹:存放二进制文件,例如startup.bat和shutdown.bat分别用来启动和关闭Tomcat;
  • conf文件夹:用来存放配置文件,如server.xml和web.xml分别存放Tomcat配置和Web应用配置;
  • java文件夹:Tomcat源码;
  • webapps文件夹:存放Web应用。

1.2 创建和部署Servlet项目

        

 (1)创建一个Servlet项目

        (2)创建自定义的Listener、Filter、Servlet,并完成web.xml配置

package com.example.servletdemo1;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

public class MyListenr implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("自定义监听器初始化");
//        ServletContextListener.super.contextInitialized(sce);   // 需要注释掉,否则报错
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
//        ServletContextListener.super.contextDestroyed(sce); // 需要注释掉,否则报错
    }
}
package com.example.servletdemo1;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("自定义过滤器初始化成功");
//        Filter.super.init(filterConfig);  // 需要注释掉,否则报错
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("hello Filter");
        filterChain.doFilter(servletRequest, servletResponse);
    }
}
package com.example.servletdemo1;

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

//@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}

        (3)将项目部署到对应的Tomcat上,用IDEA生成exploded项目

        (4)将打部署好项目放到Tomcat的webapps文件夹下,改一下名字,并将其他应用删掉(方便后面调试)。

2 Tomcat基本架构

2.1 总体架构

        Tomcat设计了两个核心组件:连接器(Connect)和容器(Container)。Server表示着整个服务器。Server下面有多个服务Service,每个服务都包含着多个连接器组件Connector(Coyote实现)和一个容器组件Container。

        Cononect负责socket通信,以及将原始的request/response转换为Servletrequest/Servletresponse,供Container使用。Container分为四层Engine(唯一)、Host(多个)、Context(多个)、Wrapper(多个),其中Host就是主机/IP/域名等,Context的集体的web项目,Wrapper是具体Servlet类的封装。Tomcat Container通过Pipeline-Valve责任链的形式,来处理servlet请求,Pipeline负责管理Valve,Valve是Container容器中真正处理请求逻辑的地方,每个Container都有自己的pipeline来关自己层级的Valve。

        Tomcat架构部分的相关实现类如下。

2.2 Tomcat生命周期——Lifecycle接口

        Tomcat的组件生命周期通过Lifecycle接口提供相应的方法来管理组件的生命周期(init、start、stop、destroy...),提供相应的事件常量来调用监听器处理不同的生命周期状态事件。这里主要采用了模板设计模式、观察者模式和状态模式。

        xxxBase实现类来提供模板,LifecycleState类定义了状态和事件的关系,LifecycleListener是监听者,LifecycleEvent是监听者准备执行的事件。

        此外LifecycleMBeanBase继承了LifecycleBase,主要负责JMX方面的内容,用于管理和操作 MBean,MBean通过暴露一组属性、操作和通知,允许外部程序监控和管理应用程序或系统的状态和行为。

        而ValveBase、StandardServer、StandardService、Connector、ContaierBase又继承了LifecycleMBeanBase,这些都Tomcat中比较核心的组件或组件基类,这里通过继承来实现组件的生命周期管理。

2.2.1 LifecycleState类

        LifecycleState类定义了状态和事件的关系,每一个状态都对应一个生命周期事件名称,即状态变了,事件也会相应的进行切换,而不同的事件会触发生命周期组件相应的监听器的执行。

2.2.2 LifecycleListener

        生命周期监听器接口,内部就一个方法,主要就是基于特定的生命周期事件进行业务处理。监听器是启动过程中,真正干活的地方,根据不同的生命周期状态,进行相应处理,例如去加载各种配置。常见的监听器包括:MapperListener、EngineConfig、HostConfig、ContextConfig。

2.2.2.1 MapperListener

        映射器监听器,其本身也是一个生命周期组件,同时又实现了LifecycleListener和ContainerListener两个监听器接口,ContainerListener属于容器生命周期组件特有的,说明MapperListener可以响应container的事件,监听tomcat组件的变化,当有Host,Context及Wrapper变更时,调用Mapper类中的相关方法,增加或者删除Host,Context,Wrapper等。

        Tomcat就是依赖Mapper类来维持请求路径到具体Servlet处理类之间的映射关系,使得Tomcat能够正确处理请求。

2.2.2.2 EngineConfig

        在Tomcat中,Engine是第一个容器组件,主要起到承上启下的作用,在Tomcat启动的处理流程中并没有出力。

2.2.2.3 HostConfig

        Host接口是一个虚拟主机组件,相当于域名或IP,StandardHost是Host的具体实现类。HostConfig为StandardHost容器组件生命周期监听器(非容器监听器)中的一员,要作用就是部署我们的应用(也称作上下文)。

2.2.2.4 ContextConfig

        Context就是具体的一个应用,在Tomcat中webapps文件下的一个每个项目就是一个Context,它回去解析应用的web.xml,在mapper中创建对应的servlet映射,管理Filter的处理逻辑。在Tomcat内存马中,重点就是要拿到项目的StandardContext(Context接口的标准实现类)。

2.2.3 LifecycleEvent

        生命周期事件类,扩展了JDK的EventObject,增加了两个成员变量type和data。data一般为空,type就是发生的事件,例如Lifecycle中的事件常量,此外在Host接口、Context接口中也有相应的事件常量,而HostConfig、ContextConfig。这个类更多是起到标识的作用。

2.2.4 LifecycleBase

        生命周期基类,一般以"Base"结尾的大部分都是抽象型的,而我们在面向对象的世界里定义抽象类,大多数是想定义一些抽象的方法让子类去实现(比如initInternal、startInternal、stopInternal、destroyInternal),除了交给子类实现的abstract methods外,抽象类中还会定义这一类型都共有的成员变量(比如listeners和state)以及执行步骤固定的业务方法(比如init、start、stop和destroy),大多数抽象类会应用设计模式中的【模板方法】,可以说只要是框架中的抽象类,无一例外都会用到。其变量和方法如下:

2.2.5 LifecycleMBeanBase

        LifecycleMBeanBase继承了LifecycleBase,主要负责JMX方面的内容,向MBeanServer注册和卸载Bean ,MBean通过暴露一组属性、操作和通知,允许外部程序监控和管理应用程序或系统的状态和行为。

2.2.6 例子server.init()

        下面就以Server实例(server)的init()方法调用过程来说明上述组件是如何工作的。Server是一个接口,具体的实现类是StandardServer,但StandardServer并没有实现init(),所以在调用server.init()的时候,会去调用间接父类LifecycleBase.init(),如下:这里需要关注两行代码。


       

  2.2.4.1 setStateInternal函数

        这里其实就是的代码执行流程:根据不同的生命周期状态,调用监听器的相应的状态事件,这里就是体现状态模式和监听器模式的地方。

         跟进fireLifecycleEvent函数,就会发现这里会调用相关的监听器执行事件。

2.2.4.2 initInternal函数

        在LifecycleBase类中initInternal函数是一个抽象函数,具体的实现方法由具体子类实现。而StandardServer便是实现了initInternal函数。这里体现了模板设计模式的思想,父类LifecycleBase提供init的模板,定义工作流程,子类StandardServer自己实现其中的某些抽象函数,实现自己的业务逻辑。不仅init(),其他的生命周期状态函数也是用了模板模式。

        顺便一提:StandardServer.init()会for循环调用service.init()。

2.3 Connector

        在Tomcat中,Connector组件被称为Coyote。这里主要采用了反应器模式来拓展Tomcat的连接池,使用命令模式来处理请求。

2.3.1 Coyote连接器架构

        每一个Service中可以包括多个Connector,Connector中有两个比较重要的变量:ProtocolHander和Adapter,ProtocolHander主要负责socket通信(Endpoint)、协议处理生产request/response(Processor)和将原始的request/response适配为适合Servlet容器使用的ServletRequest/ServletResponse(Adapter)

组件                        描述
ProtocolHandlerCoyote 协议接口,通过Endpoint和Processor,实现针对具体协议的处理能力。Tomcat按照协议和I/O提供了6个实现类:AjpNioProtocol,AjpAprProtocol,AjpNio2Protocol , Http11NioProtocol , Http11Nio2Protocol ,Http11AprProtocol
Processor用于将Endpoint接收到的socket处理封装成request对象,实现HTTP协议。
Endpoint通信端点,处理底层socket网络连接,实现TCP/IP协议。
Acceptor循环监听和限制客户端请求,客户端请求超过最大连接数,就阻塞,否则将socket交给Poller处理,属于守护线程。
Poller工作线程,将Acceptor扔过来的客户端socketChannel进行事件注册,注册后的事件称作PollerEvent,保存到队列中,poller内部维护一个selector,poller线程循环事件队列,并通过selector监听客户端通道IO读写事件,这个过程属于同步非阻塞,效率非常高,如果IO读写就绪,就将当前的channel交给SocketProcessor任务,该任务最终会扔给Tomcat扩展JDK的线程池来处理。同Acceptor,该线程也是一个守护线程。
SocketProcessor处理有IO读写事件发生的客户端channel,然后封装一番交给对应的protocolHandler进行处理。
ThreadPoolExecutor处理客户端请求的线程。
Adapter由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request 类来封装这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,不能被CoyoteAdapter使用,这是设配器模式的经典运用,连接器调用CoyoteAdapter的Service方法,传入的是Tomcat Request对象,CoyoteAdapter负责将这个对象转换成ServletRequest,再传递给容器。

        Connector类的比较核心成员如下:

public class Connector extends LifecycleMBeanBase  {
   	/**
     * The <code>Service</code> we are associated with (if any).
     * 当前connector连接器关联的service,一个service有多个connector
     */
    protected Service service = null;
   
	/**
     * Coyote protocol handler. coyote 连接器组件协议处理器
     */
    protected final ProtocolHandler protocolHandler;
    
    /**
     * Maximum size of a POST which will be automatically parsed by the
     * 容器表单 URL 参数解析将处理的 POST 的最大大小(以字节为单位)。通过将此属性设置为小于零的值,
     * 可以禁用该限制。如果未指定,则此属性设置为 2097152(2 MB)。请注意,FailedRequestFilter 可用于拒绝超过此限制的请求。
     */
    protected int maxPostSize = 2 * 1024 * 1024;
    
    /**
     * Coyote adapter.
     * coyote适配器,负责将前端的request请求转换成HttpServletRequest转交给container(就是servlet应用,后端服务)
     */
    protected Adapter adapter = null;
    
    public Connector() {
        /**默认协议处理器 {@link org.apache.coyote.http11.Http11NioProtocol}*/
        this("HTTP/1.1");
    }
    
    public Connector(String protocol) {
        boolean apr = AprStatus.getUseAprConnector() && AprStatus.isInstanceCreated()
                && AprLifecycleListener.isAprAvailable();
        ProtocolHandler p = null;
        try {
            /**基于协议类型和是否开启apr来创建协议处理器*/
            p = ProtocolHandler.create(protocol, apr);
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        }
        if (p != null) {
            protocolHandler = p;
            protocolHandlerClassName = protocolHandler.getClass().getName();
        } else {
            protocolHandler = null;
            protocolHandlerClassName = protocol;
        }
        // Default for Connector depends on this system property
        setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
    }
    
    /**以反射的方式设置protocolHandler的各个属性值,主要就是优化线程池相关的配置*/
    public boolean setProperty(String name, String value) {
        if (protocolHandler == null) {
            return false;
        }
        return IntrospectionUtils.setProperty(protocolHandler, name, value);
    }
}

        ProtocolHandler:AbstractProtocol实现类声明了endpoint变量,实现了process方法,在process方法中,会创建proceessor实例,来处理请求。

        Endpoint相关类:HTTP/1.1 对应的TCP节点默认是NioEndpoint。

        这里看AbstractEndpoint和NioEndpoint类的成员可以知道,AbstractEndpoint会关联一个endpoint,在接收到socket连接后,会到相应的endpoint线程(Poller)中,NioEndpoint这些子类自己实现了一个内部类Poller

        Processor类:初步处理请求的地方,前面几个类是用来解决socket连接线程的。     

        Adapter:CoyoteAdapter是Adapter接口的唯一实现类,这里采用了是适配器模式,通过CoyoteAdapter.service来转换request/response使得容器能处理请求。

2.3.2 Tomcat中的命令模式

        Tomcat中使用命令模式来处理请求。命令模式的类图如下:

  • Invoker是一个命令控制器,里面通过setCommand可以聚合具体的命令(中间类,解耦了,增加的command的时候不需要改变Invoker)
  • CommandImpl类:是不同的命令类,内部关联了具体的命令执行处理类
  • Receiver:是命令执行处理类,是一个命令真正作用的对象。

        Tomcat的请求处理流程如下:

1、Tomcat服务启动后,监听指定端口,客户端随后发送请求连接指定端口并附带传输文本数据;

2、Acceptor线程负责接收来自客户端的连接socket,然后交给具体的endpoint进行处理;

3、XXXEndpoint使用Poller线程对接收进来的socketChannel进行事件注册,同时轮询事件列表,如果有通道发生IO读事件,就交给指定的SocketProcessor线程进行处理;

4、SocketProcessor线程最终会调用对应的Handler去处理socketWrapper;

5、Handler定义是在AbstractEndpoint类中,但是真正的实现类ConnectionHandler却是在AbstractProtocol中,最终Handler会调用对应的应用层XXXProcessor去处理套接字输入/出流,转换成Coyote框架中定义的request和response对象;

6、但这远远不够,因为容器需要接收来自servlet规范中的request和response类型,所以需要对coyote框架中的request和response进行一道转换,转换的目的就是为了能让coyote框架可以轻松的与Container容器建立连接,这个转换器(适配器)正是CoyoteAdapter;

7、那么命令模式在哪体现呢? client端发送连接请求,Invoker调用者其实就是我们的Acceptor,Command就是我们的AbstractEndpoint,Acceptor持有一个endPoint,主要对客户端socket连接进行并发限制和IO读事件的轮询,然后转交给具体的Command(Apr,NIO,NIO2)内部的SocketProcessor进行doRun(),那么接收者是谁呢?那必然是ProtocolHandler啊,当然接收者也是有很多类型的,也就是一个endpoint反向关联着一个protocol,那么endpoint是如何将命令(socketWrapper)传递给protocol的呢(具体其实是特定协议对应的Processor),是通过Handler具体点就是ConnectionHandler传递过去的,因为Handler(连接处理)接口定义在AbstractEndpoint中,而实现却是在AbstractProtocol中,这就像牵线搭桥一样,将protocol与endpoint串联在了一起!

8、那么接收者Protocol#Processor做了什么事情呢?当然是对传递过来的socketWrapper命令进行解析了,最终将tcp层的socket输入输出流转换成应用层的request和response对象。

2.3.3 Tomcat中的反应器模式

        为更好Tomcat在Connector中的设计与实现,可以先了解下Java的NIO和反应器模式: https://so.csdn.net/so/search?q=NIO&t=blog&u=cj151525

        NIO就是同步非阻塞I/O模型。非阻塞主要体现在socket的接收/读/写函数上,在等待I/O就绪阶段都是非阻塞的,主要通过I/O多路复用技术来实现。NIO有三大核心组件: 

组件名称描述核心类
Buffer缓冲区在Java NIO中,所有数据都是通过缓冲区对象进行传输的。缓冲区是一段连续的内存块,可以保存需要读写的数据。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。ByteBuffer
Channel通道通道是一个用于读写数据的对象,类似于Java IO中的流(Stream)。与流不同的是,通道可以进行非阻塞式的读写操作,并且可以同时进行读写操作。通道分为两种类型:FileChannel和SocketChannel,分别用于文件和网络

ServerSocketChannel/

SocketChannel/

FileChannel

Selector选择器选择器是Java NIO中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。Selector又称作I/O多路复用器。Selector

        Tomcat采用了一种主从Reactor+线程池的请求连接处理模式,Acceptor线程负责接收socket请求,Poller线程(从Reatcor)作为一个Selector去遍历事件,如有相应IO事件

    

2.4 Container

2.4.1 Container

        Tomcat中的Container具有层级结构,但不是通过继承来实现,而是通过parent(父容器)和child(子容器)变量来关联到下一层级。

        ContainerBase类:模板类,里面会关联一个pipeline。

        Engine接口和StandardEngine实现类:一个Service中只有一个Engine,Engine在Connector和Container中起到承上启下的作用,但其实没什么功能,相当于Container的入口吧。

        Host和StandardHost:Host接口内定义了一些监听事件,由HostConifg类处理;StandardHost是主机,默认配置中仅一个,即localhost,但可以配置多个,主要负责Web应用的部署和Context应用上下文的创建。

        Context和StandardContext:Context接口内定义了一些监听事件,由ContextConifg类处理;StandardContext为Web应用上下文,包含多个Wrapper,负责Web配置的解析、管理整个Web相关的资源,例如应用监听器的添加、Servlet的映射,包括。Tomcat的webapps目录下的一个目录,就是一个Web应用,也即是一个StandardContext,内存马就是要找到相应Web项目的StandardContext。

        Wrapper和StandardWrapper:Wrapper作为最底层的容器,是对Servlet的封装,负责Servlet实例的创建、执行和销毁

        需要注意的是,每个service中会有一个mapper(这个mapper会传递到Engine等容器中)来保存各个容器的映射信息,确保请求能正确走到对应的Servlet中。

2.4.2 Pipeline-Valve

        Tomcat采用Valve来干活,每个容器都有自己的pipeline(继承于父类),pipeline用来管理管道中Valve的调用链过程,通过调用invoke方法实现链式调用。其实Filter和Valve的工作模式是一样的,Filter是调用doFilter方法。

3 Tomcat启动流程

        Tomcat的启动入口在org/apache/catalina/startup/Bootstrap.java的main函数中。这里主要有三个需要关注的点,重点在于后面两个函数的调用流程。daemon变量是Bootstrap实例。

  • bootstrap.init():创建Catalina实例,设置相关类加载器;
  • daemon.load():各个组件的初始化init()。
  • daemon.start():各个组件的启动start()。

3.1 bootstrap.init();

        这里只是创建Catalina实例和设置几个类加载。

3.2 daemon.load(args);

        顺便一提:daemon.setAwait(true);将awaite标志位设置位true,后面Connector才能正常的监听。

        daemon.load(args);会反射调用Catalina实例的load方法。

        catalina.load()方法里面,有三个比较终于的点:

3.2.1 Digester digester = createStartDigester()和digester.parse(inputSource)

        创建一个digester解析器,并用来解析Tomcat服务器配置文件server.xml,这里创建各个组件的实例,包括各种组件和监听器;

3.2.2 getServer().init()

        获取当前Tomcat的server实例,并执行server.init()方法。

    public void load() {

        if (loaded) {
            return;
        }
        loaded = true;

        long t1 = System.nanoTime();

        initDirs();

        // Before digester - it may be needed
        initNaming();

        // Create and execute our Digester
        Digester digester = createStartDigester();  //解析server.xml

        InputSource inputSource = null;
        InputStream inputStream = null;
        File file = null;
        try {
            try {
                file = configFile();
                inputStream = new FileInputStream(file);
                inputSource = new InputSource(file.toURI().toURL().toString());
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("catalina.configFail", file), e);
                }
            }
            if (inputStream == null) {
                try {
                    inputStream = getClass().getClassLoader()
                        .getResourceAsStream(getConfigFile());
                    inputSource = new InputSource
                        (getClass().getClassLoader()
                         .getResource(getConfigFile()).toString());
                } catch (Exception e) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("catalina.configFail",
                                getConfigFile()), e);
                    }
                }
            }

            // This should be included in catalina.jar
            // Alternative: don't bother with xml, just create it manually.
            if (inputStream == null) {
                try {
                    inputStream = getClass().getClassLoader()
                            .getResourceAsStream("server-embed.xml");
                    inputSource = new InputSource
                    (getClass().getClassLoader()
                            .getResource("server-embed.xml").toString());
                } catch (Exception e) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("catalina.configFail",
                                "server-embed.xml"), e);
                    }
                }
            }


            if (inputStream == null || inputSource == null) {
                if  (file == null) {
                    log.warn(sm.getString("catalina.configFail",
                            getConfigFile() + "] or [server-embed.xml]"));
                } else {
                    log.warn(sm.getString("catalina.configFail",
                            file.getAbsolutePath()));
                    if (file.exists() && !file.canRead()) {
                        log.warn("Permissions incorrect, read permission is not allowed on the file.");
                    }
                }
                return;
            }

            try {
                inputSource.setByteStream(inputStream);
                digester.push(this);
                digester.parse(inputSource);
            } catch (SAXParseException spe) {
                log.warn("Catalina.start using " + getConfigFile() + ": " +
                        spe.getMessage());
                return;
            } catch (Exception e) {
                log.warn("Catalina.start using " + getConfigFile() + ": " , e);
                return;
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }

        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

        // Stream redirection
        initStreams();

        // Start the new server
        try {
            getServer().init();
        } catch (LifecycleException e) {
            if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
                throw new Error(e);
            } else {
                log.error("Catalina.start", e);
            }
        }

        long t2 = System.nanoTime();
        if(log.isInfoEnabled()) {
            log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
        }
    }

        getServer().init():实际中是调用了LifecycleBase父类的init模板,调用init过程的监听器执行INITIALIZING状态事件,然后走到StandardServer.initInternal。(这里的监听器和内存马中用到的不相关,所以具体的执行流程就不写了;从下面代码可以知道,init()中除了执行监听器事件外,就是执行具体子类的initInternal,所以后面的书写中,init()和initInternal()不做区分。)

       StandardServer.initInternal的代码如下:

        

        父类LifecycleMBeanBase负责JMX方面的初始化工作。

3.2.3 getServer().init()调用了service.init()

        StandardService类实现了Serive接口,StandardService.initInternal代码如下:

        (1)engine.init():会配置安全访问模块(Realm)和调用父类容器ContainerBase,去创建单个线程的线程池,这里并不会去初始化host;

        (2)mapperListenr.init():mapperListenr没有实现initInternal方法,这里会调用LifecycleMBeanBase.initInterna来注册MBean;

        (3)connector.init():

        跟进去看protocolHandler.init()->AbstractHttp11Protocol.init()->AbstractProtocol.init()->endpoint.init()。

       endpoint是NioEndpoint实例,其init()是继承自父类AbstractJsseEndpoint的。跟进NioEndpoint.init()->AbstractJsseEndpoint.init()->AbstractEndpoint.init()->bind()。

        bind()方法是AbstractEndpoint类中的一个抽象方法,在这里AbstractEndpoint是一个模板,核心bind由子类自己实现。跟进NioEndpoint.bind()

3.3 daemon.start()

        daemon.start()用来启动组件。会反射调用Catalina实例的start()方法。

        Catalina的start()方法代码如下:跟进,发现又回到了模板模式。server.start()->LifecycleBase.start()->server.startInternal()。

        查看server.startInternal()代码:循环调用service.start()

        跟进StandardService.start():主要是三部分的工作engine.start()、mapperListner.start()、connector.start()

3.3.1 engine.start()

        作为容器启动入口,负责启动各容器,部署web上下文、关联servlet类。从engine开始,不仅需要关注自己实现的startInternal(),也要关注LifecycleBase.setStateInternal,因为监听器xxxConfig在启动流程会做事件处理。但engine只是承上启下的作用,EngineConfig并没有真正干活。这里只看startInternal()方法。

        这里会调用直接父类Container.startInternal()的方法

       

        把Callbale类型的StartChild线程加入线程池后,会另起线程异步调用StartChild.call()方法,call中会调用child.start()。在start启动过程中,真正起到作用的是LifecycleBase.start()中调用的setStateInternal,通过监听器来进行操作,最终操作点是listener.lifecycleEvent(event)。另外有一点需要注意的是,在上一节初始化的过程中,除了Engine外的其他容器都没有初始化,所以在start容器的过程中会先初始化(init)容器。这里的调试过程比较复杂,能力有限,这里只关注和内存马相关的内容。

        首先是webConfig()函数,其中有两个比较重要的地方,一个是webXmlParser.parseWebXml(contextWebXml, webXml, false):负责解析web.xml,并赋值给webXML实例。

        另一个是configureContext(webXml):将webXML实例的内容往Container中添加。

        添加Servlet的步骤为:

for (ServletDef servlet : webxml.getServlets().values()) {
            Wrapper wrapper = context.createWrapper();
            // Description is ignored
            // Display name is ignored
            // Icons are ignored

            // jsp-file gets passed to the JSP Servlet as an init-param

            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
            wrapper.setName(servlet.getServletName());
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
            wrapper.setServletClass(servlet.getServletClass());
            MultipartDef multipartdef = servlet.getMultipartDef();
            if (multipartdef != null) {
                long maxFileSize = -1;
                long maxRequestSize = -1;
                int fileSizeThreshold = 0;

                if(null != multipartdef.getMaxFileSize()) {
                    maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
                }
                if(null != multipartdef.getMaxRequestSize()) {
                    maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
                }
                if(null != multipartdef.getFileSizeThreshold()) {
                    fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
                }

                wrapper.setMultipartConfigElement(new MultipartConfigElement(
                        multipartdef.getLocation(),
                        maxFileSize,
                        maxRequestSize,
                        fileSizeThreshold));
            }
            if (servlet.getAsyncSupported() != null) {
                wrapper.setAsyncSupported(
                        servlet.getAsyncSupported().booleanValue());
            }
            wrapper.setOverridable(servlet.isOverridable());
            context.addChild(wrapper);
        }
        for (Entry<String, String> entry :
                webxml.getServletMappings().entrySet()) {
            context.addServletMappingDecoded(entry.getKey(), entry.getValue());
        }

        Tomcat中有两个默认的Servlet。

        跟踪helloServlet,发现添加Servlet的时候的重要步骤有:

        StandardContext.servlet.setLoadOnStartup:默认的StandardWrapper的loadOnStartup是-1,ServletDef的默认是null,随意自定义的helloServletloadOnStartup会保持默认值-1。

        wrapper.setServletClass(servlet.getServletClass()):设置全限定类名,全限定名一般都是用来类加载的。

        context.addChild(wrapper):将wrapper加入context中

        然后添加servlet类和url映射关系

        下一个重要的函数是loadServlet,调用链如下:

        这里直接定位Standard.startInternal(),调试是如何加载Servlet的。顺便一提,Listener、Filter、Servlet都是从这里开始load的。前面在说如何分析的时候是利用处分析如何写内存马的,其实从Standard.startInternal()分析也行。

  • listenerStart():

        这里重点看listener是如何调用的

  • filterStart():

  • loadOnstartup:

        这里有两个for循环:第一个是判断loadOnStartup是否大于0,大于就添加到map中,与自定义Servlet相关的wrapper类loadOnStartup值保存默认值为-1,所以不会添加到map中。

        第二个循环就是从map中取值,进行wrapper.load()->loadServlet。loadServlet其实就是通过wrapper的servletClass值来进行serlvet实例的类加载,并把servlet实例赋给wrapper.instance变量。

3.3.2 mappperListener.start()

        这里重点是将部署好的web项目,映射到mapper中,以便后续做请求处理。

3.3.3 connector.start()

        开启acceptor和poller线程。connector.start()->connector.startInternal->protocolHandler.start()->(父类)AbstractProtocol.start()->endpoint.start()->Nioendpoint.startInternal()

4 Tomcat请求流程

4.1 acceptor线程

        接收socket请求,并交给具体endpoint的poller线程处理先在socket = endpoint.serverSocketAccept()打断点,然后用浏览器发起请求。

        调用Nioendpoint.serverSocketAccept()方法,等到socket连接,接收后,会返回对应的channe调用endpoint.setSocketOptions(socket)->poller.register(socketWrapper)->Nioendpoint.addEvent():将socket封装为socketWrapper,并将其添加到Poller的PollerEvent队列中,然后唤醒poller。

        然后acceptor线程的工作完成,可以把acceptor线程相关端点去掉。

4.2 Poller线程

        在AbstractEndpoint.processSocket()方法打个断点。重新调试发请求,可以看到调用栈:

        NioEndpoint$Poller.run():轮询,等待IO事件,然后调用NioEndpoint.processKey处理。

        NioEndpoint.processKey(sk, socketWrapper):主要一些条件判断,然后会调用processSocket来处理,processSocket方法的实现是来自于父类AbstractEndpoint。

        AbstractEndpoint.processSocket:获取endpoint对应的处理器,并把处理线程放入worker线程队列中去。

4.3 Worker线程

        可以在NioEndpoint$SocketProcessor.doRun()打断点,需要把其他线程的断点去掉。这里是通过调用worker线程组去处理请求,请求后续的操作都会在这个线程中完成。

        直接到NioEndpoint$SocketProcessor.doRun,前头的调用栈只是启动worker中的SocketProcessBase线程,跑到doRun中去。

        NioEndpoint$SocketProcessor.doRun->AbstractProtocol.process()       

        AbstractProtocol.process()->AbstractProcessorLight.process() :Http11Processor调用父类的process()

        AbstractProcessorLight.process()->Http11Processor.service()

Http11Processor.service()解析request,并调用适配器的service,即getAdapter().service(request, response)

        CoyoteAdapter.service开始调用容器管道的Valve来处理包装后的请求。那服务端是如何获取Valve的呢?知道获取流程就可以知道怎么和在哪添加恶意Valve。

        从这段代码可以知道,response在request内,并且catalina的request会备份一个coyoute的request。从回显的角度来说,每个阶段的response可看作一致的,因为相互包含,所以只需要获得任一response,就能把消息传回前端。

4.3.1 Valve的调用流程

        查看Valve的调用栈:这里选择StandardContext的两个Valve来说明执行流程:

        首先是AuthenticatorBase,继承于ValveBase,每个Valve都有一个内部变量next,指向下一个Valve,通过getNext()获取下一个Valve实例,然后调用invoke方法。

         然后是StandardContextValve,catalina的request中有一个mappingData用来存储各种容器,通过容器的管道去获取下一层级容器的第一个Valve,并调用Valve.invoke()。

        容器会从父类ContainerBase中继承一个StandardPipeline来管理Valve。

        StandardPipeline提供了addPipeline来添加Valve

        所以如果想要添加自己的恶意Valve,需要完成如下动作:

1. 获取StandardContext(所有容器都可以,因为可以通过parent和child来获取别的容器)

2. 写一个Valve恶意类,继承ValveBase类,重写iinvoke方法,并在里面写入恶意代码

3. 通过StandardContext获取其pipeline,然后调用pipeline.addValve(Valve)将恶意类进去

4. 往后每次请求到来,就都会走到恶意Valve中执行恶意代码。

        StandardWrapperValve.invoke():StandardWrapperValve是容器最后一个工作Valve,这里会主要完成两项工作:调用Filter链和调用servlet。

4.3.2 Filter的调用流程

        先创建filterChain,然后调用filetrChain.doFilter()。

    ApplicationFilterFactory.createFilterChain(request, wrapper, servlet):从Context中获取filterMaps:存放了Filter名称和urlPatterns。

        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet):接着遍历filerMaps,将filterConfig添加到addFilter里

         filterChain.doFilter(request.getRequest(), response.getResponse());会跳到ApplicationFilterChain中,调用filterConfig.getFilter()

        filterConfig.getFilter:先看filter是否存在,不存在就去filterDef中动态加载,所以在写内存马的时候,可以把filter套进filterDef中,避免filter不存在的情况。

        最终会走到自定义的FIlter的doFilter方法中。

        上面流程中用到的filterConfigs、filterDef、filterMaps都是StandardContext的变量获取,StandardContext还提供了对应addxx方法来添加filter 相关信息。

        所以,写Tomcat的filter内存马可以通过以下步骤:在StandardContext中的FilterDefs、FilterMaps、filterConfigs中存放我们filter的内容。

1. 获取StandardContext

2. 创建一个恶意filter,在doFilter中写入恶意代码

3. 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中

4. 实例化一个FilterMap类,将恶意 Filter 和 urlpattern 相对应,存放到StandardContext.filterMaps中(一般会放在首位)

5. 通过反射获取StandardContext的filterConfigs,实例化一个filterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDef,存放到filterConfigs中

4.3.3 Servlet的调用流程

        ApplicationFilterChain.internalDoFilter,直接调用了servlet.service,ApplicationFilterChain有servlet变量。通过servlet.service()会调用对应的Servlet类代码。

        调到这里发现有些步骤漏了,那就是ApplicationFilterChain的servlet变量是什么时候赋值的呢?ApplicationFilterChain有setServlet方法,在这里打个端点,重新调试。

        ApplicationFilterFacotry.createFilterChain()调用了setServlet():发现在servlet变量是传过来的

        继续往上,回到StandardWrapperValve.invoke(),发现servlet是通过wrapper.allocate获取。

        由于自定义的wrapper.instance变量为空(理由见3.3.1),wrapper.allocate的会oadServlet加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值