Java后端自顶向下方法——Tomcat初步
(一)了解Tomcat
讲了这么多,终于讲到我们的Tomcat(不就是汤姆猫?我也不知道为啥要起这个名字)了。首先我们要明白Tomcat到底是什么?Tomcat其实就是就是一个Servlet容器。一个Servlet容器对于发送到每个Servlet的HTTP请求会完成以下事情:
(1)当Servlet 第一次被调用的时候,加载了Servlet类并调用它的init方法(仅调用一次)
(2)响应每次请求的时候,构建ServletRequest和ServletResponse实例。
(3)调用Servlet的service方法,将ServletRequest对象和ServletResponse对象当作参数传入。
(4)当servlet类关闭的时候,调用Servlet的destroy方法,并卸载Servlet类。
因此,Tomcat和Servlet是密不可分的,了解Servlet才能帮助我们更好的了解Tomcat。首先我先把Tomcat的原本的配置文件(注释已经都去掉了)写在这,方便参考:
<?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>
(二)Tomcat顶层结构
我们先来看这张图,里面有Server、Service、Connector、Container之类的东西,我们就挑选一些重要的来讲。
1. Server
Server是服务器的意思,代表整个Tomcat服务器,一个Tomcat只有一个Server。Server是Tomcat最顶层的容器,Server中包含至少一个Service组件,用于提供具体服务。Tomcat中其标准实现是:org.apache.catalina.core.StandardServer类。
我们可以思考一下,我们的Tomcat服务器主要要实现哪些功能。首先是接收客户端的请求,然后解析请求,完成相应的业务逻辑,然后把处理后的结果返回给客户端。服务器一般需要提供两个基本方法,一个start打开服务Socket连接,监听服务端口,一个stop停止服务释放网络资源。
但是这种将监听请求可处理请求放在一起的思路会影响扩展性,比如我们不想用HTTP了,我们想换一个网络协议,那我们的Tomcat就需要大量的修改,显然是很不方便的。所以,Tomcat开发者将监听请求和处理请求分开,这就有了我们的Connector模块和Container模块。
2. Connector
Connector是连接器,用于监听请求并将请求封装成ServletRequest和ServletResponse对象,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端。一个Connector会监听一个独立的端口来处理来自客户端的请求。server.xml中默认配置了一个Connector,端口号为8080。
其实Connector结构非常复杂,不过我们暂时不需要了解这么多,我们只需要知道他的功能即可,详细的东西我会在Tomcat源码分析中讲。
3. Container
我们可以看到,Container实际上是一个Engine,一个Engine里面可以有多个Host,一个Host里面有多个Context,每个Context里面又有多个Wrapper,而每个Wrapper里面都有一个Servlet。这就是Container大体的结构。下面我们来看看细节:
首先我们来看Engine,Engine表示整个Servlet引擎,一个Engine下面可以包含一个或者多个Host,即一个Tomcat实例可以配置多个虚拟主机,默认的情况下server.xml配置文件中<Engine name=“Catalina” defaultHost=“localhost”> 定义了一个名为Catalina的Engine。
然后是Host,代表一个站点,也可以叫虚拟主机,一个Host可以配置多个Context,在server.xml文件中的默认配置为<Host name=“localhost” appBase=“webapps” unpackWARs=“true” autoDeploy=“true”>。一个Engine包含多个Host的设计,使得一个服务器实例可以承担多个域名的服务,是很灵活的设计。
紧接着是最重要的Context。Context代表一个应用程序,也就是我们开发的应用,或者一个WEB-INF目录以及下面的web.xml文件,换句话说每一个运行的webapp最终都是以Context的形式存在,每个Context都有一个根路径和请求路径。Context与Host的区别是Context代表一个应用,默认配置下webapps下的每个目录都是一个应用,其中ROOT目录中存放主应用,其他目录存放别的子应用,而整个webapps是一个站点,也就是Host。
最后是Wrapper,在Tomcat中Servlet被称为Wrapper,至于为什么要用Wrapper,这与Tomcat的处理机制有关,现在暂时不用了解。
4. Service
很显然,Tomcat可以有多个Connector,同时Container也可以有多个。那这就存在一个问题,哪个Connector对应哪个Container?相信看过server.xml文件的人已经知道了Tomcat是怎么处理的了。
没错,Service就是这样来的。在server.xml文件中,可以看到Service组件包含了Connector组件和Engine组件(Engine就是一种Container),即Service相当于Connector和Engine组件的包装器,将一个或者多个Connector和一个Engine建立关联关系。
在默认的配置文件中,定义了一个叫Catalina的service,它将HTTP/1.1和AJP/1.3这两个Connector与一个名为Catalina的Engine关联起来。一个Server可以包含多个Service,它们相互独立,一个Service负责维护多个Connector和一个Container。
下面两张图帮我们将Tomcat结构总结一下:
我们要记住的就是各个模块的作用,以及所有模块的层次关系,这样在Tomcat出现问题的时候我们才能快速定位到错误的位置和原因。
(三)Tomcat启动流程
你一定会很好奇Tomcat是如何启动的,为什么在idea里面鼠标一点就可以启动我们的服务器?在这段时间中Tomcat又为我们做了什么?想了解这些,我们有必要来看看Tomcat的启动流程。
因为具体讲启动流程会涉及到源码,但初学阶段不建议查阅源码,我们就用图形大概的看看就行。以后我会出Tomcat源码分析来具体讲这个问题。
由于Tomcat组件之间的包含关系,一个组件中可以包含多个子组件,且这些组件都实现了Lifecycle接口。因此只要启动最上层的server组件,它包含的所有组件也会跟着启动了,例如启动server,所有子service都会跟着启动,service启动了,它的Container和Connector等也会跟着启动。因此,tomat要启动,只需要启动server就可以了。那我们的Server如何启动呢?这就需要Catalina类。
Catalina是整个Tomcat的管理类,他有三个方法load、start、stop分别用来管理整个服务器的生命周期。load方法用于加载Tomcat/conf目录下的server.xml配置文件,用来创建Server并调用Server的init方法进行初始化操作,start用于启动服务器,stop用于停止服务器,start和stop方法在内部分别调用Server的start和stop方法,load方法在内部调用了Server的init方法,这三个方法都会按层次分逐层调用相应的方法。Catalina的启动又依赖Bootstrap。
Bootstrap是main方法所在地,是Tomcat真正的入口类,我们可以简单看一下main方法里面有什么:
public static void main(String args[]) {
//首先判断Bootstrap daemon 是否为空,就是创建一个Bootstrap实例daemon
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init(); //初始化ClassLoader,并用ClassLoader创建了一个Catalina实例,并将这个实例赋值给了cataLinaDaemon
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
try {
String command = "start"; //指定默认的执行指令是start
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args); //BootStrap的load方法,其实是调用Calatina的load方法
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true); //setWait方法 主要是为了设置Server启动完成后是否进入等待状态的标志,如果为true则进入,否则不进入
daemon.load(args); //load方法用于加载配置文件,创建并初始化Server
daemon.start(); //start方法用于启动服务器
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
这段代码基本人人都能看得懂,就是创建了一个Bootstrap对象并借助init方法初始化了Catalina对象,接着将这个Bootstrap对象赋值给daemon。然后通过判断main函数的参数来决定调用daemon的哪个方法。很显然,如果main函数的参数为start,那么就会调用daemon的start方法,不用看源码都知道,这个方法肯定帮助我们调用了Catalina的start方法,然后启动整个服务器。具体的代码我会在Tomcat源码分析里面详细说明。
2020年5月27日