前言
在学习内存马之前,了解tomcat的运行原理是十分有必要的。
Tomcat 整体架构
Tomcat 结构比较复杂,我们先从重点的结构上分别学习各个模块。
![](https://img.haomeiwen.com/i1734768/9e45e71fc8f91872.png)
Tomcat 中最顶层的容器是Server,一个Server包含多个Service,一个Service只能有一个Container ,但可以有多个Connector。
Service主要用来提供对外服务,包含两个部分:Connector 连接器和Container 容器。
- Connector 连接器用于处理连接相关的事情,并提供Socket与请求、响应之间的转换;
- Container 容器用于封装和管理Servlet,以及具体处理Request请求;
Coyote 连接器
Coyote 是Tomcat 连接器Connector框架的名称,封装了底层的网络通信(Socket 请求及响应处理),为Servlet容器Catalina提供了统一的接口。
Coyote 中支持多种应用层协议和I/O模型。
IO 模型 | 描述 |
---|---|
NIO | 非阻塞同步I/O,采用Java NIO类库实现。 |
NIO2 | 非阻塞异步I/O,采用JDK 7最新的NIO2类库实现。 |
APR | 采用Apache可移植运行库实现,是C/C++编写的本地库。需要单独安装APR库。 |
应用层协议 | 描述 |
---|---|
HTTP/1.1 | 常见的Web访问协议 |
AJP | 用于和Web服务器集成(如Apache),以实现对静态资源的优化以及集群部署,当前支持AJP/1.3。 |
HTTP/2 | HTTP 2.0 下一代HTTP协议 |
连接器组件如下:
![](https://img.haomeiwen.com/i1734768/256469f694c571d3.png)
1、EndPoint 监听点
Coyote 通信端点,即通信监听的接口,是具体Socket接收和发送处理器,用来实现TCP/IP协议。
2、Processor 处理接口
Processor接收来自EndPoint的Socket,读取字节流解析为Tomcat Request和Response对象,实现HTTP协议。
3、ProtocolHandler 协议处理器
通过Endpoint 和 Processor,实现针对具体协议的处理能力。
4、Adapter 适配器
将ProtocolHandler 协议处理器解析出的Tomcat Request对象,适配成ServletRequest,再调用ServletRequest的Service方法。
Container 容器
Container 用于封装和管理Servlet,以及具体处理Request请求,在Container内部包含了4个子容器。
如下图:
![](https://img.haomeiwen.com/i1734768/745f625b56cb7d06.png)
1、Engine
Catalina的Servlet引擎,用来管理多个虚拟的主机,每个Service一个Engine,但是可以包含多个虚拟主机Host。
2、Host
代表一个虚拟主机,可以给Tomcat配置多个虚拟主机地址,而一个虚拟主机下可包含多个Context。
3、Context
表示一个Web应用程序,一个Web应用可包含多个Wrapper。
4、Wrapper
Servlet的别名,是容器中的最底层。
server.xml 配置文件可以充分说明这个容器结构。
<Server>
<Service>
<Engine>
<Host>
<Context></Context>
</Host>
</Engine>
</Service>
</Server>
Catalina 容器
在前文中多次提到Catalina容器,那么它又是做什么的呢?
Catalina负责管理Server,Server表示着整个服务器。也可以理解为一个Server就是一个Catalina。
如下图:
![](https://img.haomeiwen.com/i1734768/da761037eece7df3.png)
组件 | 职责 |
---|---|
Catalina | 负责解析Tomcat的配置文件,以此来创建服务器Server组件 |
Server | 表示整个Catalina Servlet容器以及其它组件,负责组装并启动Servlet引擎,Tomcat连接器。 |
Service | 一个Server包含多个Service。将若干个Connector 绑定到一个Container 容器(Engine)上。 |
Connector | 连接器,用于处理连接相关的事情,并提供Socket与请求、响应之间的转换。 |
Container | 容器,负责处理用户的servlet请求,并返回对象给Web用户。 |
Tomcat 源码安装
下载源码解压后,创建home文件夹,并将conf、wappers目录移到该文件夹中。
创建pom.xml文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>apache-tomcat-8.5.73-src</artifactId>
<name>Tomcat8.5</name>
<version>8.5</version>
<build>
<finalName>Tomcat8.5</finalName>
<sourceDirectory>java</sourceDirectory>
<!-- <testSourceDirectory>test</testSourceDirectory> -->
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<!-- <testResources>
<testResource>
<directory>test</directory>
</testResource>
</testResources>-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
</dependencies>
</project>
再以maven源码导入。
![](https://img.haomeiwen.com/i1734768/1e23fa8ea069c9eb.png)
配置启动类。
-Dcatalina.home=/Users/cseroad/IdeaProjects/Tomcat_Project/apache-tomcat-8.5.73-src/home
-Dcatalina.base=/Users/cseroad/IdeaProjects/Tomcat_Project/apache-tomcat-8.5.73-src/home
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=/Users/cseroad/IdeaProjects/Tomcat_Project/apache-tomcat-8.5.73-src/home/conf/logging.properties
在org.apache.catalina.startup
下,找到Bootstrap类的main方法开始启动。
访问tomcat如果出现500错误,需要在org.apache.catalina.startup
包下的ContextConfig类中
configureStart方法添加
// 初始化jsp解析器
context.addServletContainerInitializer(new JasperInitializer(), null);
再次重启即可。
![](https://img.haomeiwen.com/i1734768/118910418caa2ecb.png)
请求处理
先创建一个Servlet并配置web.xml,然后编译请求地址,再整个servlet_war_exploded拷贝出来。部署在Tomcat上并以源码的形式进行追踪分析请求过程。
web.xml
<servlet>
<servlet-name>BbsServlet</servlet-name>
<servlet-class>com.itcast.web.BbsServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>BbsServlet</servlet-name>
<url-pattern>/bbs/findAll</url-pattern>
</servlet-mapping>
BbsServlet
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class BbsServlet extends HttpServlet {
public BbsServlet() {
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("GET请求");
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("post请求");
}
}
![](https://img.haomeiwen.com/i1734768/22789fa0cd0f5699.png)
在启动Tomcat时,Tomcat会先初始化。
然后endpoint准备完毕,等待外部的请求连接。
从org.apache.tomcat.util.net.NioEndpoint
类内部的Acceptor类的run方法开始
![](https://img.haomeiwen.com/i1734768/1bb7f7dd43d34ed6.png)
处理接收到的socket对象,获取输入输出流,然后注册到Poller当中,添加到PollerEvent队列当中。
![](https://img.haomeiwen.com/i1734768/78184c14dd2fe6e4.png)
org.apache.tomcat.util.net
的register方法。
![](https://img.haomeiwen.com/i1734768/302fc6e09cdc012a.png)
而后Poller进行处理socket,调用org.apache.tomcat.util.net.NioEndpoint
的processKey方法处理读写。
![](https://img.haomeiwen.com/i1734768/a17c633230878da4.png)
![](https://img.haomeiwen.com/i1734768/a563977350be248c.png)
里面就到org.apache.tomcat.util.net.AbstractEndpoint.processSocket
的Executor实现的线程池进行处理。
![](https://img.haomeiwen.com/i1734768/8aa3a36deca4e7a4.png)
提交到线程池,放入线程池的 workQueue 中。
再调用org.apache.tomcat.util.net.NioEndpoint.SocketProcessor
的doRun()方法。
![](https://img.haomeiwen.com/i1734768/79cb37df00ba6ac2.png)
org.apache.coyote.AbstractProtoco.ConnectionHandler
的process方法处理socket请求。
![](https://img.haomeiwen.com/i1734768/ed822a52a14c0657.png)
该方法里面创建一个Http11Processor,解析socket请求。
这样Socket中的内容就会封装到Request 中。
通过getAdapter获取合适的处理器,调用service方法进行处理。执行的是org.apache.catalina.connector.CoyoteAdapter
类的 service 方法。
![](https://img.haomeiwen.com/i1734768/5e8936f5f38a7c8d.png)
创建了最终我们使用的ServletRequest和ServletResponse。
来到org.apache.catalina.connector.CoyoteAdapter
类的service方法。
然后调用容器,getContainer方法会拿到service关联的Engine。
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
先将请求传递到 Engine 管道中,传递到Engine Valve 这个阀中。
调用org.apache.catalina.core.StandardEngineValve
下的invoke方法。
![](https://img.haomeiwen.com/i1734768/6bba9b6f9d0bc39a.png)
获取host,接着invoke方法里面为
host.getPipeline().getFirst().invoke(request, response);
表示请求从 Engine Valve 传递到一个 Host 的管道中,在该管道中最后传递到 Host Valve 这个阀里。
调用org.apache.catalina.core.StandardHostValve
下的invoke方法。
![](https://img.haomeiwen.com/i1734768/4732dc78b7c52732.png)
获取context,接着invoke方法里面为
context.getPipeline().getFirst().invoke(request, response);
表示从 Host Valve 传递到 Context 的管道中,在该管道中最后传递到 Context Valve 中。
调用org.apache.catalina.core.StandardContextValve
下的invoke方法。
![](https://img.haomeiwen.com/i1734768/6164845b4736cfd8.png)
获取wrapper,接着invoke方法里面为
wrapper.getPipeline().getFirst().invoke(request, response);
表示请求传递到 Wrapper Valve 中,在这里会经过一个过滤器链 Filter Chain ,最终到熟悉的Servlet中。调用org.apache.catalina.core.StandardWrapperValve
下的invoke方法。
![](https://img.haomeiwen.com/i1734768/e656c472dd88a58e.png)
创建Servlet,再往下创建filterChain过滤器链。org.apache.catalina.core.StandardWrapperValve
下的invoke方法。
![](https://img.haomeiwen.com/i1734768/f37e92d78c1617de.png)
而后执行过滤器链,org.apache.catalina.core.StandardWrapperValve
下的invoke方法。
![](https://img.haomeiwen.com/i1734768/fbeabafef62be8fa.png)
执行org.apache.catalina.core.ApplicationFilterChain
下的 doFilter 方法。
![](https://img.haomeiwen.com/i1734768/719cce83ffea007f.png)
往下调用internalDoFilter方法,实际调用的org.apache.catalina.core.ApplicationFilterChain
下的 internalDoFilter 方法。
![](https://img.haomeiwen.com/i1734768/084b22e6194a1a6d.png)
到HttpServlet类的service方法,再调用我们编写的doGet()方法。
![](https://img.haomeiwen.com/i1734768/ee9925122e098bf8.png)
整个过程极为复杂,画张图来梳理一下。
![](https://img.haomeiwen.com/i1734768/7700e065c89eb46a.png)
总结:
endpoint收到socket请求,调用handler找到Processor,接着调用CoyoteAdapter,将socket请求处理为Request请求。这就是tomcat连接器的工作。接着CoyoteAdapter获取到Engine,Engine获取到host,host获取到Context,Context获取到Wrapper,Wrapper在反射调用就获取到了Servlet,然后创建过滤器、执行过滤器链,再执行Servlet。
其他配置
创建host
在server.xml中给Host添加别名,实现同一个Host拥有多个网络名称。
<Host name="www.cseroad.tomcat" appBase="webapps1"
unpackWARs="true" autoDeploy="true">
<Context docBase="E:\app" path="/app" reloadable="true" ></Context>
</Host>
docBase:Web应用目录或者War包的部署路径。可以是绝对路径,也可以是相对于Host appBase的相对路径。
path:Web应用的Context 路径。
配置错误页面
在web.xml中配置
<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500.html</location>
</error-page>
当出现404错误代码,就跳转至404.html自定义页面。
总结
针对Tomcat的源码学习有些吃力,整个过程需要结合代码、资料不断调试加深理解。
参考资料
https://blog.csdn.net/ly823260355/article/details/104181278
https://www.bilibili.com/video/BV1dJ411N7Um?p=5
网友评论