【Tomcat与网络8】从源码看Tomcat的层次结构

在前面我们介绍了如何通过源码来启动Tomcat,本文我们就来看一下Tomcat是如何一步步启动的,以及在启动过程中,不同的组件是如何加载的。

一般,我们可以通过 Tomcat 的 /bin 目录下的脚本 startup.sh 来启动 Tomcat,如果是window那就用startup.bat来启动。 那我们执行了这个脚本后发生了什么呢?

目录

1.整体结构

2.文件解析与组件创建器—Catalina

3.Catalina如何孕育出众多组件的

4.Server 组件

5.Service 组件

6.Engine 组件


1.整体结构

在一篇中,我们看到tomcat启动是从类Bootstrap里的Main方法开始的,因此对于Tomcat而说,Bootstrap就是创造万物的工具。

之后的工作,可以通过下面这张流程图来了解一下。

  1. Tomcat 本质上是一个 Java 程序,因此 startup.sh 脚本最 核心的工作就是启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap。
  2. Bootstrap 的主要任务是初始化 Tomcat 的类加载器,并且创建 Catalina。
  3. Catalina 是一个启动类,它通过解析 server.xml来创建相应的组件,并调用 Server 的 start 方法。
  4. Server 组件的职责就是管理 Service 组件,它会负责调用 Service 的 start() 方法。
  5. Service 组件的职责就是管理连接器和顶层容器 Engine,因此它会调用连接器和 Engine 的 start 方法。

这样 Tomcat 的启动就算完成了。下面我来详细介绍一下上面这个启动过程中提到的几个非常关键的启动类和组件。

如果我们以一个互联网大厂, 比如腾宝,那么 Bootstrap就是创始人马老师,而Server则是不同的事业群,例如支付宝、天猫、阿里云等等。Service 是事业群总经理,一般事业群里会有多个部门,而在Tomcat里,Service管理两个职能部门:一个是对外的市场部,也就是连接器组件;另一个是对内的研发部,也就是容器组件。

再向下,Engine 则是研发部经理,之后的Service就是具体干活的你和我,在Tomcat里就是Servlet。

2.文件解析与组件创建器—Catalina

Catalina 的主要任务就是创建 Server,它不是直接 new 一个 Server 实例就完事了,而是需要解析 server.xml,所以打开Catalina类的代码,我们会发现很多篇幅都是和解析xml或者Digester类有关系,后者也是解析文件的。

Catalina的作用就是把在 server.xml 里配置的各种组件一一创建出来,接着调用 Server 组件的 init 方法和 start 方法,这样整个 Tomcat 就启动起来了。作为“管理者”,Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩子”。

public void start() {
    //1. 如果持有的 Server 实例为空,就解析 server.xml 创建出来
    if (getServer() == null) {
        load();
    }
    //2. 如果创建失败,报错退出
    if (getServer() == null) {
        log.fatal(sm.getString("catalina.noServer"));
        return;
    }
 
    //3. 启动 Server
    try {
        getServer().start();
    } catch (LifecycleException e) {
        return;
    }
 
    // 创建并注册关闭钩子
    if (useShutdownHook) {
        if (shutdownHook == null) {
            shutdownHook = new CatalinaShutdownHook();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
 
    // 用 await 方法监听停止请求
    if (await) {
        await();
        stop();
    }
}

那什么是“关闭钩子”,它又是做什么的呢?如果我们需要在 JVM 关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向 JVM 注册一个“关闭钩子”。“关闭钩子”其实就是一个线程,JVM 在停止之前会尝试执行这个线程的 run 方法。下面我们来看看 Tomcat 的“关闭钩子”CatalinaShutdownHook 做了些什么。

protected class CatalinaShutdownHook extends Thread {
 
    @Override
    public void run() {
        try {
            if (getServer() != null) {
                Catalina.this.stop();
            }
        } catch (Throwable ex) {
           ...
        }
    }
}

从这段代码中你可以看到,Tomcat 的“关闭钩子”实际上就执行了 Server 的 stop 方法,Server 的 stop 方法会释放和清理所有的资源。

3.Catalina如何孕育出众多组件的

我们提的这些重要的组件是在Catalina的哪里创建的呢?如果没有找到位置,我们总是会感觉少了点什么。

这个创建的入口是Catalina里调用createStartDigester()来创建的,创建之后的内容通过Digester来管理。我们分别看创建各个组件的起始位置:

【1】创建Server实例

        digester.addObjectCreate("Server",
                                 "org.apache.catalina.core.StandardServer",
                                 "className");
        digester.addSetProperties("Server");
        digester.addSetNext("Server",
                            "setServer",
                            "org.apache.catalina.Server");

Catania中Server的默认实现类是类:org.apache.catalina.core.StandardServer。但是我们可以童工属性className来修改。

【2】创建全局J2EE的企业命名上下文JNDI

        digester.addObjectCreate("Server/GlobalNamingResources",
                                 "org.apache.catalina.deploy.NamingResourcesImpl");
        digester.addSetProperties("Server/GlobalNamingResources");
        digester.addSetNext("Server/GlobalNamingResources",
                            "setGlobalNamingResources",
                            "org.apache.catalina.deploy.NamingResourcesImpl");

JDNI这个名字经常见到,但是从来没用过,不管了。

【3】为Server增加生命周期监听器

        digester.addRule("Server/Listener",
                new ListenerCreateRule(null, "className"));
        digester.addSetProperties("Server/Listener");
        digester.addSetNext("Server/Listener",
                            "addLifecycleListener",
                            "org.apache.catalina.LifecycleListener");

Server元素支持配置生命周期监听器,用于为当前的Servlet实例添加LifecycleListener监听器

顺着这个方法向下看,我们会可以看到为Service添加Connector等等很多组件。

看这个有什么用呢?主要是帮助我们找到了学习的入口,知道怎么初始化,我们就能慢慢理清楚怎么工作了。

4.Server 组件

Server 组件的具体实现类是 StandardServer,我们来看下 StandardServer 具体实现了哪些功能。Server 继承了 LifeCycleBase,它的生命周期被统一管理,并且它的子组件是 Service,因此它还需要管理 Service 的生命周期,也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。Server 在内部维护了若干 Service 组件,它是以数组来保存的,那 Server 是如何添加一个 Service 到数组中的呢?

@Override
public void addService(Service service) {
 
    service.setServer(this);
 
    synchronized (servicesLock) {
        // 创建一个长度 +1 的新数组
        Service results[] = new Service[services.length + 1];
        
        // 将老的数据复制过去
        System.arraycopy(services, 0, results, 0, services.length);
        results[services.length] = service;
        services = results;
 
        // 启动 Service 组件
        if (getState().isAvailable()) {
            try {
                service.start();
            } catch (LifecycleException e) {
                // Ignore
            }
        }
 
        // 触发监听事件
        support.firePropertyChange("service", null, service);
    }
 
}

从上面的代码你能看到,它并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组,这样做的目的其实是为了节省内存空间。

这种用法很明显效率是不高的,因此在工程里极少看到,这里也算增长了我们的见识吧。

除此之外,Server 组件还有一个重要的任务是启动一个 Socket 来监听停止端口,这就是为什么你能通过 shutdown 命令来关闭 Tomcat。不知道你留意到没有,上面 Caralina 的启动方法的最后一行代码就是调用了 Server 的 await 方法。

在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。

5.Service 组件

Service 组件的具体实现类是 StandardService,我们先来看看它的定义以及关键的成员变量。

public class StandardService extends LifecycleBase implements Service {
    // 名字
    private String name = null;
    
    //Server 实例
    private Server server = null;
 
    // 连接器数组
    protected Connector connectors[] = new Connector[0];
    private final Object connectorsLock = new Object();
 
    // 对应的 Engine 容器
    private Engine engine = null;
    
    // 映射器及其监听器
    protected final Mapper mapper = new Mapper();
    protected final MapperListener mapperListener = new MapperListener(this);

StandardService 继承了 LifecycleBase 抽象类,此外 StandardService 中还有一些我们熟悉的组件,比如 Server、Connector、Engine 和 Mapper。

那为什么还有一个 MapperListener?这是因为 Tomcat 支持热部署,当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式。

作为“管理”角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。我们来看看 Service 启动方法:

protected void startInternal() throws LifecycleException {
 
    //1. 触发启动监听器
    setState(LifecycleState.STARTING);
 
    //2. 先启动 Engine,Engine 会启动它子容器
    if (engine != null) {
        synchronized (engine) {
            engine.start();
        }
    }
    
    //3. 再启动 Mapper 监听器
    mapperListener.start();
 
    //4. 最后启动连接器,连接器会启动它子组件,比如 Endpoint
    synchronized (connectorsLock) {
        for (Connector connector: connectors) {
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }
}

从启动方法可以看到,Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。

6.Engine 组件

最后我们再来看看顶层的容器组件 Engine 具体是如何实现的。Engine 本质是一个容器,因此它继承了 ContainerBase 基类,并且实现了 Engine 接口。

public class StandardEngine extends ContainerBase implements Engine {
}

我们知道,Engine 的子容器是 Host,所以它持有了一个 Host 容器的数组,这些功能都被抽象到了 ContainerBase 中,ContainerBase 中有这样一个数据结构:

protected final HashMap<String, Container> children = new HashMap<>();

ContainerBase 用 HashMap 保存了它的子容器,并且 ContainerBase 还实现了子容器的“增删改查”,甚至连子组件的启动和停止都提供了默认实现,比如 ContainerBase 会用专门的线程池来启动子容器。

for (int i = 0; i < children.length; i++) {
   results.add(startStopExecutor.submit(new StartChild(children[i])));
}

所以 Engine 在启动 Host 子容器时就直接重用了这个方法。

那 Engine 自己做了什么呢?我们知道容器组件最重要的功能是处理请求,而 Engine 容器对请求的“处理”,其实就是把请求转发给某一个 Host 子容器来处理,具体是通过 Valve 来实现的。

通过前面的学习,我们知道每一个容器组件都有一个 Pipeline,而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:

final class StandardEngineValve extends ValveBase {
 
    public final void invoke(Request request, Response response)
      throws IOException, ServletException {
  
      // 拿到请求中的 Host 容器
      Host host = request.getHost();
      if (host == null) {
          return;
      }
  
      // 调用 Host 容器中的 Pipeline 中的第一个 Valve
      host.getPipeline().getFirst().invoke(request, response);
  }
  
}

这个基础阀实现非常简单,就是把请求转发到 Host 容器。你可能好奇,从代码中可以看到,处理请求的 Host 容器对象是从请求中拿到的,请求对象中怎么会有 Host 容器呢?这是因为请求到达 Engine 容器中之前,Mapper 组件已经对请求进行了路由处理,Mapper 组件通过请求的 URL 定位了相应的容器,并且把容器对象保存到了请求对象中。

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Tomcat是一个开源的Java Servlet容器,作为Java服务器发展过程中最重要的组件之一。Tomcat的源代码,指的就是Tomcat的开源代码。 Tomcat 8.5版本的源码Tomcat项目最新的稳定版本,它包含了Tomcat运行所必需的所有代码和配置文件。这个版本的Tomcat有很多改进和优化,例如对HTTP/2的支持、WebSocket的增强、Servlet 3.1规范的实现等。 Tomcat 8.5的源码Java语言编写,使用了Java的一些高级特性和设计模式。这些源码主要包括核心组件的实现,例如Connector、Server、Service和Container等,还包括了一些相关的工具和附属组件的代码。 阅读Tomcat源码可以帮助我们更深入地理解Tomcat的运行机制和内部实现原理。通过阅读源码,我们可以学习到Tomcat是如何处理HTTP请求和响应的、如何管理Servlet的生命周期、如何处理Session和Cookie等。阅读源码还可以帮助我们更好地定位和解决Tomcat的问题,以及进行自定义的扩展和优化。 阅读Tomcat源码需要一定的Java编程基础和相关的Web开发知识。可以通过下载Tomcat源码,使用Eclipse等IDE工具进行调试和阅读,逐步理解其中的逻辑和细节。在阅读源码的过程中,可以查阅官方文档和源码注释,也可以参考一些相关的书籍和教程。此外,还可以参加Tomcat社区的讨论和交流,与其他开发者一起探讨和分享经验。 总之,阅读Tomcat 8.5的源码是一个学习和提升自己的好途径,通过深入理解Tomcat源码,我们可以更好地使用和开发Tomcat,提高Web应用的性能和稳定性。 ### 回答2: Tomcat 8.5是一个用Java编写的开源Web服务器和Servlet容器,它实现了Java Servlet和JavaServer Pages(JSP)规范。要理解Tomcat 8.5的源代码,需要对Java编程和网络通信有一定的了解。 Tomcat 8.5的源代码是一个庞大而复杂的项目,由许多模块和包组成。它包括了处理HTTP请求的核心部分,处理Servlet和JSP的部分,以及其他一些与Web应用程序部署和管理相关的模块。 在Tomcat 8.5的源代码中,你会看到许多Java类和接口,这些类和接口定义了处理HTTP请求和响应的各种功能,包括建立网络连接、解析HTTP请求、执行Servlet和JSP、处理会话状态、处理静态资源等。这些类和接口之间通过继承和组合来建立关系,形成了一个完整的处理流程。 在Tomcat 8.5的源代码中,你还会看到许多配置文件和注释,这些文件和注释用来配置Tomcat服务器和Web应用程序的行为。配置文件包括服务器配置文件、虚拟主机配置文件、Web应用程序部署描述文件等。通过配置文件,你可以在不改变源代码的情况下改变服务器和应用程序的行为。 了解Tomcat 8.5的源代码对于开发和维护Web应用程序非常有帮助。通过阅读源代码,你可以深入了解Tomcat的工作原理,了解HTTP协议和Servlet容器的细节,以及优化和定制Tomcat服务器和Web应用程序的方法。 总之,Tomcat 8.5的源代码是一个复杂而庞大的项目,通过阅读源代码,你可以深入了解Tomcat的工作原理和行为,并为开发和维护Web应用程序提供帮助。但是要理解Tomcat的源代码,需要对Java编程和网络通信有一定的了解。 ### 回答3: Tomcat8.5源码是Apache Tomcat的一个版本,它是一个流行的开源Java Servlet容器,用于在Java环境中运行Web应用程序。 Tomcat8.5的源码是以Java编写的,它的主要目的是实现Java Servlet和JavaServer Pages(JSP)规范。源码的结构非常复杂,包含多个模块和组件,比如Catalina模块用于处理Servlet容器功能,Coyote模块用于处理HTTP请求和响应,Jasper模块用于JSP页面的编译和执行,以及其他许多模块和组件用于提供各种功能。 Tomcat8.5源码的核心功能包括请求处理、会话管理、线程池管理、虚拟主机支持、安全性、类加载和内存管理等。源码的设计考虑了高性能、可扩展性和可靠性的因素。 了解Tomcat8.5源码可以帮助我们深入理解Servlet容器的工作原理,以及如何优化和扩展Tomcat的功能。阅读源码可以学习到一些Java技术的实现细节,比如多线程编程、网络编程和类加载机制等。 同时,通过阅读和分析Tomcat8.5源码,我们还可以学到一些优秀的软件工程实践,比如面向对象设计、模块化设计和代码重用等。这对我们提升自己的编程能力和代码质量都是非常有帮助的。 总之,Tomcat8.5源码是一个非常有价值的学习资源,通过研究它,我们可以深入了解Java Servlet容器的实现原理,并可以通过阅读优秀的源码来提升自己的编程能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值