目录
一、简介
Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。而 Tomcat 就是一个 Servlet 容器。为了方便使用,它也具有 HTTP 服务器的功能,因此 Tomcat 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。
1、协议
在集群架构中,为了提高效率,Web 服务器和 Tomcat 进程之间的协议不是 HTTP,而是一个特定的协议,称之为 AJP。Tomcat 支持以下协议:
- HTTP/1.1:这是大部分 Web 应用采用的访问协议。
- AJP:用于和 Web 服务器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
2、I/O 模型
Tomcat 支持以下 I/O 模型:
- NIO:非阻塞 I/O,采用 Java NIO 类库实现。
- NIO2:异步 I/O,采用 JDK 7 最新的 NIO2 类库实现。
- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。
3、模块
Tomcat 要实现的 2 个核心功能:
- 处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化
- 加载和管理 Servlet,以及具体处理 Request 请求
Tomcat 有两大组件完成上述功能,总体架构模型如下:
(1)连接器
连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。我们可以把连接器的功能需求进一步细化:
- 监听网络端口
- 接受网络连接请求
- 读取请求网络字节流
- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象
- 将 Tomcat Request 对象转成标准的 ServletRequest
- 调用 Servlet 容器,得到 ServletResponse
- 将 ServletResponse 转成 Tomcat Response 对象
- 将 Tomcat Response 转成网络字节流
- 将响应字节流写回给浏览器
因此 Tomcat 的设计者设计了 3 个组件来实现这 3 个功能,分别是 EndPoint、Processor 和 Adapter。EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。关系图如下:
Tomcat 默认提供两个 Connector 连接器,一个默认监听 8080 端口,一个默认监听 8009 端口,8080 端口监听的是通过 HTTP/1.1 协议访问的连接,而 8009 端口主要负责和其他 HTTP 服务器(如 Apache、IIS)建立连接,使用 AJP/1.3 协议,当 Tomcat 和其他服务器集成时就会使用到这个连接器。
(2)容器
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper,关系如下:
Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;
Context 表示一个 Web 应用程序;
Host 代表的是一个虚拟主机,或者说一个站点,可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序;
Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine。
可以结合Tomcat 的 server.xml 配置文件来加深理解:
所有容器组件都实现了 Container 接口:
public interface Container extends Lifecycle {
public void setName(String name);
public Container getParent();
public void setParent(Container container);
public void addChild(Container child);
public void removeChild(Container child);
public Container findChild(String name);
...
}
上述四个容器都有各自的标准实现类 StandardEngine、StandardHost、StandardContext、StandardWrapper。 StandardXXX 都是直接继承抽象类:org.apache.catalina.core.ContainerBase
二、启动流程
Tomcat 启动流程如下:
在源码中可以看到,除了 Bootstrap 和 catalina 类,其他的 Server, service 等等之类的都只是一个接口,实现类均为 StandardXXX 类。
下面是 catalina 类 load() 方法几行核心代码:
Digester digester = createStartDigester();
...
// 解析server.xml文件,加载容器
digester.parse(inputSource)
...
// 容器初始化,所有容器的初始化都是调抽象类 LifecycleBase(封装了Lifecycle)
getServer().init();
...
getServer().init() 调用 StandardServer 的 initInternal(),这个方法再调 StandardService 中的 initInternal(),以此类推。
StandardService 中的 initInternal() 再调 connector.init() 完成连接器初始化。
catalina 类 start() 方法调用逻辑和 load() 差不多,顶层容器调用底层容器,然后再初始化和启动连接器。
三、请求流程
假如有用户访问一个 URL,比如图中的 http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?
1、用户点击后,请求被发送到 user.shopping.com:8080,被在那里监听的 Coyote HTTP/1.1 Connector 获得(CoyoteAdapter 的 service 方法)。
2、Connector 把该请求交给它所在的 Service 的 Engine 来处理,并等待 Engine 的回应。
3、Engine 获得请求 user.shopping.com:8080/order/buy,匹配虚拟主机为 user.shopping.com:8080 的 Host,然后匹配名为 /order 的Context,最后再匹配映射路径为 /buy 的Servlet。
4、执行业务逻辑等程序。
5、Context把执行完之后的 HttpServletResponse 对象返回给 Host,Host 把 HttpServletResponse 对象返回给 Engine,Engine 把HttpServletResponse 对象返回 Connector。Connector 把 HttpServletResponse 对象返回给客户Browser。
四、管道模式
看完上面的请求流程,那么 Tomcat 是如何完成将请求从 Engine 一层层传到映射路径 Servlet,然后再处理的呢?
Tomcat 采用了管道模式去实现,管道是就像一条管道把多个对象连接起来,整体看起来就像若干个阀门嵌套在管道中,而处理逻辑放在阀门上。它的结构和实现是非常值得我们学习和借鉴的。
连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是 Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理,使用 Pipeline-Valve 管道来实现。Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。
Pipeline 中有 addValve 方法。Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。我们还发现 Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用 getNext.invoke() 来触发下一个 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();
...
}
不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。
首先要了解的是每一种 container 都有一个自己的 StandardValve,Pipeline 有一个实现类 StandardPipeline。
上面四个container对应的四个是:StandardEngineValve,StandardHostValve,StandardContextValve,StandardWrapperValve。调用流程图如下所示:
在 CoyoteAdapter 的service方法里,由下面这一句就进入 Container:
...
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
...
StandardPipeline 类中的getFirst()如下所示,如果 first 为空,返回 basic。
CoyoteAdapter 的 service 方法里调用 getFirst() 返回的是 StandardEngineValve(没有添加自定义 Valve 时,first为空,调用下层容器)
同理调用 StandardEngineValve 等等 Valve 的 getFirst() 逻辑类似。完成从上层容器将请求传到下层容器处理。
StandardEngineValve 的 invoke() 方法责任链处理如下所示:
// Ask this Host to process this request
host.getPipeline().getFirst().invoke(request, response);
StandardHostValve 的 invoke() 方法责任链处理如下所示:
if (!response.isErrorReportRequired()) {
context.getPipeline().getFirst().invoke(request, response);
}
StandardContextValve 的 invoke() 方法责任链处理如下所示:
wrapper.getPipeline().getFirst().invoke(request, response);
StandardWrapperValve 处理请求,没有了其他调用。
五、阈(Valve)
1、内置阈
Tomcat 在处理请求中用到了阈(Valve),阈类似于过滤器,可以截获任何输入请求和输出响应,使用比较广。Tomcat 提供了一些常用的阈,下面列举几个:
阈名 | 用途 |
AccessLogValve | 请求访问日志阀门,通过此阀门可以记录所有客户端的访问日志,包括远程主机IP,远程主机名,请求方法,请求协议,会话ID,请求时间,处理时长,数据包大小等。它提供任意参数化的配置,可以通过任意组合来定制访问日志的格式 |
JDBCAccessLogValve | 同样是记录访问日志的阀门,但是它有助于将访问日志通过 JDBC 持久化到数据库中 |
ErrorReportValve | 这是一个将错误以 HTML 格式输出的阀门 |
PersistentValve | 这是对每一个请求的会话实现持久化的阀门 |
RemoteAddrValve | 访问控制阀门,可以通过配置决定哪些 IP 可以访问 WEB 应用 |
RemoteHostValve | 访问控制阀门,通过配置觉得哪些主机名可以访问 WEB 应用 |
RemoteIpValve | 针对代理或者负载均衡处理的一个阀门,一般经过代理或者负载均衡转发的请求都将自己的 IP 添加到请求头”X-Forwarded-For” 中,此时,通过阀门可以获取访问者真实的IP |
SemaphoreValve | 这个是一个控制容器并发访问的阀门,可以作用在不同容器上 |
需要使用上述阈只需要在 server.xml 中配置一下即可,如下所示配置 RemoteAddrValve(只允许本地IP可以访问)
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127.0.0.1"/>
...
2、自定义阈
管道机制给我们带来了更好的拓展性,例如,你要添加一个额外的逻辑处理阀门是很容易的。
自定义个阀门 PrintIPValve,只要继承 ValveBase 并重写 invoke() 方法即可。注意在 invoke() 方法中一定要执行调用下一个阀门的操作,否则会出现异常。
public class PrintIPValve extends ValveBase{
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
System.out.println("------自定义阀门PrintIPValve:"+request.getRemoteAddr());
getNext().invoke(request,response);
}
}
配置 Tomcat 的核心配置文件 server.xml,这里把阀门配置到 Engine 容器下,作用范围就是整个引擎,也可以根据作用范围配置在 Host 或者是 Context 下面。
<Valve className="你的包名.PrintIPValve"/>
源码中是直接可以有效果,但是如果是运行版本,则可以将这个类导出成一个 Jar 包放入 Tomcat/lib 目录下,也可以直接将 class 文件打包进 catalina.jar 包中。