Tomcat源码阅读系列(二)Tomcat总体架构

来源:互联网 发布:促销活动数据分析 编辑:程序博客网 时间:2024/03/29 15:08

本文是Tomcat源码阅读系列的第二篇文章,本系列第一篇文章如下: 
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码       

本文介绍一下Tomcat的总体结构。

1. Tomcat6安装包的目录架构

        首先介绍一个可以正常使用Tomcat6的目录结构,并对与之与Tomcat的源码目录进行对比。首先Tomcat安装目录结构如下,

  • bin目录,Tomcat的二进制文件和启动脚本
  • conf目录,适用于全部应用的全局配置,默认情况下会有一下配置项,
    • 一个policy文件,catalina.policy文件,指定安全策略
    • 两个properties文件,catalina.properties和logging.properties两个属性文件
    • 四个XML配置文件,server.xml文件,Tomcat的主要配置文件;web.xml文件,Web应用程序的部署描述文件;context.xml文件,Tomcat的全局配置项;tomcat -users.xml文件,主要配置身份认证、访问控制和角色数据库密码等信息。
  • lib目录,可以应用于所有web应用程序的Jar文件,其中包括servlet-api即sun公司提供的servlet规范,apache Tomcat对servlet规范的实现catalina.jar,jasper.jar文件即转译jsp的jar包。如果需要对tomcat下所有的应用程序统一jar包的版本,就可以将jar包放在这个目录下。
  • logs目录,包含Catalina.{yyyy-mm-dd}.log的日志文件, localhost.{yyyy-mm-dd}.log的主机文件还有其他一些日志文件。
  • webapps目录,web应用程序的默认目录。
  • work目录,工作目录,包含servlet文件和class文件。组织层次依次为(Catalina),主机名(localhost),webapp名称,其次是Java类的封装结构。
  • temp目录

2. Tomcat6架构



        以上两张图均来至网上关于Tomcat的介绍,在此借来以展示Tomcat的架构。两张图可以组合来看。左侧的图主要体现了一个层次架构和不同层次架构之间的层级关系而右侧主要体现了大而全的架构。两张图可以结合来看。一个Server可以包含多个Service,而一个Service可以包含多个Connector和一个Engine,一个Engine可以包含多个Host,一个Host可以包含多个Context,一个Context可以包含多个Wapper。
        从功能的角度将Tomcat源代码分成5个子模块,它们分别是:
  1. Jsper子模块:这个子模块负责jsp页面的解析、jsp属性的验证,同时也负责将jsp页面动态转换为java代码并编译成class文件。在Tomcat源代码中,凡是属于org.apache.jasper包及其子包中的源代码都属于这个子模块;
  2. Servlet和Jsp规范的实现模块:这个子模块的源代码属于javax.servlet包及其子包,如我们非常熟悉的javax.servlet.Servlet接口、javax.servet.http.HttpServlet类及javax.servlet.jsp.HttpJspPage就位于这个子模块中;
  3. Catalina子模块:这个子模块包含了所有以org.apache.catalina开头的java源代码。该子模块的任务是规范了Tomcat的总体架构,定义了Server、Service、Host、Connector、Context、Session及Cluster等关键组件及这些组件的实现,这个子模块大量运用了Composite设计模式。同时也规范了Catalina的启动及停止等事件的执行流程。从代码阅读的角度看,这个子模块应该是我们阅读和学习的重点。
  4. Coyote 子模块:如果说上面三个子模块实现了Tomcat应用服务器的话,那么这个子模块就是Web服务器的实现。所谓连接器(Connector)就是一个连接客户和应用服务器的桥梁,它接收用户的请求,并把用户请求包装成标准的Http请求(包含协议名称,请求头Head,请求方法是Get还是Post等等)。同时,这个子模块还按照标准的Http协议,负责给客户端发送响应页面,比如在请求页面未发现时,connector就会给客户端浏览器发送标准的Http 404错误响应页面。
  5. Resource子模块:这个子模块包含一些资源文件,如Server.xml及Web.xml配置文件。严格说来,这个子模块不包含java源代码,但是它还是Tomcat编译运行所必需的。
         通过上图我们可以看出Tomcat中主要涉及Server,Service,Connector,Engine,Host,Context,Wapper组件。这几大组件在第一节介绍的conf/server.xml文件当中有体现,这也是为嘛,server.xml文件是Tomcat的主要配置文件的原因。默认情况下,server.xml文件的内容如下:
<?xml version='1.0' encoding='utf-8'?><!--  Licensed to the Apache Software Foundation (ASF) under one or more  contributor license agreements.  See the NOTICE file distributed with  this work for additional information regarding copyright ownership.  The ASF licenses this file to You under the Apache License, Version 2.0  (the "License"); you may not use this file except in compliance with  the License.  You may obtain a copy of the License at      http://www.apache.org/licenses/LICENSE-2.0  Unless required by applicable law or agreed to in writing, software  distributed under the License is distributed on an "AS IS" BASIS,  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the specific language governing permissions and  limitations under the License.--><!-- Note:  A "Server" is not itself a "Container", so you may not     define subcomponents such as "Valves" at this level.     Documentation at /docs/config/server.html --><Server port="8005" shutdown="SHUTDOWN"> <!--为了安全,必须修改掉此处的port和和shotdown命令!!!!!-->  <!--APR library loader. Documentation at /docs/apr.html -->  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />  <!--Initialize Jasper prior to webapps are loaded. Documentation at /docs/jasper-howto.html -->  <Listener className="org.apache.catalina.core.JasperListener" />  <!-- Prevent memory leaks due to use of particular java/javax APIs-->  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />  <!-- JMX Support for the Tomcat server. Documentation at /docs/non-existent.html -->  <Listener className="org.apache.catalina.mbeans.ServerLifecycleListener" />  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />  <!-- 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>  <!-- A "Service" is a collection of one or more "Connectors" that share       a single "Container" Note:  A "Service" is not itself a "Container",        so you may not define subcomponents such as "Valves" at this level.       Documentation at /docs/config/service.html   -->  <Service name="Catalina">      <!--The connectors can use a shared executor, you can define one or more named thread pools-->    <!--    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"         maxThreads="150" minSpareThreads="4"/>    -->            <!-- A "Connector" represents an endpoint by which requests are received         and responses are returned. Documentation at :         Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)         Java AJP  Connector: /docs/config/ajp.html         APR (HTTP/AJP) Connector: /docs/apr.html         Define a non-SSL HTTP/1.1 Connector on port 8080    -->    <Connector port="8080" protocol="HTTP/1.1"                connectionTimeout="20000"                redirectPort="8443" />    <!-- A "Connector" using the shared thread pool-->    <!--    <Connector executor="tomcatThreadPool"               port="8080" protocol="HTTP/1.1"                connectionTimeout="20000"                redirectPort="8443" />    -->               <!-- Define a SSL HTTP/1.1 Connector on port 8443         This connector uses the JSSE configuration, when using APR, the          connector should be using the OpenSSL style configuration         described in the APR documentation -->    <!--    <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"               maxThreads="150" scheme="https" secure="true"               clientAuth="false" sslProtocol="TLS" />    -->    <!-- Define an AJP 1.3 Connector on port 8009 -->    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />    <!-- An Engine represents the entry point (within Catalina) that processes         every request.  The Engine implementation for Tomcat stand alone         analyzes the HTTP headers included with the request, and passes them         on to the appropriate Host (virtual host).         Documentation at /docs/config/engine.html -->    <!-- You should set jvmRoute to support load-balancing via AJP ie :    <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">             -->     <Engine name="Catalina" defaultHost="localhost">      <!--For clustering, please take a look at documentation at:          /docs/cluster-howto.html  (simple how to)          /docs/config/cluster.html (reference documentation) -->      <!--      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>      -->              <!-- The request dumper valve dumps useful debugging information about           the request and response data received and sent by Tomcat.           Documentation at: /docs/config/valve.html -->      <!--      <Valve className="org.apache.catalina.valves.RequestDumperValve"/>      -->      <!-- This Realm uses the UserDatabase configured in the global JNDI           resources under the key "UserDatabase".  Any edits           that are performed against this UserDatabase are immediately           available for use by the Realm.  -->      <Realm className="org.apache.catalina.realm.UserDatabaseRealm"             resourceName="UserDatabase"/>      <!-- Define the default virtual host           Note: XML Schema validation will not work with Xerces 2.2.       -->      <Host name="localhost"  appBase="webapps"            unpackWARs="true" autoDeploy="true"            xmlValidation="false" xmlNamespaceAware="false">        <!-- SingleSignOn valve, share authentication between web applications             Documentation at: /docs/config/valve.html -->        <!--        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />        -->        <!-- Access log processes all example.             Documentation at: /docs/config/valve.html -->        <!--        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"                 prefix="localhost_access_log." suffix=".txt" pattern="common" resolveHosts="false"/>        -->      </Host>    </Engine>  </Service></Server>
接下来一一介绍server.xml当中的元素信息。

2.1 Server

        Server(服务器)是Tomcat构成的顶级构成元素,所有一切均包含在Server中,Server的实现类StandardServer可以包含一个到多个Services。在Tomcat当中Server接口的默认实现是
org.apache.catalina.core.StandardServer
StandardServer的继承关系图如下图所示:
StandardServer的主要功能点如下:
  1. 管理Service。包括添加,查找等
  2. 实现Lifecycle,主要管理多个Service的声明周期
  3. 主线程开启await()监听端口,接收shutdown命令。此处是重点

2.1.1 需要注意StandardServer.await()方法

/**         * Wait until a proper shutdown command is received, then return.     * This keeps the main thread alive - the thread pool listening for http     * connections is daemon threads.主要是等待shutdown命令,同时保持当前线程为活动线程。等待http连接的线程全部是守护线程。隐含意思是,只要这个线程shutdown了,那么整个Tomcat程序就退出了。     */    public void await() {        // ...anything...        try {            awaitThread = Thread.currentThread();            // Loop waiting for a connection and a valid command            while (!stopAwait) {                ServerSocket serverSocket = awaitSocket;                if (serverSocket == null) {                    break;                }                // Wait for the next connection                Socket socket = null;                StringBuilder command = new StringBuilder();                try {                    InputStream stream = null;                    long acceptStartTime = System.currentTimeMillis();                    try {                        socket = serverSocket.accept();                        socket.setSoTimeout(10 * 1000);  // Ten seconds  注意,注意!!!必须在进入阻塞操作前被启用才能生效                        stream = socket.getInputStream();                    } catch (SocketTimeoutException ste) {  //此处应该注意,setSoTimeout在此阻塞之后才设置的,因此是无效的。因此不会抛出这个异常。                        // This should never happen but bug 56684 suggests that                        // it does.                        log.warn(sm.getString("standardServer.accept.timeout",                                Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);                        continue;                    } catch (AccessControlException ace) {                        log.warn("StandardServer.accept security exception: "                                           + ace.getMessage(), ace);                        continue;                    } catch (IOException e) {                        if (stopAwait) {                            // Wait was aborted with socket.close()                            break;                        }                        log.error("StandardServer.await: accept: ", e);                        break;                    }                    // Read a set of characters from the socket                    int expected = 1024; // Cut off to avoid DoS attack                    while (expected < shutdown.length()) {                        if (random == null)                            random = new Random();                        expected += (random.nextInt() % 1024);                    }                    while (expected > 0) {                        int ch = -1;                        try {                            ch = stream.read();                        } catch (IOException e) {                            log.warn("StandardServer.await: read: ", e);                            ch = -1;                        }                        if (ch < 32)  // Control character or EOF terminates loop                            break;                        command.append((char) ch);                        expected--;                    }                } finally {                    // Close the socket now that we are done with it                    try {                        if (socket != null) {                            socket.close();                        }                    } catch (IOException e) {                        // Ignore                    }                }                // Match against our command string                boolean match = command.toString().equals(shutdown); //输入的字符串如果是shutdown的话,则break退出。因此要向自己的tomcat安全的话,必须修改掉主线程监听的port和对应的命令,如果不修改,那么直接使用telnet连接到8005端口,输入SHUTDOWN,那么你的tomcat就宕机了。                if (match) {                    break;                } else                    log.warn("StandardServer.await: Invalid command '" +                                       command.toString() + "' received");            }        } finally {            ServerSocket serverSocket = awaitSocket;            awaitThread = null;            awaitSocket = null;            // Close the server socket and return            if (serverSocket != null) {                try {                    serverSocket.close();                } catch (IOException e) {                    // Ignore                }            }        }    }

2.2 Service

        多个 Connector 和一个 Container 就形成了一个 Service,Service 的概念大家都很熟悉了,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了,所以整个 Tomcat 的生命周期由 Server 控制。Service 只是在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个 Connector,但是只能有一个 Container 容器。Tomcat中Service的默认实现是
org.apache.catalina.core.StandardService
其中StardardService还保持了其管理者Server的引用,另外而需要说明的一点就是,
org.apache.catalina.startup.Catalina
也是Service的一个实现。StandardService和Catalina的继承关系图如下:


StandardService的主要功能点如下:
  1. 管理多个Connecter
  2. 管理一个容器Container
  3. 实现Lifecycle,主要管理多个Connector和一个Container的生命周期

2.2.1 需要注意StandardService. setContainer方法

 public void setContainer(Container container) {        Container oldContainer = this.container;        if ((oldContainer != null) && (oldContainer instanceof Engine))            ((Engine) oldContainer).setService(null);        this.container = container;        if ((this.container != null) && (this.container instanceof Engine))            ((Engine) this.container).setService(this);        if (started && (this.container != null) &&            (this.container instanceof Lifecycle)) {            try {                ((Lifecycle) this.container).start();            } catch (LifecycleException e) {                ;            }        }        synchronized (connectors) {            for (int i = 0; i < connectors.length; i++)                connectors[i].setContainer(this.container);//多个Connector关联某个Container,而没有进行双向关联        }        if (started && (oldContainer != null) &&            (oldContainer instanceof Lifecycle)) {            try {                ((Lifecycle) oldContainer).stop();            } catch (LifecycleException e) {                ;            }        }        // Report this property change to interested listeners        support.firePropertyChange("container", oldContainer, this.container);    }
         先判断当前的这个 Service 有没有已经关联了 Container,如果已经关联了,那么去掉这个关联关系, oldContainer.setService(null)。如果这个 oldContainer 已经被启动了,结束它的生命周期。然后再替换新的关联、再初始化并开始这个新的 Container 的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,修改 Container 时要将新的 Container 关联到每个 Connector,此处使用的是多个Connector关联一个Container,而没有双向关联,不然这个关联关系将会很难维护。

2.2.2 需要注意StandardService. addConnector方法

public void addConnector(Connector connector) {        synchronized (connectors) {            connector.setContainer(this.container);            connector.setService(this);            Connector results[] = new Connector[connectors.length + 1];            System.arraycopy(connectors, 0, results, 0, connectors.length);            results[connectors.length] = connector;            connectors = results;            if (initialized) {                try {                    connector.initialize();                } catch (LifecycleException e) {                    log.error(sm.getString(                            "standardService.connector.initFailed",                            connector), e);                }            }            if (started && (connector instanceof Lifecycle)) {                try {                    ((Lifecycle) connector).start();                } catch (LifecycleException e) {                    log.error(sm.getString(                            "standardService.connector.startFailed",                            connector), e);                }            }            // Report this property change to interested listeners            support.firePropertyChange("connector", null, connector);        }    }
        这个方法利用了CopyOnWriteArrayList类似的思想,先将原始的数据通过生成长度+1的数组,再使用System.arraycopy()来生成一份新的数组,然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象。CopyOnWriteArrayList的核心思想是利用高并发往往是读多写少的特性,对读操作不加锁,对写操作,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性,当然写操作的锁是必不可少的了,在做复制的时候,其它线程还是可以在原有的老的对象上进行只读操作,所以不会阻塞读操作。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生ConcurrentModificationException。关于CopyOnWriteArrayList的性能说明,可以参考CopyOnWriteArrayList与Collections.synchronizedList的性能对比。因此,此处会遇到与CopyOnWriteArrayList同样的性能问题。

2.3 Connector 

         Connector组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。Container组件代码相对稳定,且对Tomcat性能的影响不大,而Connector组件对Tomcat性能的影响是比较大的,也因此其地位比较重要,会单独拿出一篇博文来进行介绍。Connector对应源代码中的
org.apache.catalina.connector.Connector
它的继承关系图如下所示:


        Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心, Connector 划分成 Connector、Processor、Protocol。根据不同的协议,可以配置实用不同的Connector,Tomcat默认提供了HTTP/1.1和AJP/1.3两种协议的实现。
        AJP表示Apache Jserv Protocol,AJP是定向包协议。因为性能原因,使用二进制格式来传输可读性文本。Apache通过TCP连接和Tomcat连接。为了减少进程生成socket的花费,Apache和Tomcat之间尝试保持持久性的TCP连接,对多个请求/回复循环重用一个连接。一旦连接分配给一个特定的请求,在请求处理循环结束之前不会再分配。前端Apache,后端Tomcat,通过ajp协议访问性能优于http协议,随着并发量的提升,效果会更加趋于明显。
        APR(Apache Portable Runtime)是一个高可移植库,它是Apache HTTP Server 2.x的核心。APR有很多用途,包括访问高级IO功能(例如sendfile,epoll和OpenSSL),OS级别功能(随机数生成,系统状态等等),本地进程管理(共享内存,NT管道和UNIX sockets)。这些功能可以使Tomcat作为一个通常的前台WEB服务器,能更好地和其它本地web技术集成,总体上让Java更有效率作为一个高性能web服务器平台而不是简单作为后台容器。Tomcat做WEB服务器的时候,应该使用Tomcat Native来提高其性能。APR就是Tomcat使用Apache的本地类库以JNI的方式来读取文件以及进行网络传输。这个东西可以大大提升Tomcat对静态文件的处理性能,同时如果你使用了HTTPS方式 传输的话,也可以提升SSL的处理性能。 APR用C实现,通过JNI调用的。主要提升对静态资源(如HTML、图片、CSS、JS等)的访问性能。现在这个库已独立出来可用在任何项目中。Tomcat在配置APR之后性能非常强劲。
        因此,HTTP/1.1和AJP/1.3两种协议与APR会组合成多个不同的Connector、Processor、Protocol。

2.4 Contanier

        Container可以理解为处理某类型请求的容器,处理的方式一般为把处理请求的处理器包装为Valve对象,并按一定顺序放入类型为Pipeline的管道里,Container 容器的设计用的是典型的责任链的设计模式。Container有多种子类型:Engine、Host、Context和Wrapper,这几种子类型Container依次包含,处理不同粒度的请求,这四个组件不是平行的,而是父子关系,Engine 包含 Host,Host 包含 Context,Context 包含 Wrapper。通常一个 Servlet class 对应一个 Wrapper,如果有多个 Servlet 就可以定义多个 Wrapper,如果有多个 Wrapper 就要定义一个更高的 Container 了,即Context,Context 还可以定义在父容器 Host 中,Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。另外Container里包含一些基础服务,如Loader、Manager和Realm。
  • Engine:Engine包含Host和Context,接到请求后仍给相应的Host在相应的Context里处理。表示整个Catalina的servlet引擎。
  • Host:就是我们所理解的虚拟主机。表示一个拥有数个上下文的虚拟主机。
  • Context:就是我们所部属的具体Web应用的上下文,每个请求都在是相应的上下文里处理的。表示一个Web应用,一个context包含一个或多个wrapper。
  • Wrapper:Wrapper是针对每个Servlet的Container,每个Servlet都有相应的Wrapper来管理。表示一个独立的servlet。
可以看出Server、Service、Connector、Container、Engine、Host、Context和Wrapper这些核心组件的作用范围是逐层递减,并逐层包含。由于Contanier的内容比较多,因此会单独写一博文进行介绍。
Engine、Host、Context和Wrapper的具体实现分别为:
org.apache.catalina.core.StandardEngineorg.apache.catalina.core.StandardHostorg.apache.catalina.core.StandardContextorg.apache.catalina.core.StandardWrapper
对应的类关系图如下:




        
        关于Tomcat的总体架构就先写到这里,其中详细介绍了Server和Service组件,关于Connecter和Contanier组件会有其他文章详细介绍
0 0
原创粉丝点击