zoukankan      html  css  js  c++  java
  • 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 组件中可以共享的线程池。
  • 相关阅读:
    java编码过滤器
    DAO设计模式
    常用的SQL语句
    IO流总结
    IO流的登录与注册
    设计模式之模板方法模式(Template Method)详解及代码示例
    设计模式之享元模式(Flyweight)详解及代码示例
    设计模式之桥接模式(Bridge)详解及代码示例
    设计模式之组合模式(Composite)详解及代码示例
    设计模式之外观模式(Facade)详解及代码示例
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/15481212.html
Copyright © 2011-2022 走看看