上篇文章,我们自定义实现了一个简单的不能再简单的Tomcat容器,大致了解了Tomcat的工作主要功能。本篇文章我们就来看一下正主——Tomcat,每一个从事Java Web开发的都需要了解的基础工具。
- Server:Tomcat顶层容器,可以包含一个或多个Service组件
- Service:Server内部的中间组件,包含Connector和Container两个核心组件,负责将一个或多个Connector组件绑定在一个Container上
- Connector:用于处理连接相关的事情,并提供Socket与Request和Response相关的转化
- Container:用于封装和管理Servlet,以及具体处理Request请求
1. Tomcat系统架构
上面的图已经能够很清楚的表述Tomcat系统架构,我们来总结一下。
一个Tomcat容器中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors(这是因为一个服务可以有不同协议,比如同时提供Http和Https链接;一个服务也可以提供相同协议的不同端口的链接)。
多个Connector和一个Container组成一个Service,有了Service就可以对外提供服务了,但是Service还要一个生存的环境,必须有人掌管他的生命周期,这就是Server的作用,所以整个Tomcat的生命周期由Server控制。我们来看一下Tomcat的配置文件server.xml。
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
Server标签设置的端口号为8005,shutdown=”SHUTDOWN” ,表示在8005端口监听“SHUTDOWN”命令,如果接收到了就会关闭Tomcat。一个service中可以有多个Connector,一个Connector会绑定一个port和protocol,所以如果一个应用需要通过相同协议的多个端口访问,就可以通过声明多个Connector实现。如果不同应用希望通过不同端口访问,就可以通过声明多个Service来实现。
一个请求发送到Tomcat之后,首先经过Service然后会交给Connector,Connector用于接收请求并将接收的请求封装为Request和Response来具体处理,Request和Response封装完之后再交由Container进行处理,Container处理完请求之后再返回给Connector,最后在由Connector通过Socket将处理的结果返回给客户端,这样整个请求的就处理完了。
2. Tomcat配置说明
我们日常接触Tomcat,配置的一般都是Service下的节点,所以我们这里重点介绍该节点以及该节点下的子节点。
- Server:声明一个服务器,提供了一种优雅的方式来启动和停止整个系统,不必单独启停连接器和容器
- Service:声明一个服务,Server中可以运行多个服务。比如一个Tomcat里面可运行订单服务、支付服务、用户服务等等。每个Service中可以包含
多个Connector
和一个Container
。因为每个服务允许同时支持多种协议,但是每种协议最终执行的Servlet却是相同的 - Connector:连接器,用于处理容器连接相关的事情,比如端口绑定,协议绑定。比如一个服务可以同时支持AJP协议、Http协议和Https协议,每种协议可使用一种连接器来支持
- Container:Servlet容器,用于封装和管理Web应用
- Engine:引擎
- Host:声明一个虚拟主机
- name:虚拟主机名
- appBase:应用程序基本目录
- Context:表示一个web应用程序
- docBase:应用程序的路径或者是WAR文件存放的路径
- path:web应用程序访问的url的前缀
- reloadable:如果为true,则tomcat会自动检测应用程序的/WEB-INF/lib 和/WEB-INF/classes目录的变化,自动装载新的应用程序,我们可以在不重起tomcat的情况下改变应用程序
- Host:声明一个虚拟主机
- Engine:引擎
- Service服务中还有各种支撑组件
- Manager:管理器,用于管理会话Session
- Logger:日志器,用于管理日志
- Loader:加载器,和类加载有关,只会开放给Context使用
- Pipeline:管道组件,配合Valve实现过滤器功能
- Valve:阀门组件,配合Pipeline实现过滤器功能
- Realm:认证授权组件
在学习Tomcat过程中,Container和Context毋庸置疑是最重要的,除此之外,管道组件和阀门组件对我们是Context处理请求的核心流程,我们下面会详细介绍。
2.1 appBase
<Host>
中的appBase
,其作用是指定Tomcat启动时,第一加载的项目,默认的为 webapps ,它代表的意思是加载Tomcat根目录下webapps文件夹中的项目,也就是Tomcat的管理页。
将项目作为默认启动项目时,需要注意的的是,appBase指定路径的项目,需要包含有ROOT的文件夹,但不需要写进XML中。比如我们配置appBase为”/Users/zhuoli/Documents/demo”:
<Host name="localhost" appBase="/Users/zhuoli/Documents/demo" unpackWARs="true" autoDeploy="true">
</Host>
则表明/Users/zhuoli/Documents/demo目录下,肯定存在一个ROOT文件夹,改文件夹下存在一个Tomcat默认的Web应用。
2.2 docBase
<Context>
中的docBase
,其作用是当需要发布多个项目时,可以在这里设置项目路径,值可以是绝对路径,也可以时相对于appBase的路径,这里我们只写绝对路径。比如我们有另一个项目demo1,存放地址是”/Users/zhuoli/Documents/demo1″,则相应的配置可以写为:
<Host name="localhost" unpackWARs="true" autoDeploy="true" appBase="webapps">
<Context docBase="/Users/zhuoli/Documents/demo1" path=" " debug="0" reloadable="true"/>
</Host>
这里appBase="webapps"
,因为我们不去关心默认启动项目,而且我们这里的path=" "
,当path=" "
时,docBase所指向的项目就是第一加载项目,访问http://localhost:8080
时,指向的是docBase
所指向的项目(这里不需要存在ROOT文件夹)
2.3 path
<Context>
中的path
,其作用是指定Context Web应用访问的前缀,和项目本身没有关系,可以是任意值,比如:
<Host name="localhost" unpackWARs="true" autoDeploy="true" appBase="webapps">
<Context docBase="/Users/zhuoli/Documents/demo1" path="project1" debug="0" reloadable="true"/>
</Host>
那么要访问demo1时,需要访问http://localhost:8080/project1
。(这里需要注意的是,因为我们的path不是” “,所以Tomcat的默认项目还是appBase指定的)。
2.4 配置示例
2.4.1 一个Web应用,多个端口
我们可以再一个Service节点下配置多个Connector实现,如下:
<Service name="Catalina">
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
<Connector port="8099" protocol="HTTP/1.1" maxThreads="150" minSpareThreads="25" maxSpareThreads="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" URIEncoding="utf-8" disableUploadTimeout="true"/>
<Engine defaultHost="localhost" name="Catalina">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false"></Host>
</Engine>
</Service>
在这个应用里,可以用8080端口号访问服务,也可以用8099端口号来访问服务。即:http://localhost:8080/examples和http://localhost:8099/examples访问的其实是一个项目。
2.4.2 多个Web应用,多个端口
比如我们有多个Web应用,部署在同一台机器上,希望使用不同的端口访问。这时候可以通过声明多个Service,每个Service绑定不同的端口,将多个Web应用分别丢在各自的Service中。
<Service name="Catalina">
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
<Engine defaultHost="localhost" name="Catalina">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false"></Host>
</Engine>
</Service>
<Service name="Catalina1">
<Connector connectionTimeout="20000" port="8099" protocol="HTTP/1.1" redirectPort="8443"/>
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
<Engine defaultHost="localhost" name="Catalina1">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
<Host appBase="webapps1" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false"></Host>
</Engine>
</Service>
<Service name="Catalina2">
<Connector connectionTimeout="20000" port="8098" protocol="HTTP/1.1" redirectPort="8443"/>
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
<Engine defaultHost="localhost" name="Catalina2">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
<Host appBase="webapps2" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false"></Host>
</Engine>
</Service>
以上三个service,发布的路径不同,项目分别发布在webapps、webapps1、webapps2下,访问不同的项目的方法:http://localhost:8080/project1、http://localhost:8099/project2、http://localhost:8088/project3。
2.4.3 一个域名,多个项目
域名是配置在Host节点的,所以如果需要实现一个域名访问多个项目,只需要配置一个Host,该Host节点下配置多个Context即可。
<Host appBase="webapps" autoDeploy="true" name="www.lidol.top" unpackWARs="true">
<!--访问路径:www.lidol.top/demo1-->
<Context docBase="/Users/zhuoli/Documents/demo/demo1" path="/demo1" reloadable="true"/>
<!--访问路径:www.lidol.top/demo2-->
<Context docBase="/Users/zhuoli/Documents/demo/demo2" path="/demo2" reloadable="true"/>
</Host>
2.4.4 多个域名,多个项目
多个域名,多个项目,只需要配置多个Host节点即可。
<!-- 访问路径:www.lidol.top -->
<Host name="www.lidol.top" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Context path="/" docBase="/Users/zhuoli/Documents/demo/demo3" reloadable="true" />
</Host>
<!-- 访问路径:www.lidol2.top -->
<Host name="www.lidol2.top" appBase="webapps" unpackWARs="true" autoDeploy="true">
<Context path="/" docBase="/Users/zhuoli/Documents/demo/demo4" reloadable="true" />
</Host>
3 Connector架构
通过上面的介绍我们知道,Connector用于接受请求并封装Request和Response,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端。因此在了解Connector架构图时,可以划分为以下几点:
- Connector如何接受请求的?
- Connector如何封装Request和Response的?
- 封装完Request和Response之后,如何交给Container进行处理的?
Connector就是使用ProtocolHandler来处理请求的,不同的ProtocolHandler代表不同的连接类型,比如:Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。ProtocolHandler由包含了三个部件:Endpoint、Processor、Adapter。
Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成Request,Adapter用于将Request交给Container进行具体的处理。
Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来封装Http Request和Response,所以必须实现HTTP协议,Adapter将请求适配到Servlet容器进行具体的处理。
Endpoint的抽象实现AbstractEndpoint里面定义的Acceptor和AsyncTimeout两个内部类和一个Handler接口。Acceptor用于监听请求,AsyncTimeout用于检查异步Request的超时,Handler用于处理接收到的Socket,在内部调用Processor进行处理。
这样,其实上述的三个问题就可以解释了。
4 Container系统架构
通过上面的介绍我们知道,Container是用来处理请求的,内部封装了很多Servlet。Container处理请求是使用Pipeline-Valve(管道-阀门)来处理的。
Pipeline-Valve是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将处理后的请求返回,再让下一个处理着继续处理。但Pipeline-Valve使用的责任链模式和普通的责任链模式有些不同!区别主要有以下两点:
- 每个Pipeline都有一个特殊的Valve,该Valve在管道最后一个执行,这个Valve叫做BaseValve,BaseValve是不可删除的
- 上层容器的管道的BaseValve中会调用下层容器的管道
Container中包含四个子容器,而这四个子容器对应的BaseValve分别为:StandardEngineValve、StandardHostValve、StandardContextValve、StandardWrapperValve。Container Pipeline的处理流程图如下:
- Connector在接收到请求后会首先调用最顶层容器的Pipeline来处理,这里的最顶层容器的Pipeline就是EnginePipeline(Engine的管道)
- 在Engine的管道中依次会执行EngineValve1、EngineValve2等等,最后会执行StandardEngineValve,在StandardEngineValve中会调用Host管道,然后再依次执行Host的HostValve1、HostValve2等,最后在执行StandardHostValve,然后再依次调用Context的管道和Wrapper的管道,最后执行到StandardWrapperValve
- 当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的Filter和Servlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法,这样请求就得到了处理
- 当所有的Pipeline-Valve都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给Connector了,Connector在通过Socket的方式将结果返回给客户端
参考链接: