文章目录
Tomcat相关原理部分
此部分是参考B站视频讲解《黑马程序员Java进阶教程Tomcat核心原理解析》以及配套教材《Tomcat架构解析·刘光瑞著》进行整理。
Tomcat的目录结构
目录 | 目录下文件 | 目录下文件 |
---|---|---|
bin | / | 存放Tomcat的启动、停止等批处理脚本文件 |
startup.bat ,startup.sh | 用于在windows和linux下的启动脚本 | |
shutdown.bat ,shutdown.sh | 用于在windows和linux下的停止脚本 | |
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 中所有应用默认的部署描述文件 | |
lib | / | Tomcat 服务器的依赖包 |
logs | / | Tomcat 默认的日志存放目录 |
webapps | / | Tomcat 默认的Web应用部署目录 |
work | / | Web 应用JSP代码生成和编译的临时目录 |
Tomcat整体架构
http 服务器请求处理
浏览器发给服务端的是一个HTTP格式的请求,HTTP服务器收到这个请求后,需要调用服务端程序来处理,所谓的服务端程序就是你写的Java类,一般来说不同的请求需要由不同的Java类来处理。
图1表示HTTP服务器直接调用具体业务类,它们是紧耦合的。当服务器接受到 http 请求时,会 if 判断 请求的是什么业务(比如通过 url 判断),然后调用业务类做出对应的响应
图2,HTTP服务器不直接调用业务类,而是把请求交给容器来处理,容器通过Servlet接口调用业务类。因此Servlet接口和Servlet容器的出现,达到了HTTP服务器与业务类解耦的目的。而Servlet接口和Servlet容器这一整套规范叫作Servlet规范。Tomcat按照Servlet规范的要求实现了Servlet容器,同时它们也具有HTTP服务器的功能。作为Java程序员,如果我们要实现新的业务功能,只需要实现一Servlet,并把它注册到Tomcat(Servlet容器)中,剩下的事情就由Tomcat帮我们处理了。
Servlet容器工作流程
为了解耦,HTTP服务器不直接调用Servlet,而是把请求交给Servlet容器来处理,当客户请求某个资源时,HTTP服务器会用一个ServletRequest对象把客户的请求信息封装起来,然后调用Servlet容器的service方法,Servlet容器拿到请求后,根据请求的URL和Servlet的映射关系,找到相应的Servlet,如果Servlet还没有被加载,就用反射机制创建这个Servlet,并调用Servlet的init方法来完成初始化,接着调用Servlet的service方法来处理请求,把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端。
tomcat 整体架构
Tomcat要实现的两个核心功能:
处理Socket连接(网络编程),负责网络字节流与Request和Response对象的转化。
加载和管理Servlet,以及具体处理Request请求。
因此Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这两件事情。浏览器发起一个请求后,这个请求会被连接器接收到,连接器将接收到的Socket请求转化为ServlerRequest,然后把ServlerRequst转发给容器,容器拿到ServlerRequest之后,就会根据请求的路径去容器内找的对应的Servler去执行具体的业务逻辑,执行完之后会形成一个ServlerResponse对应返回给连接器,连接器拿到ServlerResponse对象后会解析并通过Socket(TCP)返回响应。
连接器-Coyote
Coyote 是Tomcat的连接器框架的名称 , 是Tomcat服务器提供的供客户端访问的外部接口。客户端通过Coyote与服务器建立连接、发送请求并接受响应 。
Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Catalina 容器提供了统一的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦。Coyote 将Socket 输入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写入输出流
Coyote 作为独立的模块,只负责具体协议和IO的相关操作, 与Servlet 规范实现没有直接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而是在Catalina 中将他们进一步封装为ServletRequest 和 ServletResponse 在Coyote中,Tomcat支持的多种I/O模型和应用层协议,具体包含了以下IO模型和应用层协议:
Tomcat 支持的IO模型(自8.5/9.0 版本起,Tomcat 移除了 对 BIO 的支持改为使用NIO)
IO模型 | 描述 |
---|---|
NIO | 非阻塞IO,采用JAVA NIO实现 |
NIO2 | 异步IO,采用JDK7最新的NIO2类库实现 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库,如果选择此方案需要单独安装APR库 |
Tomcat 支持的应用层协议:
应用层协议 | 描述 |
---|---|
HTTP1.1 | 大部分web应用采用的协议 |
HTTP2 | HTTP2大幅度提升了web性能,自tomcat 9版本以后支持 |
AJP | 用于和Web服务器(Apache)集成,以实现对静态资源的优化和集群部署,当前支持AJP/1.3 |
Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service组件。这里请你注意,Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
Coyote中的连接组件分为四部分:
EndPoint
Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的。
Tomcat 并没有EndPoint 接口,而是提供了一个抽象类AbstractEndpoint , 里面定义了两个内部类:Acceptor和SocketProcessor。Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里调用协议处理组件Processor进行处理。为了提高处理能力,SocketProcessor被提交到线程池来执行。而这个线程池叫作执行器(Executor)。
Processor
Coyote 协议处理接口 ,如果说EndPoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。
ProtocolHandler
Coyote 协议接口, 通过Endpoint 和 Processor , 实现针对具体协议的处理能力。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol ,AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol ,Http11AprProtocol。我们在配置tomcat/conf/server.xml 时 , 至少要指定具体的ProtocolHandler , 当然也可以指定协议名称 , 如 : HTTP/1.1 ,如果安装了APR,那么将使用Http11AprProtocol , 否则使用 Http11NioProtocol 。
Adapter
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,也就意味着,不能用TomcatRequest作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是TomcatRequest对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的Service方法。
容器-Catalina
Tomcat是一个由一系列可配置的组件构成的Web容器,而Catalina是Tomcat的servlet容器。
Catalina 是Servlet 容器实现,包含了之前讲到的所有的容器组件,以及后续章节涉及到的安全、会话、集群、管理等Servlet 容器架构的各个方面。它通过松耦合的方式集Coyote,以完成按照请求协议进行数据读写。同时,它还包括我们的启动入口、Shell程序等。
Tomcat 本质上就是一款 Servlet 容器, 因此 Catalina 才是 Tomcat 的核心 , 其他模块都是为Catalina 提供支撑的。 比如 : 通过Coyote 模块提供链接通信,Jasper 模块提供JSP引擎,Naming 提供JNDI 服务,Juli 提供日志服务。(Servlet 可以理解为 一种规范)
Catalina 的主要组件结构如下:Catalina负责管理Server,而Server表示着整个服务器。Server下面有多个服务Service,每个服务都包含着多个连接器组件Connector(Coyote 实现)和一个容器组件Container。在Tomcat 启动的时候, 会初始化一个Catalina的实例。
Tomcat的Container中设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。, Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。
其中,一个Tomcat只有一个Catalina,一个catalina有多个Service,一个Serivce又包含了多个Connector和一个Container,一个Containner中只有一个Engine,Engine是整个Container容器的引擎,用来管理多个站点,一个Engine中可以有多个Host,一个Host代表一个站点,一个Host里有可以包含多个Context,Context就是一个Web应用,另外Host中还包含了Realm等用于控制访问安全,后面讲Web.xml配置和tomcat-user.xml配置时会讲到,一个Web应用中可以包含多个Wrapper,Wrapper就是Servlet。
我们也可以再通过Tomcat的server.xml配置文件来加深对Tomcat容器的理解。Tomcat采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件按照一定的格式要求配置在这个顶层容器中。
Tomcat的容器是具有父子关系的,形成一个树形结构,Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。Container接口扩展了LifeCycle接口,LifeCycle接口用来统一管理各组件的生命周期。
Tomcat 启动流程
流程
启动tomcat , 需要调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh),在startup.bat 脚本中, 调用了catalina.bat。
在catalina.bat 脚本文件中,调用了BootStrap 中的main方法。
在BootStrap 的main 方法中调用了 init 方法 , 来创建Catalina 及 初始化类加载器。
在BootStrap 的main 方法中调用了 load 方法 , 在其中又调用了Catalina的load方法。
在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用于解析 XML。
然后在调用后续组件的初始化操作 。加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求
源码解析
由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期的接口,从而具有了以下生命周期中的核心方法:
init():初始化组件
start():开始组件
stop():停止组件
destroy():销毁组件
各组件实现
上面我们提到的Server、Service、Engine、Host、Context都是接口, 下图中罗列了这些接口的默认实现类。当前对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint接口, 但是有一个抽象类 AbstractEndpoint ,其下有三个实现类: NioEndpoint、Nio2Endpoint、AprEndpoint , 这三个实现类,分别对应于前面讲解链接器 Coyote时, 提到的链接器支持的三种IO模型:NIO,NIO2,APR , Tomcat8.5版本中,默认采用的是 NioEndpoint。
Tomcat的main方法位置
org.apache.catalina.startup.BootStrap#main
总结
从启动流程图中以及源码中,我们可以看出Tomcat的启动过程非常准化, 统一按照生命周期管理接口Lifecycle的定义进行启动。首先调init() 方法进行组件的逐级初始化操作,然后再调用start()方法进行启动。
每一级的组件除了完成自身的处理外,还要负责调用子组件响应的生命周期管理方法,组件与组件之间是松耦合的,因为我们可以很容易的通过配置文件进行修改和替换。
Tomcat 请求处理流程
Tomcat中维护了一个Mapper组件将用户请求的URL定位到一个Servlet,它的工作原理是:Mapper组件里保存了Web应用的配置信息,其实就是容器组件与访问路径的映射关系,比如Host容器里配置的域名、Context容器里的Web应用路径,以及Wrapper容器里Servlet映射的路径,这些配置信息就像是一个多层次的Map。当一个请求到来时,Mapper组件通过解析请求URL里的域名和路径,再到自己保存的Map里去查找,就能定位到一个Servlet。一个请求URL最后只会定位到一个Wrapper容器,也就是一个Servlet。下面的示意图中 , 就描述了 当用户请求链接 http://www.abcde.com/abc/def 之后, 是如何找到最终处理业务逻辑的servlet 。
上面这幅图只是描述了根据请求的URL如何查找到需要执行的Servlet , 下面从Tomcat的设计架构层面来分析Tomcat了请求处理的过程。
Connector组件Endpoint中的Acceptor监听客户端套接字连接并接收Socket。将连接交给线程池Executor处理,开始执行请求响应任务。
Processor组件读取消息报文,解析请求行、请求体、请求头,封装成Request对象。
CoyoteAdaptor组件负责将Connector组件和Engine容器关联起来,把生成的Request对象转换成ServletRequest,后面还会把ServletResponse转成HttpResponse对象返回回去。
Mapper组件根据请求行的URL值和请求头的Host值匹配由哪个Host容器、Context容器、Wrapper容器处理请求。
Engine容器的管道开始处理,管道中包含若干个Valve、每个Valve负责部分处理逻辑。执行完Valve后会执行基础的 Valve–StandardEngineValve,负责调用Host容器的Pipeline。
Host容器的管道开始处理,流程类似,最后执行 Context容器的Pipeline。
Context容器的管道开始处理,流程类似,最后执行 Wrapper容器的Pipeline。
Wrapper容器的管道开始处理,流程类似,最后执行 Wrapper容器对应的Servlet对象的处理方法。
Tomcat中的各个组件各司其职,组件之间松耦合,确保了整体架构的可伸缩性和可拓展性, 在Tomcat中,每个Container组件采用责任链模式来完成具体的请求处理。在Tomcat中定义了Pipeline 和 Valve 两个接口,Pipeline 用于构建责任链, 后者代表责任链上的每个处理器。Pipeline 中维护了一个基础的Valve,它始终位于Pipeline的末端(最后执行),封装了具体的请求处理和输出响应的过程。当然,我们也可以调用addValve()方法, 为Pipeline 添加其他的Valve, 后添加的Valve 位于基础的Valve之前,并按照添加顺序执行。Pipiline通过获得首个Valve来启动整合链条的执行.其实就是代码的同步执行。
Jasper
JSP页面其实是通过JSP的引擎将JSP文件内容转成Java代码执行的,其实一个JSP页面就是一个Servlet。Jasper模块是Tomcat的JSP核心引擎,Tomcat使用Jasper对JSP语法进行解析,生成Servlet并生成Class字节码,用户在进行访问jsp时,会访问Servlet,最终将访问的结果直接响应在浏览器端 。另外,在运行的时候,Jasper还会检测JSP文件是否修改,如果修改,则会重新编译JSP文件。另外JSP页面是第一次访问这个Servlet时才会编译而不是启动Tomcat时就会编译,所以第一次访问时要慢一些。
目前大多采用前后端分离的形式很少企业还在直接使用JSP,只学用得到的,否则学了也是忘,所以这块不再赘述,有需要自行查询
Tomcat 服务器配置
Tomcat 服务器的配置主要集中于 tomcat/conf 下的 catalina.policy、catalina.properties、context.xml、server.xml、tomcat-users.xml、web.xml 文件。server.xml是tomcat的核心配置,tomcat-user.xml主要是配置一些访问manager页面、域安全用户角色的信息,web.xml和webapp中的web.xml功能是一致的,控制一些访问Servlet的参数等等,但这儿是全局的,本次主要讲一下Server.xml
Server.xml
server.xml 是tomcat 服务器的核心配置文件,包含了Tomcat的 Servlet 容器(Catalina)的所有配置。由于配置的属性特别多,我们在这里主要讲解其中的一部分重要配置。这是server.xml的框架
<Server>
<Listener />
<GlobaNamingResources>
</GlobaNamingResources>
<Service>
<Connector />
<Engine>
<Logger />
<Realm />
<host>
<Logger />
<Context />
</host>
</Engine>
</Service>
</Server>
Server是server.xml的根元素,用于创建一个Server实例,默认使用的实现类是org.apache.catalina.core.StandardServer。