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负责在一个指定端口监听请求。不同的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),主要任务是负责接收客户端发过来的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定向包协议,这是一种基于二进制格式传输文本的协议,一般用于服务器之间通信,不是与客户端通信,因此使用较为简单二进制格式文本效率更高。同样AJP也是通过上述体系结构支持,这里就不深究了。

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

因此,我们可以看到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)指定端口上接收客户端发送的请求,在默认的server.xml中分别在8080和8089端口上定义了两个Connector,那么就会有对应两个Acceptor分别在两个端口上监听请求。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对应一个端口,由一个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:前面已经介绍过,它是在指定地址和端口accept接受请求的,一个Endpoint含有一个Acceptor thread,accept调用是阻塞的(使用NIO也是阻塞模式,NIO2的过程也是阻塞的),比如NIO,ServletSocketChannel接收客户端请求,注意该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;

未完待续。。。

  • 11
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值