• Tomcat系统架构分析


    Tomcat系统架构分析

    关于这边blog呢,实际开发中并不会用到,但是我觉得还是很有必要认真的写一下。毕竟我们每天在本地撸码的时候使用的就是tomcat来做web服务器。一个常识就是说我们本地在tomcat里面部署了一个web应用就可以去跑这个应用了,那么这里就有一个很底层的问题,这个web应用是如何在tomcat里面跑起来的呢?我们发了一个http请求,这个请求是如何在到达tomcat上然后又做了些什么才最终返回给我们想要的结果呢?


    所以我现在认真的对tomcat的系统架构做一个分析,先交代下大致的情况。

     



    • Tomcat的整体结构
    Tomcat 的心脏是两个组件: Connector(连接)和Container(容器)。
    Connector 组件是可以被替换,这样可以提供给服务器设计者更多的选择,因为这个组件是如此重要,不仅跟服务器的设计的本身,而且和不同的应用场景也十分相关,所以一个 Container 可以选择对应多个 Connector。多个 Connector 和一个 Container 就形成了一个 Service, Service也就是服务了,有了 Service 就可以对外提供服务了,但是Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非Server莫属了。所以整个 Tomcat 的生命周期由Server 控制。

    前面的博客里面我也已经讲到说我们平时在操作tomcat的配置文件的时候,一般都是在操作server.xml这个配置文件。下面是这个文件的主要的大致标签,发现没?正好和上面所讲的每一层都一一对应的,这样子也方便以后我们记住了呢。

    <Server>
    	<Service>
    		<Engine name="Catalina" defaultHost="localhost">
    			<Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true"
    				xmlValidation="false" xmlNamespaceAware="false">
    			<Context path="/LinkinPark" docBase="E:	est1" debug="0" privileged="true"></Context>
    			</Host>
    		</Engine>
    	</Service>
    </Server>


    在对整体的组件做了大致的了解后,现在我们具体的来看每一个组件。在这里我一边研究下源码,一边来具体的介绍下每一个组件。首先这些组件肯定是面向接口编的,所以对应的每一个组件都在/tomcat源码/java/org/apache/catalina这个路径下,下面可以找到常用的tomcat里面的每一个具体的接口,然后实现类基本都是在原有的接口名字前面加上standard这个前缀,这个前缀意思就是标准的,具体的类在catalina包下的core路径下,每一个类都可以找见的。

    • 1,Service
    说白了, Service 只是在 Connector 和 Container 外面多包一层,把它们组装在一起,向外面提供服务,一个 Service 可以设置多个Connector,但是只能有一个 Container 容器。


    Tomcat 中 Service 接口的标准实现类是 StandardService 它不仅实现了 Service 借口同时还实现了 Lifecycle 接口,这样它就可以控制它下面的组件的生命周期了。下面就来看下在service添加connector和container2个组件关键的代码:

    /**
    	 * Set the <code>Container</code> that handles requests for all
    	 * <code>Connectors</code> associated with this Service.
    	 * 
    	 * @param container
    	 *            The new Container
    	 * 
    	 *            这段代码很简单,其实就是先判断当前的这个 Service 有没有已经关联了Container,如果已经关联了,那么去掉这个关联关系——oldContainer.setService(null)。
    	 *            如果这个 oldContainer已经被启动了,结束它的生命周期。然后再替换新的关联、再初始化并开始这个 新的 Container的生命周期。最后将这个过程通知感兴趣的事件监听程序。
    	 *            这里值得注意的地方就是,修改 Container 时要将新的Container 关联到每个 Connector,还好 Container 和 Connector没有双向关联,不然这个关联关系将会很难维护。
    	 */
    	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);
    		}
    		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);
    
    	}

    // --------------------------------------------------------- Public Methods
    
    	/**
    	 * Add a new Connector to the set of defined Connectors, and
    	 * associate[əˈsəʊʃieɪt] it with this Service's
    	 * Container.[添加一个新的连接到连接数组里面去,并且要关联服务的容器]
    	 * 
    	 * @param connector
    	 *            The Connector to be added
    	 *            这个方法比较简单,首先是设置关联关系,然后是初始化工作,开始新的生命周期。这里值得一提的是,注意 Connector
    	 *            用的是数组而不是 List 集合,这个从性能角度考虑可 以理解,有趣的是这里用了数组但是并没有向我们平常那样,一开始
    	 *            就分配一个固定大小[0]的数组,我去,它这里的实现机制是:重新创建一个当前大小的数组对象,然后将原来的数组对象 copy
    	 *            到新的数组中,这种方式实现了类似的动态数组的功能,这种实现方式,值得我们以后拿来借鉴,其实这也是性能调优的一点。
    	 */
    	public void addConnector(Connector connector)
    	{
    
    		synchronized (connectors)
    		{
    			connector.setContainer(this.container);
    			connector.setService(this);
    			// protected Connector connectors[] = new
    			// Connector[0];这里是上面的那个连接数组的初始化,长度是0,我去
    			// 创建一个新的数组,这个数组的要比原来的数组多一个,因为要放新的connector进来呢
    			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);
    		}
    
    	}

    • 2,Server 
    Server 要完成的任务很简单,就是要能够提供一个接口让其它程序能够访问到这个 Service 集合、同时要维护它所包含的所有 Service的生命周期,包括如何初始化、如何结束服务、如何找到别人要访问的 Service。还有其它的一些次要的任务,如您住在这个地方要向当地政府去登记啊、 可能还有要配合当地公安机关日常的安全检查什么的。

    • 3,组件的生命线“Lifecycle”

    现在我们已经知道Service 和 Server 管理它下面组件的生命周期,那它们是如何管理的呢?


    Tomcat 中组件的生命周期是通过 Lifecycle 接口来控制的,组件只要继承这个接口并实现其中的方法就可以统一被拥有它的组件控制了,这样一层一层的直到一个最高级的组件就可以控制 Tomcat 中所有组件的生命周期,这个最高的组件就是 Server,而控制 Server的是 Startup,也就是您启动和关闭 Tomcat。


    除了控制生命周期的 Start 和 Stop 方法外还有一个监听机制,在生命周期开始和结束的时候做一些额外的操作。这个机制在其它的框架中也被使用,如在 Spring 中。

    • 4,Connector 
    Connector 组件是 Tomcat 中两个核心组件之一,它的主要任务是负责接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和Response 对象分别用于和请求端交换数据,然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给处理这个请求的线程,处理这个请求的线程就是 Container 组件要做的事了。

    Tomcat5 中默认的 Connector 是 Coyote,这个 Connector 是可以选择替换的。 记住这么一句话就好了:Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求。所以这必然是多线程的,多线程的处理是 Connector 设计的核心。 Tomcat5 将这个过程更加细化,它将 Connector 划分成 Connector、 Processor、 Protocol, 另外Coyote 也定义自己的 Request 和 Response 对象。完成这个过程有点复杂,我这里大概的说下执行的相关代码:

    启动1个连接(HttpConnector.Start)-->进入等待请求的状态,直到一个新的请求到来才会激活它继续执行HttpProcessor.assign)-->处理这次请求(HttpProcessor.Run)-->解析socket(HttpProcessor.process)-->当Connector 将 socket 连接封装成 request 和 response 对象后接下来的事情就交给 Container 来处理了。


    • 5,Container

    Container (Servlet 容器)是容器的父接口,所有子容器都必须实现这个接口,Container 容器的设计用的是典型的责任链的设计模式,它有四个子容器组件构成,分别是: Engine、 Host、 Context、 Wrapper,这四个组件不是平行的,而是父子关系, Engine 包含 Host,Host 包含Context, Context 包含 Wrapper。通常一个 Servlet class 对应一个Wrapper,如果有多个 Servlet 就可以定义多个 Wrapper,如果有多个 Wrapper 就要定义一个更高的 Container 了,如 Context,Context 通常就是对应下面这个配置,在前面的文章我也讲到了,说如何通过配置文件来部署一个web应用。 

    <Context path="/LinkinPark" docBase="E:	est1" debug="0" privileged="true"></Context>

    Context 还可以定义在父容器 Host 中, Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。


    当Connector接受到一个连接请求时,将请求交给 Container,Container 是如何处理这个请求的?这四个组件是怎么分工的,怎么把请求传给特定的子容器的呢?又是如何将最终的请求交给 Servlet处理?



    StandardEngineValve 和 StandardHostValve 是 Engine 和 Host的默认的 Valve,它们是最后一个 Valve 负责将请求传给它们的子容器,以继续往下执行。从 Tomcat5 开始,子容器的路由放在了 request 中, request 中保存了当前请求正在处理的 Host、 Context 和 wrapper。

    • 6,Engine 
    Engine 容器比较简单,它只定义了一些基本的关联关系。它的标准实现类是 StandardEngine,值得注意的是Engine没有父容器了,如果调用setParent方法时将会报错。添加子容器也只能是Host类型的。它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。

    • 7,Host 

    Host 是 Engine 的字容器,一个 Host 在 Engine 中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。


    除了所有容器都继承的ContainerBase外,StandardHost 还实现了Deployer接口,上图清楚的列出了这个接口的主要方法,这些方法都是安装、展开、启动和结束每个web application。Deployer 接口的实现是 StandardHostDeployer,这个类实现了的最要的几个方法, Host可以调用这些方法完成应用的部署等。


    • 8,Context 
    Context 代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat可以没有 Engine 和 Host。Context 最重要的功能就是管理它里面的 Servlet 实例, Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢? Tomcat5 以前是通过一个 Mapper 类来管理的, Tomcat5 以后这个功能被移到了 request中,在前面的时序图中就可以发现获取子容器都是通过 request 来分配的。Context 准备 Servlet 的运行环境是在 Start 方法开始的。


    我们知道 Context 的配置文件中有个 reloadable 属性,如下面配置当这个 reloadable 设为 true 时, war 被修改后 Tomcat 会自动的重新加载这个应用。如何做到这点的呢 ? 这个功能是在StandardContext 的 backgroundProcess 方法中实现的,这个方法的代码如下:
    public void backgroundProcess() {
     if (!started) return;
     count = (count + 1) % managerChecksFrequency;
     if ((getManager() != null) && (count == 0)) {
     try {
     getManager().backgroundProcess();
     } catch ( Exception x ) {
     log.warn("Unable to perform background process on manager",x);
     }
     }
     if (getLoader() != null) {
     if (reloadable && (getLoader().modified())) {
     try {
     Thread.currentThread().setContextClassLoader
     (StandardContext.class.getClassLoader());
     reload();
     } finally {
     if (getLoader() != null) {
     Thread.currentThread().setContextClassLoader
     }
     }
     }
     if (getLoader() instanceof WebappLoader) {
     ((WebappLoader) getLoader()).closeJARs(false);
     }
     }
    }
    它会调用 reload 方法,而 reload 方法会先调用 stop 方法然后再调用 Start 方法,完成 Context 的一次重新加载。可以看出执行
    reload 方法的条件是 reloadable 为 true 和应用被修改,那么这个backgroundProcess 方法是怎么被调用的呢?
    这个方法是在 ContainerBase 类中定义的内部类ContainerBackgroundProcessor 被周期调用的,这个类是运行在一个后台线程中,它会周期的执行 run 方法,它的 run 方法会周期调用所有容器的 backgroundProcess 方法,因为所有容器都会继承ContainerBase 类,所以所有容器都能够在 backgroundProcess 方法中定义周期执行的事件。

    • 9,Wrapper 
    Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的Servlet 的装载、初始化、执行以及资源回收。 Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。Wrapper 的实现类是 StandardWrapper, StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig,由此看出StandardWrapper 将直接和 Servlet 的各种信息打交道。


    它基本上描述了对 Servlet 的操作,当装载了Servlet 后就会调用Servlet的init方法,同时会传一个StandardWrapperFacade对象给Servlet,这个对象包装了StandardWrapper, Servlet可以获得的信息都在StandardWrapperFacade封装,这些信息又是在 StandardWrapper 对象中拿到的。所以 Servlet 可以通过 ServletConfig 拿到有限的容器的信息。当 Servlet 被初始化完成后,就等着 StandardWrapperValve 去调用它的 service 方法了,调用 service 方法之前要调用 Servlet 所有的filter。


    上面这段话说的有点绕,我通俗点讲就是说:wrapper是来管理servlet的,它的实现类在装载一个servlet的时候会调用servlet的一些方法,而且还把自己的一个对象包装成了StandardWrapperFacade这个参数参入给了servlet,所以那么servlet可以拿到配置中的所有的容器的信息了,就这么简单,下面是StandardWrapper.loadServlet核心源码:

    	try {
                    instanceSupport.fireInstanceEvent(InstanceEvent.BEFORE_INIT_EVENT,servlet);
    
                    if( Globals.IS_SECURITY_ENABLED) {
                        boolean success = false;
                        try {
                            Object[] args = new Object[]{ facade };
                            SecurityUtil.doAsPrivilege("init",servlet,classType,args);
                            success = true;
                        } finally {
                            if (!success) {
                                // destroy() will not be called, thus clear the reference now
                                SecurityUtil.remove(servlet);
                            }
                        }
                    } else {
                        servlet.init(facade);
                    }
    
                    // Invoke jspInit on JSP pages
                    if ((loadOnStartup >= 0) && (jspFile != null)) {
                        // Invoking jspInit
                        DummyRequest req = new DummyRequest();
                        req.setServletPath(jspFile);
                        req.setQueryString(Constants.PRECOMPILE + "=true");
                        DummyResponse res = new DummyResponse();
    
                        if( Globals.IS_SECURITY_ENABLED) {
                            Object[] args = new Object[]{req, res};
                            SecurityUtil.doAsPrivilege("service",servlet,classTypeUsedInService,args);
                            args = null;
                        } else {
                            servlet.service(req, res);
                        }
                    }
                    instanceSupport.fireInstanceEvent(InstanceEvent.AFTER_INIT_EVENT,servlet);
                } catch (UnavailableException f) {
                   。。。。。
                } 

    • 10,Tomcat 中其它组件
    Tomcat 还有其它重要的组件,如安全组件 security、 logger 日志组件、 session、 mbeans、 naming 等其它组件。这些组件共同为Connector 和 Container 提供必要的服务。
  • 相关阅读:
    解决VMWARE NAT SERVICE服务无法启动或服务消失的问题
    Struts2+Spring+Hibernate框架整合总结详细教程
    Hibernate框架搭建实例
    spring官网下载
    LVS(一)调度原理以及调度算法
    HyperText Transfer Protocol
    Nginx(三) Nginx负载均衡以及反向代理
    Nginx(二) Nginx主机配置、日志分割、日志自动备份
    Nginx(一) Nginx安装
    Oracle VM VirtualBox(一) 快速拷贝一个新的虚拟机
  • 原文地址:https://www.cnblogs.com/LinkinPark/p/5233080.html
Copyright © 2020-2023  润新知