Tomcat启动分析
Tomcat作为独立的Servlet容器启动时,由引导类Bootstrap启动,Bootstrap中定义了main函数,是Tomcat启动的入口。
在启动的流程中(参数为start),main方法主要做了两件事情:
- 初始化类加载器
- 加载Catalina类并调用start方法
所以整个启动的流程是从Catalina.start开始的。那么可不可以不要Bootstrap直接用Catalina类来启动呢?也是可以的,不过就需要将tomcat依赖的库都加到classpath中,不够灵活。使用Bootstrap引导,Catalina是通过CatalinaLoader加载的,CatalinaLoader的加载路径由配置文件配置,更加灵活。
Catalina.start:
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal(sm.getString("catalina.noServer"));
return;
}
long t1 = System.nanoTime();
// Start the new server
try {
getServer().start();
...以下省略
整个start方法主要也是做了两件事情:
- 创建server
- 调用server.start()
start方法执行完成后,就完成了tomcat的启动,但是整个流程还是相当复杂的,要了解整个流程,必须先对tomcat的各个组件和他们相互之间的联系有基本了解,它们构成了整个tomcat的骨架。引用之前学习的文章中的图片:
可以看到,服务器最顶层的抽象是server对象,一个server可以包含多个service来提供服务,每个service又包含多个Connector和一个Container,其中Connector负责接受请求,处理连接相关的逻辑,将连接数据转化为Request、Response交由Container处理,Container是Servlet容器,负责寻找目标Servlet来处理请求。Container从上到下又分为Engine、Host、Context、Wrapper,一个service包含一个Engine,表示一个完整的容器引擎,一个Engine又可包含一个或多个Host,每一个表示一个抽象主机,一个Host又包含一个或多个Context,每一个表示一个Webapp, 一个Context又包含一个或多个Wrapper,Wrapper是对具体Servlet的包装。
下面回到server的创建和启动,主要就是围绕上述组建进行的。
server对象的创建
server对象的创建是通过Digester来解析server.xml配置文件完成的。server.xml定义来server的结构,对service、connector、Engine、Host、Context等组件进行了配置。Diggest基于Sax对server.xml进行解析,定义了各个节点的事件规则,解析完成后即完成了整个server对象的创建,实现类为StandardServer。
server的启动
Tomcat的组件都实现了Lifecycle接口来对生命周期进行管理。
抽象类LifecycleBase对生命周期的各种方法进行了实现,定义了公共的抽象流程,并定义了模板方法startInternal又具体的类来实现。
standardServer.startInternal:
...
// Start our defined Services
synchronized (servicesLock) {
for (int i = 0; i < services.length; i++) {
services[i].start();
}
}
...
可以看到server.start的实现主要就是对包含的所有service的start进行调用。这里可以猜测一下,service.start的实现,应该也是对包含的connectors和container进行start,具体源码就不贴了。
connector和container的启动时整个服务器启动的重头戏。
connector的启动
前面讲过connector主要是处理客户端连接请求的,主要功能是接受客户端的tcp请求,封装成Request、Response模型给container处理。下面通过源码来看下它启动的流程。
Connector创建的时候需要传入protocol,以确定支持的协议和使用的io模型,默认值是Http11NioProtocol,也就是http1.1的nio实现。connector.start调用了protocolHandler.start,以Http11NioProtocol为例, Http11NioProtocol创建时创建了NioEndpoint,start也是对NioEndpoint进行启动,由此看出connector的io处理都是委托给对应的Endpoint进行处理的。
NioEndPoint在初始化时,会初始化serverSocket:
protected void initServerSocket() throws Exception {
if (!getUseInheritedChannel()) {
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
serverSock.socket().bind(addr,getAcceptCount());
} else {
Channel ic = System.inheritedChannel();
if (ic instanceof ServerSocketChannel) {
serverSock = (ServerSocketChannel) ic;
}
if (serverSock == null) {
throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
}
}
serverSock.configureBlocking(true);
}
创建ServerSocket并绑定设置的端口,此时还未准备接收请求,在启动时才开启线程进行接收的。
NioEndpoint启动的部分代码:
...
// Create worker collection
if (getExecutor() == null) {
createExecutor();
}
initializeConnectionLatch();
// Start poller thread
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
startAcceptorThread();
启动的过程中,会启动三类线程,Executor,Poller,Acceptor。Acceptor线程负责接受客户端的请求,即上面的serverSock.accept操作,连接建立后,会将socket注册到poller线程,等待读写事件触发,Nio就体现在poller线程中,用极少的线程(默认是1个)管理多个连接,一旦poller线程中注册的连接有数据可读,即提交到executor线程池进行处理,这三类线程创建完毕后,connector的创建流程基本就结束了。从这个流程中也对tomcat调优最常用的三个参数有了深入的理解:maxThreads,maxConnections,acceptCount。
maxThreads是创建executor线程池时传入的最大线程数,也就是tomcat最多能同时处理的请求数。
maxConnections是Acceptor线程在accept时判断的连接数,是一个锁,在已建立的连接(包括正在处理的和线程池队列中等待处理的)达到maxConnections时进行阻塞,不再调用accept,此时客户端仍然可以发起连接,还可以请求的连接数由acceptCount决定,acceptCount是传入tcp的backlog,表示在应用层接受连接前,tcp可等待的队列长度,但是这个参数的实现和系统有关,不一定会生效。
container的启动
前面讲过container由一个Engine和它包含的Hosts以及Contexts和Wrappers组成。但是刚创建的server未必都有这些组件,常见的情况是有只有一个Engine和它旗下的一个Host, Host会设置一个appBase目录,在Host启动时对appBase内的war包和目录进行自动部署,每一个项目对应一个Context,然后解析每个项目中的web.xml,将其中定义的Servlet解析出来,创建对于的Wrapper对象。容器类都扩展自ContainerBase,触发生命周期方法时会同时调起子容器对应的生命周期。container的启动由Engine.start触发,依次调用各个子容器的start,完成各自的准备工作。
connector和container都启动完成后,整个服务器的启动也就完成了,下面就可以接受请求进行处理了。