2.1.5 Pipeline 和 Valve

从架构设计的角度来考虑,至此的应用服务器设计主要完成了我们对核心概念的分解,确保 了整体架构的可伸缩性和可扩展性,除此之外,我们还要考虑如何提高每个组件的灵活性,使其同样易于扩展。

在增强组件的灵活性和可扩展性方面,职责链模式是一种比较好的选择。Tomcat即采用该模 式来实现客户端请求的处理——请求处理也是职责链模式典型的应用场景之一。换句话说,在 Tomcat中每个Container组件通过执行一个职责链来完成具体的请求处理。

Tomcat定义了Pipeline (管道)和Valve (阀)两个接口。前者用于构造职责链,后者代表职 责链上的每个处理器。当然,我们还可以从字面意思来理解这两个接口所扮演的角色一来自客 户端的请求就像是流经管道的水一般,经过每个阀进行处理。其设计如图2-11所示。Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第1张图片

Pipeline中维护了一个基础的Valve,它始终位于Pipeline的末端(即最后执行),封装了具体 的请求处理和输出响应的过程。然后,通过addValve()方法,我们可以为Pipeline添加其他的Valve 0 后添加的Valve位于基础Valve之前,并按照添加顺序执行。Pipeline通过获得首个Valve来启动整个链条的执行。

Tomcat容器组件的灵活之处在于,每个层级的容器(Engine, Host、Context, Wrapper )均有对应的基础Valve实现,同时维护了一个Pipeline实例。也就是说,我们可以在任何层级的容器 上针对请求处理进行扩展。

由于Tomcat每个层级的容器均通过Pipeline和Valve进行请求处理,那么,我们很容易将一些 通用的Valve实现根据需要添加到任何层级的容器上。

修改后的应用服务器设计如图2-12所示。Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第2张图片

2.1.5 Connector 设计

前面我们重点讨论了容器组件的设计,集中于如何设计才能确保容器的灵活性和可扩展性, 并做到合理的解耦。接下来,我们再细化一下服务器设计中的另一个重要组件一onnector0

要想与Container配合实现一个完整的服务器功能,Connector至少要完成如下几项功能。

口监听服务器端口,读取来自客户端的请求。

□将请求数据按照指定协议进行解析。

□根据请求地址匹配正确的容器进行处理。

□将响应返回客户端。

只有这样才能保证将接收到的客户端请求交由与请求地址匹配的容器处理。我们知道,Tomcat支持多协议,默认支持HTTP和AJP。同时,Tomcat还支持多种I/O方式, 包括BIO ( 8.5版本之后移除)、NIO、APR。而且在Tomcat 8之后新增了对NIO2和HTTP/2协议的 支持。因此,对协议和I/O进行抽象和建模是需要重点关注的。

Tomcat的设计方案如图2-13所示。Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第3张图片

在Tomcat中,ProtocolHandler表示一个协议处理器,针对不同协议和I/O方式,提供了不同的 实现,如Http 11 NioProtocol表示基于NIO的HTTP协议处理器。ProtocolHandler包含一个Endpoint 用于启动Socket监听,该接口按照I/O方式进行分类实现,如Nio2Endpoint表示非阻塞式Socket I/Oo 还包含一个Processor用于按照指定协议读取数据,并将请求交由容器处理,如HttpllNioProcessor 表示在NIO的方式下HTTP请求的处理类。

注意 :Tomcat并没有Endpoint接口,仅有AbstractEndpoint抽象类,此处仅作为概念讨论,故将其 视为 Endpoint 接口 。

在Connector启动时,Endpoint会启动线程来监听服务器端口,并在接收到请求后调用 Processor进行数据读取。具体过程见后续章节。

当Processor卖取客户端请求后,需要按照请求地址映射到具体的容器进行处理,这个过程即 为请求映射。由于Tomcat各个组件釆用通用的生命周期管理,而且可以通过管理工具进行状态变 更,因此请求映射除考虑映射规则的实现外,还要考虑容器组件的注册与销毁。

Tomcatil过Mapper和MapperListener两个类实现上述功能。前者用于维护容器映射信息,同 时按照映射规则(Servlet规范定义)查找容器。后者实现了ContainerListener和LifecycleListener, 用于在容器组件状态发生变更时,注册或者取消对应的容器映射信息。为了实现上述功能, MapperListener实现了Lifecycle接口,当其启动时(在Service启动时启动),会自动作为监听器注 册到各个容器组件上,同时将已创建的容器注册到Mapper。

注意 在Tomcat7及之前的版本中,Mapper由Connector维护,而在Tomcat8中,改由Service维护, 因为Service本来就是用于维护Connector和Container的组合,两者从概念上讲更密切一些。Tomcat通过适配器模式(Adapter )实现了Connector与Mapper、Container的解耦。Tomcat默 认的Connector实现(Coyote )对应的适配器为CoyoteAdapter。也就是说,如果你希望使用Tomcat 的链接器方案,但是又想脱离Servlet容器(虽然这种情况几乎不可能出现,但是从架构可扩展性 的角度来讲,还是值得讨论一下),此时只需要实现我们自己的Adapter即可。当然,我们还需要按照Container的定义开发我们自己的容器实现(不一定遵从Servlet规范)。

按照上述描述,Connector设计如图2-14所示。Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第4张图片

2.1.5 Executor

完成了Connector的设计之后,我们再进一步审视一下当前的应用服务器方案,很明显,我们 忽略了一个问题——并发。这对应用服务器而言是尤其需要考虑的,我们不可能让所有来自客户 端的请求均以串行的方式执行。那么,我们应如何设计应用服务器的并发方案?

首先,回顾已经讲解的Tomcat设计方案,既然Tomcat提供了一致的可插拔的组件环境,那么 我们自然也希望线程池作为一个组件进行统一管理。因此,Tomcat提供了Executor接口来表示一 个可以在组件间共享的线程池(默认使用了JDK5提供的线程池技术),该接口同样继承自 Lifecycle,可按照通用的组件进行管理。

其次,线程池的共享范围如何确定?在Tomcat中Executor由Service维护,因此同一个Service 中的组件可以共享一个线程池。

当然,如果没有定义任何线程池,相关组件(如Endpoint)会自动创建线程池,此时,线程 池不再共享。

在Tomcat中,Endpoint会启动一组线程来监听Socket端口,当接收到客户端请求后,会创建 请求处理对象,并交由线程池处理,由此支持并发处理客户端请求。

这里我们仅从概念层面进行描述,Tomcat具体的线程池实现、使用方式、注意事项等会在后 续章节中详细描述。

添加Executor后,总体设计如图2-15所示。Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第5张图片

2.1.5 Bootstrap 和 Catalina

我们在前面几个小节中讲解了 Tomcat总体架构中的主要核心组件,它们代表了应用服务器程 序本身,这就如楼房的主体。但是,除了主体建筑外,楼房还需要外墙等装饰,Tomcat也一样, 我们还需要提供一套配置环境来支持系统的可配置性,便于我们通过修改相关配置来优化应用服务器。

当然,我们没有涉及集群、安全等组件,尽管它们也非常重要,但是,我们还是希望更多 地关注于一些通用概念。虽然集群、安全等作为一个完备的应用服务器必不可少,但是它们的 缺失并不会影响我们去理解应用服务器的基本概念和设计方式。这些内容将会在后续章节中详细讲解。

在第1章中,我们列举了Tomcat的几个重要配置文件,其中最核心的文件为server.xml。通过 这个文件,我们可以修改Tomcat组件的配置参数甚至添加相关组件,这也是后续性能调优阶段重 点涉及的文件。

Tomcatil过类Catalina提供了一个Shell程序,用于解析server.xml创建各个组件,同时,负责 启动、停止应用服务器(只需要启动Tomcat顶层组件Server即可)。

Tomcat 使用Digester 解析 XML 文件,包括 server.xml 以及 web.xml 等,具体可参见 http://commons. apache.org/proper/commons-digester/,在讲解Tomcat酉己置时,我们也会再做进一步说明。

最后,Tomcat提供了Bootstrap作为应用服务器启动入口。Bootstrap负责创建Catalina实例,根 据执行参数调用Catalina相关方法完成针对应用服务器的操作(启动、停止)。

也许你会有疑问,为什么Tomcat不直接通过Catalina启动,而是又提供了Bootstrap呢?你可 以查看一下Tomcat的发布包目录,Bootstrap并不位于Tomcat的依赖库目录下($CATALINA_ HOME/lib ),而是直接在$CATALINA_HOME/bin目录下。Bootstrap与Tomcat应用服务器完全松耦 合(通过反射调用Catalina实例),它可以直接依赖JRE运行并为Tomcat应用服务器创建共享类加 载器,用于构造Catalina实例以及整个Tomcat服务器。

注意 Tomcat的启动方式可以作为非常好的示范来指导中间件产品设计。它实现了启动入口与 核心环境的解耦,这样不仅简化了启动(不必配置各种依赖库,因为只有独立的几个API), 而且便于我们更灵活地组织中间件产品的结构,尤其是类加载器的方案,否则,我们所 有的依赖库将统一放置到一个类加载器中,而无法做到灵活定制。

至此,我们应用服务器的完整设计如图2-16所示。Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第6张图片

上述是Tomcat标准的启动方式。但是正如我们所说,既然Server及其子组件代表了应用服务 器本身,那么我们就可以不通过Bootstrap和Catalina来启动服务器。

Tomcat提供了一个同名类org.apache.catalina.startup.Tomcat,使用它我们可以将Tomcat 服务器嵌入到我们的应用系统中并进行启动。当然,你可以自己编写代码来启动Server,也可以 自定义其他配置方式启动,如YAML。这就是Tomcat灵活的架构设计带给我们的便利,也是我们 设计中间件产品的架构关注点之一。

最后,我们再整体回顾一下上述讲解涉及的Tomcat服务器中的概念,如表2-2所示。
Tomcat总体架构:Pipeline 和 Valve+Connector 设计+Executor等_第7张图片
至此,我们循序渐进地介绍了Tomcat总体架构的静态设计,接下来我们将从两个方面介绍 Tomcat的动态设计:应用服务器启动和客户端请求处理。这两个方面也是应用服务器基本的处理 过程。

提前get tomcat PDF文档:https://shimo.im/docs/TC9Jq63Tp6HvTXdg