关于后端:Tomcat系统架构浅析

42次阅读

共计 8629 个字符,预计需要花费 22 分钟才能阅读完成。

大家好,我是易安!

明天咱们就来一步一步剖析 Tomcat 的设计思路,看看 Tomcat 的设计者们是如何设计一个简单零碎,怎么设计顶层模块,以及模块之间的关系。

Tomcat 总体架构

咱们晓得如果要设计一个零碎,首先是要理解需要。Tomcat 有 2 个外围性能:

  • 解决 Socket 连贯,负责网络字节流与 Request 和 Response 对象的转化。
  • 加载和治理 Servlet,以及具体解决 Request 申请。

因而 Tomcat 设计了两个外围组件连接器(Connector)和容器(Container)来别离做这两件事件。连接器负责对外交换,容器负责外部解决。

所以连接器和容器能够说是 Tomcat 架构里最重要的两局部,须要破费大量精力了解分明。

在开始讲连接器前,我先讲讲 Tomcat 反对的多种 I / O 模型和应用层协定。

Tomcat 反对的 I / O 模型有:

  • NIO:非阻塞 I /O,采纳 Java NIO 类库实现。
  • NIO.2:异步 I /O,采纳 JDK 7 最新的 NIO.2 类库实现。
  • APR:采纳 Apache 可移植运行库实现,是 C /C++ 编写的本地库。

Tomcat 反对的应用层协定有:

  • HTTP/1.1:这是大部分 Web 利用采纳的拜访协定。
  • AJP:用于和 Web 服务器集成(如 Apache)。
  • HTTP/2:HTTP 2.0 大幅度的晋升了 Web 性能。

Tomcat 为了实现反对多种 I / O 模型和应用层协定,一个容器可能对接多个连接器,就好比一个房间有多个门。然而独自的连接器或者容器都不能对外提供服务,须要把它们组装起来能力工作,组装后这个整体叫作 Service 组件。这里请你留神,Service 自身没有做什么重要的事件,只是在连接器和容器里面多包了一层,把它们组装在一起。Tomcat 内可能有多个 Service,这样的设计也是出于灵活性的思考。通过在 Tomcat 中配置多个 Service,能够实现通过不同的端口号来拜访同一台机器上部署的不同利用。

到此咱们失去这样一张关系图:

从图上你能够看到,最顶层是 Server,这里的 Server 指的就是一个 Tomcat 实例。一个 Server 中有一个或者多个 Service,一个 Service 中有多个连接器和一个容器。连接器与容器之间通过规范的 ServletRequest 和 ServletResponse 通信。

连接器

连接器对 Servlet 容器屏蔽了协定及 I / O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个规范的 ServletRequest 对象。

咱们能够把连接器的性能需要进一步细化,比方:

  • 监听网络端口。
  • 承受网络连接申请。
  • 读取网络申请字节流。
  • 依据具体应用层协定(HTTP/AJP)解析字节流,生成对立的 Tomcat Request 对象。
  • 将 Tomcat Request 对象转成规范的 ServletRequest。
  • 调用 Servlet 容器,失去 ServletResponse。
  • 将 ServletResponse 转成 Tomcat Response 对象。
  • 将 Tomcat Response 转成网络字节流。
  • 将响应字节流写回给浏览器。

需要列分明后,咱们要思考的下一个问题是,连接器应该有哪些子模块?优良的模块化设计应该思考 高内聚、低耦合

  • 高内聚 是指相关度比拟高的性能要尽可能集中,不要扩散。
  • 低耦合 是指两个相干的模块要尽可能减少依赖的局部和升高依赖的水平,不要让两个模块产生强依赖。

通过剖析连接器的具体性能列表,咱们发现连接器须要实现 3 个 高内聚 的性能:

  • 网络通信。
  • 应用层协定解析。
  • Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化。

因而 Tomcat 的设计者设计了 3 个组件来实现这 3 个性能,别离是 Endpoint、Processor 和 Adapter。

组件之间通过形象接口交互。这样做还有一个益处是 封装变动。 这是面向对象设计的精华,将零碎中常常变动的局部和稳固的局部隔离,有助于减少复用性,并升高零碎耦合度。

网络通信的 I / O 模型是变动的,可能是非阻塞 I /O、异步 I / O 或者 APR。应用层协定也是变动的,可能是 HTTP、HTTPS、AJP。浏览器端发送的申请信息也是变动的。

然而整体的解决逻辑是不变的,Endpoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。

如果要反对新的 I / O 计划、新的应用层协定,只须要实现相干的具体子类,下层通用的解决逻辑是不变的。

因为 I / O 模型和应用层协定能够自由组合,比方 NIO + HTTP 或者 NIO.2 + AJP。Tomcat 的设计者将网络通信和应用层协定解析放在一起思考,设计了一个叫 ProtocolHandler 的接口来封装这两种变动点。各种协定和通信模型的组合有相应的具体实现类。比方:Http11NioProtocol 和 AjpNioProtocol。

除了这些变动点,零碎也存在一些绝对稳固的局部,因而 Tomcat 设计了一系列形象基类来 封装这些稳固的局部 ,形象基类 AbstractProtocol 实现了 ProtocolHandler 接口。每一种应用层协定有本人的形象基类,比方 AbstractAjpProtocol 和 AbstractHttp11Protocol,具体协定的实现类扩大了协定层形象基类。上面我整顿一下它们的继承关系。

通过下面的图,你能够清晰地看到它们的继承和档次关系,这样设计的目标是尽量将稳固的局部放到形象基类,同时每一种 I / O 模型和协定的组合都有相应的具体实现类,咱们在应用时能够自由选择。

小结一下,连接器模块用三个外围组件:Endpoint、Processor 和 Adapter 来别离做三件事件,其中 Endpoint 和 Processor 放在一起形象成了 ProtocolHandler 组件,它们的关系如下图所示。

上面我来具体介绍这两个顶层组件 ProtocolHandler 和 Adapter。

ProtocolHandler 组件

由上文咱们晓得,连接器用 ProtocolHandler 来解决网络连接和应用层协定,蕴含了 2 个重要部件:Endpoint 和 Processor,上面我来具体介绍它们的工作原理。

  • Endpoint

Endpoint 是通信端点,即通信监听的接口,是具体的 Socket 接管和发送处理器,是对传输层的形象,因而 Endpoint 是用来实现 TCP/IP 协定的。

Endpoint 是一个接口,对应的形象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比方在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。

其中 Acceptor 用于监听 Socket 连贯申请。SocketProcessor 用于解决接管到的 Socket 申请,它实现 Runnable 接口,在 run 办法里调用协定解决组件 Processor 进行解决。为了进步解决能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)

  • Processor

如果说 Endpoint 是用来实现 TCP/IP 协定的,那么 Processor 用来实现 HTTP 协定,Processor 接管来自 Endpoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器解决,Processor 是对应用层协定的形象。

Processor 是一个接口,定义了申请的解决等办法。它的形象实现类 AbstractProcessor 对一些协定共有的属性进行封装,没有对办法进行实现。具体的实现有 AjpProcessor、Http11Processor 等,这些具体实现类实现了特定协定的解析办法和申请解决形式。

咱们再来看看连接器的组件图:

从图中咱们看到,Endpoint 接管到 Socket 连贯后,生成一个 SocketProcessor 工作提交到线程池去解决,SocketProcessor 的 run 办法会调用 Processor 组件去解析应用层协定,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 办法。到这里咱们学习了 ProtocolHandler 的总体架构和工作原理

Adapter 组件

后面说过,因为协定不同,客户端发过来的申请信息也不尽相同,Tomcat 定义了本人的 Request 类来“寄存”这些申请信息。ProtocolHandler 接口负责解析申请并生成 Tomcat Request 类。然而这个 Request 对象不是规范的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 设计者的解决方案是引入 CoyoteAdapter,这是适配器模式的经典使用,连接器调用 CoyoteAdapter 的 sevice 办法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 service 办法。

讲完了 Tomcat 的外围组件:连接器,上面咱们紧接着来看下另外一个外围组件和容器

容器,顾名思义就是用来装载货色的用具,在 Tomcat 里,容器就是用来装载 Servlet 的。那 Tomcat 的 Servlet 容器是如何设计的呢?

容器

Tomcat 设计了 4 种容器,别离是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。比方下图,你就能看出他们的关系:

你可能会问,为什么要设计成这么多层次的容器,这不是减少了复杂度吗?其实这背地的思考是,Tomcat 通过一种分层的架构,使得 Servlet 容器具备很好的灵活性。

Context 示意一个 Web 应用程序;Wrapper 示意一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet;Host 代表的是一个虚拟主机,或者说一个站点,能够给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下能够部署多个 Web 应用程序;Engine 示意引擎,用来治理多个虚构站点,一个 Service 最多只能有一个 Engine。

你能够再通过 Tomcat 的 server.xml 配置文件来加深对 Tomcat 容器的了解。Tomcat 采纳了组件化的设计,它的形成组件都是可配置的,其中最外层的是 Server,其余组件依照肯定的格局要求配置在这个顶层容器中。

那么,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.comuser.shopping.com,网站管理人员通过 manage.shopping.com 域名拜访 Tomcat 去治理用户和商品,而用户治理和商品治理是两个独自的 Web 利用。终端客户通过 user.shopping.com 域名去搜寻商品和下订单,搜寻性能和订单治理也是两个独立的 Web 利用。

针对这样的部署,Tomcat 会创立一个 Service 组件和一个 Engine 容器组件,在 Engine 容器下创立两个 Host 子容器,在每个 Host 容器下创立两个 Context 子容器。因为一个 Web 利用通常有多个 Servlet,Tomcat 还会在每个 Context 容器里创立多个 Wrapper 子容器。每个容器都有对应的拜访门路,能够通过上面这张图来帮忙了解。

如果有用户拜访一个 URL,比方图中的 http://user.shopping.com:8080/order/buy,Tomcat 如何将这个 URL 定位到一个 Servlet 呢?

1 依据协定和端口号选定 Service 和 Engine。

咱们晓得 Tomcat 的每个连接器都监听不同的端口,比方 Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口。下面例子中的 URL 拜访的是 8080 端口,因而这个申请会被 HTTP 连接器接管,而一个连接器是属于一个 Service 组件的,这样 Service 组件就确定了。咱们还晓得一个 Service 组件里除了有多个连接器,还有一个容器组件,具体来说就是一个 Engine 容器,因而 Service 确定了也就意味着 Engine 也确定了。

2 依据域名选定 Host。

Service 和 Engine 确定后,Mapper 组件通过 URL 中的域名去查找相应的 Host 容器,比方例子中的 URL 拜访的域名是 user.shopping.com,因而 Mapper 会找到 Host2 这个容器。

3 依据 URL 门路找到 Context 组件。

Host 确定当前,Mapper 依据 URL 的门路来匹配相应的 Web 利用的门路,比方例子中拜访的是 /order,因而找到了 Context4 这个 Context 容器。

4 依据 URL 门路找到 Wrapper(Servlet)。

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。我还是通过一张图来解释。

整个调用过程由连接器中的 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 的整体架构蕴含了两个外围组件连接器和容器。连接器负责对外交换,容器负责外部解决。连接器用 ProtocolHandler 接口来封装通信协议和 I / O 模型的差别,ProtocolHandler 外部又分为 Endpoint 和 Processor 模块,Endpoint 负责底层 Socket 通信,Processor 负责应用层协定解析。连接器通过适配器 Adapter 调用容器。
  • Tomcat 设计了多层容器是为了灵活性的思考,灵活性具体体现在一个 Tomcat 实例(Server)能够有多个 Service,每个 Service 通过多个连接器监听不同的端口,而一个 Service 又能够反对多个虚拟主机。一个 URL 网址能够用不同的主机名、不同的端口和不同的门路来拜访特定的 Servlet 实例。
  • 申请的链式调用是基于 Pipeline-Valve 责任链来实现的,这样的设计使得零碎具备良好的可扩展性,如果须要扩大容器自身的性能,只须要减少相应的 Valve 即可
  • 通过对 Tomcat 整体架构的学习,咱们能够失去一些设计简单零碎的基本思路。首先要剖析需要,依据高内聚低耦合的准则确定子模块,而后找出子模块中的变动点和不变点,用接口和形象基类去封装不变点,在形象基类中定义模板办法,让子类自行实现形象办法,也就是具体子类去实现变动点。

本文由 mdnice 多平台公布

正文完
 0