Tomcat的整体架构你了解吗

212 阅读20分钟
原文链接: m.toutiaocdn.com

Tomcat整体架构

作者:兔哥


前言

俗话说,要站在巨人的肩膀上看世界。一般学习的时候也是先总览整体,然后逐步个个击破,最后形成思路,了解具体细节。Tomcat的结构很复杂,但是 Tomcat 非常的模块化,找到了 Tomcat最核心的模块,问题才可以迎刃而解。因此说,了解了Tomcat的整体架构对以后深入了解Tomcat来说至关重要!

一、Tomcat顶层架构

先上一张Tomcat的顶层结构图,如下:

Tomcat的整体架构你了解吗

Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,用于具体提供服务。

Service主要包含两个部分:Connector和Container。从上图可以看出 Tomcat 的心脏就是这两个组件,他们的作用如下:

1、Connector用于处理连接相关的事情,并提供Socket与Request和Response相关的转化;

2、Container用于封装和管理Servlet,以及具体处理Request请求;

一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供Http和Https链接,也可以提供向相同协议不同端口的连接,示意图如下(Engine、Host、Context下边会说到):

Tomcat的整体架构你了解吗

多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了!所以整个 Tomcat 的生命周期由 Server 控制。

另外,上述的包含关系或者说是父子关系,都可以在tomcat的conf目录下的server.xml配置文件中看出,下图是删除了注释内容之后的一个完整的server.xml配置文件(Tomcat版本为8.0.X)

Tomcat的整体架构你了解吗

详细的配置文件文件内容可以到Tomcat官网查看:http://tomcat.apache.org/tomcat-8.0-doc/index.html

上边的配置文件,还可以通过下边的一张结构图更清楚的理解:

Tomcat的整体架构你了解吗

Server标签设置的端口号为8005,shutdown=”SHUTDOWN” ,表示在8005端口监听“SHUTDOWN”命令,如果接收到了就会关闭Tomcat。一个Server有一个Service,当然还可以进行配置,一个Service有多个,Service左边的内容都属于Container的,Service下边是Connector。


二、Tomcat顶层架构小结:

(1)Tomcat中只有一个Server,一个Server可以有多个Service,一个Service可以有多个Connector和一个Container; (2) Server掌管着整个Tomcat的生死大权; (4)Service 是对外提供服务的; (5)Connector用于接受请求并将请求封装成Request和Response来具体处理; (6)Container用于封装和管理Servlet,以及具体处理request请求;

Tomcat加载时相应组件(容器)的配置参数都是从上面的文件读进去的,这个文件也是Tomcat性能优化的关键。接下来我们就根据上图以及conf/server.xml的内容来一步步描述一下上面所说的各种组件吧。

三、Server

Server相当于一个实体的家,这是它们在社会上生存之本,有了家它们就行安心的为人民服务了。

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

Server是Tomcat中最顶层的组件,它可以包含多个Service组件。Server 的类结构图例如以下:

Tomcat的整体架构你了解吗

在Tomcat源代码中Server组件的标准实现类 是 org.apache.catalina.core.StandardServer 。StandardServer的继承关系图如下图所示:

Tomcat的整体架构你了解吗

Lifecycle是Tomcat的生命周期接口。保持组件启动和停止一致的的机制通过实现org.apache.catalina.Lifecycle接口来实现。

StandardServer 实现了上面 Server的方法,同一时候也实现了 Lifecycle、MbeanRegistration 两个接口的全部方法,以下主要看一下 StandardServer 重要的一个方法 addService 的实现:

StandardServer.addService

@Override
public void addService(Service service) {

service.setServer(this);

synchronized (servicesLock) {
Service results[] = new Service[services.length + 1];
System.arraycopy(services, 0, results, 0, services.length);
results[services.length] = service;
services = results;

if (getState().isAvailable()) {
try {
service.start();
} catch (LifecycleException e) {
// Ignore
}
}

// Report this property change to interested listeners
support.firePropertyChange("service", null, service);
}

}

从上面第一句就知道了 Service 和 Server 是相互关联的,Server 也是和 Service 管理 Connector 一样管理它,也是将 Service 放在一个数组中,后面部分的代码也是管理这个新加进来的 Service 的生命周期。

四、Service

接下来咱们来看看Service组件,Service组件相当于Connetor和Engine组件的包装器,它将一个或者多个Connector组件和一个Engine建立关联。上述配置文件中,定义一个叫Catalina的服务,并将Http,AJP(定向包的协议)这两个Connector关联到了一个名为Catalina的Service,注意一个Connetor对应处理一种协议。

我们将 Tomcat 中 Connector、Container 作为一个总体比作一对情侣的话,Connector 主要负责对外交流,能够比作为 Boy。Container 主要处理 Connector 接受的请求,主要是处理内部事务,能够比作为 Girl。那么这个 Service 就是连接这对男女的结婚证了。

是 Service 将它们连接在一起。共同组成一个家庭。

当然要组成一个家庭还要非常多其他的元素。

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

这个 Service 接口的方法列表例如以下:

Tomcat的整体架构你了解吗

从 Service 接口中定义的方法中能够看出,它主要是为了关联 Connector 和 Container,同一时候会初始化它以下的其他组件,注意接口中它并没有规定一定要控制它以下的组件的生命周期。全部组件的生命周期在一个 Lifecycle 的接口中控制。这里用到了一个重要的设计模式,关于这个接口将在后面介绍。

Service组件对应Tomcat源代码中的标准实现类 是org.apache.catalina.core.StandardService,StandardService的继承关系图如下图所示:

Tomcat的整体架构你了解吗


StandardService 它不仅实现了 Service 借口同一时候还实现了 Lifecycle 接口,这样它就能够控制它以下的组件的生命周期了。StandardService 类结构图例如以下:

Tomcat的整体架构你了解吗


从上图中能够看出除了 Service 接口的方法的实现以及控制组件生命周期的 Lifecycle 接口的实现。还有几个方法是用于在事件监听的方法的实现,不仅是这个 Service 组件,Tomcat 中其他组件也相同有这几个方法,这也是一个典型的设计模式。将在后面介绍。

接下来我们看一下 StandardService 中基本的几个方法实现的代码,以下是 setContainer 和 addConnector 方法的源代码:

1. StandardService. SetContainer

@Override
public void setContainer(Container container) {
setContainer((Engine) container);
}

这段代码非常easy,事实上就是先推断当前的这个 Service 有没有已经关联了 Container,假设已经关联了,那么去掉这个关联关系—— oldContainer.setService(null)。假设这个 oldContainer 已经被启动了,结束它的生命周期,然后再替换新的关联、再初始化并開始这个新的 Container 的生命周期。最后将这个过程通知感兴趣的事件监听程序。这里值得注意的地方就是,改动 Container 时要将新的 Container 关联到每一个 Connector,还好 Container 和 Connector 没有双向关联,不然这个关联关系将会非常难维护。

2. StandardService. addConnector

@Override public void addConnector(Connector connector) {

synchronized (connectorsLock) {
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 (getState().isAvailable()) {
try {
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);
}

}

上面是 addConnector 方法,这种方法也非常easy。首先是设置关联关系,然后是初始化工作,開始新的生命周期。

这里值得一提的是,注意 Connector 用的是数组而不是 List 集合,这个从性能角度考虑能够理解。有趣的是这里用了数组可是并没有向我们寻常那样,一开始就分配一个固定大小的数组,它这里的实现机制是:又一次创建一个当前大小的数组对象,然后将原来的数组对象 copy 到新的数组中,这样的方式实现了类似的动态数组的功能,这样的实现方式,值得我们以后拿来借鉴。

五、Connector

既然Tomcat需要提供http服务,而我们知道http应用层协议最终都是需要通过TCP层的协议进行传递的,而Connector正是Tomcat中监听TCP网络连接的组件,一个Connector会监听一个独立的端口来处理来自客户端的连接。缺省的情况下Tomcat提供了如下两个Connector。我们分别描述一下:

HTTP/1.1 <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> 上面定义了一个Connector,它缺省监听端口8080,这个端口我们可以根据具体情况进行改动。connectionTimeout定义了连接超时时间,单位是毫秒,redirectPort定义了ssl的重定向接口,根据缺省的配置,Connector会将ssl请求重定向到8443端口。 AJP/1.3 AJP表示Apache Jserv Protocol,此连接器将处理Tomcat和Aapache http服务器之间的交互,这个连接器是用来处理我们将Tomcat和Apache http服务器结合使用的情况。假如在同样的一台物理Server上面部署了一台Apache http服务器和多台Tomcat服务器,通过Apache服务器来处理静态资源以及负载均衡的时候,针对不同的Tomcat实例需要AJP监听不同的端口。

Connector对应源代码中的org.apache.catalina.connector.Connector,它的继承关系图如下所示

Tomcat的整体架构你了解吗


Connector用于接受请求并将请求封装成Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端。

因此,我们可以把Connector分为四个方面进行理解:

(1)Connector如何接受请求的?

(2)如何将请求封装成Request和Response的?

(3)封装完之后的Request和Response如何交给Container进行处理的?

(4)Container处理完之后如何交给Connector并返回给客户端的?

首先看一下Connector的结构图,如下所示:

Tomcat的整体架构你了解吗

看一下Connector 的 startInternal 方法:

protected void startInternal() throws LifecycleException {
// Validate settings before starting
if (getPort() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPort())));
}

setState(LifecycleState.STARTING);

try {
protocolHandler.start();
} catch (Exception e) {
String errPrefix = "";
if(this.service != null) {
errPrefix += "service.getName(): \"" + this.service.getName() + "\"; ";
}

throw new LifecycleException
(errPrefix + " " + sm.getString
("coyoteConnector.protocolHandlerStartFailed"), e);
}
}

Connector 最重要的功能就是接收连接请求,然后分配线程让 Container 来处理这个请求,所以这必定是多线程的,多线程的处理是 Connector 设计的核心。 Connector就是使用ProtocolHandler来处理请求的,将具体的协议处理托管给了不同的ProtocolHandler实现类。不同的ProtocolHandler代表不同的连接类型,比如:Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。

Tomcat的整体架构你了解吗


这些实现类都内置这一个很重要的对象,AbstractEndpoint

protected AbstractEndpoint.Handler getHandler() {
return cHandler;
}

这是一个抽象类,不同的协议需要提供不同的Endpoint.这个类的作用就是提供底层的网络I/O的处理.不同的ProtocolHandler所内置的Endpoint是不同的.但是AbstractEndpoint为这些类抽象出了一个具体的实现框架.

Tomcat的整体架构你了解吗

其实ProtocolHandler由包含了三个部件:Endpoint、Processor、Adapter。

(1)Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成Request,Adapter用于将Request交给Container进行具体的处理。

(2)Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来实现HTTP协议的,Adapter将请求适配到Servlet容器进行具体的处理。

(3)Endpoint的抽象实现AbstractEndpoint里面定义的Acceptor和AsyncTimeout两个内部类和一个Handler接口。Acceptor用于监听请求,AsyncTimeout用于检查异步Request的超时,Handler用于处理接收到的Socket,在内部调用Processor进行处理。

至此,我们应该很轻松的回答(1)(2)(3)的问题了,但是(4)还是不知道,那么我们就来看一下Container是如何进行处理的,以及处理完之后是如何将处理完的结果返回给Connector的?

六、Container

Tomcat中有一个容器的概念,而Engine,Host,Context都属于Contanier。Container用于封装和管理Servlet,以及具体处理Request请求,在Connector内部包含了4个子容器,结构图如下:

Tomcat的整体架构你了解吗

Engine

我们先来说说最顶层的容器Engine

Engine:引擎,用来管理多个站点,一个Service最多只能有一个Engine。

一个Engine可以包含一个或者多个Host,也就是说我们一个Tomcat的实例可以配置多个虚拟主机。 缺省的情况下<Engine name="Catalina" defaultHost="localhost">定义了一个名称为Cataline的Engine.

Tomcat的整体架构你了解吗

Engine对应Tomcat源代码中的标准实现类 是org.apache.catalina.core.StandardEngine,它的继承关系图如下图所示:

Tomcat的整体架构你了解吗

StandardEngine这个类注意一点就是 Engine 没有父容器了,假设调用 setParent 方法时将会报错。加入子容器也仅仅能是 Host 类型的。


@Override
public void addChild(Container child) { if (!(child instanceof Host))
throw new IllegalArgumentException
(sm.getString("standardEngine.notHost"));
super.addChild(child);

}/**
* Disallow any attempt to set a parent for this Container, since an
* Engine is supposed to be at the top of the Container hierarchy.
*
* @param container Proposed parent Container
*/
@Override
public void setParent(Container container) {

throw new IllegalArgumentException
(sm.getString("standardEngine.notParent"));

}

它的初始化方法也就是初始化和它相关联的组件,以及一些事件的监听。

除了 Host之外还包含如下组件

Listener 组件:可以在 Tomcat 生命周期中完成某些Engine 容器相关工作的监听器。
AccessLog 组件:客户端的访问日志,所有客户端访问都会被记录。
Cluster 组件:提供集群功能,可以将 Engine 容器需要共享的数据同步到集群中的其他 Tomc实Engine 中包含 的 Host 组件可以共享 Cluster 。
Pipeline 组件: Engine 容器对请求进行处理的管道。
Realm 组件:提供了 Engine 容器级别的用户-密码-权限的数据对象,配合资源认证模块使用。


Host

Host:代表一个站点,也可以叫虚拟主机,通过配置Host就可以添加站点.一个虚拟主机可以有多个Context,缺省的配置如下: <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">….</Host> 其中appBase为webapps,也就是<CATALINA_HOME>\webapps目录,unpackingWARS属性指定在appBase指定的目录中的war包都自动的解压,缺省配置为true,autoDeploy属性指定是否对加入到appBase目录的war包进行自动的部署,缺省为true. Host对应Tomcat源代码中的标准实现类 org.apache.catalina.core.StandardHost,它的继承关系图如下所示:

Tomcat的整体架构你了解吗

Host 子容器一般是Context,它除了关联子容器外,还包括以下组件

Listener 组件:可以在 Tomcat 生命周期中完成某些Host 容器相关工作的监听器。

AccessLog 组件:客户端的访问日志,所有客户端访问都会被记录。

Cluster 组件:提供集群功能,可以将 Host 容器需要共享的数据同步到集群中的其他 Tomcat 实例上。

Pipeline 组件: Host 容器对请求进行处理的管道。

Realm 组件:提供了 Host 容器级别的用户-密码-权限的数据对象,配合资源认证模块使用。


Context

Context:代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF目录以及下面的web.xml文件;在Tomcat中,每一个运行的webapp其实最终都是以Context的形成存在,每个Context都有一个根路径和请求URL路径,Context对应Tomcat源代码中的标准实现类 org.apache.catalina.core.StandardContext,它的继承关系图如下图所示:

Tomcat的整体架构你了解吗


在Tomcat中我们通常采用如下的两种方式创建一个Context.下面分别描述一下:

1.在webapps目录中创建一个目录,这个时候将自动创建一个context,默认context的访问url为http://host:port/dirname,你也可以通过在ContextRoot\META-INF中创建一个context.xml的文件,其中包含如下的内容来指定应用的访问路径。 <Context path="/yourUrlPath" /> 2.conf\server.xml文件中增加context元素。 第二种创建context的方法,我们可以选择在server.xml文件的<Host>元素,比如我们在server.xml文件中增加如下内容:

.......
<span style="line-height: 1.5;"> </span><span style="line-height: 1.5;">.........</span>
<Context path="/mypath" docBase="/Users/xxx" reloadable="true">
</Context>
</Host>
</Engine>
</Service>
</Server>

这样的话,我们就可以通过http://host:port/mypath访问上面配置的context了。

Context 准备 Servlet 的执行环境是在 startInternal方法开始的。

Context 组件是 Web 应用的抽象,我们发开的 Web 应用部署到 Tomcat 后运行时就会转化成 Context 对象。它包 含了各种静态资源、若干 Servlet ( Wrapper容器)以及各种其他动态资源。它主要包括如下组件。

Listener 组件:可以在 Tomcat 生命周期中完成某些 Context 容器相关工作的监听器。

AccessLog 组件:客户端的访问日志,所有客户端访问都会被记录。

Pipeline 组件: Context 容器对请求进行处理的管道。

Realm 组件:提供了 Context 容器级别的用户-密码-权限的数据对象,配合资源认证模块使用。

Loader 组件: Web 应用加载器,用于加载 Web 应用的资源,它要保证不同 Web 应用之间的资源隔离。

Manager 组件:会话管理器,用于管理 Web 容器的会话,包括维护会话的生成、更新、销毁。

NamingResource 组件:命名资源,它负责将 server.xml 和 web 应用的 context.xml 资源映射到内存中。

Mapper 组件: servlet 映射器,它属于 context 内部的路由映射器,负责 context 容器的路由导航。

Wrapper 组件: Context 子容器。

Wrapper

Wrapper:每一Wrapper封装着一个Servlet;它负责管理一个 Servlet,包含的 Servlet 的装载、初始化、运行以及资源回收。

Wrapper 的实现类是org.apache.catalina.core. StandardWrapper。StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。

以下看一下很重要的一个方法 loadServlet。

它基本上描写叙述了对 Servlet 的操作,当装载了 Servlet 后就会调用 Servlet 的 init 方法,同一时候会传一个 对象给 Servlet,这个对象包装了 StandardWrapper,ServletConfig 与它们的关系图例如以下:

Tomcat的整体架构你了解吗

Servlet 能够获得的信息都在 StandardWrapperFacade 封装,这些信息又是在 StandardWrapper 对象中拿到的。所以 Servlet 能够通过 ServletConfig 拿到有限的容器的信息。

当 Servlet 被初始化完毕后,就等着 StandardWrapperValve 去调用它的 service 方法了,调用 service 方法之前要调用 Servlet 全部的 filter。

Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。是Tomcat 4 个级别的容器中最小的,与之相对应的是 Servlet ,一个 Wrapper 对应一个 Servlet 。它主要包括如 下组件。

Servlet 组件: Servlet 即 Web 应用开发中常用的 Servlet ,我们会在 Servlet 中编写好请求的逻辑处理。

ServletPool 组件: Servlet 对象池,当 Web 应用的 Servlet 实现了。 SingleThreadModel 接口时则会在 Wrapper 中产生 一个 Servlet 对象池。线程执行时,需先从对象池中获取一个Servlet 对象, ServletPool 组件能保证 Servlet 对象的线程 安全。

Pipeline 组件: Wrapper 容器对请求进行处理的管道。

下面找一个Tomcat的文件目录对照一下,如下图所示:

Tomcat的整体架构你了解吗

Context和Host的区别是Context表示一个应用,我们的Tomcat中默认的配置下webapps下的每一个文件夹目录都是一个Context,其中ROOT目录中存放着主应用,其他目录存放着子应用,而整个webapps就是一个Host站点。

我们访问应用Context的时候,如果是ROOT下的则直接使用域名就可以访问,例如:www.ledouit.com,如果是Host(webapps)下的其他应用,则可以使用www.ledouit.com/docs进行访问,当然默认指定的根应用(ROOT)是可以进行设定的,只不过Host站点下默认的主营用是ROOT目录下的。

看到这里我们知道Container是什么,但是还是不知道Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?别急!下边就开始探讨一下Container是如何进行处理的!


七、Container如何处理请求的

Container处理请求是使用Pipeline-Value管道来处理的!

Pipeline-Value是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将处理后的请求返回,再让下一个处理着继续处理。

Tomcat的整体架构你了解吗

但是!Pipeline-Value使用的责任链模式和普通的责任链模式有些不同!区别主要有以下两点:

(1)每个Pipeline都有特定的Value,而且是在管道的最后一个执行,这个Value叫做BaseValue,BaseValue是不可删除的;

(2)在上层容器的管道的BaseValue中会调用下层容器的管道。

我们知道Container包含四个子容器,而这四个子容器对应的BaseValue分别在:StandardEngineValue、StandardHostValue、StandardContextValue、StandardWrapperValue。

Pipeline的处理流程图如下:

Tomcat的整体架构你了解吗

(1)Connector在接收到请求后会首先调用最顶层容器的Pipeline来处理,这里的最顶层容器的Pipeline就是EnginePipeline(Engine的管道);

(2)在Engine的管道中依次会执行EngineValue1、EngineValue2等等,最后会执行StandardEngineValue,在StandardEngineValue中会调用Host管道,然后再依次执行Host的HostValue1、HostValue2等,最后在执行StandardHostValue,然后再依次调用Context的管道和Wrapper的管道,最后执行到StandardWrapperValue。

(3)当执行到StandardWrapperValue的时候,会在StandardWrapperValue中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的Filter和Servlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法,这样请求就得到了处理!

(4)当所有的Pipeline-Value都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给Connector了,Connector在通过Socket的方式将结果返回给客户端.

至此,我们已经对Tomcat的整体架构有了大致的了解,知道了每一个组件的基本要素和作用。并且通过上面的分析,我们发现Server,Service,Engine,Host,Context都实现了org.apache.catalina.Lifecycle接口,通过这个接口管理了这些核心组件的生命周期,关于这些组件的生命周期,大家如果感兴趣可以联系兔哥