1.工作原理
HTTP协议是浏览器与服务器之间的数据传送协议。作为应用层协议,HTTP是基于TCP/IP协议来传递数据的(HTML文件、图片、查询结果等),HTTP协议不涉及数据包(Packet)传输,主要规定了客户端和服务器之间的通信格式。它的整个过程如下图所示:
1. 用户通过浏览器进行了一个操作,比如输入网址并回车,或者是点击链接,接着浏览器获取了这个事件。
2. 浏览器向服务端发出TCP连接请求。
3. 服务程序接受浏览器的连接请求并经过TCP三次握手建立连接。
4. 浏览器将请求数据打包成一个HTTP协议格式的数据包。
5. 浏览器将该数据包推入网络,数据包经过网络传输,最终达到端服务程序。
6. 服务端程序拿到这个数据包后,同样以HTTP协议格式解包,获取到客户端的意图。
7. 得知客户端意图后进行处理,比如提供静态文件或者调用服务端程序获得动态结果。
8. 服务器将响应结果(可能是HTML或者图片等)按照HTTP协议格式打包。
9. 服务器将响应数据包推入网络,数据包经过网络传输最终达到到浏览器。
10. 浏览器拿到数据包后,以HTTP协议的格式解包,然后解析数据,假设这里的数据是 HTML。
11. 浏览器将HTML文件展示在页面上。
那我们想要探究的Tomcat作为一个HTTP服务器,在这个过程中都做了些什么事情呢?
主要是接受连接、解析请求数据、处理请求和发送响应这几个步骤。
2.Tomcat整体架构
Tomcat要实现两个核心功能:
处理Socket连接,负责网络字节流与Request和Response对象的转化。
加载和管理Servlet,以及具体处理Request请求。
因此Tomcat设计了两个核心组件连接器(Connector)和容器(Container)来分别做这 两件事情。连接器负责对外交流,容器负责内部处理。
3.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支持的IO模型,请看下表:
IO模型 | 描述 |
NIO | 非阻塞I/O,采用Java NIO类库实现。 |
NIO2 | 异步I/O,采用JDK 7最新的NIO2类库实现。 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库。如果选择该方案,需要单独安装APR库。 |
在Coyote中,Tomcat支持的应用层协议,请看下表:
应用层协议 | 描述 |
HTTP/1.1 | 这是大部分Web应用采用的访问协议。 |
AJP/1.3 | 用于和Web服务器集成(如Apache),以实现对静态资源的优化以及集群部署。 |
HTTP/2 | HTTP 2.0大幅度的提升了Web性能。下一代HTTP协议 , 自8.5以及9.0版本之后支持。 |
协议分层:
在8.0之前,Tomcat 默认采用的I/O方式为 BIO,之后改为 NIO。 无论 NIO、NIO2还是 APR,在性能方面均优于以往的BIO。 如果采用APR,甚至可以达到 Apache HTTP Server的影响性能。
Tomcat为了实现支持多种I/O模型和应用层协议,一个容器可能对接多个连接器,就好比一个房间有多个门。但是单独的连接器或者容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作Service组件。这里请你注意,Service本身没有做什么重要的事情,只是在连接器和容器外面多包了一层,把它们组装在一起。Tomcat内可能有多个Service,这样的设计也是出于灵活性的考虑。通过在Tomcat中配置多个Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。
Coyote的主要组件结构如下:
Coyote的各个组件的作用如下:
- EndPoint
EndPoint:Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的。
Tomcat 并没有EndPoint接口,而是提供了一个抽象类AbstractEndpoint,里面定义了两个内部类:Acceptor和SocketProcessor。Acceptor用于监听Socket连接请求。SocketProcessor用于处理接收到的Socket请求,它实现Runnable接口,在Run方法里调用协议处理组件Processor进行处理。为了提高处理力,SocketProcessor被提交到线程池来执行,而这个线程池叫作执行器(Executor)。 - Processor
Processor:Coyote 协议处理接口,如果说EndPoint是用来实现TCP/IP协议的,那么Processor用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。
-
ProtocolHandler
ProtocolHandler:Coyote 协议接口,通过Endpoint和Processor,实现针对具体协议的处理能力。Tomcat按照协议和I/O 提供了6个实现类 : AjpNioProtocol、AjpNio2Protocol、AjpAprProtocol、Http11NioProtocol、Http11Nio2Protocol、Http11AprProtocol。我们在配置tomcat / conf / server.xml时,至少要指定具体的ProtocolHandler , 当然也可以指定协议名称,如:HTTP/1.1,如果安装了APR,那么将使用Http11AprProtocol,否则使用 Http11NioProtocol 。 -
Adapter
由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request类 来“存放”这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。 但是这个Request对象不是标准的ServletRequest,也就意味着,不能用Tomcat Request作为参数来调用容器。Tomcat设计者的解决方案是引入CoyoteAdapter,这是 适配器模式的经典运用,连接器调用CoyoteAdapter的Sevice方法,传入的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调用容器的Service方法。
4.Catalina容器架构
Tomcat的模块分层结构如下:
Tomcat本质上就是一款 Servlet 容器,因此Catalina 才是 Tomcat 的核心,其他模块都是为Catalina提供支撑的。比如:通过Coyote模块提供连接通信,Jasper 模块提供JSP引擎,Naming 提供JNDI 服务,Juli提供日志服务。
Catalina的主要组件结构如下:
Catalina的各个组件的作用如下:
-
Catalina
负责解析Tomcat的配置文件 , 以此来创建服务器Server组件,并根据 命令来对其进行管理。 - Server
服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动Servlet引擎、Tomcat连接器。Server通过实现Lifecycle接口,提供了 一种优雅的启动和关闭整个系统的方式。 -
Service
服务是Server内部的组件,一个Server包含多个Service。它将若干个Connector组件绑定到一个Container(Engine)上。 - Connector
连接器主要是处理与客户端的通信,它负责接收客户请求,然后转给相关的容器处理,最后向客户返回响应结果。 - Container
容器负责处理用户的Servlet请求,并返回对象给web用户的模块。
Container的主要结构如下:
Tomcat设计了4种容器,分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系,而是父子关系。Tomcat通过一种分层的架构,使得Servlet容器具有很好的灵活性。
Container的各个组件的作用如下:
-
Engine
表示整个Catalina的Servlet引擎,用来管理多个虚拟站点,一个Service最多只能有一个Engine,但是一个引擎可包含多个Host。 -
Host
代表一个虚拟主机或者说一个站点,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context。 -
Context
表示一个Web应用程序, 一个Web应用可包含多个Wrapper。 -
Wrapper
表示一个Servlet,Wrapper 作为容器中的最底层,不能包含子容器。
我们也可以再通过Tomcat的server.xml配置文件来加深对Tomcat容器的理解。Tomcat采用了组件化的设计,它的构成组件都是可配置的,其中最外层的是Server,其他组件按照一定的格式要求配置在这个顶层容器中。
那么,Tomcat是怎么管理这些容器的呢?你会发现这些容器具有父子关系,形成一个树形结构,你可能马上就想到了设计模式中的组合模式。没错,Tomcat就是用组合模式来管理这些容器的。具体实现方法是,所有容器组件都实现了Container接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper,组合容器对象指的是上面的Context、Host或者Engine。
5.Jasper处理流程
Tomcat在默认的web.xml中配置了一个org.apache.jasper.servlet.JspServlet,用于处理所有的.jsp或 .jspx结尾的请求,该Servlet 实现即是运行时编译的入口。
JspServlet 处理流程图:
- 如果在 tomcat/conf/web.xml 中配置了参数scratchdir,则jsp编译后的结果,就会存储在该目录下 。
- 如果没有配置该选项,则会将编译后的结果,存储在Tomcat安装目录下的 work/Catalina(Engine名称)/localhost(Host名称)/Context名称 。
除了运行时编译,我们还可以直接在Web应用启动时, 一次性将Web应用中的所有的JSP页面一次性编译完成。在这种情况下,Web应用运行过程中,便可以不必再进行实时编译,而是直接调用JSP页面对应的Servlet完成请求处理, 从而提升系统性能。
Tomcat 提供了一个Shell程序JspC,用于支持JSP预编译,而且在Tomcat的安装目录下提供了一个 catalina-tasks.xml 文件声明了Tomcat 支持的Ant任务, 因此,我们很容易使用 Ant 来执行JSP 预编译。(要想使用这种方式,必须得确保在此之前已经下载并安装了Apache Ant)。
6.JSP编译过程
Compiler 编译工作主要包含代码生成和编译两部分:
- 代码生成
-
1) Compiler 通过一个 PageInfo 对象保存JSP 页面编译过程中的各种配置,这些配置可能来源于 Web 应用初始化参数, 也可能来源于JSP页面的指令配置(如 page , include)。
2) 调用ParserController 解析指令节点, 验证其是否合法,同时将配置信息保存到 PageInfo 中, 用于控制代码生成。
3) 调用ParserController 解析整个页面, 由于 JSP 是逐行解析, 所以对于每一行会创建一个具体的Node 对象。如静态文本(TemplateText)、Java代码(Scriptlet)、定制标签(CustomTag)、Include指令(IncludeDirective)。
4) 验证除指令外其他所有节点的合法性, 如脚本、定制标签、EL表达式等。
5) 收集除指令外其他节点的页面配置信息。
6) 编译并加载当前 JSP 页面依赖的标签。
7) 对于JSP页面的EL表达式,生成对应的映射函数。
8) 生成JSP页面对应的Servlet类源代码 编译 代码生成完成后,Compiler还会生成 SMAP信息。 如果配置生成 SMAP信息,Compiler则会在编译阶段将SMAP信息写到class文件中 。 -
编译阶段
-
Compiler的两个实现 AntCompiler 和 JDTCompiler 分别调用先关框架的 API 进行源代码编译。
对于 AntCompiler 来说, 构造一个 Ant 的javac 的任务完成编译。
对于 JDTCompiler 来说, 调用 org.eclipse.jdt.internal.compiler.Compiler 完成编译。