Tomcat的系统架构简介
1.Servlet
在介绍Tomcat之前,我们先来看下Servlet。Servlet这个词,对于我们从事Javaweb开发的小伙伴肯定并不陌生,可能我们写的第一个web程序就是用Servlet做了一个Hello world。那么问题来了,Servlet是什么?可能并不是所有的小伙伴能在第一时间回答出来。为了更加形象的说明这个问题,我们以实际的例子来说明下。
我们通过浏览器访问一个网站,相当于发送一个http请求到http服务器,如果我们请求的是一个静态资源,比如一个图片或者html,那么http服务器只要找到相应的文件目录,就可以给浏览器做出相应;但是如果我们请求的是一个动态数据,比如是商品的库存,显示,库存并不是一个静态的资源,需要我们调用相应的方法(caclNum())来处理,把处理结果返回给http服务器,http服务器再响应给浏览器。那么问题来了,http服务器怎么知道要调用caclNum()这个方法呢?第一种方式:我们可以修改http服务器的源码,用最笨的if else 逻辑判断,根据不同的Url来调用不同的方法。可以想象一下,如果有1万个方法,就要写1万个逻辑判断,这样的耦合性就太高了,并且编码的效率也很低,没修改或者增加一个功能,既要修改程序源码,也要修改http服务器的源码。为了解决耦合问题,根据面向接口编程的思想, Servlet 接口应运而生,为了简化,我们一般把实现了 Servlet 接口的业务类叫作 Servlet。虽然出现了Servlet,但是http服务器还是不知道应该请求哪个Servlet,所以又产生了Servlet容器,用来加载和管理业务类。这样,HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器去处理,Servlet 容器会将请求转发到具体的 Servlet,如果这个 Servlet 还没创建,就加载并实例化这个 Servlet,然后调用这个 Servlet 的接口方法。从上面的叙述我们可以得出: Servlet 接口其实是 Servlet 容器跟具体业务类之间的接口。Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范。Tomcat 就是按照 Servlet 规范的要求实现了 Servlet 容器,同时也具有 HTTP 服务器的功能。在编码的时候,如果我们要实现新的业务功能,只需要实现一个 Servlet,并把它注册到 Tomcat(Servlet 容器)中,剩下的事情就由 Tomcat 帮我们处理了。
2.Tomcat整体架构
1.总括
通过上面的分析我们知道,Tomcat要想给客户端响应动态的内容,需要具备功能:
- 作为Http服务器:处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化;
- 作为Servlet的容器:加载和管理 Servlet,以及具体处理 Request 请求。
在Tomcat实际的架构设计上,也是基于上面这两个功能来划分的,设计了两个大的组件,分别是连接器(Connector)和容器(Container)。连接器负责对外交流,容器负责内部处理。
2.Tomcat的组件-连接器(Connector)
谈到连接,就不可避免的要说到IO。那么我们以Tomcat8为例,来说一下Tomcat支持的IO模型以及应用层协议。
1.IO模型及应用层协议
IO模型 | 说明 |
---|---|
NIO | 非阻塞 I/O,默认使用,采用 Java NIO 类库实现 |
NIO.2 | 异步 I/O,采用 JDK 7 最新的 NIO.2 类库实现 |
APR | 采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库 |
协议 | 说明 |
---|---|
HTTP/1.1 | 大部分 Web 应用采用的访问协议 |
AJP | 用于和 Web 服务器集成(如 Apache) |
HTTP/2 | HTTP 2.0 大幅度的提升了 Web 性能 |
2.连接器与Servlet容器的总体关系
Tomcat作为是作为一个整体对外提供服务的,单独的连接器或者Servlet容器是无法对外服务的,所以需要把连接器和Servlet整个在一起,组装完成后的组件,我们成为service组件。Service 只是在连接器和容器外面多包了一层,把它们组装在一起,它本身并没有其他功能。一个Tomcat可以有多个Service 组件,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。同时,一个Service组件可以有多个连接器连接Servlet容器,即一个容器可以对应多个连接器。
3.连接器的执行过程
通过上面的流程图我们看出,连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。其中主要涉及三大块内容:网络通信、字节流的数据解析和对象转化。根据这三个核心内容,Tomcat设计了三个组件,来实现这 3 个功能,分别是 Endpoint、Processor 和 Adapter。
4.连接器的三大组件
连接器模块用三个核心组件:Endpoint、Processor 和 Adapter 来分别做三件事情,其中 Endpoint 和 Processor 放在一起抽象成了 ProtocolHandler 组件。
1.ProtocolHandler 组件
ProtocolHandler 来处理网络连接和应用层协议,包含了 2 个重要部件:Endpoint 和 Processor。我这里以Http11NioProtocol.java来说明一下类之间的继承关系:
1.ProtocolHandler 组件之Endpoint
Endpoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 Endpoint 是用来实现 TCP/IP 协议的。Endpoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。
2.ProtocolHandler 组件之Processor
Processor 用来实现 HTTP 协议,Processor 接收来自 Endpoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AjpProcessor、Http11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。
2.Adapter 组件
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来“存放”这些请求信息。ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 通过引入 CoyoteAdapter,连接器调用 CoyoteAdapter 的 sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 方法。
3.Tomcat的组件-Servlet容器
1.Tomcat容器的层次结构
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。关于它的层次结构,我们以xml的配置来说明一下。
<Server> <!--顶层组件,可以包括多个Service -->
<Service> <!--顶层组件,可以包含一个Engine,多个连接器 -->
<Connector> <!--连接器组件,负责处理网路通信 -->
</Connector>
<Engine> <!--容器顶层组件,一个Engine组件处理Service中的所有请求,可以包含多个Host -->
<Host> <!--容器组件,处理特定Host下客户请求,可以包含多个Context -->
<Context> <!--容器组件,为特定的Web应用处理所有客户请求,可以包含多个Servlet,一个Context就表示一个Web应用程序-->
<Wrapper> <!--容器组件,处理特定Servlet请求,一个Wrapper代表一个Servlet-->
</Wrapper>
</Context>
</Host>
</Engine>
</Service>
</Server>
Context 表示一个 Web 应用程序;Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
那么到这里我们思考两个个问题:
1.Tomcat 内的 Context 组件跟 Servlet 规范中的 ServletContext 接口有什么区别?跟 Spring 中的 ApplicationContext 又有什么关系?
答:a. Servlet规范中ServletContext表示web应用的上下文环境,而web应用对应tomcat的概念是Context,所以ServletContext是tomcat的Context具体实现的一个成员变量。
b. 在tomcat内部,ServletContext对应tomcat实现是org.apache.catalina.core.ApplicationContext,Context容器对应tomcat实现是org.apache.catalina.core.StandardContext。ApplicationContext是StandardContext的一个成员变量。
c. Spring的ApplicationContext,tomcat启动过程中ContextLoaderListener会监听到容器初始化事件,它的contextInitialized方法中,Spring会初始化全局的Spring根容器ApplicationContext,初始化完毕后,Spring将其存储到ServletContext中。
即,Servlet规范中ServletContext是tomcat的Context实现的一个成员变量,而Spring的ApplicationContext是Servlet规范中ServletContext的一个属性。
2.我们知道,在filter中没有办法直接使用@autowired的方式注入bean(filter是tomcat管理,bean是spring容器管理,filter先于spring bean初始化),但是为什么在filter的init方法中使用applicationContext.getBean的方式就可以获取了呢?
答:这是因为初始化Filter时,Spring的容器已经初始化好了。在Tomcat的StandardContext组件的启动方法startInternal中,有关键的三步,定义了启动的顺序:
1. 调用启动事件监听器ServletContextListener:
listenerStart() ->这里创建了Spring容器
2.始化Filter:
filterStart()
3.初始化配置了"load on startup"的Servlet
loadOnStartup()