Tomcat架构解析
参考书籍《Tomcat架构解析》
一、Tomcat基础
1. 静态/动态资源
浏览器只能解析静态资源(html,css,js,jpg),而动态资源(例如Servlet,jsp等)需要转换成静态资源再返回给浏览器。
2. Web服务器软件
Tomcat作为一款Web服务器软件,需要兼备 服务器 和 服务器软件 的功能。
具有:接收用户的请求,处理请求,做出响应,同时可以部署Web应用,让用户通过浏览器来访问这些项目。但Tomcat作为一个中小型的JavaEE服务器,仅仅支持少量的JavaEE规范servlet/jsp。
3. Tomcat主要目录结构
bin目录,存放Tomcat启动、停止的脚本
startup.bat ,startup.sh:用于在windows和linux下的启动脚本
shutdown.bat ,shutdown.sh:用于在windows和linux下的停止脚本还有Tomcat的startup启动时,实际调用的Catalina脚本
conf目录,用于存放Tomcat的相关配置文件
Catalina:用于存储针对每个虚拟机的Context配置
context.xml:用于定义所有web应用均需加载的Context配置,如果web应用指定了自己的context.xml,该文件将被覆盖
catalina.properties:Tomcat 的环境变量配置
catalina.policy:Tomcat 运行的安全策略配置
logging.properties:Tomcat 的日志配置文件, 可以通过该文件修改Tomcat 的日志级别及日志路径等
server.xml:Tomcat 服务器的核心配置文件
tomcat-users.xml:定义Tomcat默认的用户及角色映射信息配置
web.xml:Tomcat 中所有应用默认的部署描述文件,主要定义了基础Servlet和MIME映射。
注意:Tomcat就是根据conf
路径下的server.xml
的配置文件信息,来创建的服务器实例。
- lib:Tomcat 服务器的依赖包
- logs:Tomcat 默认的日志存放目录
- webapps:Tomcat 默认的Web应用部署目录
- work:Web 应用JSP代码生成和编译的临时目录
4. Http工作原理
HTTP协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP是基于TCP/IP协议来传递数据的(HTML文件、图片、查询结果等),HTTP协议不涉及数据包(Packet)传输,只是一种规定的通信格式。
浏览器向服务端发出TCP连接请求,服务程序接受浏览器的连接请求,并经过TCP三次握手建立连接。浏览器将请求数据打包成一个HTTP协议格式的数据包,浏览器将该数据包推入网络,数据包经过网络传输,最终达到端服务程序。服务端到客户端亦然,浏览器将HTML文件展示在页面上,则Tomcat的功能步骤就分析出来了。
Tomcat作为一个Web应用服务器,主要是接受连接、解析请求数据、处理请求、发出响应这四个步骤。
二、Tomcat总体架构
1. Servlet容器
容器:是一类组件的总称,例如后面的引擎Engine容器,应用Context容器,作用:处理请求返回响应数据。
Http服务器请求处理的过程:
浏览器发给服务端的是一个HTTP格式的请求,服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的Java类(servlet),一般来说不同的请求需要由不同的Java类(servlet)来处理。
Tomcat不直接调用业务类,而是把请求交给容器来处理,从而达到解耦合的目的。Servlet容器通过Servlet接口调用业务类,Tomcat按照Servlet规范的要求实现了Servlet容器,同时它们也具有HTTP服务器的功能。作为Java程序员,如果我们要实现新的业务功能,只需要实现一个Servlet,并把它注册到Tomcat(Servlet容器)中,剩下的事情就由Tomcat帮我们处理了。
Servlet接口和Servlet容器这一整套规范叫作Servlet规范。
2. Servlet容器工作流程
把请求交给Servlet容器来处理,那么Servlet容器工作步骤为:
- 当客户请求某个资源时,请求信息发送给HTTP服务器一个Request对象数据,然后服务器会用一个ServletRequest对象把客户的请求信息封装起来。
- 调用Servlet容器的service方法,然后根据Web应用配置文件中的请求的Url路径和Servlet的映射关系,找到Servlet,没有就根据反射机制动态创建这个Servlet,调用init方法完成初始化,接着调用servlet的service方法处理请求(根据get,或者post来进行处理),然后将封装好的ServletResponse对象返回给HTTP服务器,然后服务器再返回给客户端Response对象。
综上所述,我们就了解了需求,了解了Tomcat服务器的核心功能:处理连接 和 加载Servlet处理Requst请求
3. Tomcat整体架构
Tomcat要实现两个核心功能:
1) 开启Socket连接,监听服务器端口,负责网络字节流与Request和Response对象的转化。
2) 加载和管理Servlet,以及具体处理Request请求。
为了方便解耦,例如适配多种网络协议但请求处理却相同的时候,Tomcat设计了两个组件来实现这两个功能,连接器(Connector)和容器(Container)。
连接器负责对外交流,处理Socket连接,容器(引擎Engine)负责加载和管理Servlet,和内部逻辑处理。
注意:Tomcat架构的重点便是连接器和容器内部的具体架构和源码分析。
4. Connector和Container
1. Container容器
容器只是个统称,其实下面是继承实现了Engine、Host、Context、Wrapper(Servlet)。Container可以添加并且维护这些个子容器,并且是弱依赖的关系,根据不同级别来加入处理请求的组件,例如8.56之前直接由Service维护一个Context。
Lifecycle:所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期的性质,所以我们又可以进行一次接口抽象。
Tomcat核心的组件默认实现均是继承自LifecycleMBeanBase抽象类,可以进行组件状态转换和维护(自身注册为MBean,通过管理工具维护)。
2. Pipeline和Valve(针对Container组件)
在增强组件灵活性和可扩展性方面,Tomcat采用了职责链模式来实现客户端请求的处理,这也是职责链模式的经典应用场景。
每次请求通过执行一个职责链来完成具体的请求处理,Tomcat定义了Pipeline管道和Valve阀两个接口,前者构建职责链,后者代表职责链上每个处理器。
Pipeline中维护了一个基础的Valve,始终位于末端,负责后序组件管道调用,封装了请求处理和输出响应,然后addValve通过头插法,添加到它之前,每次按照顺序执行。
3. Connector连接器
接下来我们来细化一下连接器这个组件——Connector,连接器的功能包括:
- 监听服务器的端口,例如http协议则为8080端口,读取来自客户端的请求
- 将请求的数据按照对应的指定协议进行解析
- 重点:将请求地址匹配到正确的容器进行处理
- 返回响应
只有这样才能保证接收的客户端请求交由请求地址匹配的容器处理。
- ProtocolHandler协议处理器
在Tomcat中,ProtocolHandler表示一个协议处理器,针对不同的协议和I/O方式,提供了不同的实现,例如Http11NioProtocol表示基于NIO的HTTP协议处理器。
在Connector启动时,Endpoint启动线程来监听服务器端口,接收到请求后调用Processor进行数据读取,根据请求地址映射(Servlet规范定义)通过Mapper实现到具体的容器进行处理。
- CoyoteAdapter:适配器模式下Tomcat默认的Connector实现。
5. Bootstrap和Catalina
1. Tomcat有几个重要的配置文件,其中最核心的就是server.xml
。通过这个文件,可以修改Tomcat组件的配置参数,甚至添加组件,后序展开。
2. Tomcat通过Catalina中的Digester来解析XML文件,包括server.xml
和web.xml
等等,最后提供了Bootstrap作为了应用服务器的启动入口,负责创建Catalina实例,根据Catalina来完成服务器的操作。
注意:Bootstrap在bin目录下,与Tomcat服务器完全的松耦合,通过反射来创建Catalina实例,用于构造整个服务器。好处:Bootstrap实现了启动入口与核心环节的解耦,简化启动(避免各种依赖),更加灵活的组织我们的中间件产品结构。
三、Coyote连接器
Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Servlet容器Catalina提供了统一的接口。
Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而是在Catalina 中将他们进一步封装为ServletRequest 和 ServletResponse。
1. 协议与I/O模型
在Coyote中支持多种应用层协议和I/O模型:
在 8.0 之前,Tomcat 默认采用的I/O方式为 BIO,之后改为 NIO。无论 NIO、NIO2 还是 APR,在性能方面均优于以往的BIO。
Tomcat支持多种协议与I/O,则一个容器对接多个连接器,多个连接器和容器组装后成为一个Service组件,同时Tomcat可以配置多个Service,来使得更加灵活。实现通过不同的端口号来访问同一台机器上部署的不同应用。
2. 连接器组件
连接器各个组件的作用如下图,如二、Tomcat总体架构中的Connector连接器所述:
连接器内部维护一个ProtocolHandler协议处理器和Adapter适配器,来分别进行请求监听和处理和封装request对象成容器能够解析的servletRequest。
1. EndPoint监听点
监听即通信监听,EndPoint作为Coyote的通信端点,即通信监听的端口。
注意:EndPoint是具体Socket接受和发送处理器,是对传输层的一层抽象,用TCP/IP协议来实现的。
Tomcat提供的是抽象类AbstractEndpoint , 里面定义了两个内部类:Acceptor和SocketProcessor。
Acceptor用于监听Socket连接请求,继承了Runnable,本质就是个线程,一直死循环监听连接。
SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里调用协议处理组件Processor进行处理。
为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor)
public abstract static class Acceptor implements Runnable { |
2. Processor处理接口
Coyote的协议处理的接口,如果说EndPoint是出来TCP/IP协议的,那么Processor用来实现Http协议的,是对应用层的抽象。
功能:接收前者的Socket -> 转换成字节流解析成为Request,然后将Requst对象通过适配器Adapter将其提交到容器来处理。
3. ProtocolHandler协议处理器
在第二、Tomcat总体架构中,我们知道了ProtocolHandler是对 EndPoint监听点 和 Processor协议 处理的一层封装,根据协议和I/O提供了实现类:
AjpNioProtocol , AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol , Http11AprProtocol。
一般我们在配置文件server.xml
中进行配置,指定对应的协议名称,默认使用Http11NioProtocol 。
<Connector port="8080" protocol="HTTP/1.1" |
4. Adapter适配器
Tomcat定义了自己的Request类来“存放”各种各样的请求信息,前面讲到的ProtocolHandler解析请求,然后生成Tomcat的Requst类,但是容器中需要的是ServletRequest对象。
Tomcat解决方案:引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的Service方法。
功能:将协议处理器解析出的Tomcat Request对象,适配成ServletRequest,再调用ServletRequest的Service方法,同理容器给的ServletResponse转换成Response。
3. 线程池
Tomcat的线程池,是在连接器Connector部分的用到的,下图是Tomcat线程池的执行流程
监听:
- LimitLatch(类似于JUC里面的信号量Semaphore):顾名思义,是用来限流的,用来控制最大的连接数,防止服务器被压垮。
- Acceptor 只负责 接收新的Socket连接,本质就是个线程,一直死循环执行监听Socket连接。
- Poller也是个线程,也是一直死循环监听SocketChannel通道是否有 可读的I/O事件(现在是服务器端,所以读就是浏览器传数据过来),发现有,则将其封装成任务对象socketProcessor,交给线程池处理。
处理:
- Executor线程池中的 worker工作线程最终负责 处理请求。
四、Catalina容器
Catalina 是Servlet 容器实现,包含了之前讲到的所有的容器组件。
Tomcat 本质上就是一款 Servlet 容器, 因此Catalina 才是 Tomcat 的核心 , 其他模块都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供JSP引擎。其实在Tomcat源码中也有体现:
在java.org.apache包下
1. Catalina结构
Catalina的主要组件结构如下:
Catalina管理这Server,而Server是表示整个服务器的,每个Server下面都有很多个Service服务,每个服务都包含着多个连接器组件Connector(Coyote实现的)和一个容器组件Container。在Tomcat启动时,就会初始化一个Catalina的实例。
总结:所以Servlet容器Catalina = 1个Server(Server下面有多个Service,每个Service下面又有多个连接器Connector + 容器Container)
2. Catalina组件
Catalina容器各个组件的功能:
- Catalina:负责解析Tomcat的配置文件 , 以此来创建服务器Server组件,并根据命令来对其进行管理
- Server:整个Catalina Servlet容器以及其它组件,负责组装并启动Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接口,提供了一种优雅的启动和关闭整个系统的方式(职责链模式)
- Service:服务是Server内部的组件,一个Server包含多个Service。它将若干个Connector组件绑定到一个Container(Engine)上(多个连接器 —— 一个容器)
- Connector:处理与客户端的通信,它负责监听接收客户请求,然后转给相关的容器处理,最后向客户返回响应结果
- Container:负责处理用户的servlet请求,并返回对象给web用户的模块
3. Container容器
Tomcat设计了4种容器:分别是Engine,Host,Context 和 Wrapper(Container是他们的盖称)。但是这四种容器是父子继承关系,Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性:
Container各个组件的含义 :
Engine:Catalina的Servlet引擎,来管理多个虚拟的主机,每个Service一个Engine,但是可以包含多个虚拟主机Host
Host:代表一个虚拟主机,或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context
Context:表示一个Web应用程序, 一个Web应用可包含多个Wrapper
Wrapper:Servlet的别名,Wrapper 作为容器中的最底层,不能包含子容器
4. server.xml结构
<Server> |
我们也可以再通过Tomcat的server.xml
配置文件来加深对Tomcat容器的理解。Tomcat采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件按照一定的格式要求配置在这个顶层容器中。Tomcat用组合模式来管理这些容器的。
具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对Wrapper和Context、Host或者Engine的使用具有一致性。
Container接口扩展了LifeCycle接口,LifeCycle接口用来统一管理各组件的生命周期.
五、Tomcat启动流程
1. 启动流程分析
前面我们已经讲到Tomcat的启动入口Bootstrap(来创建Catalina实例)、Shell程序Catalina的以及各个组件的关系。
所以Tomcat的启动步骤:
(1)启动Tomcat,需要调用的是bin
路径下面的startup.bat
(linux是startup.sh
),启动脚本后调用了Catalina.bat
(2)在Catalina.bat
脚本中是调用了Bootstrap里面的main方法。
(3)main方法调用init
方法来初始化Catalina和类加载器。
(4)main方法然后调用load
方法,在其中调用Catalina的load方法。
(5)Catalina的load需要对组件进行初始化,这时就要用到里面的一个内部类Digester对象来解析server.xml
(6)然后调用后序组件的init初始化操作,加载Tomcat的配置文件,初始化容器组件,监听端口号,准备接受客户端的请求。
2. Lifecycle
二、Tomcat总体架构 中,我们提到了Lifecycle这个接口,因为组件均存在初始化、启动、停止等生命周期方法,所以要拥有生命周期管理
包括组件 Server、Service、Container、Executor、Connector,都实现了一个生命周期的接口,从而具有了以下生命周期中的核心方法:
3. 组件的默认实现
对于Endpoint而言,Tomcat没有对应的Endpoint接口,但有一个抽象类AbstractEndpoint,有NioEndpoint、 Nio2Endpoint、AprEndpoint,分别对应着Coyote连接器中支持的三种IO模型:NIO、NIO2、APR,默认是NioEndpoint。
ProtocolHandler : Coyote协议接口,通过封装Endpoint和Processor , 实现针对具体协议的处理功能。Tomcat按照协议和IO提供了6个实现类。
注意:实际上Tomcat的默认组件 = ProtocolHandler默认实现(例如Http11NioProtocol) + Standard组件(例如StandardServer、StandardEngine、StandardContext)
1) AjpNioProtocol :采用NIO的IO模型。
2) AjpNio2Protocol:采用NIO2的IO模型。
3) AjpAprProtocol :采用APR的IO模型,需要依赖于APR库。
1) Http11NioProtocol :采用NIO的IO模型,默认使用的协议(如果服务器没有安装APR)。
2) Http11Nio2Protocol:采用NIO2的IO模型。
3) Http11AprProtocol :采用APR的IO模型,需要依赖于APR库。
4. 源码解析
分析源码先从启动脚本startup.bat
开始分析
(1)startup.bat
实际上是调用的catalina的启动脚本catalina.bat
(2)在catalina.bat
中调用的是BootStrap的Main方法,MainClass:BootStrap —-> main(String[] args),所以我们去看源码
1. BootStrap启动类
查看Tomcat的源代码:BootStrap先进行init方法,然后通过反射创建Catalina对象和调用load方法,实际上是Catalina的load方法,对组件进行初始化,都是通过反射执行。
/** |
2. Catalina类load方法
直接进入Catalina类的load方法:先是通过Digerster解析核心的配置文件(因为这样才能去加载Server后面的组件),然后调用Server的init,然后逐级init。
/** |
3.组件的init初始化
组件们在实现类中,都是调用了Standard组件名 或者是Standard组件名 的initInternal。
/** |
在Service的实现类里面也是用到了模板方法设计模式,调用initInternal(),然后初始化了engine、executor、connector。
然后Engine再去初始化后序的host,context,Connector再去初始化后序的Adapter、ProtocolHandler的init,然后继续下去EndPoint的init。
/** |
4. 组件的start启动
当Bootstrap启动类的init以及后序组件的init初始化后,Bootstrap启动类再调用start方法
// 3.进入此,调用bootstrap的load和start方法 |
Server和Service都是调用了StandardServer或者是StandardService的initInternal或者startInternal
5. 启动总结
从启动流程图中以及源码中,我们可以看出Tomcat的启动过程非常标准化, 统一按照生命周期管理接口Lifecycle的定义进行启动。首先调用init() 方法进行组件的逐级初始化操作,然后再调用start()方法进行启动。
每一级的组件除了完成自身的处理外,还要负责调用子组件响应的生命周期管理方法,组件与组件之间是松耦合的,因为我们可以很容易的通过配置文件进行修改和替换。
六、Tomcat请求处理流程(执行)
Tomcat的层次很多,那么确定每一个请求应该由哪个Wrapper容器里的Servlet来处理,这就是一个问题。也就是连接器生成的servletrequest是如何找到容器中对应的servlet来处理,Tomcat是用Mapper组件来完成这个任务的。
1. Mapper组件
Mapper组件里保存了Web应用的配置信息,其实就是容器组件与请求URL里的域名和路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里Servlet映射的路径,你可以想象这些配置信息就是一个多层次的Map。
下面的示意图中,就描述了当用户请求链接 http://www.itcast.cn/bbs/findAll 之后, 是如何找到最终处理业务逻辑的servlet。
通过端口8080确定是Http请求,根据对应的IO类型,从而确定连接器,然后通过域名host找到对应的引擎(虚拟主机),引擎下面同时对应appbase(context的路径):即webapps,进入对应的context里面的web.xml中,通过映射关系找到对应的Wrapper,然后构造过滤器链FilterChain,执行每个Filter,最后转换成Servlet。
2. 请求处理流程分析
步骤如下:
1) Connector组件Endpoint中的Acceptor监听客户端Socket连接并接收Socket。将连接交给线程池Executor处理,开始执行请求响应任务。Processor组件读取消息报文,解析请求行、请求体、请求头,封装成Request对象。
2) CoyoteAdaptor适配器组件负责将Connector组件和Engine容器关联起来,把Processor生成的Request生成对应的ServletRequset传递到Engine容器中,调用 Pipeline。
3) Mapper组件根据请求行的URL值和请求头的Host值匹配由哪个Host容器、Context容器、Wrapper容器处理请求。
4) Engine容器的管道开始处理,管道中包含若干个Valve、每个Valve负责部分处理逻辑。执行完Valve后会执行基础的Valve–StandardEngineValve,负责调用Host容器的Pipeline。Host、Context、Wrapper容器流程类似,最后执行Wrapper容器对应的Servlet对象的处理方法
5) 构造一个过滤器链FilterChain,然后执行各个Filter,最后执行Servlet.service()。
3. 源码分析
1)发起请求后,Tomcat的NioEndpoint中的Acceptor就会监听到Socket连接,然后调用内部维护的线程池Executor的executor方法执行请求。executor方法会调用Processor
protected class Acceptor extends AbstractEndpoint.Acceptor { |
protected class SocketProcessor extends SocketProcessorBase<NioChannel> { |
2)Processor组件处理Http请求,解析Socket请求,读取消息报文,解析请求行、请求体、请求头,封装成Request对象。
处理完后,调用CoyoteAdapter的Service方法,把刚才解析的request和response传进去
3)到了CoyoteAdapter的Service方法,调用容器connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
|
4)engine后序的组件Host、Context、Wrapper都是先拿到管道,再getFirst拿到valve阀(因为是头插法式的add)
都是调用对应StandardXXX的invoke方法
5)最后执行Wrapper容器对应的allocate处理方法,拿到Servlet对象
Tomcat中的各个组件各司其职,组件之间松耦合,Tomcat中定义了Pipeline 和 Valve 两个接口,Pipeline 用于构建责任链,只需要调用第一个处理器,它就会依次执行。