Tomcat 架构原理解析到架构设计借鉴(上)

简介: Tomcat 发展这么多年,已经比较成熟稳定。在如今『追新求快』的时代,Tomcat 作为 Java Web 开发必备的工具似乎变成了『熟悉的陌生人』,难道说如今就没有必要深入学习它了么?学习它我们又有什么收获呢?

静下心来,细细品味经典的开源作品 。提升我们的「内功」,具体来说就是学习大牛们如何设计、架构一个中间件系统,并且让这些经验为我所用。


美好的事物往往是整洁而优雅的。但这并不等于简单,而是要将复杂的系统分解成一个个小模块,并且各个模块的职责划分也要清晰合理。


与此相反的是凌乱无序,比如你看到城中村一堆互相纠缠在一起的电线,可能会感到不适。维护的代码一个类几千行、一个方法好几百行。方法之间相互耦合糅杂在一起,你可能会说 what the f*k!


学习目的


掌握 Tomcat 架构设计与原理提高内功


宏观上看


Tomcat 作为一个 「Http 服务器 + Servlet 容器」,对我们屏蔽了应用层协议和网络通信细节,给我们的是标准的 RequestResponse 对象;对于具体的业务逻辑则作为变化点,交给我们来实现。我们使用了SpringMVC 之类的框架,可是却从来不需要考虑 TCP 连接、 Http 协议的数据处理与响应。就是因为 Tomcat 已经为我们做好了这些,我们只需要关注每个请求的具体业务逻辑。


微观上看


Tomcat 内部也隔离了变化点与不变点,使用了组件化设计,目的就是为了实现「俄罗斯套娃式」的高度定制化(组合模式),而每个组件的生命周期管理又有一些共性的东西,则被提取出来成为接口和抽象类,让具体子类实现变化点,也就是模板方法设计模式。


当今流行的微服务也是这个思路,按照功能将单体应用拆成「微服务」,拆分过程要将共性提取出来,而这些共性就会成为核心的基础服务或者通用库。「中台」思想亦是如此。

设计模式往往就是封装变化的一把利器,合理的运用设计模式能让我们的代码与系统设计变得优雅且整洁。


这就是学习优秀开源软件能获得的「内功」,从不会过时,其中的设计思想与哲学才是根本之道。从中借鉴设计经验,合理运用设计模式封装变与不变,更能从它们的源码中汲取经验,提升自己的系统设计能力。


宏观理解一个请求如何与 Spring 联系起来


在工作过程中,我们对 Java 语法已经很熟悉了,甚至「背」过一些设计模式,用过很多 Web 框架,但是很少有机会将他们用到实际项目中,让自己独立设计一个系统似乎也是根据需求一个个 Service 实现而已。脑子里似乎没有一张 Java Web 开发全景图,比如我并不知道浏览器的请求是怎么跟 Spring 中的代码联系起来的。


为了突破这个瓶颈,为何不站在巨人的肩膀上学习优秀的开源系统,看大牛们是如何思考这些问题。


学习 Tomcat 的原理,我发现 Servlet 技术是 Web 开发的原点,几乎所有的 Java Web 框架(比如 Spring)都是基于 Servlet 的封装,Spring 应用本身就是一个 ServletDispatchSevlet),而 Tomcat 和 Jetty 这样的 Web 容器,负责加载和运行 Servlet。如图所示:


image.png


提升自己的系统设计能力


学习 Tomcat ,我还发现用到不少 Java 高级技术,比如 Java 多线程并发编程、Socket 网络编程以及反射等。之前也只是了解这些技术,为了面试也背过一些题。但是总感觉「知道」与会用之间存在一道沟壑,通过对 Tomcat 源码学习,我学会了什么场景去使用这些技术。


还有就是系统设计能力,比如面向接口编程、组件化组合模式、骨架抽象类、一键式启停、对象池技术以及各种设计模式,比如模板方法、观察者模式、责任链模式等,之后我也开始模仿它们并把这些设计思想运用到实际的工作中。


整体架构设计


今天咱们就来一步一步分析 Tomcat 的设计思路,一方面我们可以学到 Tomcat 的总体架构,学会从宏观上怎么去设计一个复杂系统,怎么设计顶层模块,以及模块之间的关系;另一方面也为我们深入学习 Tomcat 的工作原理打下基础。


Tomcat 启动流程:startup.sh -> catalina.sh start ->java -jar org.apache.catalina.startup.Bootstrap.main()


Tomcat 实现的 2 个核心功能:


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


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


所以 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)。连接器负责对外交流,容器负责内部 处理


Tomcat为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。


image.png


  • Server 对应的就是一个 Tomcat 实例。


  • Service 默认只有一个,也就是一个 Tomcat 实例默认一个 Service。


  • Connector:一个 Service 可能多个 连接器,接受不同连接协议。


  • Container: 多个连接器对应一个容器,顶层容器其实就是 Engine。


每个组件都有对应的生命周期,需要启动,同时还要启动自己内部的子组件,比如一个 Tomcat 实例包含一个 Service,一个 Service 包含多个连接器和一个容器。而一个容器包含多个 Host, Host 内部可能有多个 Contex t 容器,而一个 Context 也会包含多个 Servlet,所以 Tomcat 利用组合模式管理组件每个组件,对待过个也想对待单个组一样对待。整体上每个组件设计就像是「俄罗斯套娃」一样。


连接器


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


Tomcat支持的 I/O 模型有:


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


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


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


Tomcat 支持的应用层协议有:


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


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


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


所以一个容器可能对接多个连接器。连接器对 Servlet 容器屏蔽了网络协议与 I/O 模型的区别,无论是 Http 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。


细化连接器的功能需求就是:


  • 监听网络端口。


  • 接受网络连接请求。


  • 读取请求网络字节流。


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


  • Tomcat Request 对象转成标准的 ServletRequest


  • 调用 Servlet容器,得到 ServletResponse


  • ServletResponse转成 Tomcat Response 对象。


  • Tomcat Response 转成网络字节流。


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


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


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


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


我们发现连接器需要完成 3 个高内聚的功能:


  • 网络通信。


  • 应用层协议解析。


  • Tomcat Request/ResponseServletRequest/ServletResponse 的转化。


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


网络通信的 I/O 模型是变化的, 应用层协议也是变化的,但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 ProcessorProcessor负责提供 Tomcat Request 对象给 AdapterAdapter负责提供 ServletRequest对象给容器。

封装变与不变


因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分,抽象基类 AbstractProtocol实现了 ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocolAbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。


这就是模板方法设计模式的运用。


image.png


总结下来,连接器的三个核心组件 EndpointProcessorAdapter来分别做三件事情,其中 EndpointProcessor放在一起抽象成了 ProtocolHandler组件,它们的关系如下图所示。


image.png


ProtocolHandler 组件


主要处理 网络连接应用层协议 ,包含了两个重要部件 EndPoint 和 Processor,两个组件组合形成 ProtocoHandler,下面我来详细介绍它们的工作原理。


EndPoint


EndPoint是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint是用来实现 TCP/IP 协议数据读写的,本质调用操作系统的 socket 接口。


EndPoint是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint的具体子类,比如在 NioEndpointNio2Endpoint中,有两个重要的子组件:AcceptorSocketProcessor


其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor用于处理 Acceptor 接收到的 Socket请求,它实现 Runnable接口,在 Run方法里调用应用层协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。


我们知道,对于 Java 的多路复用器的使用,无非是两步:


  1. 创建一个 Seletor,在它身上注册各种感兴趣的事件,然后调用 select 方法,等待感兴趣的事情发生。


  1. 感兴趣的事情发生了,比如可以读了,这时便创建一个新的线程从 Channel 中读数据。


在 Tomcat 中 NioEndpoint 则是 AbstractEndpoint 的具体实现,里面组件虽然很多,但是处理逻辑还是前面两步。它一共包含 LimitLatchAcceptorPollerSocketProcessorExecutor 共 5 个组件,分别分工合作实现整个 TCP/IP 协议的处理。


  • LimitLatch 是连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。


  • Acceptor跑在一个单独的线程里,它在一个死循环里调用 accept方法来接收新连接,一旦有新的连接请求到来,accept方法返回一个 Channel 对象,接着把 Channel对象交给 Poller 去处理。


  • Poller 的本质是一个 Selector,也跑在单独线程里。Poller在内部维护一个 Channel数组,它在一个死循环里不断检测 Channel的数据就绪状态,一旦有 Channel可读,就生成一个 SocketProcessor任务对象扔给 Executor去处理。


  • SocketProcessor 实现了 Runnable 接口,其中 run 方法中的 getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL); 代码则是获取 handler 并执行处理 socketWrapper,最后通过 socket 获取合适应用层协议处理器,也就是调用 Http11Processor 组件来处理请求。Http11Processor 读取 Channel 的数据来生成 ServletRequest 对象,Http11Processor 并不是直接读取 Channel 的。这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 AsynchronousSocketChannel 和 SocketChannel,为了对 Http11Processor 屏蔽这些差异,Tomcat 设计了一个包装类叫作 SocketWrapper,Http11Processor 只调用 SocketWrapper 的方法去读写数据。


  • Executor就是线程池,负责运行 SocketProcessor任务类,SocketProcessorrun方法会调用 Http11Processor 来读取和解析请求数据。我们知道,Http11Processor是应用层协议的封装,它会调用容器获得响应,再把响应通过 Channel写出。


工作流程如下所示:


image.png


Processor


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


image.png


从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 HttpProcessor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法,方法内部通过 以下代码将请求传递到容器中。


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


Adapter 组件


由于协议的不同,Tomcat 定义了自己的 Request 类来存放请求信息,这里其实体现了面向对象的思维。但是这个 Request 不是标准的 ServletRequest ,所以不能直接使用 Tomcat 定义 Request 作为参数直接容器。


Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapterSevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service方法。


容器


连接器负责外部交流,容器负责内部处理。具体来说就是,连接器处理 Socket 通信和应用层协议的解析,得到 Servlet请求;而容器则负责处理 Servlet请求。


容器:顾名思义就是拿来装东西的, 所以 Tomcat 容器就是拿来装载 Servlet


Tomcat 设计了 4 种容器,分别是 EngineHostContextWrapperServer 代表 Tomcat 实例。


要注意的是这 4 种容器不是平行关系,属于父子关系,如下图所示:


image.png


你可能会问,为啥要设计这么多层次的容器,这不是增加复杂度么?其实这背后的考虑是,Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。因为这里正好符合一个 Host 多个 Context, 一个 Context 也包含多个 Servlet,而每个组件都需要统一生命周期管理,所以组合模式设计这些容器


Wrapper 表示一个 ServletContext 表示一个 Web 应用程序,而一个 Web 程序可能有多个 ServletHost表示一个虚拟主机,或者说一个站点,一个 Tomcat 可以配置多个站点(Host);一个站点( Host) 可以部署多个 Web 应用;Engine 代表 引擎,用于管理多个站点(Host),一个 Service 只能有 一个 Engine


可通过 Tomcat 配置文件加深对其层次关系理解。


<Server port="8005" shutdown="SHUTDOWN"> // 顶层组件,可包含多个 Service,代表一个 Tomcat 实例
  <Service name="Catalina">  // 顶层组件,包含一个 Engine ,多个连接器
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <!-- Define an AJP 1.3 Connector on port 8009 -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />  // 连接器
  // 容器组件:一个 Engine 处理 Service 所有请求,包含多个 Host
    <Engine name="Catalina" defaultHost="localhost">
    // 容器组件:处理指定Host下的客户端请求, 可包含多个 Context
      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
      // 容器组件:处理特定 Context Web应用的所有客户端请求
      <Context></Context>
      </Host>
    </Engine>
  </Service>
</Server>


如何管理这些容器?我们发现容器之间具有父子关系,形成一个树形结构,是不是想到了设计模式中的 组合模式


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


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);
}


我们看到了getParentSetParentaddChildremoveChild等方法,这里正好验证了我们说的组合模式。我们还看到 Container接口拓展了 Lifecycle ,Tomcat 就是通过 Lifecycle 统一管理所有容器的组件的生命周期。通过组合模式管理所有容器,拓展 Lifecycle 实现对每个组件的生命周期管理 ,Lifecycle 主要包含的方法init()、start()、stop() 和 destroy()

相关文章
|
机器学习/深度学习 文字识别 监控
安全监控系统:技术架构与应用解析
该系统采用模块化设计,集成了行为识别、视频监控、人脸识别、危险区域检测、异常事件检测、日志追溯及消息推送等功能,并可选配OCR识别模块。基于深度学习与开源技术栈(如TensorFlow、OpenCV),系统具备高精度、低延迟特点,支持实时分析儿童行为、监测危险区域、识别异常事件,并将结果推送给教师或家长。同时兼容主流硬件,支持本地化推理与分布式处理,确保可靠性与扩展性,为幼儿园安全管理提供全面解决方案。
727 3
|
网络协议 Java 应用服务中间件
框架源码私享笔记(01)Tomcat核心架构功能 | 配置详解
本文首先分享了《活出意义来》一书序言中的感悟,强调成功如同幸福,不是刻意追求就能得到,而是全心投入时的副产品。接着探讨了Tomcat的核心功能与架构解析,包括网络连接器(Connector)和Servlet容器(Container),并介绍了其处理HTTP请求的工作流程。文章还详细解释了Tomcat的server.xml配置文件,涵盖了从顶级容器Server到子组件Connector、Engine、Host、Context等的配置参数及作用,帮助读者理解Tomcat的内部机制和配置方法。
|
安全 算法 网络协议
解析:HTTPS通过SSL/TLS证书加密的原理与逻辑
HTTPS通过SSL/TLS证书加密,结合对称与非对称加密及数字证书验证实现安全通信。首先,服务器发送含公钥的数字证书,客户端验证其合法性后生成随机数并用公钥加密发送给服务器,双方据此生成相同的对称密钥。后续通信使用对称加密确保高效性和安全性。同时,数字证书验证服务器身份,防止中间人攻击;哈希算法和数字签名确保数据完整性,防止篡改。整个流程保障了身份认证、数据加密和完整性保护。
|
机器学习/深度学习 算法 数据挖掘
解析静态代理IP改善游戏体验的原理
静态代理IP通过提高网络稳定性和降低延迟,优化游戏体验。具体表现在加快游戏网络速度、实时玩家数据分析、优化游戏设计、简化更新流程、维护网络稳定性、提高连接可靠性、支持地区特性及提升访问速度等方面,确保更流畅、高效的游戏体验。
420 22
解析静态代理IP改善游戏体验的原理
|
编解码 缓存 Prometheus
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
本期内容为「ximagine」频道《显示器测试流程》的规范及标准,我们主要使用Calman、DisplayCAL、i1Profiler等软件及CA410、Spyder X、i1Pro 2等设备,是我们目前制作内容数据的重要来源,我们深知所做的仍是比较表面的活儿,和工程师、科研人员相比有着不小的差距,测试并不复杂,但是相当繁琐,收集整理测试无不花费大量时间精力,内容不完善或者有错误的地方,希望大佬指出我们好改进!
1474 16
「ximagine」业余爱好者的非专业显示器测试流程规范,同时也是本账号输出内容的数据来源!如何测试显示器?荒岛整理总结出多种测试方法和注意事项,以及粗浅的原理解析!
|
机器学习/深度学习 数据可视化 PyTorch
深入解析图神经网络注意力机制:数学原理与可视化实现
本文深入解析了图神经网络(GNNs)中自注意力机制的内部运作原理,通过可视化和数学推导揭示其工作机制。文章采用“位置-转移图”概念框架,并使用NumPy实现代码示例,逐步拆解自注意力层的计算过程。文中详细展示了从节点特征矩阵、邻接矩阵到生成注意力权重的具体步骤,并通过四个类(GAL1至GAL4)模拟了整个计算流程。最终,结合实际PyTorch Geometric库中的代码,对比分析了核心逻辑,为理解GNN自注意力机制提供了清晰的学习路径。
1013 7
深入解析图神经网络注意力机制:数学原理与可视化实现
|
机器学习/深度学习 缓存 自然语言处理
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
Tiktokenizer 是一款现代分词工具,旨在高效、智能地将文本转换为机器可处理的离散单元(token)。它不仅超越了传统的空格分割和正则表达式匹配方法,还结合了上下文感知能力,适应复杂语言结构。Tiktokenizer 的核心特性包括自适应 token 分割、高效编码能力和出色的可扩展性,使其适用于从聊天机器人到大规模文本分析等多种应用场景。通过模块化设计,Tiktokenizer 确保了代码的可重用性和维护性,并在分词精度、处理效率和灵活性方面表现出色。此外,它支持多语言处理、表情符号识别和领域特定文本处理,能够应对各种复杂的文本输入需求。
1711 6
深入解析Tiktokenizer:大语言模型中核心分词技术的原理与架构
|
存储 机器学习/深度学习 应用服务中间件
阿里云服务器架构解析:从X86到高性能计算、异构计算等不同架构性能、适用场景及选择参考
当我们准备选购阿里云服务器时,阿里云提供了X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器以及高性能计算等多种架构,每种架构都有其独特的特点和适用场景。本文将详细解析这些架构的区别,探讨它们的主要特点和适用场景,并为用户提供选择云服务器架构的全面指南。
1392 18
|
监控 Java 应用服务中间件
Tomcat log日志解析
理解和解析Tomcat日志文件对于诊断和解决Web应用中的问题至关重要。通过分析 `catalina.out`、`localhost.log`、`localhost_access_log.*.txt`、`manager.log`和 `host-manager.log`等日志文件,可以快速定位和解决问题,确保Tomcat服务器的稳定运行。掌握这些日志解析技巧,可以显著提高运维和开发效率。
1862 13
|
消息中间件 人工智能 数据可视化
文生图架构设计原来如此简单之用户界面架构
节点式界面是文生图工具中一种强大而灵活的设计范式,以 ComfyUI 为代表。这种设计将复杂的图像生成过程分解为可视化的模块化组件,使用户能够精确控制生成流程的每个环节。
737 2

推荐镜像

更多
  • DNS