Tomcat:总体架构

Server、Service、Lifecycle

tomcat有一个配置文件server.xml,我们的很多配置在该配置文件中配置,然后tomcat启动后读取到配置。

对于tomcat的架构,我们从server.xml中也可见一斑。

下面是一个server.xml:

<?xml version="1.0" encoding="UTF-8"?>

<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- 实现JNDI
        Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
       执行资源的配置信息
  -->
  <GlobalNamingResources>

    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>

  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">

      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <!--主机-->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">

        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>

</Server>

根目录是一个server标签,代表着整个tomcat服务器,而server标签中可以配置GlobalNamingResources标签,对应着JNDI的相关实现,还可以配置多个service,而tomcat官方默认的service是Catalina服务。而源码中恰好有一个类叫做org.apache.catalina.Server

image-20211029094109236

再看这个类的方法,可以配置port,可以addService,可以设置GlobalNamingResources,所以猜测server标签对应着Server类。那么是不是Service类对应着service标签,是的,没错。

org.apache.catalina.Service类的方法:

image-20211029094649208

通过addConnector方法可以添加一个Connector,译为连接器;通过addExecutor方法可以添加一个Executor,作为Service的线程池,此外同一个Service中的组件可以共享一个线程池(如果没有配置会自动创建默认的线程池);通过setContainer方法可以添加一个Engine,译为引擎。而上述在Service中配置的三个组件:Connector、Executor、Engine,在我们的server.xml文件service标签中同样是可以配置的。

我们再看Connector、Executor、Engine、Service、Server类,这些类都有一个父接口Lifecycle,译为生命周期,提供了四个声明周期方法,init初始化、start启动、stop停止、destroy销毁方法。

image-20211029100147844

Container

我们知道一个Server中可以配置多个Service。这里我们先做一个小实验,修改server.xml。

<?xml version="1.0" encoding="UTF-8"?>

<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <!-- Security listener. Documentation at /docs/config/listeners.html
  <Listener className="org.apache.catalina.security.SecurityListener" />
  -->
  <!-- APR library loader. Documentation at /docs/apr.html -->
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

  <!-- 实现JNDI
        Global JNDI resources
       Documentation at /docs/jndi-resources-howto.html
       执行资源的配置信息
  -->
  <GlobalNamingResources>
    <!-- Editable user database that can also be used by
         UserDatabaseRealm to authenticate users
    -->
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>


  <Service name="Catalina">

    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <!--主机-->
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />

      </Host>
    </Engine>
  </Service>

  <Service name="wj">

    <Connector port="8081" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8444" />

    <Engine name="wj" defaultHost="localhost">

      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>
      <!--主机-->
      <Host name="localhost"  appBase="wj"
            unpackWARs="true" autoDeploy="true">

        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

这里我又配置了一个service,而这个service监听8081端口,旗下所有的请求处理都去wj目录下去找(因为我配置了appBase),并且主机是localhost(在Host标签中配置),然后新建一个index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    hello tomcat
</body>
</html>

目录层级如下:

image-20211029111835048

启动tomcat后,直接访问localhost:8081/hello,能直接访问到我写的index.html

image-20211029111931029

Connector负责开启Socket并监听客户端请求、返回响应数据。当Connector收到请求后,会将请求交给Engine(引擎),Engine只负责请求的处理,并不需要考虑请求链接、协议的处理。

Engine中可以配置Host(虚拟主机),通过配置多个Host,我们就可以提供多个域名的服务。

注意到:Engine、Host的父接口是Container容器,该类中有一个重要的方法:addChild,通过此方法来设定组件的层级关系。

image-20211029141226734

而Engine中的addChild方法:则是添加Host

    @Override
    public void addChild(Container child) {

        if (!(child instanceof Host)) {
            throw new IllegalArgumentException
                (sm.getString("standardEngine.notHost"));
        }
        super.addChild(child);

    }

Host中的addChild:是添加Context

    @Override
    public void addChild(Container child) {

        if (!(child instanceof Context)) {
            throw new IllegalArgumentException
                (sm.getString("standardHost.notContext"));
        }

        child.addLifecycleListener(new MemoryLeakTrackingListener());

        // Avoid NPE for case where Context is defined in server.xml with only a
        // docBase
        Context context = (Context) child;
        if (context.getPath() == null) {
            ContextName cn = new ContextName(context.getDocBase(), true);
            context.setPath(cn.getPath());
        }

        super.addChild(child);

    }

在tomcat设计中,一个应用的所有信息就是一个Context。

在tomcat设计中,Engine既可以包含Host,又可以包含Context。但在Tomcat提供的默认实现StandardEngine中只能包含Host

Context中的addChild,是添加Wrapper:

    @Override
    public void addChild(Container child) {

        // Global JspServlet
        Wrapper oldJspServlet = null;

        if (!(child instanceof Wrapper)) {
            throw new IllegalArgumentException
                (sm.getString("standardContext.notWrapper"));
        }
        ......

Wrapper是什么?

public class StandardWrapper extends ContainerBase
    implements ServletConfig, Wrapper, NotificationEmitter {

    private final Log log = LogFactory.getLog(StandardWrapper.class); // must not be static

    protected static final String[] DEFAULT_SERVLET_METHODS = new String[] {
                                                    "GET", "HEAD", "POST" };

Wrapper实现了javax.servlet.ServletConfig,ServletConfig中定义了Servlet的信息,一个ServletConfig对应了一个Servlet,所以在tomcat中,我们可以简单理解Wrapper就是tomcat的Servlet。

那么Container到底应该怎么理解?在上面,容器有时候指Engine、有时候指Host,但是它却代表了一类组件,这类组件的作用就是处理接收客户端的请求并返回响应数据。可能具体操作会委派到子容器完成,但是从行为上,它们是一致的。

注意:既然tomcat的Container可以表示不同的概念级别:Servlet引擎、虚拟主机、web应用和Servlet,那么我们就可以将不同级别的容器作为处理客户端请求的组件,这由我们提供的服务器的复杂度决定。例如我们以嵌入式的方式启动tomcat,且运行极其简单的请求处理,不必支持多web应用的场景,那么我们完全可以只在Service中维护一个简化版的Engine(甚至直接维护一个Context)。

下面简单画出一个类图:

image-20211029144839229

Pipeline和Valve

从架构设计的角度上来看,上面已经完成了对核心概念的分解,确保了整体架构的可伸缩性和可扩展性,除此之外,应当提高每一个组件的灵活性,使其同样易于扩展。

tomcat中采用了责任链模式来实现客户端请求的处理(请求处理也是责任链模式的典型应用场景之一)。换句话说,tomcat中,每个Container组件通过执行一个职责链来完成具体的请求处理。

Tomcat中定义Pipleline(管道)和Valve(阀门)两个接口。前者用于构造职责链,后者代表链上的每个处理器。有点像来自客户端上的请求就像流经管道里的水一样,会经过每个阀进行处理。

设计如下图所示:

image-20211029150037712

Pipeline中维护了一个基础的Valve,定义为basic,它始终位于Pipeline的末端(最后执行),封装了具体的请求处理和输出响应的过程,我们可以为Pipeline添加其他的Valve,后添加的Valve在基础Valve之前,并按照添加顺序执行。Pipeline通过获得首个Valve来启动整个链的顺序执行。具体可参考org.apache.catalina.core.StandardPipeline类的addValve方法和setBasic方法实现。

Tomcat容器灵活之处在于,每个层级的容器(Engine、Host、Context、Wrapper)均有对应的基础Valve实现,同时维护了一个Pipeline实例,所以我们能够在任意层级的容器上对请求处理进行扩展。

下面是简单的类图:

image-20211029151402670

Connector

在tomcat中,有另一个非常重要的组件Connector。想要与Container配合实现一个完整的服务器功能,Connector至少要完成如下几个功能:

  • 监听服务器端口,读取来自客户端的请求
  • 将请求数据按照指定协议进行解析
  • 根据请求地址匹配正确的容器进行处理
  • 将响应返回给客户端

tomcat支持多协议,默认支持HTTP和AJP,同时tomcat中还支持多种I/O方式,包括BIO(8.5后被移除)、NIO、APR。tomcat8之后新增了对NIO2和HTTP/2协议的支持。

tomcat的设计方案如下:

image-20211029153330897

在tomcat中,ProtocolHandler代表协议处理器,针对不同的协议和I/O方式,提供了不同的实现。ProtocolHandler包含一个Endpoint用于启动Socket监听,该接口按照I/O方式进行分类实现。(tomcat并没有Endpoint接口,仅有AbstractEndpoint抽象类,此处作为概念讨论,将其视为Endpoint接口)还包含一个Processor用于按照指定协议读取数据,并将请求交由容器处理。

当Processor读取客户端请求后,需要按照请求地址映射到具体的容器进行处理,这个过程称为请求映射。tomcat中各个组件采用通用的生命周期管理,而且通过管理工具进行状态变更。

tomcat通过MapperMapperListener两个类实现上述功能。前者用于维护容器映射信息,同时按照映射规则(Servlet规范定义)查找容器,后者实现了ContainerListenerLifecycleListener,用于在容器组件状态发生变更时,注册或取消对应的容器映射信息。同时MapperListener继承LifecycleMBeanBase,间接相当于实现了Lifecycle接口,当Service启动时,会自动作为监听器注册到各个组件上,同时将已创建的容器注册到Mapper

tomcat通过适配器(org.apache.coyote.Adapter)模式实现了Connector与Mapper、Container的解耦。tomcat默认的Connector实现对应的适配器为CoyoteAdapter

类图如下:

image-20211029161138050

Executor

tomcat提供Executor接口来表示一个可以在组件间共享的线程池,该接口同样继承自Lifecycle,可按照通用的组件进行管理。

在tomcat中,Executor由Service维护,因此同一个Service中的组件可以共享一个线程池。如果没有定义任何线程池,相关组件(如Endpoint)会自动创建线程池,此时,线程池不再共享。

在tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户端请求后,会创建请求处理对象,并交由线程池处理,由此支持并发处理客户端请求。

加入线程池后,类图如下:

image-20211029161907929

Boortstrap和Catalina

tomcat通过类Catalina提供了一个Shell程序,用于解析server.xml创建各个组件,同时负责启动、停止应用服务器

tomcat使用Digester解析xml文件,包括server.xml和web.xml。具体可参考org.apache.catalina.startup.Catalina#parseServerXml

最后tomcat提供了Bootstrap作为应用服务器的启动入口,Bootstrap负责创建Catalina实例,根据执行参数调用Catalina相关方法完成针对应用服务器的操作。

至此,tomcat应用完整的设计类图如下:

image-20211029163219578

总结

组件名称 说明
Server 表示整个Servlet容器,因此tomcat运行环境中只有唯一一个Server实例。
Service Service表示一个或者多个Connector的集合,这些Connector共享同一个Container来处理请求。
Connector tomcat连接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理,支持不同协议以及不同的I/O方式。
Container 表示能够执行客户端请求并返回响应的一类对象。
Engine 表示整个Servlet引擎,最为最高层级的容器对象,尽管不是直接处理请求的容器,却是获取目标容器的入口。
Host 表示Servlet引擎中的虚拟机,与一个服务器的网络名有关。
Context 表示ServletContext,在Servlet规范中,一个ServletContext表示一个独立的web应用。
Wrapper 表示web应用中定义的Servlet。
Executor 组件中可以共享的线程池。
原文地址:https://www.cnblogs.com/wwjj4811/p/15481212.html