《深入拆解Tomcat&Jetty》总结二:Tomcat系统架构

3 Tomcat系统架构


两个核心功能:

处理Socket连接,负责网络字节流与Request和Response对象的转化。
加载和管理Servlet,以及具体处理Request请求、回送Response响应。

对应的总体架构:

Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。
连接器负责对外交流,容器负责内部处理。

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性能。

Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个
门。
但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作
Service组件。
Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。
Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑,通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用:https://blog.csdn.net/hunhun1122/article/details/80654805
Tomcat则称为Server:

3.1 连接器的设计

连接器对Servlet容器屏蔽了协议及I/O模型等的区别,无论是HTTP还是AJP,在容器中获取到的都是一个标准的ServletRequest对象。

功能/需求细化:

监听网络端口。
接受网络连接请求。
读取请求网络字节流。
根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的Tomcat Request对象。
将Tomcat Request对象转成标准的ServletRequest。
调用Servlet容器,得到ServletResponse。
将ServletResponse转成Tomcat Response对象。
将Tomcat Response转成网络字节流。
将响应字节流写回给浏览器。

连接器应该有哪些子模块?

优秀的模块化设计应该考虑高内聚、低耦合:
高内聚是指相关度比较高的功能要尽可能集中,不要分散。
低耦合是指两个相关的模块要尽可能减少依赖的部分和降低依赖的程度,不要让两个模块产生强依赖。

通过分析连接器的功能需求,发现连接器需要完成3个高内聚的功能:
网络通信。
应用层协议解析。
Tomcat Request/Response与ServletRequest/ServletResponse的转化。
对应的组件分别为:EndPoint、Processor和Adapter

网络通信的I/O模型是变化的,可能是非阻塞I/O、异步I/O或者APR;
应用层协议也是变化的,可能是HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的;
但是整体的处理逻辑是不变的,如果要支持新的I/O方案或新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。:
EndPoint负责提供字节流给Processor;
Processor负责提供Tomcat Request对象给Adapter;
Adapter负责提供ServletRequest对象给容器。

由于I/O模型和应用层协议可以自由组合,比如NIO + HTTP或者NIO2 + AJP,Tomcat的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫ProtocolHandler的接口来封装这两种变化点。
各种协议和通信模型的组合有相应的具体实现类,比如:Http11NioProtocol和AjpNioProtocol。
除了这些变化点,系统也存在一些相对稳定的部分,因此Tomcat设计了一系列抽象基类来封装这些稳定的
部分;
抽象基类AbstractProtocol实现了ProtocolHandler接口。每一种应用层协议有自己的抽象基类,比如
AbstractAjpProtocol和AbstractHttp11Protocol,具体协议的实现类扩展了协议层抽象基类:

这样设计的目的是尽量将稳定的部分放到抽象基类,同时每一种I/O模型和协议的组合都有相应的具体实现类,在使用时可以自由选择。

小结:连接器模块用三个核心组件:Endpoint、Processor和Adapter来分别做三件事情,其中
Endpoint和Processor放在一起抽象成了ProtocolHandler组件,达到高内聚低耦合的目的:


3.1.1 ProtocolHandler组件


连接器用ProtocolHandler来处理网络连接和应用层协议,包含了2个重要部件:EndPoint和Processor

3.1.1.1 EndPoint


EndPoint是通信端点,即通信监听的接口,是具体的Socket接收和发送处理器,是对传输层的抽象,因此EndPoint是用来实现TCP/IP协议的。

EndPoint是一个接口,它的抽象实现类AbstractEndpoint里面定义了两个内部类:Acceptor和SocketProcessor。

Acceptor用于监听Socket连接请求。

SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里调用协议处理组件Processor进行处理。
为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor),扩展了原生的Java线程池。

3.1.1.2 Processor


如果说EndPoint是用来实现TCP/IP协议的,那么Processor就是用来实现HTTP协议的

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

Processor是一个接口,定义了请求的处理等方法。它的抽象实现类AbstractProcessor对一些协议共有的属
性进行封装,没有对方法进行实现。具体的实现有AJPProcessor、HTTP11Processor等,这些具体实现类实
现了特定协议的解析方法和请求处理方式。

3.1.1.3 小结


细化连接器组件图:

EndPoint接收到Socket连接后,生成一个SocketProcessor任务提交到线程池去处理,SocketProcessor的Run方法会调用Processor组件去解析应用层协议,Processor通过解析生成Request对象后,会交给Adaptor

Tomcat的每个连接器都监听不同的端口,比如Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口,这就是一个容器可能对应多个连接器的原因

Acceptor和SocketProcessor可以类比Netty服务端的boss和worker

3.1.2 Adapter组件


由于协议不同,客户端发过来的请求信息也不尽相同

Tomcat定义了自己的Request类来“存放”这些请求信息。

ProtocolHandler接口负责解析请求并生成Tomcat Request类。
但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。
Tomcat设计者的解决方案是引入CoyoteAdapter,使用了适配器模式,连接器调用CoyoteAdapter
的Service方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,最后再调用容器的Service方法。

3.2 多层容器的设计


3.2.1 容器的层次结构


Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系:

Tomcat通过这种分层的架构,虽然增加了复杂性,但是使得Servlet容器具有很好的灵活性。

Wrapper表示一个Servlet;
Context表示一个Web应用程序,一个Web应用程序中可能会有多个Servlet;
Host代表的是一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下
可以部署多个Web应用程序;
Engine表示引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine。

Tomcat如何管理这些容器的?使用组合模式

所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具
有一致性。
单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。

Container接口扩展了LifeCycle接口,LifeCycle接口用来统一管理各组件的生命周期

3.2.2 请求定位Servlet的过程


Tomcat是怎么确定请求是由哪个Wrapper容器里的Servlet来处理的呢?使用Mapper组件来完成这个任务

Mapper组件的功能:将用户请求的URL定位到一个Servlet

工作原理是:Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里Servlet映射的路径;
可以想象这些配置信息就是一个多层次的Map。

当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里去查找,就能
定位到一个Servlet。
一个请求URL最后只会定位到一个Wrapper容器,也就是一个Servlet。

案例:

通过这种父子容器机制,可以在每层容器都做一些事情:最先拿到请求的是Engine容器,Engine容器对请求做一些处理后,会把请求传给自己子容器Host继续处理,依次类推,最后这个请求会传给Wrapper容器,Wrapper会调用最终的Servlet来处理。
这个调用过程具体是怎么实现的呢?使用Pipeline-Valve管道:

Pipeline-Valve是责任链模式;
责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。

public interface Valve {
   public Valve getNext();
   public void setNext(Valve valve);
   public void invoke(Request request, Response response)//处理请求
}

public interface Pipeline extends Contained {
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}

Valve表示一个处理点,比如权限认证和记录日志

Pipeline中维护了Valve链表,Valve可以插入到Pipeline中,对请求做某些处理;
整个调用链的触发是Valve来完成的,Valve完成自己的处理后,调用getNext.invoke()来触发下一个Valve调用。

每一个容器都有一个Pipeline对象,只要触发这个Pipeline的第一个Valve,这个容器里Pipeline中的Valve就都会被调用到。

不同容器的Pipeline是怎么链式触发的呢,比如Engine中Pipeline如何调用下层容器Host中的Pipeline?
Pipeline中有个getBasic方法,对应的BasicValve处于Valve链表的末端,负责调用下层容器的Pipeline里的第一个Valve:

 

整个调用过程由连接器中的Adapter触发的,它会调用Engine的第一个Valve:

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

Wrapper容器的最后一个Valve会创建一个Filter链,并调用doFilter()方法,最终会调到Servlet的service方
法。(面试:拦截链在哪里调用?)

补充:到业务代码的Controller流程:Wrapper -> Filter -> DispatcherServlet -> Controller

对比Valve和Filter:

Valve是Tomcat的私有机制,与Tomcat的基础架构/API是紧耦合的;
Servlet Filter是公有的标准,所有的Web容器包括Jetty都支持Filter机制。

是Valve工作在Web容器级别,拦截所有应用的请求;
而Servlet Filter工作在应用级别,只能拦截某个Web应用的所有请求。如果想做整个Web容器的拦截器,必须通过Valve来实现。

补充:一般不同的Service用来部署不相关的项目,同一个Service内的多层容器则用来部署相关的项目

问题:Tomcat内的Context组件跟Servlet规范中的ServletContext接口有什么区别?跟Spring中的
ApplicationContext又有什么关系?

Servlet规范中ServletContext表示web应用的上下文环境,而web应用对应tomcat的概念是Context,
所以从设计上,ServletContext自然会成为tomcat的Context具体实现的一个成员变量。

tomcat内部实现也是这样完成的:
ServletContext对应tomcat实现是org.apache.catalina.core.ApplicationContext;
Context容器对应tomcat实现是org.apache.catalina.core.StandardContext;
ApplicationContext是StandardContext的一个成员变量。

Spring的ApplicationContext:在tomcat启动过程中ContextLoaderListener会监听到容器初始化事件,它的contextInitialized方法中,Spring会初始化全局的Spring根容器ApplicationContext,初始化完毕后,Spring将其存储到ServletContext中。

小结:Servlet规范中ServletContext是tomcat的Context实现的一个成员变量,而Spring的ApplicationContext是Servlet规范中ServletContext的一个成员变量。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值