Tomcat之整体架构

Tomcat整体架构

了解Tomcat整体架构能够从中学习到Tomcat的工作原理,是学习Tomcat必不可少的部分。文本会介绍Tomcat的核心组件,最后再介绍从源码层面如何把Tomcat访问Servlet处理过程串联起来。

1. 总体架构

Tomcat要实现2个核心功能:

  1. 处理Socket连接,负责网络字节流与Request和Response对象的转换。
  2. 加载和管理Servlet,以及具体处理Request请求。

因此Tomcat设计了两个核心组件:连接器(Connector)和容器(Container)来分别处理这两件事。连接器负责对外交流,容器负责内部处理。

Tocamt支持的I/O模型有:

  • NIO:非阻塞IO,采用Java NIO类库实现。
  • NIO2:异步IO,采用JDK7最新的NIO2类库实现。
  • APR:采用Apache可移植运行库实现,是C/C++编写的本地库。

Tomcat支持的应用层协议有:

  • Http/1.1:大部分web应用采用的访问协议。
  • AJP:用于和Web服务器集成(如:Apache)
  • HTTP/2:Http2.0协议大幅度的提升了Web的性能。

Tomcat为了支持多种IO模型和应用层协议,一个容器可能对接多个连接器。但是单独的连接器或者容器不能对外提供服务,需要把它们组合起来才能工作,组装后的这个整体就叫作Service组件。Service只是在连接器和容器外部多包了一层,把它们组装起来。Tomcat内可能有多个Service,这样设计也是出于灵活性的考虑。关系图如下:

从图中可以看出:

  • 最顶层是Server,这里的Server也就是Tomcat的实例。
  • 一个Server中有一个或者多个Service,一个Service中有多个连接器和一个容器。
  • 连接器和容器之间通过标准的ServletRequest和ServletResponse通信。

2. 连接器

连接器对Servlet容器屏蔽协议以及I/O模型等的区别,无论是HTTP还是AJP,从容器总获取到的都是一个标准的ServletRequest对象。

连接器大致要做3个高内聚的功能:

  • 网络通信。
  • 应用层协议解析。
  • Tomcat Request/Response 与ServletRequest/ServletResponse的转换。

对应的Tomcat设计了3个组件实现这3个功能,分别是Endpoint、Processor和Adapter。整体逻辑为:EndPoint负责提供字节流给Processor,Processor负责提供Tomcat Request给Adapter,Adapter负责提供ServletRequest对象给容器。

由于I/O模型和应用协议可以自由组合,比如NIO + HTTP或者NIO2 + AJP。Tomcat将网络通信和应用层协议解析放在一起考虑,设计了一个叫ProtocolHandler的接口来封装这两种变化。具体的组合有:Http11NIOProtocol和AjpNioProtocol。当然还设计了一系列抽象基类来封装通用的部分,如:AbstractProtocol实现了ProtocolHandler接口,整个继承关系如下:

总结一下:连接器的三个核心组件:EndPoint、Processor和Adapter分别做三件事情,其中EndPoint和Processor放在一起抽象成一个PortocolHandler组件,关系如下图所示

2.1 ProtocolHandler组件

ProtocolHandler内部包括EndPoint和Processor,下面介绍它们的工作原理:

EndPoint:通信端点,是具体的Socket接收和发送处理器,是对传输层的抽象。因此EndPoint是用来实现TCP/IP协议的。EndPoint是一个接口,AbstractEndPoint是其抽象实现类,该抽象类具体的子类中,有两个重要的子组件:Acceptor和SocketProcessor。

Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,实现了Runnable接口,在run方法中调用Processor进行处理。为了提高处理能力,SocketProcessor被提交到线程池中执行。这个线程池为Executor。

Processor:如果说EndPoint是用来实现TCP/IP协议的,那么Processor是用来实现HTTP协议。Processor接收来自Endpoint的Socket,读取字节流解析成Tomcat Requst和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。 Processor是一个接口,定义了请求的处理等方法,它的抽象实现类AbstactProcessor对一些协议共有的属性进行封装。具体的实现有:HTTP11Processor,实现了特定协议的解析方法和请求处理方式。

连接器中,详细的组件关系如下:

2.2 Adapter

因为协议不同,客户端发送的请求信息也不同,所以Tomcat定义了自己的Request类,来"存放"这些请求信息。ProtocolHandler接口负责即系请求并生成TomcatRequest类。但这并不是标准的HttpServletRequest,意味着不能用Tomcat Request作为参数调用容器,Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的service方法,传入Tomcat Request对象,转换成ServletRequest对象,在调用容器的service方法。

3. 容器

3.1 容器的层次结构

Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper,这4种容器不是平行关系,而是父子关系。示意图如下:

之所以设计成这么多层次,是因为Tomcat通过一种分层的架构,是的Servlet容器具有很好的灵活性。

  • Context表示一个Web应用程序;Wrapper表示一个Servlet,一个Web应用程序中可能会有多个Servlet。
  • Host代表一个虚拟主机或一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可以部署多个Web应用程序。
  • Engine表示引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine。

可以通过Tomcat的server.xml配置文件,来加深对Tomcat容器的理解。Tomcat采用了组件化的设计,它的构成组件都是可配置的,最外层是Server,其他组件按照一定的格式要求配置在这个顶层容器中。

java 复制代码
<Server> //顶层组件,可以有多个Service
    <Service> //顶层组件,可包含一个Engine,多个Connection
        <Connection> //连接器组件,代表通信接口。
        </Connection>
        <Engine>  //容器组件,一个Engine助理Service中的所有请求,包含多个Host
            <Host> //容器组件,处理特定的Host下客户请求,可包含多个Context
                <Context> //容器组件,为特定的Web应用处理所有的客户请求
                </Context>
            </Host>
        </Engine>
    </Service>
</Server>

Tomcat如何管理这些容器呢?是通过组合模式来管理。具体实现方法:所有容器组件都实现了Container接口,因此组合模式可以使得用户对但容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是,最底层的Wrapper,组合容器对象是指上面的Context、Host或Engine。Container接口定义如下:

java 复制代码
public interface Container extends Lifecycle {
    public void setName(String name);
    public Container getParent();
    public void setParent(Container container);
    public void addChild(Container child);
    public void removeChild(Container child);
    public Container findChild(String name);
}

可以看到getParent、setParent、addChild和removeChild等方法。还实现了LiftCycle接口,LiftCycle接口用来统一管理各组件的生命周期。

3.2 请求定位Servlet过程

设计了这么多层次的容器,Tomcat如何确定请求是由哪个Wrapper容器里的Servlet来处理呢?Tomcat使用Mapper组件来完成这个任务。

Mapper组件的功能就是将用户请求URL定位到一个Servlet,它的工作原理是:Mapper组件里保存 了Web应用的配置信息,其实就是容器组件与访问路径的映射关系。比如Host容器里配置的域名、Context容器里的Web应用路径以及Wrapper容器里Servlet映射的路径。

当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里查找,就能定位到一个Servlet。一个请求URL最后只会定位到一个Wrapper容器,也就是一个Servlet

举例说明,如何定位的过程。 假设有两个系统运行在同一个Tomcat下,配置了两个虚拟域名:manage.shopping.com用于管理用户和商品,内部有两个Web应用;user.shopping.com用于终端客户搜索商品和购买,内部也有两个Web应用。

针对这样的部署,Tomcat会创建一个Service组件,一个Engine容器组件,创建两个Host子容器,每个Host容器下创建两个Context子容器。由于一个Web应用通常会有多个Servlet,每个Context里还有多个Wrapper容器。示意图如下:

假设:访问的URL为:http://user.shopping.com:8080/order/buy,Tomcat如何定位?

  • 首先根据协议和端口号选定Service和Engine。Tomcat每个连接器都监听不同的端口,默认Http连接器监听8080,默认AJP监听8009。上面例子访问8080端口,会被Http连接器接收,而一个连接器属于一个Service组件,这样Service组件就确定了。Service内只有一个Engine容器,所以Engine也就确认了。
  • 然后根据域名选定Host。Service和Engine确定后,Mapper组件通过URL中的域名找相应的Host容器。如例子中访问user.shopping.com,就会找到Host2容器。
  • 根据URL找到Context组件。Host确认后,Mapper根据URL的路径来匹配相应的Web应用路径。例子中访问/order,因此找到Context4这个Context容器。
  • 最后根据URL路径找到Wrapper(Servlet) 。Context确定后,Mapper再根据web.xml中配置的Servlet映射路径来找到具体的Wrapper和Servlet。

然后是一层层父子容器找到某个Servlet,但并不是只有Servlet才能处理请求。实际在查找路径上父子容器会对请求做一些处理。最先拿到Engine容器对请求做一些处理后,会把请求传给自己子容器Host继续处理,最后请求会传给Wrapper,这个调用过程是通过Pipeline-Valve管道实现的。

3.3 Pipeline-Valve

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

Valve表示一个处理点,如权限认证和日志打印。接口如下:

java 复制代码
public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

invoke方法就是处理请求,getNext则会调用下一个Valve。

Pipeline接口如下:

java 复制代码
public interface Pipeline extends Contained {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

有addValve方法,Pipeline中维护了Valve链表,Valve可以插入到Pipeline中,对请求做某些处理。Pipeline中没有invoke方法,因为整个调用链的触发是Valve来完成的。Valve完成自己的处理后,会调用getNext().invoke触发下一个Valve调用。

每个容器都有一个Pipeline对象,只要触发第一个Valve这个容器里的所有Valve就都会被调用。不同容器的调用通过getBasic方法,这个BasicValve处于Valve末端,它是Pipeline必不可少的一个Valve,负责调用下一层容器的Pipeline的第一个Valve。整个过程如下图所示:

Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFilter方法,最后会调用Servlet的service方法。

Filter和Valve区别:

  • Valve是Tomce私有机制,与Tomcat基础架构/API是紧耦合的。Servlet API是公有的标准,所有的Web容器包括Jetty都支持Filter机制。
  • Valve工作在Web容器级别,拦截所有应用的请求。而Filter工作在应用级别,只能拦截某个Web应用的所有请求。如果要做整个Web容器的拦截,必须通过Valve来实现。

4. Tomcat访问Servlet流程

记录一下Tomcat访问Servlet流程,方便源码跟踪。

  1. 当Tomcat从网络可以读数据时,会封装成Runnable,丢到线程池处理,其中实现了Runnnable的方法为SocketProcessorBase。
java 复制代码
//AbstractEndpoint#processSocket
 executor.execute(sc);
  1. NioEndpoint$SocketProcessor的doRun方法内,会调用ProtocolHandler的process方法。
ini 复制代码
state = getHandler().process(socketWrapper, event);
  1. AbstractProtocol方法内部会创建Processor,然后调用Processor的process方法。
java 复制代码
 processor = getProtocol().createProcessor();
//...
state = processor.process(wrapper, status);
  1. 会先到AbstractProcessorLight类的process方法,内部会调用service方法。这是一个抽象类,由具体的类实现。
ini 复制代码
state = service(socketWrapper);
  1. Http11Processor会调用Adapter的service方法。
scss 复制代码
getAdapter().service(request, response);
  1. CoyoteAdapter的service方法会调用postParseRequest,放方法内部会调用。内部通过Mapper解析这个请求归属的容器(哪个Host、哪个Context)。
scss 复制代码
connector.getService().getMapper().map(serverName, decodedURI,
                    version, request.getMappingData())
  1. 然后会调用Pipeline执行Valve链,先后会执行StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。
scss 复制代码
connector.getService().getContainer().getPipeline().getFirst().invoke(
                        request, response);
  1. StandardWrapperValve的invoke方法中,会初始化Servlet(如果未初始化)
ini 复制代码
 servlet = wrapper.allocate();
  1. 随后构造ApplicationFilterChain,并调用FilterChain
vbscript 复制代码
ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
//....                
filterChain.doFilter(request.getRequest(),
                                    response.getResponse());
  1. ApplicationFilterChain内部会递归调用Filter
vbscript 复制代码
filter.doFilter(request, response, this);
  1. 所有Filter调用完之后,会调用Servlet的service方法。
vbscript 复制代码
servlet.service(request, response);

5. 参考资料

  1. 《深入拆解Tomcat & Jetty》- 极客时间
  2. Tomcat源码分支8.5.x
相关推荐
2401_8576363910 分钟前
计算机课程管理平台:Spring Boot与工程认证的结合
java·spring boot·后端
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
2401_857610035 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端
代码小鑫5 小时前
A027-基于Spring Boot的农事管理系统
java·开发语言·数据库·spring boot·后端·毕业设计
颜淡慕潇6 小时前
【K8S问题系列 | 9】如何监控集群CPU使用率并设置告警?
后端·云原生·容器·kubernetes·问题解决
独泪了无痕6 小时前
WebStorm 如何调试 Vue 项目
后端·webstorm
怒放吧德德8 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端
代码小鑫8 小时前
A025-基于SpringBoot的售楼管理系统的设计与实现
java·开发语言·spring boot·后端·毕业设计
前端SkyRain8 小时前
后端SpringBoot学习项目-项目基础搭建
spring boot·后端·学习
梦想画家8 小时前
理解Rust 生命周期、所有权和借用机制
开发语言·后端·rust