【Tomcat】Tomcat整体架构及其设计精髓分析(上)
(2)server.xml 的Context标签下配置Context
(3)在 /conf/Catalina/localhost/ 下新建一个mvc2.xml
问:为啥要这么隔离?为什么要这样子设计?一个Tomcat(Server)里面为什么要有多个Service???
【Tomcat】Tomcat整体架构及其设计精髓分析(上)
一、Tomcat整体架构
1. 什么是Tomcat
开源的Java Web 应用服务器,实现了 Java EE(Java Platform Enterprise Edition)的部分技术规范。
总的来说,Tomcat是一款高效、稳定和易于使用的Web服务器。
Tomcat的核心就是:Http服务器 + Servlet容器
左图,就是我们早期刚刚学习Java Web的,是用Servlet的方式表示请求,一个请求就要一个Servlet,所以会创建大量的Servlet!HTTP 服务器直接调用具体业务类,它们是紧耦合的。
再看右图,HTTP 服务器不直接调用业务类,而是把请求交给容器来处理,容器通过 Servlet 接口调用业务类。因此 Servlet 接口和 Servlet 容器的出现,达到了 HTTP 服务器与业务类解耦的目的。
Servlet容器内通过 Servlet定位 -> 加载Servlet(加业务类加载进来) -> Servlet 调用 来实现。
而且如果是Spring框架,它将Servlet做了进一步的封装,我们只要配一个统一的入口——Dispatcherservlet即可。
而 Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范(Servlet接口 + Servlet容器)。Tomcat 按照 Servlet 规范的要求实现了 Servlet 容器,同时也具有 HTTP 服务器的功能。
引入了 Servlet 规范后,你不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心你的业务类是如何被实例化和调用的,因为这些都被 Servlet 规范标准化了,你只要关心怎么实现的你的业务逻辑。
2. Servlet详解
Servlet接口
Servlet 接口定义了下面五个方法
其中最重要是的 service 方法,具体业务类在这个方法里实现处理逻辑。这个方法有两个参数:ServletRequest 和 ServletResponse。
ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。比如 HTTP 协议中的请求和响应就是对应了 HttpServletRequest 和 HttpServletResponse 这两个类。你可以通过 HttpServletRequest 来获取所有请求相关的信息,包括请求路径、Cookie、HTTP 头、请求参数等。而 HttpServletResponse 是用来封装 HTTP 响应的。
接口中还有两个跟生命周期有关的方法 init 和 destroy,这是一个比较贴心的设计,Servlet 容器在加载 Servlet 类的时候会调用 init 方法,在卸载的时候会调用 destroy 方法。我们可能会在 init 方法里初始化一些资源,并在 destroy 方法里释放这些资源。
比如 Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建了自己的 Spring 容器。
ServletConfig 的作用就是封装 Servlet 的初始化参数。你可以在 web.xml 给 Servlet 配置参数,并在程序里通过 getServletConfig 方法拿到这些参数。
Servlet 规范提供了 GenericServlet 抽象类,我们可以通过扩展它来实现 Servlet。Servet 规范还提供了 HttpServlet 来继承 GenericServlet,并且加入了 HTTP 特性。这样我们通过继承 HttpServlet 类来实现自己的 Servlet,只需要重写两个方法:doGet 和 doPost。
Servlet容器工作原理
- 当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来。
- 然后调用 Servlet 容器的 service 方法,Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet。
- 如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化。
- 接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器,HTTP 服务器会把响应发送给客户端。
Servlet代码实现
实现Servlet有两种方式,即 XML的方式 和 注解的方式 。
(1)web.xml+HttpServlet实现类(XML)
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.tuling.servletdemo.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
(2) @WebServlet + HttpServlet实现类(注解)
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("<h1>hello world</h1>");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
3. Tomcat的目录结构
- bin
bin目录主要是用来存放tomcat的脚本,如startup.sh , shutdown.sh
- conf
catalina.policy: Tomcat安全策略文件,控制JVM相关权限,具体可以参考java. security.Permission
catalina.properties : Tomcat Catalina行为控制配置文件,比如Common ClassLoader
logging.properties : Tomcat日志配置文件, JDK Logging
server.xml : Tomcat Server配置文件
GlobalNamingResources :全局JNDI资源
context.xml :全局Context配置文件
tomcat-users.xml : Tomcat角色配置文件
web.xml : Servlet标准的web.xml部署文件, Tomcat默认实现部分配置入内:
org.apache.catalina.servlets.DefaultServlet
org.apache.jasper.servlet.JspServlet
- lib
公共类库
- logs
tomcat在运行过程中产生的日志文件
- webapps
用来存放应用程序,当tomcat启动时会去加载webapps目录下的应用程序(放war包的!)
- work
用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件
4. web应用部署的方式(了解)
(1)拷贝到webapps目录下
我们可以先看一下 conf/Catalina 目录中的 server.xml 文件
server.xml:
<Host name="localhost" appBase="webapps" startStopThreads="0"
unpackWARs="true" autoDeploy="true">
里面有一个appBase定义的是“webapps” ,表示webapps这个目录下面的子目录将自动被部署为应用。这个目录下面的 .war 文件将被自动解压缩并部署为应用。(可以是相对路径,也可以是绝对路径)
unpackWARs:表示自动将war包解压。
autoDeploy:表示是否为热部署,即向webapps目录下添加对象,不用重启项目,tomcat会自动帮你部署。(有后台进程去监听该应用)
(2)server.xml 的Context标签下配置Context
一些开发工具(IDEA),部署应用的时候,一般会采用这种方式。
<Context docBase="F:\Resource\mvc\target\mvc-1.0-SNAPSHOT"
path="/mvc" reloadable="true" />
支持热加载,但是不支持热部署 !
path:指定访问该Web应用的URL入口(访问路径)
docBase:指定Web应用的文件路径(项目部署路径),可以给定绝对路径,也可以给定相对于<Host>的appBase 属性的相对路径。
reloadable:热加载(不是部署)如果这个属性设为true,tomcat服务器在运行状态下会监视在WEB-INF/classes和 WEB-INF/lib目录下class文件的改动,如果监测到有class文件被更新的,服务器会自动重新加载 Web应用。
(3)在 /conf/Catalina/localhost/ 下新建一个mvc2.xml
创建mvc2.xml,在localhost下的文件名就是path ,即 path就是/mvc2(访问路径),
<Context docBase="D:\mvc" reloadable="true" />
5. 结合Server.xml理解Tomcat架构
我们可以通过 Tomcat 的 server.xml 配置文件来加深对 Tomcat 架构的理解。
Tomcat 采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是 Server,其他组件按照一定的格式要求配置在这个顶层容器中。
<Server> //顶层组件,可以包括多个Service
<Service> //顶层组件,可包含一个Engine,多个连接器
<Connector/> //连接器组件,代表通信接口
<Engine> //容器组件,一个Engine组件处理Service中的所有请求,包含多个Host
<Host> //容器组件,处理特定的Host下客户请求,可包含多个Context
<Context/> //容器组件,为特定的Web应用处理所有的客户请求
</Host>
</Engine>
</Service>
</Server>
浏览器发起请求,连接器接收,经过3次握手后拿到socket对象,将其解析转发给容器。
Engine(容器)负责转发到Host(主机),Host有多个(不同的IP),每一个Host主机上可以有多个Web应用,所以有多个Context,再到Wrapper。
Tomcat 要实现 2 个核心功能(重点)
- 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
- 加载和管理 Servlet,以及具体处理 Request 请求。
因此 Tomcat 设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。连接器负责对外交流,容器负责内部处理。
二、Tomcat核心组件详解
1. Server 组件
指的就是整个 Tomcat 服务器,包含多组服务(Service),负责管理和启动各个Service,同时监听 8005 端口发过来的 shutdown 命令,用于关闭整个容器 。
2. Service组件
一个Tomcat(Server)里面可以有多个Service。
例如:我们需要启动两个应用,自然是需要2个端口,那么我们可以启动两个Tomcat;或者可以启动一个Tomcat,但是里面可以配置两个Service。
每个 Service 组件都包含了若干用于接收客户端消息的 Connector 组件和处理请求的 Engine 组件。
Service 组件还包含了若干 Executor 组件,每个 Executor 都是一个线程池,它可以为 Service 内所有组件提供线程池执行任务。
问:为啥要这么隔离?为什么要这样子设计?一个Tomcat(Server)里面为什么要有多个Service???
这样做可以实现通过不同的端口号来访问同一台机器上部署的不同应用
Tomcat 支持的多种 I/O 模型和应用层协议。
Tomcat 支持的 I/O 模型有:
- BIO:阻塞式I/O,性能低下,8.5版本之后已经移除
- NIO:非阻塞 I/O,采用 Java NIO 类库实现,Tomcat内部实现了Reactor线程 模型,性能较高
- AIO(NIO2):异步 I/O,采用 JDK 7 最新的NIO2 类库实现。
- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。是从操作系统级别来解决异步的IO问题,大幅度提高了性能。
Tomcat 支持的应用层协议有:
- HTTP/1.1:这是大部分 Web 应用采用的访问协议。
- AJP:用于和 Web 服务器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
Tomcat 为了实现支持多种 I/O 模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门,但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。
Service 本身没有做什么重要的事情, 只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat 内可能有多个Service,这样的设计也是出于灵活性的考虑。
通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用!!!
从上图可以看出,最顶层是 Server,这里的 Server 指的就是一个 Tomcat 实例。一个Server 中有一个或者多个 Service,一个 Service 中有多个连接器和一个容器。
连接器与容器之间通过标准的 ServletRequest 和 ServletResponse 通信。
关于多个Service的好处,ChatGPT的回答:
3. 连接器Connector组件
Tomcat 与外部世界的连接器,监听固定端口接收外部请求,传递给 Container,并将 Container 处理的结果返回给外部。
在连接器内部包含两块内容——ProtocolHandler 和 Adapter
其中 ProtocolHandler 又包含两个部件——EndPoint 和 Processor
功能概述:
- EndPoint: 负责提供字节流给 Processor;
- Processor: 负责提供 Tomcat Request 对象给 Adapter;
- Adapter: 负责提供 ServletRequest 对象给容器。
具体的细节在第三部分论述!
4. 容器Container组件
容器,顾名思义就是用来装载东西的器具,在 Tomcat 里,容器就是用来装载 Servlet的。Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。
Tomcat 设计了 4 种容器,分别是Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而 是父子关系。
- Engine:引擎,Servlet 的顶层容器,用来管理多个虚拟站点(host),一个Service 最多只能有一个 Engine;
- Host:虚拟主机,负责 web 应用的部署和 Context 的创建。可以给Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;
- Context: Web 应用上下文,包含多个 Wrapper,负责 web 配置的解析、管理所有的 Web 资源。一个Context对应一个 Web 应用程序。
- Wrapper:表示一个 Servlet,最底层的容器,是对 Servlet 的封装,负责Servlet 实例的创建、执行和销毁。
三、Tomcat架构设计精髓分析
优秀的模块化设计应该考虑高内聚、低耦合
- 高内聚:是指相关度比较高的功能要尽可能集中,不要分散。
- 低耦合:是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。
接下来我们来研究一下连接器,它会涉及到网络通信。里面有很多关于高内聚、低耦合的设计思想!
1. Connector高内聚低耦合设计
Tomcat的连接器需要实现的功能
- 监听网络端口。
- 接受网络连接请求。
- 读取请求网络字节流。
- 根据具体应用层协议(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。
- EndPoint:负责提供字节流给 Processor
- Processor:负责提供 Tomcat Request 对象给 Adapter
- Adapter:负责提供 ServletRequest 对象给容器
组件之间通过抽象接口交互。这样做的好处是封装变化。
这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度!!!
Connector 变化点:
由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP。
所以这样组合的情况可能有很多,故Tomcat中设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。
Connector 稳定点:
除了这些变化点,系统也存在一些相对稳定的部分,因此 Tomcat 设计了一系列抽象基类来封装这些稳定的部分。
抽象基类 AbstractProtocol 实现了 ProtocolHandler 接口。
每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol 和 AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。
ProtocolHandler
连接器用 ProtocolHandler 来处理网络连接和应用层协议
包含了 2 个重要部件:EndPoint 和 Processor
连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异,ProtocolHandler 内部又分为 EndPoint 和 Processor 模块。
EndPoint 负责底层 Socket 通信,Proccesor 负责应用层协议解析。连接器通过适配器 Adapter 调用容器。
(1)EndPoint
EndPoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint 是用来实现 TCP/IP 协议的。
EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行,而这个线程池叫作执行器(Executor)。
(2)Processor
Processor 用来实现 HTTP/AJP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。
Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。
EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。
Adapter
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来“存放”这些请求信息。ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service 方法。
设计复杂系统的基本思路:
首先要分析需求,根据高内聚低耦合的原则确定子模块,然后找出子模块中的变化点和不变点,用接口和抽象基类去封装不变点,在抽象基类中定义模板方法,让子类自行实现抽象方法,也就是具体子类去实现变化点。