目录
Tomcat总体架构(一)
八、PipeLine 和 Valve
从架构设计的角度来考虑,至此的应用服务器设计主要完成了我们对核心概念的分解,确保了整体架构的可伸缩性和可扩展性,除此之外,我们还要考虑如何提高每个组件的灵活性,使其同样易于扩展.
在增强组件的灵活性和可扩展性方面,职责链模式是一种比较好的选择.Tomcat即采用该模式来实现客户端请求的处理——请求处理也是职责链模式典型的应用场景之一.换句话说,在Tomcat中每个Container组件通过执行一个职责链来完成具体的请求处理.
Tomcat定义了Pipeline(管道)和Valve(阀)两个接口.前者用于构造职责链,后者代表职责链上的每个处理器.
当然,我们还可以从字面意思来理解这两个接口所扮演的角色——来自客户端的请求就像是流经管道的水一般,经过每个阀进行处理. 其设计如图2-11所示.
Pipeline中维护了一个基础的Valve,它始终位于Pipeline的末端(即最后执行),封装了具体的请求处理和输出响应的过程.然后,通过addvalve()方法,我们可以为Pipeline添加其他的Valve.后添加的Valve位于基础Valve之前,并按照添加顺序执行.Pipeline通过获得首个Valve来启动整个链条的执行.
Tomcat容器组件的灵活之处在于,每个层级的容器(Engine、Host、Context、 Wrapper)均有对应的基础Valve实现,同时维护了一个Pipeline实例.也就是说,我们可以在任何层级的容器上针对请求处理进行扩展.
由于Tomcat每个层级的容器均通过Pipeline和Valve进行请求处理,那么,我们很容易将一些通用的Valve实现根据需要添加到任何层级的容器上. 修改后的应用服务器设计如图2-12所示.
九、Connector
前面我们重点讨论了容器组件的设计,集中于如何设计才能确保容器的灵活性和可扩展性,并做到合理的解耦.
接下来,我们再细化一下服务器设计中的另一个重要组件——Connector.
要想与Container配合实现一个完整的服务器功能,Connector至少要完成如下几项功能.
-
监听服务器端口,读取来自客户端的请求.
-
将请求数据按照指定协议进行解析.
-
根据请求地址匹配正确地容器进行处理.
-
将响应返回客户端.
只有这样才能保证将接收到的客户端请求交由与请求地址匹配的容器处理.
我们知道,Tomcat支持多协议,默认支持HTTP和AJP.同时,Tomcat还支持多种IO方式, 包括BIO ( 8.5版本之后移除)、NIO、APR.而且在Tomcat 8之后新增了对NIO2和HTTP/2协议的支持.因此,对协议和I/O进行抽象和建模是需要重点关注的.
Tomcat的设计方案如图2-13所示.
在Tomcat中,ProtocolHandler表示一个协议处理器,针对不同协议和IO方式,提供了不同的实现,如: Http11NioProtocol表示基于NIO的HTTP协议处理器.
ProtocolHandler包含一个Endpoint用于启动Socket监听,该接口按照IO方式进行分类实现,如: Nio2Endpoint表示非阻塞式Socket I/O.
还包含一个Processor用于按照指定协议读取数据,并将请求交由容器处理,如: Http1lNioProcessor表示在NIO的方式下HTTP请求的处理类.
注意 Tomcat并没有Endpoint接口,仅有AbstractEndpoint抽象类,此处仅作为概念讨论,故将其视为Endpoint接口. |
在Connector启动时,Endpoint会启动线程来监听服务器端口,并在接收到请求后调用Processor进行数据读取.具体过程见后续再讲.
当Processor读取客户端请求后,需要按照请求地址映射到具体的容器进行处理,这个过程即为请求映射.由于Tomcat各个组件采用通用的生命周期管理,而且可以通过管理工具进行状态变更,因此请求映射除考虑映射规则的实现外,还要考虑容器组件的注册与销毁
Tomcat通过Mapper和MapperListener两个类实现上述功能.前者用于维护容器映射信息,同时按照映射规则(Servlet规范定义)查找容器.后者实现了ContainerListener和LifecycleListener,用于在容器组件状态发生变更时,注册或者取消对应的容器映射信息.为了实现上述功能,MapperListener实现了Lifecycle接口,当其启动时(在Service启动时启动),会自动作为监听器注册到各个容器组件上,同时将已创建的容器注册到Mapper.
注意﹑在Tomcat7及之前的版本中,Mapper由Connector维护,而在Tomcat 8中,改由Service维护, 因为Service本来就是用于维护Connector和Container的组合,两者从概念上讲更密切一些. |
Tomcat通过适配器模式(Adapter )实现了Connector与Mapper、Container的解耦.Tomcat默认的Connector实现(Coyote )对应的适配器为CoyoteAdapter.也就是说,如果你希望使用Tomcat的链接器方案,但是又想脱离Servlet容器(虽然这种情况几乎不可能出现,但是从架构可扩展性的角度来讲,还是值得讨论一下),此时只需要实现我们自己的Adapter即可.当然 我们还需要 按照Container的定义开发我们自己的容器实现(不一定遵从Servlet规范).
按照上述描述,Connector设计如图2-14所示.
十、Executor
完成了Connector的设计之后,我们再进一步审视一下当前的应用服务器方案,很明显,我们忽略了一个问题——并发.这对应用服务器而言是尤其需要考虑的,我们不可能让所有来自客户端的请求均以串行的方式执行.那么,我们应如何设计应用服务器的并发方案?
首先,既然Tomcat提供了一致的可插拔的组件环境,那么我们自然也希望线程池作为一个组件进行统―管理.因此,Tomcat提供了Executor接口来表示一个可以在组件间共享的线程池(默认使用了JDK5提供的线程池技术),该接口同样继承自Lifecycle,可按照通用的组件进行管理.
其次,线程池的共享范围如何确定?
在Tomcat中Executor由Service维护,因此同一个Service中的组件可以共享一个线程池.
当然,如果没有定义任何线程池,相关组件(如Endpoint)会自动创建线程池,此时,线程池不再共享.
在Tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户端请求后,会创建请求处理对象,并交由线程池处理,由此支持并发处理客户端请求.这里我们仅从概念层面进行描述,Tomcat具体的线程池实现、使用方式、注意事项等会在后续详细描述.
添加Executor后,总体设计如图2-15所示.
十一、Bootstrap 和 Catalina
我们在前面几个小节中讲解了Tomcat总体架构中的主要核心组件,它们代表了应用服务器程序本身,这就如楼房的主体.但是,除了主体建筑外,楼房还需要外墙等装饰,Tomcat也一样,我们还需要提供一套配置环境来支持系统的可配置性,便于我们通过修改相关配置来优化应用服务器.
当然,我们没有涉及集群、安全等组件,尽管它们也非常重要,但是,我们还是希望更多地关注于一些通用概念.虽然集群、安全等作为一个完备的应用服务器必不可少,但是它们的缺失并不会影响我们去理解应用服务器的基本概念和设计方式.这些内容将会在后续有机会再详细讲解.
在第1章中,我们列举了Tomcat的几个重要配置文件,其中最核心的文件为server.xml.通过这个文件,我们可以修改Tomcat组件的配置参数甚至添加相关组件,这也是后续性能调优阶段重点涉及的文件.Tomcat通过类Catalina提供了一个Shell程序,用于解析server.xml创建各个组件,同时,负责启动、停止应用服务器(只需要启动Tomcat顶层组件Server即可).Tomcat使用Digester解析XML文件,包括server.xml以及web.xml等,具体可参见http://commons.apache.org/proper/commons-digester/.
最后,Tomcat提供了Bootstrap作为应用服务器启动人口.Bootstrap负责创建Catalina实例,根据执行参数调用Catalina相关方法完成针对应用服务器的操作(启动、停止).
也许你会有疑问,为什么Tomcat不直接通过Catalina启动,而是又提供了Bootstrap呢?你可以查看一下Tomcat的发布包目录,Bootstrap并不位于Tomcat的依赖库目录下(SCATALINA_HOME/lib ),而是直接在SCATALINA_HOME/bin目录下.Bootstrap与Tomcat应用服务器完全松耦合(通过反射调用Catalina实例),它可以直接依赖JRE运行并为Tomcat应用服务器创建共享类加载器,用于构造Catalina实例以及整个Tomcat服务器.
注意Tomcat的启动方式可以作为非常好的示范来指导中间件产品设计.它实现了启动入口与 核心环境的解耦,这样不仅简化了启动(不必配置各种依赖库,因为只有独立的几个API ),而且便于我们更灵活地组织中间件产品的结构,尤其是类加载器的方案,否则,我们所有的依赖库将统一放置到一个类加载器中,而无法做到灵活定制. |
上述是Tomcat标准的启动方式.但是正如我们所说,既然Server及其子组件代表了应用服务器本身,那么我们就可以不通过Bootstrap和Catalina来启动服务器.
Tomcat提供了一个同名类org.apache.catalina.startup.Tomcat,使用它我们可以将Tomcat服务器嵌入到我们的应用系统中并进行启动.当然,你可以自己编写代码来启动Server,也可以自定义其他配置方式启动,如YAML.这就是Tomcat灵活的架构设计带给我们的便利,也是我们设计中间件产品的架构关注点之一.
十二、组件总结
最后,我们再整体回顾一下上述讲解涉及的Tomcat服务器中的概念,如表2-2所示.
表2-2 Tomcat的组件说明
组件名称 | 说明 |
---|---|
Server | 表示整个Servlet容器,因此Tomcat运行环境中只有唯一一个Server实例. |
Service | 表示一个或者多个Connector的集合,这些Connector共享同一个Container来处理其请求.在同一个Tomcat实例内可以包含任意多个Service实例,它们彼此独立. |
Connector | 即Tomcat链接器,用于监听并转化Socket请求,同时将读取的Socket请求交由Container处理, Connector 支持不同协议以及不同的I/O方式. |
Container | Container表示能够执行客户端请求并返回响应的一类对象.在Tomcat中存在不同级别的容 Container 器:Engine、Host、Context、Wrapper. |
Engine | Engine表示整个Servlet引擎.在Tomcat中,Engine为最高层级的容器对象.尽管Engine不是直 Engine 接处理请求的容器,却是获取目标容器的入口. |
Host | Host作为一类容器,表示Servlet引擎((即Engine)中的虚拟机,与一个服务器的网络名有关, Host 如域名等.客户端可以使用这个网络名连接服务器,这个名称必须要在DNS服务器上注册. |
Context | Context作为一类容器,用于表示ServletContext,在Servlet规范中,一个ServletContext即表示 Context 一个独立的Web应用. |
Wrapper | Wrapper作为一类容器,用于表示Web应用中定义的Servlet Executor 表示Tomcat组件间可以共享的线程池 |