0
点赞
收藏
分享

微信扫一扫

二. Tomcat 从启动流程到请求处理

yellowone 2022-03-16 阅读 111

二. Tomcat 从启动流程到请求处理

说明: 使用的源码版本是tomcat 8.5

1. Tomcat 启动流程

1. Tomcat 源码目录

image-20220312181157376

catalina 目录

catalina 包含所有的 Servlet 容器实现,以及涉及到安全、会话、集群、部署管理 Servlet 容器的各个方面,同时,它还包含了启动入口。

coyote 目录

coyote 是 Tomcat 链接器框架的名称,是 Tomcat 服务器提供的客户端访问的外部接口,客户端通过 Coyote 与服务器建立链接、发送请求并接收响应。

EL 目录,提供 java 表达式语言

Jasper 模块提供 JSP 引擎

Naming 模块提供 JNDI 的服务

Jul 提供服务器日志的服务

tomcat 提供外部调用的接口 api

2. Tomcat 启动流程分析

启动流程解析注意是标准的启动,也就是从 bin 目录下的启动文件中启动 Tomcat(Spring Boot启动方式通过OnRefresh方法实例化嵌入式的Tomcat)

image-20220312175344997

我们可以看到这个流程非常的清晰,同时注意到,Tomcat 的启动非常的标准,除去 Boostrap 和 Catalin,我们可以对照一下 Server.xml 的配置文件。Server,service 等等这些组件都是一一对照,同时又有先后顺序。

基本的顺序是先 init 方法,然后再 start 方法。

**加入调试信息:**注意是标准的启动,也就是从bin 目录下的启动文件中启动 Tomcat

在适当的位置打印对象信息, 便于分析流程, 添加打印的位置

org.apache.catalina.startup.Bootstrap#load

private void load(String[] arguments)
        throws Exception {
    System.out.println("Bootsrap--load()"); // 这里
    // Call the load() method
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments == null || arguments.length == 0) {
        paramTypes = null;
        param = null;
    } else {
        paramTypes = new Class[1];
        paramTypes[0] = arguments.getClass();
        param = new Object[1];
        param[0] = arguments;
    }
    Method method =
            catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled())
        log.debug("Calling startup class " + method);
    method.invoke(catalinaDaemon, param);

}

org.apache.catalina.startup.Catalina#load()

/**
 * 启动新的服务器实例。
 */
public void load() {
    System.out.println("Catalina--load()"); // 这里
    if (loaded) {
        return;
    }
    ...
}

org.apache.catalina.startup.Catalina#load(java.lang.String[])

public void load(String args[]) {
    System.out.println("Catalina--load()"); // 这里
    try {
        if (arguments(args)) {
            load();
        }
    } catch (Exception e) {
        e.printStackTrace(System.out);
    }
}

除了 Bootstrap 和 catalina 类,其他的 Server,service 等等之类的都只是一个接口,实现类均为 StandardXXX 类。 我们来看下 StandardServer 类,问题来了,我们发现 StandardServer 类中没有 init 方法,只有一个类似于 init 的 initInternal 方法,这个是为什么? 带着问题我们进入下面的内容。

2. 分析 Tomcat 请求过程

解耦:网络协议与容器的解耦。

Connector 链接器封装了底层的网络请求(Socket 请求及相应处理),提供了统一的接口,使 Container 容器与具体的请求协议以及 I/O 方式解耦。

Connector 将 Socket 输入转换成 Request 对象,交给 Container 容器进行处理,处理请求后,Container 通过 Connector 提供的 Response 对象将结果写入输出流。

因为无论是 Request 对象还是 Response 对象都没有实现 Servlet 规范对应的接口,Container 会将它们进一步分装成 ServletRequest 和 ServletResponse.

org.apache.coyote.Request: 连接器使用的Request

org.apache.catalina.connector.Request: 符合servlet规范, 有输入流, Response类似

问题来了,在 Engine 容器中,有四个级别的容器,他们的标准实现分别是 StandardEngine、StandardHost、StandardContext、StandardWrapper。

3. 组件的生命周期管理

1. 各种组件如何统一管理

Tomcat 的架构设计是清晰的、模块化、它拥有很多组件,加入在启动 Tomcat 时一个一个组件启动,很容易遗漏组件,同时还会对后面的动态组件拓 展带来麻烦。如果采用我们传统的方式的话,组件在启动过程中如果发生异常,会很难管理,比如你的下一个组件调用了 start 方法,但是如果它的上级 组件还没有 start 甚至还没有 init 的话,Tomcat 的启动会非常难管理,因此,Tomcat 的设计者提出一个解决方案:用 Lifecycle 管理启动,停止、关闭。

2. 生命周期统一接口–Lifecycle

Tomcat 内部架构中各个核心组件有包含与被包含关系,例如:Server 包含了 Service.Service 又包含了 Container 和 Connector,这个结构有一点像数据结 构中的树,树的根结点没有父节点,其他节点有且仅有一个父节点,每一个父节点有 0 至多个子节点。所以,我们可以通过父容器启动它的子容器,这 样只要启动根容器,就可以把其他所有的容器都启动,从而达到了统一的启动,停止、关闭的效果。

4. 分析 Tomcat 请求过程

1. Host 设计的目的

Tomcat 诞生时,服务器资源很贵,所以一般一台服务器其实可以有多个域名映射,满了满足这种需求,比如,我的这台电脑,有一个 localhost 域名, 同时在我的 hosts 文件中配置两个域名,一个 www.a.com 一个 localhost。

2. Context 设计的目的

container 从上一个组件 connector 手上接过解析好的内部 request,根据 request 来进行一系列的逻辑操作,直到调用到请求的 servlet,然后组装好 response, 返回给 connecotr

先来看看 container 的分类吧:

Engine

Host

Context

Wrapper

它们各自的实现类分别是 StandardEngine, StandardHost, StandardContext,StandardWrapper,他们都在 tomcat 的 org.apache.catalina.core 包下。

它们之间的关系,可以查看 tomcat 的 server.xml 也能明白(根据节点父子关系),这么比喻吧:除了 Wrapper 最小,不能包含其他 container 外,Context内可以有零或多个 WrapperHost 可以拥有零或多个 ContextEngine 可以有零到多个 Host

Standard 的 container 都是直接继承抽象类:org.apache.catalina.core.ContainerBase:

public abstract class ContainerBase extends LifecycleMBeanBase implements Container {

image-20220313231034689

3. Tomcat 处理一个 HTTP 请求的过程

用户点击网页内容,请求被发送到本机端口 8080,被在那里监听的 Coyote HTTP/1.1 Connector 获得。

Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待 Engine 的回应。

Engine 获得请求 localhost/test/index.jsp,匹配所有的虚拟主机 Host。

Engine 匹配到名为 localhost 的 Host(即使匹配不到也把请求交给该 Host 处理,因为该 Host 被定义为该 Engine 的默认主机),名为 localhost 的 Host 获得请求/test/index.jsp,匹配它所拥有的所有的 Context。Host 匹配到路径为/test 的 Context(如果匹配不到就把该请求交给路径名为“”的 Context 去处理)。 path=“/test”的 Context 获得请求/index.jsp,在它的 mapping table 中寻找出对应的 Servlet。Context 匹配到 URL PATTERN 为*.jsp 的 Servlet,对应于 JspServlet类。

构造 HttpServletRequest 对象和 HttpServletResponse 对象,作为参数调用 JspServlet 的 doGet()或 doPost().执行业务逻辑、数据存储等程序。 Context 把执行完之后的 HttpServletResponse 对象返回给 Host。

Host 把 HttpServletResponse 对象返回给 Engine。

Engine 把 HttpServletResponse 对象返回 Connector。

Connector 把 HttpServletResponse 对象返回给客户 Browser。

5. 管道模式

1. 管道与阀门

在一个比较复杂的大型系统中,如果一个对象或数据流需要进行繁杂的逻辑处理,我们可以选择在一个大的组件中直接处理这些繁杂的逻辑处理, 这个方式虽然达到目的,但是拓展性和可重用性差。因为牵一发而动全身。

管道是就像一条管道把多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,而处理逻辑放在阀门上。

它的结构和实现是非常值得我们学习和借鉴的。

每一种 container 都有一个自己的 StandardValve

StandardEngineValve

StandardHostValve

StandardContextValve

StandardWrapperValve

Pipeline 就像一个工厂中的生产线,负责调配工人(valve)的位置,valve 则是生产线上负责不同操作的工人。 一个生产线的完成需要两步:

1,把原料运到工人边上

2,工人完成自己负责的部分

而 tomcat 的 Pipeline 实现是这样的:

1,在生产线上的第一个工人拿到生产原料后,二话不说就人给下一个工人,下一个工人模仿第一个工人那样扔给下一个工人,直到最后一个工人,而最后一个工人被安排为上面提过的 StandardValve,他要完成的工作居然是把生产资料运给自己包含的 container 的 Pipeline 上去。

2,四个 container 就相当于有四个生产线(Pipeline),四个 Pipeline 都这么干,直到最后的 StandardWrapperValve 拿到资源开始调用 servlet。完成后返回来,一步一步的 valve 按照刚才丢生产原料是的顺序的倒序一次执行。如此才完成了 tomcat 的 Pipeline 的机制。

2. 手写管道模式实现

在管道中连接一个或者多个阀门,每一个阀门负责一部分逻辑处理,数据按照规定的顺序往下流。此种模式分解了逻辑处理任务,可方便对某个任务单元进行安装、拆卸,提高流程的可拓展性,可重用性,机动性,灵活性。

管道接口

public interface Pipeline {
    //获取第一个阀门
    Valve getFirst();

    Valve getBasic();

    //设置阀门
    void setBasic(Valve valve);

    //添加阀门
    void addVave(Valve valve);
}

阀门接口

public interface Valve {
    Valve getNext();

    void setNext(Valve valve);

    void invoke(String handing);
}

阀门实现类

public class FirstValve implements Valve {
    protected Valve next = null;

    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        this.next = valve;
    }

    @Override
    public void invoke(String request) {
        System.out.println("定制阀门1处理");
        getNext().invoke(request);
    }
}
public class StandardValve implements Valve {
    protected  Valve next =null; // 每一个都保存next
    @Override
    public Valve getNext() {
        return next;
    }

    @Override
    public void setNext(Valve valve) {
        this.next =valve;
    }

    @Override
    public void invoke(String request) {
        request = request+"xxoo,";
        System.out.println("基础阀门处理");
    }
}

管道实现类

public class StandardPipeline implements Pipeline {
    //阀门(非基础,定义一个first)
    protected Valve first = null;
    //基础阀门
    protected Valve basic = null;

    @Override
    public Valve getBasic() {
        return basic;
    }

    @Override
    public void setBasic(Valve valve) {
        this.basic = valve;
    }

    @Override
    public Valve getFirst() {
        return first;
    }

    //添加阀门,链式构建阀门的执行顺序(先定制、最后基础阀门)
    @Override
    public void addVave(Valve valve) {
        if (first == null) {
            first = valve;
            valve.setNext(basic);
        } else {
            Valve current = first;
            while (current != null) {
                if (current.getNext() == basic) {
                    current.setNext(valve);
                    valve.setNext(basic);
                }
                current = current.getNext();
            }
        }
    }
}

测试类

public static void main(String[] args) {
    String request = "这个是一个Servlet请求";
    //new出一个管道
    StandardPipeline pipeline = new StandardPipeline();
    //三个阀门(一个基础、2个定制)
    StandardValve standardValve = new StandardValve();
    FirstValve firstValve = new FirstValve();
    SecondValve secondValve = new SecondValve();
    //设置基础阀门(定制阀门)
    pipeline.setBasic(standardValve);
    //设置非基础阀门
    pipeline.addVave(firstValve);
    pipeline.addVave(secondValve);
    //调用对象管道中的第一个阀门
    pipeline.getFirst().invoke(request);
}

3. 源码分析

在 CoyoteAdapter 的 service 方法里,由下面这一句就进入 Container 的。

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

来自 org.apache.catalina.core.StandardEngineValve 的 invoke 方法:

public final void invoke(Request request, Response response)
    throws IOException, ServletException {

    // Select the Host to be used for this Request
    Host host = request.getHost(); // 拿到自己的host
    if (host == null) {
        response.sendError
            (HttpServletResponse.SC_BAD_REQUEST,
             sm.getString("standardEngine.noHost",
                          request.getServerName()));
        return;
    }
    if (request.isAsyncSupported()) { // 设置是不支持标记
        request.setAsyncSupported(host.getPipeline().isAsyncSupported());
    }

    // Ask this Host to process this request
    host.getPipeline().getFirst().invoke(request, response);
    //  感觉StandardEngineValve无法扩展了?, 其实是理解错了, 代码执行到这个invoke时候,说明一定是basic, 因为这个被定义为basic阀门, 我们自定义的只能是非基础阀门, 不然整个管道与管道之间不能保证连通没有问题, basic阀门可以理解成管道之间的接口
}

// 注意, 这里
// org.apache.catalina.core.StandardEngine#StandardEngine
public StandardEngine() {

    super();
    pipeline.setBasic(new StandardEngineValve()); // 基础阀门
    /* Set the jmvRoute using the system property jvmRoute */
    try {
        setJvmRoute(System.getProperty("jvmRoute"));
    } catch(Exception ex) {
        log.warn(sm.getString("standardEngine.jvmRouteFail"));
    }
    // By default, the engine will hold the reloading thread
    backgroundProcessorDelay = 10;

}

其他的类似 StandardHostValve、StandardContextValve、StandardWrapperValve

basic阀门可能理解成管道之间或者是任何管道对接的接口, tomcat也只提供了默认的basic阀门, 其他定制需要我们自己添加非基础的阀门

4. Tomcat 中定制阀门

管道机制给我们带来了更好的拓展性,例如,你要添加一个额外的逻辑处理阀门是很容易的。

  1. 自定义个阀门 PrintIPValve,只要继承 ValveBase 并重写 invoke 方法即可。注意在 invoke 方法中一定要执行调用下一个阀门的操作,否则会出现异常。
public class PrintIPValve extends ValveBase{ 
    @Override public void invoke(Request request, Response response) throws IOException, ServletException {
        System.out.println("------自定义阀门 PrintIPValve:"+request.getRemoteAddr()); 
        getNext().invoke(request,response); 
    } 
}
  1. 配置Tomcat的核心配置文件server.xml,这里把阀门配置到Engine容器下,作用范围就是整个引擎,也可以根据作用范围配置在Host或者是Context 下

    <Valve className="org.apache.catalina.valves.PrintIPValve" />
    
  2. 源码中是直接可以有效果,但是如果是运行版本,则可以将这个类导出成一个 Jar 包放入 Tomcat/lib 目录下,也可以直接将.class 文件打包进 catalina.jar 包中。

5. Tomcat 中提供常用的阀门

AccessLogValve,请求访问日志阀门,通过此阀门可以记录所有客户端的访问日志,包括远程主机 IP,远程主机名,请求方法,请求协议,会话 ID, 请求时间,处理时长,数据包大小等。它提供任意参数化的配置,可以通过任意组合来定制访问日志的格式。

JDBCAccessLogValve,同样是记录访问日志的阀门,但是它有助于将访问日志通过 JDBC 持久化到数据库中。

ErrorReportValve,这是一个将错误以 HTML 格式输出的阀门

PersistentValve,这是对每一个请求的会话实现持久化的阀门RemoteAddrValve,访问控制阀门。可以通过配置决定哪些 IP 可以访问 WEB 应用

RemoteHostValve,访问控制阀门,通过配置觉得哪些主机名可以访问 WEB 应用

RemoteIpValve,针对代理或者负载均衡处理的一个阀门,一般经过代理或者负载均衡转发的请求都将自己的 IP 添加到请求头”X-Forwarded-For”中,此时,通过阀门可以获取访问者真实的 IP。

SemaphoreValve,这个是一个控制容器并发访问的阀门,可以作用在不同容器上

举报

相关推荐

0 条评论