tomcat启动流程分析
前言
tomcat究竟在启动的时候做了哪些事情呢?从直观上讲,当tomcat启动完毕后,部署在tomcat里的项目,就可以通过外部的http形式进行访问了,但有个疑问是,为什么可以访问呢?其内部都做了哪些准备工作呢?本篇通过源码来了解一下tomcat启动的过程吧
通过上一篇的分析,我们初步了解了tomcat内部的基本构成,包括的基本组件,这些组件的配合构成了tomcat的逻辑上的架构
从大的方面划分,tomcat在启动过程中,主要完成了2件事情,第一初始化容器组件,第二启动相关的线程等待读写事件的接入
tomcat 的IO模型
从tomcat8之后,tomcat默认使用的是NIO,即异步IO,我们知道,在tomcat8之前的版本中有大多使用同步IO,同步IO和异步IO相比,在高并发的请求处理场景中性能相差非常大,这个得益于NIO的底层特殊的线程处理机制,即reactor模型
reactor模型
简单解释来说,reactor模型将一个请求的处理过程划分成不同的步骤,在bio中,一个请求过来了,tomcat提供一个线程处理,如果请求没有处理完毕,这个线程将一直阻塞,但通过NIO的方式,将工作线程和接收请求的线程分开,并通过非阻塞的IO去处理,就大大提升了整体的性能和吞吐量
小编从实际的压测结果来看,使用tomcat7的BIO的方式,和tomcat8的NIO的方式在相同条件下进行压测,tomcat8的吞吐量比tomcat7要多出1.5倍,这只是在本地环境模拟的效果
tomcat8默认使用的是NIO的方式,在server.xml中可以找到下面的这段配置,即在HTTP/1.1的情况下
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
我们可以修改protocol属性使用NIO2 (异步非阻塞)
<Connector port="8080" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443" />
简单了解了tomcat8的IO模型,我们正式来看tomcat的启动流程,初始化各组件+线程就绪,即init+start
关于init过程
回忆上一篇我们谈到的关于tomcat的基本组件的构成,见下面这张图
其实不难理解,tomcat在启动过程中,也是按照server.xml中各个组件标签的顺序依次进行初始化的(各个组件的作用见上一篇)
其大概的步骤如下:
- 调用 bin/startup.bat (在linux 目录下 , 需要调用 bin/startup.sh) , 在startup.bat 脚本中, 调用了catalina.bat
- 在catalina.bat 脚本文件中,调用了BootStrap 中的main方法
- 在BootStrap 的main 方法中调用了 init 方法 , 来创建Catalina 及 初始化类加载器
- 在BootStrap 的main 方法中调用了 load 方法 , 在其中又调用了Catalina的load方法
- 在Catalina 的load 方法中 , 需要进行一些初始化的工作, 并需要构造Digester 对象, 用 于解析 XML
- 然后在调用后续组件的初始化操作(见时序图中的组件init过程)
- 加载Tomcat的配置文件,初始化容器组件 ,监听对应的端口号, 准备接受客户端请求
源码分析,从程序中的Bootstrap入手,
我们找到main方法
在学习源码之前,我们有必要先简单了解一下时序图中主要组件的类结构,由于所有的组件均存在初始化、启动、停止等生命周期方法,拥有生命周期管理的特 性, 所以Tomcat在设计的时候, 基于生命周期管理抽象成了一个接口 Lifecycle ,而组 件 Server、Service、Container、Executor、Connector 组件 , 都实现了一个生命周期 的接口,从而具有了以下生命周期中的核心方法:
- init():初始化组件
- start():启动组件
- stop():停止组件
- destroy():销毁组件
我们不妨随意找一个组件,像connector来说,其*接口还是Lifecycle
各组件的默认实现
上面我们提到的Server、Service、Engine、Host、Context都是接口。当前对于 Endpoint组件来说,在Tomcat中没有对应的Endpoint 接口, 但是有一个抽象类 AbstractEndpoint ,其下有三个实现类: NioEndpoint、 Nio2Endpoint、AprEndpoint , 这三个实现类,分别对应于前面讲解链接器 Coyote 时, 提到的链接器支持的三种IO模型:NIO,NIO2,APR , Tomcat8.5版本中,默认采 用的是 NioEndpoint
Server
Service
Engine
Host
Context
从各个组件的类的结构图也可以看出,它们都纳管于Lifecycle的生命周期的过程
源码启动入口
Bootstrap ->main()
从main方法入手,可以分为2步,第一步init()的过程,即初始化上述时序图中各个组件的过程
第二步,start()过程,调其reactor涉及到的各个线程,下面通过断点简单看其中几个组件的init()过程吧
调起catalina的getServer().init()方法
调起server的init()
在initInternal的init方法中,可以看到像globalNamingResources这样的方法初始化,其实就是在依次解析server.xml中的标签
来到本方法最后一段,看到调用了service的init()方法
调起service的init()方法,该方法中要初始化的组件比较多,对应于server.xml中的各个层次的标签,举例来说,一个service里面可以配置多个connector,那么就需要在这里进行初始化
跳过中间的engine,host和context,我们最后直接来到connector的初始化
调起AbstractProtocol的init()方法
最终来到endpoint的init()
这段代码中,我们似乎看到了socket的痕迹,在前一篇提到,EndPoint是 Coyote的通信端点,即通信监听的接口,是具体Socket接收和发送处理 器是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的
如果继续点进bind方法,可以看到其实就是在创建并绑定相应的连接信息
那么到这里,初始化的工作就基本完成了,具体的各个组件中初始化代码块和要完成的详细工作,有兴趣的同学可以依次步骤点进去一探究竟
下面的start()过程按照时序图,参照上面的过程我们仍然走一遍流程,最终走到
AbstractProtocol的start()方法,
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}
来到这里,其实很容易看出来,Acceptor属于一个线程类,继续进入,最终来到NioEndPoint的Acceptor方法中,
protected class Acceptor extends AbstractEndpoint.Acceptor {
@Override
public void run() {
int errorDelay = 0;
// Loop until we receive a shutdown command
while (running) {
// Loop if endpoint is paused
while (paused && running) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}
if (!running) {
break;
}
state = AcceptorState.RUNNING;
try {
//if we have reached max connections, wait
countUpOrAwaitConnection();
SocketChannel socket = null;
try {
// Accept the next incoming connection from the server
// socket
socket = serverSock.accept();
} catch (IOException ioe) {
// We didn't get a socket
countDownConnection();
if (running) {
// Introduce delay if necessary
errorDelay = handleExceptionWithDelay(errorDelay);
// re-throw
throw ioe;
} else {
break;
}
}
// Successful accept, reset the error delay
errorDelay = 0;
// Configure the socket
if (running && !paused) {
// setSocketOptions() will hand the socket off to
// an appropriate processor if successful
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else {
closeSocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
在run方法中我们注意到下面的这句,翻译过来,就是等待请求的连接,
总结一下,到这里tomcat通过初始化和start的过程,完成加载容器依赖的各个组件的初始化,并启动IO线程,等待外部请求的接入,下一步,就是具体的请求过来之后如何进行处理的过程,在后续的文章中会继续提到,本篇到此结束,最后感谢观看!