【Tomcat与网络3】Tomcat的整体架构

news2024/12/28 3:48:47

目录

1.演进1:将连接和处理服务分开

2演进2:Container的演进

3 再论Tomcat的容器结构

4 Tomcat处理请求的过程

5 请求的处理过程与Pipeline-Valve管道


在前面我们介绍了Servlet的基本原理,本文我们结合Tomcat来分析一下如何设计一个大型的Web服务器。

说到底,web服务器本质上也就是一台计算机,只不顾这台机器的功能是接收其他机器发来的请求并进行解析,完成相关业务处理,然后将结果返回给目标机器。因此如果你电脑上部署了支付宝系统,我向你这里存钱,你就是我的机器。这台服务器一般要响应很多人、不定时的访问,但是不需要安装微信、抖音等等,所以和我们平时用的电脑相比 ,服务器的工作单一而重复,本身并不肾秘密。

通常情况下,我们通过使用Socket监听服务器指定端口发来实现该功能,一个服务器最基本的结构是:

我们通过start()来启动服务器,例如打开Socket连接以及其他必要的功能,之后就用process()一直监听是否有请求发过来,只要收到请求处理。如果需要关机了,就通过stop()来停止服务器并释放网络资源。

事实上,任何设备都应该具备这么三个功能,但是对于应用服务器,我们需要思考的更多,需要做的也更多。

1.演进1:将连接和处理服务分开

首先,我们的监听和请求处理都在process()里扩展性非常差。假如我们需要接入多种类型的网络,但是要做的事情确是一样的,比如现在很多服务都会支持http和rpc两种对外请求方式,但其实内部干的事情是样的,而适配不同协议的场景更多。

那么我们该如何优化结构来解决这个问题呢?我们可以将协议通信想象为前台,要招待不同类型的客人,而负责具体业务的往往只处理特定的事务,例如程序员只负责写代码,测试人员只负责测试等等。因此,我们可以将结构变成下面的样子:

一个Server可以包含多个Connector和Container,前者专门负责各种类型的网络监听和处理,后者则负责具体要干的事情。两者分别有自己的start()和stop()来加载和释放自己的资源。

但是这里有个明显的缺陷,既然Server可以包含多个Connector和Container,那如何知道某个Connector的请求是来自哪个Container呢?如果要维护这个映射关系太复杂了。

在Tomcat里使用了一个非常灵活的方式:一个Server包含多个Service,这些Service相互独立,只是共享一个JVM以及系统类库。一个Service负责维护多个Connector和一个Container,这样来自Connector的请求就只能由他所属的Container来处理。

在Tomcat里有很多地方都叫Container,这里我们将Container命名为Engine,用于表示整个Servlet引擎,修改后的结果如下:

Connector的功能设计多种IO的问题, 后面单独看,接下来,我们继续看看一下Engine的问题。

2演进2:Container的演进

这一节,我们一起看一下上面的Engine如何进一步做的足够灵活、足够强大。

我们的Web请求是用来管理web服务的,所以收到Connector的请求之后,Engine容器里能够找到一个合适的web应用来处理。如果我们用Context表示一个Web应用则可以绘制出如下的图:

一个应用服务器里又不止一个应用,所以这里Engine与Conext是一对多的关系。

这里为什么给Context也增加start()和stop()方法呢?这个主要为了将加载和卸载资源的过程分解到每个组件中,使组件充分解耦,从而提高服务的灵活性。

上面的结构足够了吗?假如我们有一个服务器,需要为公司各个业务服务。例如一个是news.company.com和order.company.com均由该机器处理,此时我们在该机器上需要运行多个服务,也就是部署多个Tomcat,但是如果我们想在一个Tomcat里部署呢?此时为了提供多域名的方式,我们可以将每个域名视为一个虚拟主机,每个虚拟机下可以包含多个Web应用。所以我们需要给容器再增加一层:

这里的Host表示虚拟主机,一个Host可以包含多个Context。

在Servlet里,一个Web应用可以包含多个Servlet来处理不同的链接请求,因此还需要一个组件来管理Servlet。在Tomcat里,Servlet定义被称为Wrapper,因此我们可以继续修改设计图:

右下方这四个结构其实都是具体处理用户请求的,有很多共性功能,所以我们可以统称为容器Container,然后让Engine、Host、Context、Wrapper都继承该类。这也是为什么在Tomcat里经常看到各种各样的容器,其实就因为这个。

3 再论Tomcat的容器结构

总结一下,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,其他组件按照一定的格式要求配置在这个顶层容器中。

那么,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 接口用来统一管理各组件的生命周期,我们后面再看。

4 Tomcat处理请求的过程

看到这里,Tomcat设计了这么多层次的容器,那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 子容器。每个容器都有对应的访问路径,你可以通过下面这张图来帮助你理解。

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

首先,根据协议和端口号选定 Service 和 Engine。

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

然后,根据域名选定 Host。

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

之后,根据 URL 路径找到 Context 组件。

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

最后,根据 URL 路径找到 Wrapper(Servlet)。

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

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

5 请求的处理过程与Pipeline-Valve管道

在上面连接器中的 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。我还是通过一张图来解释。

          整个调用过程由连接器中的 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 容器的层次结构、根据请求定位 Servlet 的过程,以及请求在容器中的调用过程。Tomcat 设计了多层容器是为了灵活性的考虑,灵活性具体体现在一个 Tomcat 实例(Server)可以有多个 Service,每个 Service 通过多个连接器监听不同的端口,而一个 Service 又可以支持多个虚拟主机。一个 URL 网址可以用不同的主机名、不同的端口和不同的路径来访问特定的 Servlet 实例。

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

总结

今天我们学习了 Tomcat 容器的层次结构、根据请求定位 Servlet 的过程,以及请求在容器中的调用过程。Tomcat 设计了多层容器是为了灵活性的考虑,灵活性具体体现在一个 Tomcat 实例(Server)可以有多个 Service,每个 Service 通过多个连接器监听不同的端口,而一个 Service 又可以支持多个虚拟主机。一个 URL 网址可以用不同的主机名、不同的端口和不同的路径来访问特定的 Servlet 实例。

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

上面还有很多内容可以研究,今天我们先学习这些。

     参考:

本文不少内容参考了李号双老师的文章!                                               

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1421977.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小程序真机调试websocket错误码1006

有心跳机制 模拟器上都没问题连接稳定 一到真机就30秒断连 怎么解决救救我T_T

Flink CEP实现10秒内连续登录失败用户分析

1、什么是CEP? Flink CEP即 Flink Complex Event Processing,是基于DataStream流式数据提供的一套复杂事件处理编程模型。你可以把他理解为基于无界流的一套正则匹配模型,即对于无界流中的各种数据(称为事件),提供一种组合匹配的…

C#,欧拉数(Eulerian Number)的算法与源代码

1 欧拉数 欧拉数特指 Eulerian Number,不同于 Euler numbers,Eulers number 哦。 组合数学中,欧拉数(Eulerian Number)是从1到n中正好满足m个元素大于前一个元素(具有m个“上升”的排列)条件的…

C++ 之LeetCode刷题记录(二十三)

😄😊😆😃😄😊😆😃 开始cpp刷题之旅。 目标:执行用时击败90%以上使用 C 的用户。 118. 杨辉三角 给定一个非负整数 numRows,生成「杨辉三角」的前 numRows …

力扣hot100 括号生成 递归回溯 超简洁版

Problem: 22. 括号生成 Code 使用 static 会被复用 class Solution {List<String> ans new ArrayList<>();public List<String> generateParenthesis(int n){dfs(n, n, "");return ans;}/*** param l 左括号待补个数* param r 右括号待补个数*…

flask+django基于python的网上美食订餐系统_3lyq1

设计旨在提高顾客就餐效率、优化餐厅管理、提高订单准确性和客户的满意度。本系统采用 Python 语言作为开发语言&#xff0c;采用Django框架及其第三方库和第三方工具来进行开发。该方案分为管理员功能模块&#xff0c;商家功能模块以及用户前后功能模块三部分。开发前期根据用…

方法阻塞的解决方案之一

1、简单使用 一个h一个cpp文件 #pragma once #include <iostream> #include <thread> #include <atomic> #include <chrono> #include <string>class Person {public:struct dog {std::string name;int age;};public:void a(std::atomic<bo…

系统架构设计师-22年-下午题目

系统架构设计师-22年-下午题目 更多软考知识请访问 https://ruankao.blog.csdn.net/ 试题一必答&#xff0c;二、三、四、五题中任选两题作答 试题一 (25分) 说明 某电子商务公司拟升级其会员与促销管理系统&#xff0c;向用户提供个性化服务&#xff0c;提高用户的粘性。…

在线版XD,免费使用,功能全面,设计神器!

Adobe XD是什么软件&#xff1f; Adobe XD软件是一个结合设计和建立原型功能的一站式UX/UI设计平台。在XD软件中&#xff0c;数字团队可以进行移动应用、网页设计和原型制作。与此同时&#xff0c;Adobe XD软件也是一种跨平台设计产品&#xff0c;结合设计和建立原型功能&…

探索世界的奇妙之旅——Google Earth Pro(谷歌地球)软件介绍

Google Earth Pro&#xff08;谷歌地球&#xff09;是一款由谷歌公司开发的地图浏览和虚拟地球软件&#xff0c;为用户提供了全球范围内精确的地理浏览和探索功能。该软件整合了卫星图像、地图、地形、建筑物和3D视图等多种信息&#xff0c;让用户可以深入了解世界各地的地理特…

SQL注入:宽字节注入

SQL注入系列文章&#xff1a; 初识SQL注入-CSDN博客 SQL注入&#xff1a;联合查询的三个绕过技巧-CSDN博客 SQL注入&#xff1a;报错注入-CSDN博客 SQL注入&#xff1a;盲注-CSDN博客 SQL注入&#xff1a;二次注入-CSDN博客 ​SQL注入&#xff1a;order by注入-CSDN博客 …

nacos启动成功,程序连接失败

问题&#xff1a;nacos服务器启动成功后可以访问&#xff0c;但是程序连接却超时 解决&#xff1a;检查端口&#xff0c;2.0以上的版本需要开放的端口一共是三个&#xff01;&#xff01; 8848 9848 9849 找了很久是因为后面两个端口没有开放&#xff0c;原因是因为2.0以上…

STM32F407移植OpenHarmony笔记3

接上一篇&#xff0c;搭建完环境&#xff0c;找个DEMO能跑&#xff0c;现在我准备尝试从0开始搬砖。 首先把/device和/vendor之前的代码全删除&#xff0c;这个时候用hb set命令看不到任何项目了。 /device目录是硬件设备目录&#xff0c;包括soc芯片厂商和board板级支持代码…

Redis内存设置

通过redis-cli进入Redis命令行 redis权限认证命令&#xff1a;auth 查看redis内存使用情况的命令&#xff1a;info memory 查看最大内存命令&#xff1a;config get maxmemory 设置最大内存命令&#xff1a;config set maxmemory 也可以通过redis.conf配置文件修改最大内存…

腾讯云SDK并发调用优化方案

目录 一、概述 二、 网关的使用 2.1 核心代码 三、腾讯云SDK依赖包的改造 一、概述 此网关主要用于协调腾讯云SDK调用的QPS消耗&#xff0c;使得多个腾讯云用户资源能得到最大限度的利用。避免直接使用腾讯云SDK 时&#xff0c;在较大并发情况下导致接口调用异常。网关的工…

【前端-VUE+TS】Vue3组件化-知识补充(六)

一. 动态组件 比如我们现在想要实现了一个功能&#xff1a; 点击一个tab-bar&#xff0c;切换不同的组件显示&#xff1b; 案例截图 这个案例我们可以通过两种不同的实现思路来实现&#xff1a; 方式一&#xff1a;通过v-if来判断&#xff0c;显示不同的组件&#xff1b;方式二…

Flink中StateBackend(工作状态)与Checkpoint(状态快照)的关系

State Backends 由 Flink 管理的 keyed state 是一种分片的键/值存储&#xff0c;每个 keyed state 的工作副本都保存在负责该键的 taskmanager 本地中。另外&#xff0c;Operator state 也保存在机器节点本地。Flink 定期获取所有状态的快照&#xff0c;并将这些快照复制到持…

力扣349两个数的交集

题目连接&#xff1a;349. 两个数组的交集 - 力扣&#xff08;LeetCode&#xff09; 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a; nums1 [1,2,2…

学习嵌入式的第十二天-------二维数组函数的调用和指针的运算

二维数组函数调用 输入设备-------cpu------输出设备 | V 存储器 总线&#xff1a; 总线宽度&#xff1a;32位或64位 &#xff08;1.数据总线2.控制总线3.地址总线&#xff09; 练习&#xff1a; 定义一个二维整型数组&#xff0c;实现一个函数…

风水+起名测算小程序源码系统 带完整的安装代码包以及搭建教程

风水学是中国古老的一门学问&#xff0c;讲究人与自然的和谐&#xff0c;通过调整环境气场来影响人的运势和发展。而起名测算则是根据个人五行属性和命理特点&#xff0c;为其起一个好名字&#xff0c;以助其运势和发展。这两者结合&#xff0c;可以帮助用户更好地了解自己的命…