You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

136 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | Tomcat系统架构聊聊多层容器的设计
专栏上一期我们学完了连接器的设计今天我们一起来看一下Tomcat的容器设计。先复习一下上期我讲到了Tomcat有两个核心组件连接器和容器其中连接器负责外部交流容器负责内部处理。具体来说就是连接器处理Socket通信和应用层协议的解析得到Servlet请求而容器则负责处理Servlet请求。我们通过下面这张图来回忆一下。
![](https://static001.geekbang.org/resource/image/ee/d6/ee880033c5ae38125fa91fb3c4f8cad6.jpg)
容器顾名思义就是用来装载东西的器具在Tomcat里容器就是用来装载Servlet的。那Tomcat的Servlet容器是如何设计的呢
## 容器的层次结构
Tomcat设计了4种容器分别是Engine、Host、Context和Wrapper。这4种容器不是平行关系而是父子关系。下面我画了一张图帮你理解它们的关系。
![](https://static001.geekbang.org/resource/image/cc/ed/cc968a11925591df558da0e7393f06ed.jpg)
你可能会问,为什么要设计成这么多层次的容器,这不是增加了复杂度吗?其实这背后的考虑是,**Tomcat通过一种分层的架构使得Servlet容器具有很好的灵活性。**
Context表示一个Web应用程序Wrapper表示一个Servlet一个Web应用程序中可能会有多个ServletHost代表的是一个虚拟主机或者说一个站点可以给Tomcat配置多个虚拟主机地址而一个虚拟主机下可以部署多个Web应用程序Engine表示引擎用来管理多个虚拟站点一个Service最多只能有一个Engine。
你可以再通过Tomcat的`server.xml`配置文件来加深对Tomcat容器的理解。Tomcat采用了组件化的设计它的构成组件都是可配置的其中最外层的是Server其他组件按照一定的格式要求配置在这个顶层容器中。
![](https://static001.geekbang.org/resource/image/82/66/82b3f97aab5152dd5fe74e947db2a266.jpg)
那么Tomcat是怎么管理这些容器的呢你会发现这些容器具有父子关系形成一个树形结构你可能马上就想到了设计模式中的组合模式。没错Tomcat就是用组合模式来管理这些容器的。具体实现方法是所有容器组件都实现了Container接口因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。这里单容器对象指的是最底层的Wrapper组合容器对象指的是上面的Context、Host或者Engine。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);
}
```
正如我们期望的那样我们在上面的接口看到了getParent、setParent、addChild和removeChild等方法。你可能还注意到Container接口扩展了Lifecycle接口Lifecycle接口用来统一管理各组件的生命周期后面我也用专门的篇幅去详细介绍。
## 请求定位Servlet的过程
你可能好奇设计了这么多层次的容器Tomcat是怎么确定请求是由哪个Wrapper容器里的Servlet来处理的呢答案是Tomcat是用Mapper组件来完成这个任务的。
Mapper组件的功能就是将用户请求的URL定位到一个Servlet它的工作原理是Mapper组件里保存了Web应用的配置信息其实就是**容器组件与访问路径的映射关系**比如Host容器里配置的域名、Context容器里的Web应用路径以及Wrapper容器里Servlet映射的路径你可以想象这些配置信息就是一个多层次的Map。
当一个请求到来时Mapper组件通过解析请求URL里的域名和路径再到自己保存的Map里去查找就能定位到一个Servlet。请你注意一个请求URL最后只会定位到一个Wrapper容器也就是一个Servlet。
读到这里你可能感到有些抽象,接下来我通过一个例子来解释这个定位的过程。
假如有一个网购系统有面向网站管理人员的后台管理系统还有面向终端客户的在线购物系统。这两个系统跑在同一个Tomcat上为了隔离它们的访问域名配置了两个虚拟域名`manage.shopping.com`和`user.shopping.com`,网站管理人员通过`manage.shopping.com`域名访问Tomcat去管理用户和商品而用户管理和商品管理是两个单独的Web应用。终端客户通过`user.shopping.com`域名去搜索商品和下订单搜索功能和订单管理也是两个独立的Web应用。
针对这样的部署Tomcat会创建一个Service组件和一个Engine容器组件在Engine容器下创建两个Host子容器在每个Host容器下创建两个Context子容器。由于一个Web应用通常有多个ServletTomcat还会在每个Context容器里创建多个Wrapper子容器。每个容器都有对应的访问路径你可以通过下面这张图来帮助你理解。
![](https://static001.geekbang.org/resource/image/be/96/be22494588ca4f79358347468cd62496.jpg)
假如有用户访问一个URL比如图中的`http://user.shopping.com:8080/order/buy`Tomcat如何将这个URL定位到一个Servlet呢
**首先根据协议和端口号选定Service和Engine。**
我们知道Tomcat的每个连接器都监听不同的端口比如Tomcat默认的HTTP连接器监听8080端口、默认的AJP连接器监听8009端口。上面例子中的URL访问的是8080端口因此这个请求会被HTTP连接器接收而一个连接器是属于一个Service组件的这样Service组件就确定了。我们还知道一个Service组件里除了有多个连接器还有一个容器组件具体来说就是一个Engine容器因此Service确定了也就意味着Engine也确定了。
**然后根据域名选定Host。**
Service和Engine确定后Mapper组件通过URL中的域名去查找相应的Host容器比如例子中的URL访问的域名是`user.shopping.com`因此Mapper会找到Host2这个容器。
**之后根据URL路径找到Context组件。**
Host确定以后Mapper根据URL的路径来匹配相应的Web应用的路径比如例子中访问的是`/order`因此找到了Context4这个Context容器。
**最后根据URL路径找到WrapperServlet。**
Context确定后Mapper再根据`web.xml`中配置的Servlet映射路径来找到具体的Wrapper和Servlet。
看到这里我想你应该已经了解了什么是容器以及Tomcat如何通过一层一层的父子容器找到某个Servlet来处理请求。需要注意的是并不是说只有Servlet才会去处理请求实际上这个查找路径上的父子容器都会对请求做一些处理。我在上一期说过连接器中的Adapter会调用容器的Service方法来执行Servlet最先拿到请求的是Engine容器Engine容器对请求做一些处理后会把请求传给自己子容器Host继续处理依次类推最后这个请求会传给Wrapper容器Wrapper会调用最终的Servlet来处理。那么这个调用过程具体是怎么实现的呢答案是使用Pipeline-Valve管道。
Pipeline-Valve是责任链模式责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理每个处理者负责做自己相应的处理处理完之后将再调用下一个处理者继续处理。
Valve表示一个处理点比如权限认证和记录日志。如果你还不太理解的话可以来看看Valve和Pipeline接口中的关键方法。
```
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void invoke(Request request, Response response)
}
```
由于Valve是一个处理点因此invoke方法就是来处理请求的。注意到Valve中有getNext和setNext方法因此我们大概可以猜到有一个链表将Valve链起来了。请你继续看Pipeline接口
```
public interface Pipeline extends Contained {
public void addValve(Valve valve);
public Valve getBasic();
public void setBasic(Valve valve);
public Valve getFirst();
}
```
没错Pipeline中有addValve方法。Pipeline中维护了Valve链表Valve可以插入到Pipeline中对请求做某些处理。我们还发现Pipeline中没有invoke方法因为整个调用链的触发是Valve来完成的Valve完成自己的处理后调用`getNext.invoke`来触发下一个Valve调用。
每一个容器都有一个Pipeline对象只要触发这个Pipeline的第一个Valve这个容器里Pipeline中的Valve就都会被调用到。但是不同容器的Pipeline是怎么链式触发的呢比如Engine中Pipeline需要调用下层容器Host中的Pipeline。
这是因为Pipeline中还有个getBasic方法。这个BasicValve处于Valve链表的末端它是Pipeline中必不可少的一个Valve负责调用下层容器的Pipeline里的第一个Valve。我还是通过一张图来解释。
![](https://static001.geekbang.org/resource/image/b0/ca/b014ecce1f64b771bd58da62c05162ca.jpg)
整个调用过程由连接器中的Adapter触发的它会调用Engine的第一个Valve
```
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
```
Wrapper容器的最后一个Valve会创建一个Filter链并调用doFilter方法最终会调到Servlet的service方法。
你可能会问前面我们不是讲到了Filter似乎也有相似的功能那Valve和Filter有什么区别吗它们的区别是
* Valve是Tomcat的私有机制与Tomcat的基础架构/API是紧耦合的。Servlet API是公有的标准所有的Web容器包括Jetty都支持Filter机制。
* 另一个重要的区别是Valve工作在Web容器级别拦截所有应用的请求而Servlet Filter工作在应用级别只能拦截某个Web应用的所有请求。如果想做整个Web容器的拦截器必须通过Valve来实现。
## 本期精华
今天我们学习了Tomcat容器的层次结构、根据请求定位Servlet的过程以及请求在容器中的调用过程。Tomcat设计了多层容器是为了灵活性的考虑灵活性具体体现在一个Tomcat实例Server可以有多个Service每个Service通过多个连接器监听不同的端口而一个Service又可以支持多个虚拟主机。一个URL网址可以用不同的主机名、不同的端口和不同的路径来访问特定的Servlet实例。
请求的链式调用是基于Pipeline-Valve责任链来完成的这样的设计使得系统具有良好的可扩展性如果需要扩展容器本身的功能只需要增加相应的Valve即可。
## 课后思考
Tomcat内的Context组件跟Servlet规范中的ServletContext接口有什么区别跟Spring中的ApplicationContext又有什么关系
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。