Tomcat系统架构浅析

news/2024/11/29 22:39:46/

大家好,我是易安!

今天咱们就来一步一步分析Tomcat的设计思路,看看Tomcat的设计者们是如何设计一个复杂系统,怎么设计顶层模块,以及模块之间的关系。

Tomcat总体架构

我们知道如果要设计一个系统,首先是要了解需求。Tomcat有2个核心功能:

  • 处理Socket连接,负责网络字节流与Request和Response对象的转化。

  • 加载和管理Servlet,以及具体处理Request请求。

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

所以连接器和容器可以说是Tomcat架构里最重要的两部分,需要花费大量精力理解清楚。

在开始讲连接器前,我先讲讲Tomcat支持的多种I/O模型和应用层协议。

Tomcat支持的I/O模型有:

  • NIO:非阻塞I/O,采用Java NIO类库实现。

  • NIO.2:异步I/O,采用JDK 7最新的NIO.2类库实现。

  • APR:采用Apache可移植运行库实现,是C/C++编写的本地库。

Tomcat支持的应用层协议有:

  • HTTP/1.1:这是大部分Web应用采用的访问协议。

  • AJP:用于和Web服务器集成(如Apache)。

  • HTTP/2:HTTP 2.0大幅度的提升了Web性能。

Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service组件。这里请你注意,Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。

到此我们得到这样一张关系图:

alt

从图上你可以看到,最顶层是Server,这里的Server指的就是一个Tomcat实例。一个Server中有一个或者多个Service,一个Service中有多个连接器和一个容器。连接器与容器之间通过标准的ServletRequest和ServletResponse通信。

连接器

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

我们可以把连接器的功能需求进一步细化,比如:

  • 监听网络端口。

  • 接受网络连接请求。

  • 读取网络请求字节流。

  • 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象。

  • 将Tomcat Request对象转成标准的ServletRequest。

  • 调用Servlet容器,得到ServletResponse。

  • 将ServletResponse转成Tomcat Response对象。

  • 将Tomcat Response转成网络字节流。

  • 将响应字节流写回给浏览器。

需求列清楚后,我们要考虑的下一个问题是,连接器应该有哪些子模块?优秀的模块化设计应该考虑 高内聚、低耦合

  • 高内聚 是指相关度比较高的功能要尽可能集中,不要分散。

  • 低耦合 是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。

通过分析连接器的详细功能列表,我们发现连接器需要完成3个 高内聚 的功能:

  • 网络通信。

  • 应用层协议解析。

  • Tomcat Request/Response与ServletRequest/ServletResponse的转化。

因此Tomcat的设计者设计了3个组件来实现这3个功能,分别是Endpoint、Processor和Adapter。

组件之间通过抽象接口交互。这样做还有一个好处是 封装变化。 这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。

网络通信的I/O模型是变化的,可能是非阻塞I/O、异步I/O或者APR。应用层协议也是变化的,可能是HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。

但是整体的处理逻辑是不变的,Endpoint负责提供字节流给Processor,Processor负责提供Tomcat Request对象给Adapter,Adapter负责提供ServletRequest对象给容器。

如果要支持新的I/O方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。

由于I/O模型和应用层协议可以自由组合,比如NIO + HTTP或者NIO.2 + AJP。Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫ProtocolHandler的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol和AjpNioProtocol。

除了这些变化点,系统也存在一些相对稳定的部分,因此Tomcat设计了一系列抽象基类来 封装这些稳定的部分,抽象基类AbstractProtocol实现了ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。下面我整理一下它们的继承关系。

alt

通过上面的图,你可以清晰地看到它们的继承和层次关系,这样设计的目的是尽量将稳定的部分放到抽象基类,同时每一种I/O模型和协议的组合都有相应的具体实现类,我们在使用时可以自由选择。

小结一下,连接器模块用三个核心组件:Endpoint、Processor和Adapter来分别做三件事情,其中Endpoint和Processor放在一起抽象成了ProtocolHandler组件,它们的关系如下图所示。

alt

下面我来详细介绍这两个顶层组件ProtocolHandler和Adapter。

ProtocolHandler组件

由上文我们知道,连接器用ProtocolHandler来处理网络连接和应用层协议,包含了2个重要部件:Endpoint和Processor,下面我来详细介绍它们的工作原理。

  • Endpoint

Endpoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此Endpoint是用来实现TCP/IP协议的。

Endpoint是一个接口,对应的抽象实现类是AbstractEndpoint,而AbstractEndpoint的具体子类,比如在NioEndpoint和Nio2Endpoint中,有两个重要的子组件:Acceptor和SocketProcessor。

其中Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在run方法里调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor)

  • Processor

如果说Endpoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自Endpoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。

Processor是一个接口,定义了请求的处理等方法。它的抽象实现类AbstractProcessor对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有AjpProcessor、Http11Processor等,这些具体实现类实现了特定协议的解析方法和请求处理方式。

我们再来看看连接器的组件图:

alt

从图中我们看到,Endpoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池去处理,SocketProcessor的run方法会调用Processor组件去解析应用层协议,Processor通过解析生成Request对象后,会调用Adapter的Service方法。到这里我们学习了ProtocolHandler的总体架构和工作原理

Adapter组件

前面说过,由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的service方法。

讲完了Tomcat的核心组件:连接器,下面我们紧接着来看下另外一个核心组件和容器

容器,顾名思义就是用来装载东西的器具,在Tomcat里,容器就是用来装载Servlet的。那Tomcat的Servlet容器是如何设计的呢?

容器

Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。比如下图,你就能看出他们的关系:

alt

你可能会问,为什么要设计成这么多层次的容器,这不是增加了复杂度吗?其实这背后的考虑是, Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。

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

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

alt

那么,Tomcat是怎么管理这些容器的呢?你会发现这些容器具有父子关系,形成一个树形结构,你可能马上就想到了设计模式中的组合模式。没错,Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。Container接口定义如下:

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等方法。你可能还注意到Container接口扩展了Lifecycle接口,Lifecycle接口用来统一管理各组件的生命周期。

请求定位Servlet的过程

你可能好奇,设计了这么多层次的容器,Tomcat是怎么确定请求是由哪个Wrapper容器里的Servlet来处理的呢?答案是,Tomcat是用Mapper组件来完成这个任务的。

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

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

读到这里你可能感到有些抽象,接下来我通过一个例子来解释这个定位的过程。

假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个Tomcat上,为了隔离它们的访问域名,配置了两个虚拟域名: manage.shopping.comuser.shopping.com,网站管理人员通过 manage.shopping.com 域名访问Tomcat去管理用户和商品,而用户管理和商品管理是两个单独的Web应用。终端客户通过 user.shopping.com 域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的Web应用。

针对这样的部署,Tomcat会创建一个Service组件和一个Engine容器组件,在Engine容器下创建两个Host子容器,在每个Host容器下创建两个Context子容器。由于一个Web应用通常有多个Servlet,Tomcat还会在每个Context容器里创建多个Wrapper子容器。每个容器都有对应的访问路径,可以通过下面这张图来帮助理解。

alt

假如有用户访问一个URL,比如图中的 http://user.shopping.com:8080/order/buy,Tomcat如何将这个URL定位到一个Servlet呢?

1根据协议和端口号选定Service和Engine。

我们知道Tomcat的每个连接器都监听不同的端口,比如Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口。上面例子中的URL访问的是8080端口,因此这个请求会被HTTP连接器接收,而一个连接器是属于一个Service组件的,这样Service组件就确定了。我们还知道一个Service组件里除了有多个连接器,还有一个容器组件,具体来说就是一个Engine容器,因此Service确定了也就意味着Engine也确定了。

2根据域名选定Host。

Service和Engine确定后,Mapper组件通过URL中的域名去查找相应的Host容器,比如例子中的URL访问的域名是 user.shopping.com,因此Mapper会找到Host2这个容器。

3根据URL路径找到Context组件。

Host确定以后,Mapper根据URL的路径来匹配相应的Web应用的路径,比如例子中访问的是 /order,因此找到了Context4这个Context容器。

4根据URL路径找到Wrapper(Servlet)。

Context确定后,Mapper再根据 web.xml 中配置的Servlet映射路径来找到具体的Wrapper和Servlet。

看到这里,我想你应该已经了解了什么是容器,以及Tomcat如何通过一层一层的父子容器找到某个Servlet来处理请求。需要注意的是,并不是说只有Servlet才会去处理请求,实际上这个查找路径上的父子容器都会对请求做一些处理。连接器中的Adapter会调用容器的Service方法来执行Servlet,最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,会把请求传给自己子容器Host继续处理,依次类推,最后这个请求会传给Wrapper容器,Wrapper会调用最终的Servlet来处理。那么这个调用过程具体是怎么实现的呢?答案是使用Pipeline-Valve管道。

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

Valve表示一个处理点,比如权限认证和记录日志。如果你还不太理解的话,可以来看看Valve和Pipeline接口中的关键方法。

public interface Valve {
  public Valve getNext();
  public void setNext(Valve valve);
  public void invoke(Request request, Response response)
}

由于Valve是一个处理点,因此invoke方法就是来处理请求的。注意到Valve中有getNext和setNext方法,因此我们大概可以猜到有一个链表将Valve链起来了。请你继续看Pipeline接口:

public interface Pipeline extends Contained {
  public void addValve(Valve valve);
  public Valve getBasic();
  public void setBasic(Valve valve);
  public Valve getFirst();
}

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

每一个容器都有一个Pipeline对象,只要触发这个Pipeline的第一个Valve,这个容器里Pipeline中的Valve就都会被调用到。但是,不同容器的Pipeline是怎么链式触发的呢,比如Engine中Pipeline需要调用下层容器Host中的Pipeline。

这是因为Pipeline中还有个getBasic方法。这个BasicValve处于Valve链表的末端,它是Pipeline中必不可少的一个Valve,负责调用下层容器的Pipeline里的第一个Valve。我还是通过一张图来解释。

alt

整个调用过程由连接器中的Adapter触发的,它会调用Engine的第一个Valve:

// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

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

你可能会问,Filter似乎也有相似的功能,那Valve和Filter有什么区别吗?它们的区别是:

  • Valve是Tomcat的私有机制,与Tomcat的基础架构/API是紧耦合的。Servlet API是公有的标准,所有的Web容器包括Jetty都支持Filter机制。

  • 另一个重要的区别是Valve工作在Web容器级别,拦截所有应用的请求;而Servlet Filter工作在应用级别,只能拦截某个Web应用的所有请求。如果想做整个Web容器的拦截器,必须通过Valve来实现。

总结

  • Tomcat的整体架构包含了两个核心组件连接器和容器。连接器负责对外交流,容器负责内部处理。连接器用ProtocolHandler接口来封装通信协议和I/O模型的差异,ProtocolHandler内部又分为Endpoint和Processor模块,Endpoint负责底层Socket通信,Processor负责应用层协议解析。连接器通过适配器Adapter调用容器。

  • Tomcat设计了多层容器是为了灵活性的考虑,灵活性具体体现在一个Tomcat实例(Server)可以有多个Service,每个Service通过多个连接器监听不同的端口,而一个Service又可以支持多个虚拟主机。一个URL网址可以用不同的主机名、不同的端口和不同的路径来访问特定的Servlet实例。

  • 请求的链式调用是基于Pipeline-Valve责任链来完成的,这样的设计使得系统具有良好的可扩展性,如果需要扩展容器本身的功能,只需要增加相应的Valve即可

  • 通过对Tomcat整体架构的学习,我们可以得到一些设计复杂系统的基本思路。首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。

本文由 mdnice 多平台发布


http://www.ppmy.cn/news/112742.html

相关文章

济南认证ISO9001审核中 常见问题点大汇总太实用了

iso认证的目标是企业的质量体系,评价的重点是操作过程。这鼓励组织提高产品和服务的质量,并有助于减少浪费和客户投诉。鼓励内部沟通,提高员工士气。iso认证用于证明组织有能力提供满足客户要求和适用法规要求的产品,以提高客户满…

聊聊部署在K8S的项目如何获取客户端真实IP

前言 最近部门有个需求,需要对一些客户端IP做白名单,在白名单范围内,才能做一些业务操作。按我们的部门的一贯做法,我们会封装一个client包,提供给业务方使用。(注: 我们的项目是运行在K8S上&a…

【面试】Java面试频繁问到的题最新整理(附答案)

文章目录 一、Java基础部分面试题1.1. Java面向对象的三个特征1.2. Java中基本的数据类型有哪些 以及他们的占用字节1.3. int和Integer的区别1.4. String、StringBuilder、StringBuffer的区别及使用场景1.5. ArrayList、Vector和LinkedList的区别及使用场景1.6. Collection和Co…

idea统计代码量

点击File->setting->Plugins搜索Statistic ,之后install,再重启idea,之后在idea编辑器左下角有个Statistic,点击/Refresh 会显示 由于要写其他更加重要的博客,这一篇快速总结

【VS】使用VS查看源代码

VS2017,VS2019都可用 工具--选项--文本编译器--C#--高级--勾选支持导航到反编译源 或 直接在选项对话框的搜索栏搜索反编译快速定义到选项 查看源代码第一次,弹框选择是即可

idea 下查看项目代码量、行数

1、需要下载Statistic插件 1)idea 版本 ultimate 2020.1 2)打开File/Settings,选择Plugins,搜索Statistic,右上角选择install,结果如下: 3)重启idea 4)底部查看&#xff…

分布式电源选址定容与优化配置MATLAB程序基于多目标粒子群算法

分布式电源选址定容与优化配置MATLAB程序基于多目标粒子群算法 (1)该程序为基于多目标粒子群算法的分布式电源优化配置与选址定容程序,期刊论文源程序,配有该论文。 (2)本程序可有效配置分布式电源容量与安…

在c语言程序中整型常量不能表示的数制是,C源程序中不能表示的数制有哪些

C源程序中不能表示的数制有哪些 发布时间:2020-07-08 10:25:25 来源:亿速云 阅读:142 作者:Leah 本篇文章给大家分享的是有关C源程序中不能表示的数制有哪些,小编觉得挺实用的,因此分享给大家学习&#xff…