Tomcat 核心组件
![](https://img.haomeiwen.com/i27963539/6a766d8990ebe638.png)
在了解Tomcat各个核心组件的时候强烈建议大家读下源码,之前也看过好多次tomcat的文章,但是基本都是看的时候明白,看过就忘记了。如果伴着源码去读下各个组件能更好的理解并加深记忆,光看博文不实际去动手试试是难以真正理解的。“纸上得来终觉浅,绝知此事要躬行”学习任何东西都是这个道理。
![](https://img.haomeiwen.com/i27963539/298134fdc6acf60d.png)
Tomcat整体的组件架构如上图所示,这个跟server.xml中的配置是保持一致的。顶级组件为Server跟Service,一个Tomcat中只有一个Server,Server下可以启动多个Service。Service下最主要的两大组件Connector跟Container,Connector负责接收请求包装成request、response,而Container负责处理请求返回response。下面我们来从上到下介绍下每个组件。
Server
Server代表完整的Tomcat实例在Java虚拟集中是单例,主要是用来管理容器下各个Serivce组件的生命周期。下图描述了Server组件的关键方面。如图所示,Server实例是通过server.xml配置文件来配置的;其根元素所代表的正是Tomcat实例,默认实现为org.apache.catalina.core.StandardServer。但是,你也可以通过标签的class属性来自定义服务器实现。
![](https://img.haomeiwen.com/i27963539/f4e18f7cc3239463.png)
服务器重要的一方面就是它打开了8005端口(默认端口)来监听关闭服务命令(默认情况下命令为SHUTDOWN)。当收到shutdown命令后,服务器会优雅的关闭自己。同时出于安全考虑,发起关闭请求的连接必须来自同一台机器上的同一个运行中的Tomcat实例。
此外,Server还提供了一个Java命名服务和JNDI服务,可以通过这两个服务可以使用名称来注册专用对象(如数据源配置)。在运行期,单个组件(如Servlet)可以使用对象名称来通过服务器的JNDI绑定服务来查找需要的对象相关信息。
Service
Service将一组Connector组件和Engine关联起来,代表Tomcat中一组请求处理的组件。Service仅仅是一个分组结构,它并不包含任何其他的附加功能。
![](https://img.haomeiwen.com/i27963539/dd520575196c3ec9.png)
一个Service集中了一些连接器,每个连接器监控一个指定的IP及端口并通过指定的协议做出响应。客户端请求首先到达连接器(Connector),连接器在再将这些请求轮流传入Engine中处理,Engine是Tomcat中请求处理的关键组件。上图中展示了HTTP连接器、HTTPS连接以及AJP组件。
一般很少会修改这个元素,默认的Service实例通常就足够使用了。
Connector
Connector是客户端连接到Tomcat容器的服务点,它为引擎提供协议服务来将引擎与客户端各种协议隔离开来,如HTTP、HTTPS、AJP协议。
Connector需要完成的核心工作是:1)网络通信2)应用层协议解析3)Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。为了完成这些事情Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter,其中 Endpoint和 Processor放在一起抽象成了 ProtocolHandler组件。一个请求来了处理的过程如下图所示。
![](https://img.haomeiwen.com/i27963539/013c6bc3d929f876.png)
ProtocolHandler
Tomcat 支持的应用层协议有:
- HTTP/1.1:这是大部分 Web 应用采用的访问协议。
- AJP:用于和 Web 服务器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
Tomcat支持的 I/O 模型有:
- NIO:非阻塞 I/O,采用 Java NIO 类库实现。
- NIO2:异步I/O,采用 JDK 7 最新的 NIO2 类库实现。
- APR:采用 Apache可移植运行库实现,是 C/C++ 编写的本地库。
应用层协议是变化的,I/O模型也是变化的,但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor负责提供 Tomcat Request 对象给 Adapter,Adapter负责提供 ServletRequest对象给容器。Tomcat使用模板方法设计模式,设计了一系列抽象基类来封装这些稳定的部分,抽象基类 AbstractProtocol实现了 ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如 AbstractAjpProtocol和 AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类。
![](https://img.haomeiwen.com/i27963539/abb62e333d8b0773.png)
EndPoint
![](https://img.haomeiwen.com/i27963539/d8287bed5ad405a1.png)
要了解EndPoint可以先看这张图,已NioEndPoint为例,EndPoint中主要包含LimitLatch、Acceptor、Poller、SocketProcessor 和Executor 5 个组件,共同来完成对TCP/IP协议的处理。
LimitLatch是连接控制器,它负责控制最大连接数,NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。
Acceptor线程组,单独线程,死循环,用于接受新连接,并将新连接封装一下,选择一个Poller将SocketChannel添加到 Poller 的事件队列中。
Poller线程组,也是单独线程,死循环。每个Poller线程都有自己的Queue。每个Poller线程可能被多个 Acceptor线程注册PollerEvent。Poller不断的通过内部的Selector对象向内核查询Channel的状态,一旦可读就生成任务类 SocketProcessor 交给 Executor 去处理。Poller 的另一个重要任务是循环遍历检查自己所管理的 SocketChannel是否已经超时,如果有超时就关闭这个SocketChannel。Acceptor与Poller线程之间通过Queue通信,典型的生产者消费者。
SocketProcessor主要是创建相应的Processor,并将SocketWrapperBase对象交由Processor处理。
Executor就是线程池,这个线程池维护的线程就是我们非常熟悉的“http-nio-8080-exec-N”线程,也就是用户请求的实际处理线程。主要负责运行SocketProcessor任务类,以及后续的处理。
Processor
Processor 主要是对应用层协议的抽象。Processor 接收来自 EndPoint 的 SocketWrapper,读取字节流做解析,例如Http11Processor会按照HTTP1.1协议进行数据解析,构建 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理。
Adapter
由于协议的不同,Tomcat 定义了自己的 Request 类来存放请求信息,这里其实体现了面向对象的思维。但是这个 Request 不是标准的 ServletRequest ,所以不能直接使用 Tomcat 定义 Request 作为参数直接容器。
Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service方法。在转换 ServletRequest 的时候会对请求链接进行解析,映射到请求指定的 Host 以及 Context 上(存放在ServletRequest中的MappingData),为了后续传递给容器提供基础。
Container
容器是用于封装和管理Servlet,以及具体处理Request请求。它由四个子容器组件构成,分别是Engine、Host(站点)、Context(应用)、Wrapper(Servlet)。这四个组件不是平行关系,而是父子包含关系。除Engine外都可存在多个。
![](https://img.haomeiwen.com/i27963539/0d0f31eab4428a1b.png)
你可能会问,为啥要设计这么多层次的容器,这不是增加复杂度么?其实这背后的考虑是,Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。因为这里正好符合一个 Host 多个 Context, 一个 Context 也包含多个 Servlet,而每个组件都需要统一生命周期管理,所以使用组合模式设计这些容器。
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);
}
所有容器组件都实现了 Container 接口,因此在使用容器组件的时候具有一致性。我们看到了getParent、SetParent、addChild和 removeChild等方法,这里就是通过组合模式来统一管理所有容器。我们还看到 Container 接口拓展了 Lifecycle ,Tomcat 就是通过 Lifecycle 统一管理所有容器组件的生命周期。
Engine
引擎表示可运行的Catalina的Servlet引擎实例。在一个服务中只能有一个引擎,同时,作为一个真正的容器,Engine元素之下可以包含一个或多个虚拟主机。
作为请求处理的主要组件,它接收Connector传入请求的对象以及输出相应结果。Engine没有复杂的功能,代码也只有400多行,它主要功能是将传入请求委托给适当的虚拟主机处理。
![](https://img.haomeiwen.com/i27963539/d344934f14fe94cf.png)
Host
Host 顾名思义就是虚拟主机。Host 是 Engine 的子容器,一个 Host 在 Engine 中代表一个虚拟主机,这个虚拟主机的作用就是运行应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器是 Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。每个虚拟主机下都可以部署一个或者多个 Web App,每个 Web App 对应于一个 Context,当 Host 获得一个请求时,会将请求转发到某个 Context 来处理。
![](https://img.haomeiwen.com/i27963539/b52bf5b1ca37fb99.png)
像上图所示,不同域名的请求在经过 Engine 后会交由不同的 Host 来处理(如果找不到指定的Host会交由默认的 Host 来处理)。但是现实中为了保证每个服务的稳定性、隔离性等,基本上都是一台机器只会部署一个 Host(localhost),也是默认的 Host。同时如果每个服务都配置相应的域名的话也违背了依赖倒置原则,造成扩展性比较差。
Context
Context它表示就是Web应用程序本身(我们部署在Tomcat中的应用),它具备了 Servlet 运行的基本环境。理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的。
![](https://img.haomeiwen.com/i27963539/71e4ec71742838f9.png)
Wrapper
Wrapper是 Context 容器的子容器,表示一个 Servlet(或者由jsp文件转换而来的servlet)。它之所以称为包装器是因为它包装了java.servlet.Servlet实例。这是容器层次结构的最底层,添加任何子类都会导致异常。
![](https://img.haomeiwen.com/i27963539/7492be23f3b8677c.png)
Wrapper负责它所包装的Servlet的整个生命周期,包括加载、初始化、执行、回收,执行Servlet的方法如init()、service()、destroy()。
其他组件
Valve
注意是Valve不是Value,开始的时候还因为看错迷惑了半天。Valve是处理元素,它可以被包含在每个Tomcat容器的中,负责处理请求,并将请求传递给下层容器。但是对于容器来说,它并不会直接持有Valve的引用,而是会持有一个称作管道Pipeline 的单一实体的引用,并用这个管道来关联的一系列Valve。
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response)
}
public interface Pipeline {
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}
Pipeline+Valve使用的是典型的责任链模式,Pipeline中有addValve方法,维护了Valve链表。Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve完成自己的处理后,调用 getNext.invoke() 来触发下一个 Valve 调用。
Pipeline中还有个 getBasic方法。BasicValve处于 Valve链表的末端,它是 Pipeline中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。
![](https://img.haomeiwen.com/i27963539/bb08a575c71e004d.png)
Tomcat中的请求传递过程如上图所示,Value的机制跟Filter的机制非常像,主要是有以下的区别
- Valve是 Tomcat的私有机制,与 Tomcat 的基础架构 API是紧耦合的。Servlet API是公有的标准,所有的 Web 容器包括 Jetty 都支持 Filter 机制。
- Valve工作在 Web 容器级别,拦截所有应用的请求;而 Servlet Filter 工作在应用级别,只能拦截某个 Web 应用的所有请求。如果想做整个 Web容器的拦截器,必须通过 Valve来实现。
京东年薪60W架构师带你深入拆解Tomcat,Endpoint
Apache Tomcat 8 Configuration Reference
详解Tomcat系列(一)-从源码分析Tomcat的启动
Tomcat系列(4)——Tomcat 组件及架构详细部分
谈谈 Tomcat 请求处理流程