大家好,我是易安!
明天咱们就来一步一步剖析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.com
和 user.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 containerconnector.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多平台公布