zoukankan      html  css  js  c++  java
  • Servlet深入学习,规范,理解和实现(中)——深入理解Tomcat(一)

    心得:在写这篇博客之前。我大致阅读一些关于Tomcat的书籍和博客等资料。有些资料由于时间的关系,解说的Tomcat版本号太老。有些资料能够非常好的说明Tomcat整理结构和设计思想可是非常多重要的问题由于篇幅的原因不能非常好的说明。还有写资料非常能够说明一些类和API但不能结合实际问题说明实际执行中的流程和设计思路等等。

    当然我并非说它们不好。仅仅是不同的人的看到同一个事物和问题的角度和思路可能不同,出发点不同立意也会不同。

    因此我想基于自己使用和学习Tomcat过程的一些问题和想法进行探究,这可能对于学习Tomcat这样比較“庞大”的系统来说是一个适合自己的方法。

    另外,Tomcat是应用广泛十分经典的Servlet容器实现,了解其内部实现原理对进一步理解Servlet规范,以及Web服务器端开发中非常多问题都有非常大帮助。

    确实。Tomcat相对于Jetty等轻量级Servlet容器来说显得非常“重”,但假设能够结合一些自己的思考和实际问题把握Tomcat设计的核心路线。深入进去,一定能够有丰厚的收获。

    学习參考资料
    (1)Servet 3.1 final 规范;
    (2)《Java Web高级编程》;
    (3)《深入分析Java Web技术内幕》(第2版);
    (4)JMX一步步来系列博客
    (5)《深入Java虚拟机》(第2版)。
    (6)《Tomcat权威指南》。
    (7)《How Tomcat Works》;
    (8)Apache Tomcat 8 Configuration Reference

    在(上)篇中,我们提到Tomcat是由几层容器共同构成,这里就是依据源代码详细的分析它的体系结构,以及不同模块之间的组成与关联,我查看的版本号是8.0.28。

    1. Tomcat总体结构

    以下这张图尽管非常老,但展示了Tomcat的体系结构,尽管版本号更新了非常多,但核心结构仍然例如以下图所看到的,可是实现的细节随版本号已经进行了非常多改变。


    tomcat-structure

    Server和Service的关系
    一个Server包括多个Service,Server负责管理Service,同一时候提供对外的接口,控制请求和訪问。一个JVM实例存在一个Server,可能存在多个Service。普通情况下我们并不须要配置多个Service,使用conf/server.xml默认的“Catalina”的<Service>就能够了。

    Service和Connector,Container的关系
    Service中包括多个Connector,一个Connector负责在一个指定port监听请求。

    不同的Connector能够使用不同协议处理。假设你全面了解过tomcat的配置,就会知道conf/server.xml<Connector>等元素事实上可能配置项非常多,功能十分强大。我们相同能够从server.xml的结构中看出tomcat的体系结构特点。

    Connector接受到请求后,会将请求交给Container,Container处理完了之后将结果返回给Connector。而Container我们上篇已经提过是分层的,正是通过一层层组合起来的容器将请求分派到它的目的地(Servlet)。

    结合以下的类图我们能看到Container有4个子类:
    (1)Engine:也就是最外层的容器。对它调用setParent会抛出异常。Engine就是Tomcat的Servlet执行引擎,;
    (2)Host:Engine能够包括多个Host。每一个Host代表一个虚拟主机,每一个虚拟主机相应的一个域名的,不同Host容器接受处理相应不同域名的请求;
    (3)Context:Context容器是Servlet规范的实现。它提供了Servlet的基本环境,一个Context代表一个Web应用。上一篇中我们已经说明了StandardContext(Context的详细实现)在启动时会读取web.xml部署描写叙述符的内容,并依次创建和初始化:Initializer。Listener,Filter。Servlet(LoadOnStartup >= 0)等Servlet所需的重要基本工作。
    (4)Wrapper:一个Wrapper代表一个Servlet。它是最里层的容器。对它调用addChild相同会抛出异常。Wrapper是Tomcat对ServletConfig的内部表示。通过Facade传入Servlet的init方法。

    tomcat-structure
    注意:
    (1)图中StandardServerServiceStandardServiceConnector之间的聚合关系应该是1对多,它们都是通过动态增长(拷贝到新数组方式)的数组来实现的。由于画图工具的缘故图中标成了1对1。
    (2)Engine。Host。Context。Wrapper上面还有一个骨架类ContainerBase。负责一些容器共同拥有的基础性工作。比方调用详细容器的backgroundProcess方法,向Context的该方法负责在后台线程中检查那些在war改动时须要reload的Web应用等等。

    2. Container基本结构

    Tomcat的核心作用是Servlet容器,在了解了Tomcat的总体结构之后,我们重点看一看容器之间的关系和组成。
    Tomcat中Engine。Host,Context,Wrapper有一个骨架类org.apache.catalina.core.ContainerBase。它实现了非常多容器的基本功能和特性。因此在分析Engine等详细的容器类之前,首先要对ContainerBase进行主要的分析。

    除了上面介绍的ContainerBase以及四个基本容器类。还有两个类:org.apache.catalina.Pipelineorg.apache.catalina.Valve它们各自是管道阀门。类似于Filter和Servlet,Container、Pipeline、Valve一起组成一个责任链模式的容器结构。
    tomcat-container-structure
    依据上图,能够得到例如以下关系:
    (1)Container能够包括多个子容器,一个父容器(组合模式)。
    (2)每一个Container包括一个Pipeline。一般以在成员变量定义处完毕实例初始化:

        /**
         * The Pipeline object with which this Container is associated.
         */
        protected final Pipeline pipeline = new StandardPipeline(this);

    (3)每一个Pipeline包括一个Valve链,一个Pipeline上有多个Valve。Pipeline拥有第一个Valve的引用。Valve之间通过next链接。每一个节点仅仅需知道下一个节点而不须要维护整个Valve链这也是责任链模式的优点。
    (4)每一个容器相应的Pipeline有一个基本Valve。如图中的StandardEngineValve,它通常至于这个Pipeline相应Valve链的最后保证请求数据能够传递到下一个容器。
    一般在容器的构造器中进行设置;

    2.1 ContainerBase分析

    通过ContainerBase我们进一步看看容器的基本功能和特点。
    结合上述关系首先看下ContainerBase的实例变量:
    (1)通过HashMap保存子容器

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

    (2)相应的Pipeline

    protected final Pipeline pipeline = new StandardPipeline(this);

    (3)通过CopyOnWriteArrayList维护容器监听器注冊表

    protected final List<ContainerListener> listeners = new CopyOnWriteArrayList<>();

    在多线程环境下。基于事实不可变对象(读)底层数组复制(写)的CopyOnWriteArrayList非常适合构建事件通知系统,由于正常情况下,注冊/注销(写)的发生远少于通知(读)。
    (4)backgroundProcess后台共享线程

        private Thread thread = null;
        //该方法在ContainerBase.startInternal()的最后进行调用
        protected void threadStart() {
    
            if (thread != null)
                return;
            if (backgroundProcessorDelay <= 0)
                return;
    
            threadDone = false;
            String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
            thread = new Thread(new ContainerBackgroundProcessor(), threadName);
            thread.setDaemon(true);
            thread.start();
        }

    这个后台线程负责执行一些执行时的后台任务。
    从ContainerBase.backgroundProcess()中能够看到例如以下任务:
    (1)Cluster的后台任务:发送心跳包,监听集群部署的改变(Engine和Host包括)
    (2)Realm的后台任务,假设子类定义实现了话(Engine。Host,Context包括);
    (3)整个Valve链中每一个Valve的后台任务,假设定义了话;
    另外详细的容器也能够依据须要实现自己的backgroundProcess。这主要在StandardContext中,它的backgroundProcess负责例如以下工作:
    (1)Loader的后台任务,Context的reload设置为true,将会在后台任务中周期性检查资源(包括WEB-INF/classes下的类文件和WEB-INF/lib中的jar包)是否改动,假设发现改动将调用StandardContext.reload()进行重新启动。详细内容在之后总结Tomcat类载入机制中进行;
    (2)Manager的后台任务,ManagerBase实现了会话管理的一个重要任务:将全部超时的会话进行失效。并进行清理(从Manager的session集合中删除)。
    (3)WebResourceRoot的后台任务,上面Loader也是基于WebResourceRoot的,但WebResourceRoot除了class文件和jar包之外,还包括Web应用中其它全部资源的抽象,它的后台任务主要是清除过时的缓存记录。


    Web应用的资源被抽象成WebResource,具有lastModified,ETag等属性能够支持Loader的reload的机制的同一时候也能够支持HTTP的缓存机制。
    (5)启动子容器任务线程池

    private int startStopThreads = 1; //线程池的线程数量
    protected ThreadPoolExecutor startStopExecutor;

    子容器的启动并非在父容器启动的同一线程中进行。而是通过线程池(基于LinkedBlockingQueue,在initInternal中实例化)进行的,线程池中的线程都是后台线程,在默认情况下。线程数为1。

    结合ContainerBase.startInternal
    ContainerBase.stopInternal来看看这样的机制是怎样执行的:
    startInternal方法中子容器的启动:

            // Start our child containers, if any
            Container children[] = findChildren();
            List<Future<Void>> results = new ArrayList<>();
            for (int i = 0; i < children.length; i++) {
                results.add(startStopExecutor.submit(new StartChild(children[i])));
            }
            boolean fail = false;
            for (Future<Void> result : results) {
                try {
                    result.get();
                } catch (Exception e) {
                    log.error(sm.getString("containerBase.threadedStartFailed"), e);
                    fail = true;
                }
    
            }
            if (fail) {
                throw new LifecycleException(
                        sm.getString("containerBase.threadedStartFailed"));
            }

    默认情况下,线程池数量为1,也就是基于单线程+无界队列执行子容器的启动过程。通过Future,能够按顺序堵塞的等待启动任务的完毕。那这样的做法和不使用线程池有什么不同或者说带来了什么优点呢?
    (6)集群,角色和用户管理
    集群

    protected Cluster cluster = null;
    private final ReadWriteLock clusterLock = new ReentrantReadWriteLock();

    角色和用户管理:

    private volatile Realm realm = null;
    private final ReadWriteLock realmLock = new ReentrantReadWriteLock();

    结合server.xml的配置。我们知道能够为容器配置<Cluster><Realm>。这里使用ReadWriteLock是一个非常合适的选择,由于仅仅有一个线程在读取和解析xml文件并构建组件间对象网络的,后面2.3 Tomcat是怎样完毕启动的?中我们将详细的探讨这个过程。

    3. Tomcat中的相关问题探索

    总结了Tomcat的体系结构,我们对Tomcat的静态总体结构已经有了一定了解。可是对于Tomcat这样复杂的系统这显然是远远不够的,最好还是带着一些思考和问题对Tomcat进行探索和总结。

    3.1 Tomcat中的观察者模式

    观察者模式的相关内容这里就不展开了。假设疑问能够查看wiki或者GOF等资料。这里简单的解释就是将事务状态的变化和对变化的响应分离开来,这就是被观察者(Observable)观察者(Observer),典型的应用就是事件监听,尤其在GUI开发中非经常见。

    Tomcat中非常多组件具有生命周期,因此它们的状态变化中包括非常多“点”,比方由为初始化变为初始化等等。一个组件的状态变化可能须要非常多相关组件随之变化来配合。这就是观察者模式的用武之地了。

    组件的生命周期大多具有共性。因此Tomcat中将其抽象为接口Lifecycle,它是Tomcat中组件一个核心设计路线。

    Lifecycle接口的主要方法:
    tomcat-structure
    能够看到主要包括:
    (1)注冊,查找和注销Listener;
    (2)生命周期方法:
    初始化init()
    启动start()
    停止stop()
    销毁destroy()
    (3)获取状态。
    这三个部分了,结合骨架类LifecycleBase能够看到,骨架类中实现了上述生命周期方法,负责一些共同的基础部分,同一时候定义了initInternalstartInternal等抽象方法共子类实现。这样的模板设计模式在框架中非经常见。

    下图中。LifecycleListener是Oberserver。而Lifecycle是Observable。

    Tomcat中添加一个叫LifecycleSupport的辅助类。Lifecycle通过LifecycleSupport来集中管理和通知LifecycleListener。而不是直接操作LifecycleListener。这也是对观察者模式一种改进。
    LifecycleBase是骨架类。StandardServerStandardServiceConnector和上面4个容器等非常多组件都直接或间接的继承了LifecycleBase


    tomcat-structure

    3.2 Tomcat中的执行时监控/管理和JMX

    假设查看你就会发现组件并非直接派生子LifecycleBase的,而是派生自LifecycleMBeanBase

    这个类除了继承了Lifecyle这条设计路线外还引入了JMX的功能。假设你不熟悉JMX,JMX一步步来这个系列博客或许可能帮你了解JMX和简单的用法。

    这里简单来说,就是Tomcat为了使用JMX提供的监控和管理能力,其组件能够作为MBean能够注冊到MBeanServer中。从而我们能够在执行时对Tomcat进行管理和监控。

    (1)javax.management.MBeanReigstration是JMX中的接口。定义了MBeanServer在在注冊和注销MBean时进行回调的一些方法(preRegister,postRegister,preDeregister和postDergister)。
    (2)org.apache.catalina.JmxEnabled简单扩展了MBeanReigstration。
    tomcat-structure

    如今你可能要问,Tomcat组件作为MBean是什么时候注冊到MBeanServer的?在上一小节中。我们提到LifecycleBase实现了init等生命周期方法的基本逻辑,并提供了xxxInternal方法供子类扩展,在LifecycleMBean就在这些生命周期回调扩展方法定义了注冊和注销MBean的逻辑:

        @Override
        protected void initInternal() throws LifecycleException {
            // If oname is not null then registration has already happened via
            // preRegister().
            if (oname == null) {
                mserver = Registry.getRegistry(null, null).getMBeanServer();
    
                oname = register(this, getObjectNameKeyProperties());
            }
        }
    
        @Override
        protected void destroyInternal() throws LifecycleException {
            unregister(oname);
        }

    当中Registry负责管理和维护了Tomcat中MBean的注冊表。并负责创建MBeanServer。它的getMBeanServer()是一个synchronized方法从而保证MBeanServer的单例

    3.3 Tomcat是怎样完毕启动的?

    查看Tomcat的启动脚本bin/catalina.sh(Linux平台)就会发现,Tomcat的“起点”是org.apache.catalina.startup.BootStrap的main方法,catalina.sh脚本的參数(start/stop)都会传入BootStrap的main函数。

    而BootStrap进一步依据參数调用org.apache.catalina.startup.Catalina的相应方法来完毕启动过程的任务。

    BootStrap类和实例分别有两个重要的属性:

    private static Bootstrap daemon = null;
    private Object catalinaDaemon = null;

    非常明显,Bootstarp是一个单例的守护对象负责引导这个Tomcat启动,而catalinaDaemon实际是Catalina类实例,相应于bin/catalina.sh脚本负责实际的启动和停止过程。

    这个问题相同非常复杂,包括怎样读取和解析配置。创建和启动组件的顺序,以及这个环节中重要的事件和相应的处理等等。依照时间顺序分析启动过程的同一时候我们能够一个个解决这些问题。

    [1] 初始化类载入器

    在BootStrap.main函数的第一个步骤是进行初始化类载入,包括commonLoadercatalinaLoadersharedLoader,默认情况这三个引用指向同一个commonLoader实例,后面专门总结Tomcat类载入器结构的时候在详细分析。
    BootStrap.init方法:

          initClassLoaders();
          Thread.currentThread().setContextClassLoader(catalinaLoader);
          SecurityClassLoad.securityClassLoad(catalinaLoader);
          //创建Catalina实例
          Object startupInstance = startupClass.newInstance();
          /* 通过反射设置Catalina的parentClassLoader属性 */
          catalinaDaemon = startupInstance;

    [2] 载入和配置的读取&解析

    相应BootStrap.main方法中:

            if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
            }

    (1)setAwait(true),终于是设置Server的await标志。作用是在启动工作完毕后,StandardServer会调用其await()方法堵塞当前线程(也是启动工作线程)。等待关闭时才中断这个线程的等待状态。
    (2)load(args);实际上会调用Catalina.load方法,进行:
    验证java.io.tmpdir所指缓存文件夹是否可用;
    读取和解析server.xml
    初始化日志输入,输出流(正常,异常)。
    開始Tomcat初始化过程,从Server.init()開始
    (3)start(),调用Catalina.start。将開始Tomcat组件和容器的启动,从Server.start()開始;

    先看看载入和解析的过程:
    问题:Tomcat载入和解析XML的方式?
    首先,使用过Tomcat的人应该知道。CATALINA_BASE文件夹(默认是Tomcat的安装文件夹)下conf中有非常多Tomcat配置文件:
    server.xml(Tomcaat主配置文件);
    web.xml(适用于全部Web应用的servlet规范配置文件);
    tomcat-users.xml(用户认证。角色等信息,UserDatabaseRealm相关)。
    catalina.policy(Java安全防护策略文件)。
    (content.xml);
    context.xml(默认context设置,应用于全部部署内容);

    这里首当其冲应该是server.xml。Tomcat中一个读取和解析XML配置文件的主要方法是SAX,使用SAX而不是DOM是由于SAX的事件驱动型型特点,这样能够一边扫描一边进行响应的配置。Tomcat解决这一问题的主要逻辑在org.apache.tomcat.util.digester包中。

    Tomcat在应用SAX事件驱动型解析中有几个重要的概念:
    (1)Handler,在解析过程中不同的事件能够调用Handler相应的回调方法(包括EntityResolver, DTDHandler, ContentHandler, ErrorHandler),org.apache.tomcat.util.digester.Digester是详细的实现;
    (2)Rule,这是Tomcat中定义的类,尽管Handler提供了不同回调方法能够实现。可是Tomcat未定义大量的Handler,而是一次解析一个xml文件仅仅定义单个Digester实例,而将“遇到一个<Server>创建一个StandardServer对象”这样的需求抽象成Rule,一个Digester包括解析所需的全部Rule(聚合)。每一个Rule能够选择性定义begin。body,end,finish这些回调。
    tomcat-container-structure
    (3)Digester除了用数组保存既定的规则外,还通过Stack保存解析过程创建的相应的组件对象,由于嵌套标签解析过程中begin-body(包括子标签)-end这样的递归嵌套方式和方法调用一样适合用LIFO的栈来表示。

    比方。Catalina在创建解析server.xml的Digester实例的createStartDigester()方法中加入了这样一个条规则:

            digester.addObjectCreate("Server/Service",
                                     "org.apache.catalina.core.StandardService",
                                     "className");
            digester.addSetProperties("Server/Service");
            digester.addSetNext("Server/Service",
                                "addService",
                                "org.apache.catalina.Service");

    意为:遇到一个<Server>包括的<Service>标签,首先创建一个StandardService对象(通过反射),继续处理它的子标签(递归)。子标签处理完后。对其父元素也就是Server对象调用addService方法建立对象之间的父子组合关系。最后该<Service>解析完,相应的组件对象出栈。上述代码中addObjectCreate一般用于在匹配标签開始解析时进行创建对象。addSetNext一般在标签解析最后建立其与父标签组件对象的父子组合关系。

    假设里完整看完Catalina.createStartDigester()就会明确我为什么要费这么多口舌说明这个问题,由于实际上在 server.xml定义的重要组件包括:Server。Service,Engine,Host,Context,以及Valve,Listener等等都是以这样的方式在解析的过程中创建和组配的。结合我们在上面Container结构中的分析,包括Pileline,Valve与Container的联系也能够在此建立好。因此Tomcat在容器和其它组件进行初始化之前。已经构建起了一个完整的对象网络。

    [3] 容器初始化

    依据上面的说明,我们知道如今组件已经创建好了,能够開始初始化了。Server.init就是起点,这是在daemon.load()中进行的。

    在上两个小节,将会提到Tomcat中组件一般实现Lifecycle。骨架类LifecycleBase在实现init,start,stop。destroy的基本逻辑之外定义了initIntertal等扩展的回调方法,这样大部分组件仅仅要实现initIntertal等方法就能够了。

    以下我沿着Server—>Service->Container(Engine—>Host—>Context—>Wrapper—>Servlet)这一核心顺序一一总结Tomcat初始化的过程。
    [1] StandardServer.initInternal():
    (1)创建和注冊(注冊到MBeanServer)全局的StringCache。
    (2)初始化GlobalNamingResources;
    (3)初始化该Server包括的全部service组件(尽管通常都仅仅有一个名为“Catalina”的service);

    [2] StandardService.initInternal():
    这一层事实上包括了非常多重要组件的初始化:Container,Executor,MapperListener,Connector。
    (1)初始化容器,从容器的最外层(Engine)開始,一层层開始;
    (2)假设定义了org.apache.catalina.Executor。初始化Executor。还是说明一下,它实现了J.U.C中的Executor。定义一个为全部Connector共享的线程池(因此在server.xml中Executor必须定义在Connector,由于前面提到了解析server.xml使用SAX的方式)。
    (3)初始化mapperListener,MapperListener是Tomcat中用来保存整个容器必要结构信息用于将请求URL映射到相应容器;
    (4)初始化Connector,这个过程会初始化每一个Connector包括的ProtocolHandldr等组件。让连接处理部分做好准备。

    [3] 容器初始化
    结合3.1节我们能够知道,初始化和启动调用栈包括LifecyleBase-LifecyleMBeanBase-详细容器类几个层次。并且在相应的时间点/状态变化点通知LifecycleLsitener:
    (1)首先是LifycycleBase的init()方法:
    LifecycleState.INITIALIZING——>initInternal()——>LifecycleState.INITIALIZED。
    (2)initInternal调用栈从LifecycleMBeanBase開始,LifecycleMBeanBase.initInternal()将容器注冊为MBean;
    (3)再来看ContainerBase.initInternal,在2.1节我们提到过它负责创建和设置用于执行子容器启动任务的线程池,以下是其代码:

        @Override
        protected void initInternal() throws LifecycleException {
            BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
            startStopExecutor = new ThreadPoolExecutor(
                    getStartStopThreadsInternal(),
                    getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
                    startStopQueue,
                    new StartStopThreadFactory(getName() + "-startStop-"));
            startStopExecutor.allowCoreThreadTimeOut(true);
            super.initInternal();
        }

    (4)接下来看详细的容器类,StandardHost。StandardWrapper没有覆盖父类的行为,StandardEngine也仅仅是简单准备了下Realm。StandardContext中主要是:将Context包括的NamingResoource注冊到MBeanServer以及WebResourceRoot启动(假设存在的话)。在2.1节说明容器的后台任务时,我们提到WebResourceRoot会在后台线程中周期性的清除过期缓存。

    Resources<Resources>元素能够定义在<Context>当中。Tomcat 8的官方配置手冊是这样解释的:

    The Resources element represents all the resources available to the web application. This includes classes, JAR files, HTML, JSPs and any other files that contribute to the web application.
    一般在Context含有未存储在Tomcat的本机硬盘上的资源或者对资源的缓存等细节有定制需求时。才会须要此元素。不定义该元素。将会使用基于默认文件系统(项目根文件夹)的WebResourceRoot对象。
    Tomcat 8相较之前版本号,对<Resources>进行较大幅度的改动,对该元素的实现类是org.apache.catalina.WebResourceRoot的子类(通常是org.apache.catalina.webresources.StandardRoot)。而不是原来的javax.naming.directory.DirContext。Tomcat 8在此基础上为<Resources>定义了非常多新的属性。包括缓存的相关细节以及一些载入顺序有关的内嵌标签。详细能够參考文章开头的參考资料(8)Tomcat 8官方配置手冊。

    在StandardContext初始化完毕后,会通知注冊的LifecycleListener,当中包括ContextConfig,调用ContextConfig.init()解析/conf文件夹下的context.xml以及Context自身的配置文件。解析方式和前面servlet.xml一样。基于org.apache.tomcat.util.digester.Digester一边读取解析。一边构建对象网络。

    [4] 容器启动

    BootStrap在load()载入完的下一个步骤就是启动了daemon.start(),启动相同是从Server開始:
    [1] StandardServer.startInternal():启动全局NamingResources。启动全部Service(一般就是名为catalina的Service)。

    [2] StandardService.startInternal()
    和初始化的顺序类似:
    (1)启动Container。container.start()
    (2)启动Executor;
    (3)启动mapperListener;
    (4)启动全部的Connector;
    Executor,MapperListener,Connector都是接收请求直接相关的,当中Executor负责为Connector处理请求提供共用的线程池。MapperListener负责将请求映射到相应的容器中,Connector负责接收和解析请求,详细的过程在后面2.4小节详细的探讨。这里全部Connector启动完毕后,Tomcat就准备好能够接受处理请求。当然我们相同要深入看看容器的启动过程。

    [3] 容器启动
    首先看看ContainerBase.startInternal()。2.1节分析ContainerBase结构时,已经提过一项基础的工作就是通过线程池执行子容器启动任务

    依照顺序主要的工作包括:
    (1)Cluster服务启动。
    (2)Realm服务启动。
    (3)子容器的启动。
    (4)Pipeline的启动(Pipeline进一步启动相应Valve链上全部的Valve)。
    (5)通知执行STARTING相应的Listener。
    (6)后台任务共享线程的启动;

    进而来看看详细容器类的启动过程:
    Engine(没有额外的工作)——>Host(Set error report valve)。
    接下来是Context,查看StandardContext我们能够看到直接相应于一个Web应用的Context的启动过程是非常复杂的,并且它并没有调用super.startInternal(),在上篇已经较为详细的总计,这里再系统的顺利一下:
    (1)创建读取资源文件的对象:假设我们没有上面在初始化过程中提到的<Resources>元素,将会创建一个默认的StandardRoot。
    (2)创建ClassLoader对象,为了实现不同应用类的隔离,每一个Context有自己的WebappLoader,创建相应的WebappClassLoader;
    (3)设置应用的工作文件夹。
    (4)启动相关辅助类:Logger,Cluster。Realm;
    (5)创建会话管理器;

    (6)通知ContextConfig读取和解析Web应用web.xml和注解;
    调用过程:
    StandardContext.startInternal()触发通知fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);——>ContextConfig.configureStart()——>通过WebXml读取web.xml配置,扫描读取注解配置——>ContextConfig依据读取到的ServletDef创建StandardWrapper和FilterDef。Listener等组件配置信息一起注入Context中。

    之后返回到StandardContext.startInternal(),StandardContext将依照在上篇中提到的顺序载入和初始化各个组件:
    (7)启动子容器,也就是上一步创建的全部StandardWrapper;
    (8)启动Pipeline
    (9)启动会话管理器Manager
    (10)获取ServletContext并设置必要的參数,ServletContext在Tomcat中的内部表示即ApplicationContext。返回到Servlet中的是它的门面对象ApplicationContextFacade;
    (11)调用Initializer的onStartup。
    (12)创建Context中配置的Listener
    (13)创建和初始化配置的Filter
    (14)创建和初始化loadOnStartup大于等于0的Servlet,StandardWrapper的门面类StandardWrapperFacade作为ServletConfig传入Servlet的init方法;

    至此,结合上篇的内容,我们对Tomcat容器的结构,载入,初始化和启动的过程已经有了比較清除的认识和理解。

    3.4 Tomcat内部是怎样处理请求和返回的?

    Connector组件是Tomcat两个核心组件之中的一个(还有一个是Container),主要任务是负责接收client发过来的TCP连接请求。创建一个Request和Response对象用于和请求端交换数据。

    Tomcat使用Apache Coyote库来处理网络I/O的。Connector是通过适配器将自己“置入”这个框架中的,详细是org.apache.catalina.connector.CoyoteAdapter。

    Adapter位于Coyote框架处理请求的末端。解析和得到的org.apache.coyote.Request和org.apache.coyote.Response将会传入Adapter,因此它作为Connector的适配器又能够訪问到Tomcat组件包括容器。因此能够终于将请求传入Tomcat的核心容器中。

    Connector体系结构:
    tomcat-container-structure

    Connector负责创建Adapter(CoyoteAdapter)和ProtocolHandler(在构造器中依据指定协议反射创建相应的ProtocolHandler)。
    ProtocolHandler负责依据详细的协议和I/O模型对请求数据进行接受,解析和处理,ProtocolHandler创建并托付Endpoint进行详细的处理。Endpoint经过一层处理后将请求传入Processor。终于由Processor将请求传入Adapter进而进入容器。

    说明一下Endpoint比方NioEndpoint是通过一个内部接口Handler来将请求转入Processor的,NioEndpoint.Handler的详细实现类是。Http11NioProtocol.Http11ConnectionHandler相应Http11NioProcessor引用。

    我们在server.xml中配置<Connector>元素时能够选择详细的协议,Tomcat有四种不同的协议可供选择,类型包括BIO,AIO,NIO,APR。这将决定上面体系的详细类型,比方设置protocol="org.apache.coyote.http11.Http11NioProtocol"。将使用NIO,相应上面的接口的详细实现是:
    ProtocolHandler——>Http11NioProtocol;
    Endpoint——>NioEndpoint;
    Processor——Http11NioProcessor;
    详细四种协议的区别将在下一节中详细解释描写叙述。

    另外,server.xml的还有一个<Connector>使用AJP定向包协议。这是一种基于二进制格式传输文本的协议,一般用于服务器之间通信,不是与client通信,因此使用较为简单二进制格式文本效率更高。相同AJP也是通过上述体系结构支持。这里就不深究了。

    Connector初始化:
    Connector在初始化initInternal()中创建Adapter对象(CoyoteAdapter)。并初始化ProtocolHandler,ProtocolHandler初始化会调用相应Endpoint的初始化即bind,这样就能够開始在绑定指定地址和port准备监听请求。

    因此。我们能够看到Endpoint在这个体系当中起着十分核心的作用。

    负责监听接受请求并转入请求到相应处理程序。

    Request和Response对象的创建:
    AbstractProcessor负责创建org.apache.coyote.Request和org.apache.coyote.Response;

    CoyoteAdapter调用Connector.createRequset()和createResponse()创建org.apache.catalina.connector.Request和Response这就是实现了我们熟悉的HttpServletRequest和HttpServletResponse接口的贯穿整个容器,Filter&Servlet生命周期的两个对象了。

    请求是怎样传入容器的终于到达Servlet的:
    Endpoint对象包括几个重要的内部类:Acceptor,SocketProcessor,Hanlder。当中Acceptor负责在一个后台线程中(之后称为Acceptor thread)指定port上接收client发送的请求,在默认的server.xml中分别在8080和8089port上定义了两个Connector,那么就会有相应两个Acceptor分别在两个port上监听请求。SocketProcessor依据socket的状态进行第一层处理,另外SSL的握手也是由它负责。Hanlder接口是每一个详细的Endpoint的内部接口。一般由相应Protocol的一个Handler内部类实现。比方JIoEndponit的handler相应的就是Http11Protocol的Http11ConnectionHandler。SocketProcessor将会调用handler.process()将socket请求内容传入Processor.process()。

    AbstractHttp11Processor.process()调用CoyoAdapter.service()。
    CoyoAdapter.service()将调用Service拥有的容器(也就是Engine)相应Pipeline的第一个Valve的invoke方法。这样Request和Response对象就进入容器。

    每一个容器Pipeline上的最后一个Valve负责将Request和Response传入下一个容器(也就是每一个容器的第一个Valve)。经过了Pipeline上全部的Valve,最后一个Valve也就是StandardWrapperValve。它的invoke方法将调用FilterChain.doFIlter(),将把设置好的Request和Response对象传入Filter链。这就进入我们熟悉的部分了。终于将被分派的正确的Servlet进行处理。

    连接数控制:
    Tomcat定义一个基于AQS的同步工具org.apache.tomcat.util.threads.LimitLatch控制并发连接数。LimitLatch构造器接受一个limit整型參数。表示最大数量,达到limit时候当前线程堵塞直到连接释放。在Acceptor thread中调用:

    countUpOrAwaitConnection();

    当连接数达到最大限制时。等待(默认的最大连接数是10000)。

    一个Connector相应一个port,由一个Endpoint实例负责处理。

    网络编程中I/O模型一般有5种:堵塞I/O。非堵塞I/O,I/O多路复用,信号驱动I/O。异步I/O。

    当中Tomcat的Connector没有基于信号驱动I/O的方式,可能是由于信号驱动使用上比較复杂,另外信号仅仅发送一次,还须要信号队列。I/O多路复用通常是结合非堵塞I/O,Tomcat中NIO指的就是基于非堵塞I/O的多路复用。

    前四种I/O模型都是同步I/O,注意信号驱动是同步模型,由于接收到信号之后应用程序仍然要去自己读取(通过系统调用,用户态/内核态切换等,这个过程是同步的)。

    已经分析过,使用不同模型处理I/O的关键类是不同的Endpoint类:
    tomcat-endpoint
    当中:
    JIoEnpoint:堵塞I/O。
    NioEnpoint:I/O多路复用,同步非堵塞。
    Nio2Enpoint:AIO,异步I/O。
    AprEnpoint:基于JNI的I/O多路复用;

    Endpoint的结构

    四种模型的结构不同,可是相同有一些类似的基本结构:
    tomcat-endpoint
    当中JIoEndpoint是结构最为简单的,当中Acceptor。SocketProcessor,Handler是每一个Endpoint都具有的基本组件,当中Acceptor,和SocketProcessor派生Runnable,执行在各自后台线程中:
    Acceptor:前面已经介绍过,它是在指定地址和portaccept接受请求的。一个Endpoint含有一个Acceptor thread,accept调用是堵塞的(使用NIO也是堵塞模式。NIO2的过程也是堵塞的)。比方NIO。ServletSocketChannel接收client请求,注意该ServletSocketChannel实例是堵塞的。并没有注冊到Selector中,因此它在一个独立的后台线程中执行。
    SocketProcessor:执行在工作者线程池中,它会依据socket连接的状态进行相应的处理,包括在可读/写时调用Handler进行处理。它所在的线程池就是我们的业务代码终于执行的线程环境。
    Worker threads pool(工作者线程池)
    假设在server.xml定义了<Executor>,将会有该Executor负责执行请求处理任务。

    假设未定义,在Tomcat 8中会在XXXEndpoint的startInternal()创建一个Executor

    public void createExecutor() {
            internalExecutor = true;
            TaskQueue taskqueue = new TaskQueue();
            TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
            executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
            taskqueue.setParent( (ThreadPoolExecutor) executor);
        }

    默认是一个基于无界队列(TaskQueue派生自LinkedBlockingQueue。默认大小是Integer.MAX_INTEGER)的有界线程池(默认核心线程池大小为10。最大大小为200)。默认情况下。线程池的工作线程都是后台线程。

    Endpoint的生命周期:

    不同模型生命周期细节不一样,但具有一些共同特性;
    (1)初始化:Connector组件初始化时会进行ProtocolHandler,进而初始化Endpoint,默认在此时进行bind。
    (2)启动
    假设初始化没有bind,这里必须进行bind;
    检查创建工作线程池;
    初始化启动控制连接数的Latch。
    (3)暂停
    用一个“伪请求”暂停Acceptor接受请求的过程。


    (4)恢复
    (5)停止
    释放控制连接数的Latch;
    暂停Endpoint;
    处理已存在的连接;
    假设存在对象池(比方NioEndpoint中nioChannels。eventCache,processorCache对象池来降低对象创建的次数)。要进行清除;
    unbind;

    未完待续。。

  • 相关阅读:
    [LeetCode] 17. Letter Combinations of a Phone Number 电话号码的字母组合
    [LeetCode] 11. Container With Most Water 装最多水的容器
    [LeetCode] 42. Trapping Rain Water 收集雨水
    Meta标签中的format-detection属性及含义(转)
    html marquee 标签(转)
    css 样式引入的方法 link 与import的区别
    html meta标签使用
    backface-visibility
    zepto.js 总结
    HTTP 请求的组成 方法 已经 请求的状态码
  • 原文地址:https://www.cnblogs.com/yutingliuyl/p/7049740.html
Copyright © 2011-2022 走看看