基于Tomcat9.0.21
Tomcat的IO模型
- BIO:一个连接对应一个线程。(在tomcat8.5后被淘汰)
- NIO:一个请求对应一个线程。(多个长连接,有连接,不一定有请求,如果用BIO就会有大量闲置线程。)
- APR:Apache Portable Runtime。Apache可移植运行库,以JNI的形式调用阿帕奇HTTP服务器的核心动态链接库,调用系统底层的IO接口。(不通过java的nio包)
-
NIO概述:Tomcat的NIO是基于I/O复用模型。
可以发现,tomcat6以后利用java对read request body与write response使用了BIO,对read request headers和wait for next request实现了NIO。Tomcat8.5后全面取消BIO
- 为什么要这样设置?nio在网络不好的时候,处理http request head的时候根本不会不浪费socketProcesser线程去等待读(bio是阻塞的,nio直接继续丢到poller池轮询,就绪了再分配socketProcesser线程处理)。另外bio在处理长连接的时候天生就弱势,必须要浪费一个socketProcesser等待读下一次http请求,指导超时才会释放socket(然而tomcat一般默认200个socketProcesser线程,bio超过75%长连接后面就直接当短连接处理就知道bio对长连接处理多弱),而nio是poller处理TCP连接,有数据就绪就分配到线程池处理http;单线程优势啊,几乎无内核用户态切换,不浪费cpu。
-
(Tomcat6.x)执行过程:客户端连接到达 -> nio接收连接 -> nio使用轮询方式读取文本并且解析HTTP协议(单线程) -> 生成ServletRequest、ServletResponse,取出请求的Servlet -> 从线程池取出线程,并在该线程执行这个Servlet -> 把ServletResponse的内容发送到客户端连接 -> 关闭连接。
组件与框架概述
-
以下内容参考:
-
server对应多个service,service中具有connectors(多个connector)和一个contaioner,在tomcat9.x源码中,connector和engine持有service的引用,但是engine和connector之间并没有引用关系。
图中server表示tomcat启动的服务,service表示webapp,每个webapp都包括n组engine和connector,connector负责接收用户请求,engine是逻辑容器。host为主机名,因为虚拟主机的存在,因此host也可能有多个,context就是web.xml转化而来的,wrapper就是servlet。 -
组件详细介绍
- server 读取server.xml中的配置文件,将配置文件加载到Server的实现类StandardServer中。若关闭server,将关闭全部组件。包含方法有获取端口,地址,持有多个service引用。
- service Service接口实现类standardService,逻辑上持有Connector和Container。并且持有向server的引用。service和server是相互关联的,server向外提供访问这些service的接口
- connector,负责处理客户端发来的连接,默认协议有http,https和AJP。主要作用为根据不同请求解析客户端请求,将解析好的请求转发给connector关联的engine容器,即container。这个组件就是IO模型应用的地方,本质上是为了解决请求等待的问题。100000个长连接,不可能开那么多线程,那就开少量的selector线程轮询,将请求放到线程池中执行即可。
以下四个都继承自Container接口,都属于Container
manger用于管理session,resources对每个webaap对应部署结构的封装,loader对每个webapp自有的classloader的封装,mapper封装了请求资源uri和相对应处理wrapper容器的映射关系。
- 以上组件中的cluster用于tomcat管理,Realm实现用户权限管理,pipeline和value利用职责链模式处理pipeline上的各个value。
生命周期、启动、停止
-
tomcat中所有组件都继承LifeCycle接口,lifecycle的抽象实现类中有initt方法,如果当前状态为NEW,那么就将状态转换为INITIALIZING状态并进行初始化,初始化完毕后状态为INITIALIZED。
//LifeCycleBase.java //init方法 @Override public final synchronized void init() throws LifecycleException { if (!state.equals(LifecycleState.NEW)) { invalidTransition(Lifecycle.BEFORE_INIT_EVENT); } try { setStateInternal(LifecycleState.INITIALIZING, null, false); initInternal(); setStateInternal(LifecycleState.INITIALIZED, null, false); } catch (Throwable t) { handleSubClassException(t, "lifecycleBase.initFail", toString()); } } ``
-
以StandardServer为例,他的initInternal方法中调用类加载器加载jar包,这是双亲委派的,然后遍历services组件并init。service组件对其中的connectors和engine还有Executors进行init。
//StandardServer.java //initInternal方法 for (int i = 0; i < services.length; i++) { services[i].init(); }
-
init之后是start,以StandardServer为例,让内部的services调用start,以此类推。
//LifeCycleBase.java //start方法节选 try { setStateInternal(LifecycleState.STARTING_PREP, null, false); startInternal(); if (state.equals(LifecycleState.FAILED)) { // This is a 'controlled' failure. The component put itself into the // FAILED state so call stop() to complete the clean-up. stop(); } else if (!state.equals(LifecycleState.STARTING)) { // Shouldn't be necessary but acts as a check that sub-classes are // doing what they are supposed to. invalidTransition(Lifecycle.AFTER_START_EVENT); } else { setStateInternal(LifecycleState.STARTED, null, false); } }
-
小结:每个组件的生命周期是统一的,init->start->stop->destory,这是由LifeCycleBase统一通知的,每个组件单独实现initInternal、startInternal、stopInternal、destoryInternal,使用方法前先判断LifeCycle枚举类状态,然后根据状态执行不同的生命周期方法,执行完成后改变状态。
-
观察者设计模式。为什么要用观察者设计模式呢?多个观察者对象同时监听一个主体对象,当主体对象作出某种操作时,主体对象会通知观察者,然后观察者作出相应动作。当一个对象改变的同时需要改变其他对象时,可以采用观察者模式,易于扩展且降低耦合。 所以狼来了的故事里,小孩是通知者,也就是subject,大人们是监听者,在大话设计模式里,前台是通知者,同事们是监听者。
//监听者(观察者) //public class EngineConfig implements LifecycleListener @Override public void lifecycleEvent(LifecycleEvent event) { // Identify the engine we are associated with try { engine = (Engine) event.getLifecycle(); } catch (ClassCastException e) { log.error(sm.getString("engineConfig.cce", event.getLifecycle()), e); return; } // Process the event that has occurred if (event.getType().equals(Lifecycle.START_EVENT)) start(); else if (event.getType().equals(Lifecycle.STOP_EVENT)) stop(); } //被监听者(被观察者) server组件 protected void startInternal() throws LifecycleException { fireLifecycleEvent(CONFIGURE_START_EVENT, null); setState(LifecycleState.STARTING); //当被监听者start的时候,监听器会通知所有 protected void fireLifecycleEvent(String type, Object data) { LifecycleEvent event = new LifecycleEvent(this, type, data); for (LifecycleListener listener : lifecycleListeners) { listener.lifecycleEvent(event); } }
-
EngineConfig,HostConfig,ContextConfig都是通过监听器来实现的,达到了配置逻辑和容器的解耦,修改配置逻辑不需要改动容器,修改容器也不用改动配置逻辑。
-
当我们运行tomcat/bin/startup的时候发生了什么?我们相当于调用Bootstrap类的main方法,并传入一个“start”参数,Bootstrap会根据这个字符串反射调用方法。
//Bootstrap.java public static void main(String args[]) { synchronized (daemonLock) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to // prevent a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } } try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } 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); } }
-
在这个这个类中还值得关注一点类加载器。根据我之前的了解,除了webapp的类加载器默认子加载器优先外,其他都还是执行双亲委派的。
private void initClassLoaders() { try { commonLoader = createClassLoader("common", null); if (commonLoader == null) { // no config file, default to this loader - we might be in a 'single' env. commonLoader = this.getClass().getClassLoader(); } //commonLoader是catalinaLoader和sharedLoader的父加载器。根据上面的英文注释,默认情况下这三个加载器的加载范围是同一个,通过查阅配置文件也可以得知,只有common有加载路径,shared和catalina都是没有加载路径的。 catalinaLoader = createClassLoader("server", commonLoader); sharedLoader = createClassLoader("shared", commonLoader); } catch (Throwable t) { handleThrowable(t); log.error("Class loader creation threw exception", t); System.exit(1); } }
catalina.properties关于类加载器配置如下:
common.loader=" c a t a l i n a . b a s e / l i b " , " {catalina.base}/lib"," catalina.base/lib","{catalina.base}/lib/.jar"," c a t a l i n a . h o m e / l i b " , " {catalina.home}/lib"," catalina.home/lib","{catalina.home}/lib/.jar"
server.loader=
shared.loader=源码中解析路径的时候还要做${字符判断的原因就在这里。
-
上文中看到了catalina_base和catalina_home,有什么区别呢?home是安装目录,base是工作目录。home主要包括bin和lib,base包括conf,logs,temps,webapps,work和shared。所以IDEA运行tomcat其实是为每个webapp都复制了一份catalina_base(work,logs,conf),但是公用同一个tomcat的bin和lib,同一个版本但是可以多实例的运行,部署难度降低,不然你就要安装多个tomcat。然后还要再conf->catalina->localhost中放一个工程.xml以表示工程位置在哪里。
-
总结
启动过程为:main方法接收到参数start交给bootstrap类,boostrap类委托给catalina类,catalina类中initserver,一旦开始组件的初始化就一发不可收拾了,就链式将该组件下的所有组件init。然后执行boostrap的start,委托给catalina进行start,然后server先start,之后所有组件start。
请求过程
- tomcat9.x的IO模型
- nio模式是基于Java nio包实现的,能提供非阻塞I/O操作,拥有比传统I/O操作(bio)更好的并发运行性能。在Tomcat9中是默认模式。
- apr模式(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。可以简单地理解为Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高了对静态文件的处理性能。
- 概述
- 启动顺序
- Connector组件的构造方法通过反射创建Http11NioProtocol类实例,这个实例内部有一个NioEndpoint(负责接收请求)和一个ConnectorHandler(负责处理请求)。NioEndpoint和ConnectorHandler在创建Http11NioProtocol的时候被创建出来。
- NioEndPoint包含三个组件:Acceptor(负责监听请求)、Poller(接收监听到的请求socket)、SocketProcessor(Worker,处理socket,本质上委托给ConnectionHandler处理)。
Acceptor
-
Acceptor在NioEndpoint中被创建出来,Acceptor这个类本身是实现Runnable的,因此是一个线程。在9.0.21版本中,这里是直接运行的,在9.x的早期版本中应该还有一个acceptors保存了所有的acceptor。
AbstractNioEndPoint.java protected void startAcceptorThread() { acceptor = new Acceptor<>(this); String threadName = getName() + "-Acceptor"; acceptor.setThreadName(threadName); Thread t = new Thread(acceptor, threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon()); t.start(); } //public class Acceptor<U> implements Runnable
在Acceptor.java -> run()中,
socket = endpoint.serverSocketAccept();
这里的endpoint.serverSocketAccept本质上是调用的serverSocketChannel.accept。再看下一段代码,NioEndpoint->initServerSocket()中,serverSock.configureBlocking(true);
说明是阻塞的,因此Acceptor在监听连接的时候是阻塞的。当有请求过来时,endpoint.setSocketOptions(socket)
,这里是将socket包装起来生成secketWrapper(设置socket发送、接收缓存大小,心跳检测),将SocketChannel包装成NioChannel,调用poller池中的register方法提交给poller。public class NioEndpoint { protected boolean setSocketOptions(SocketChannel socket) { // Process the connection try { // Disable blocking, polling will be used socket.configureBlocking(false); Socket sock = socket.socket(); socketProperties.setProperties(sock); NioChannel channel = null; if (nioChannels != null) { channel = nioChannels.pop(); } if (channel == null) { SocketBufferHandler bufhandler = new SocketBufferHandler( socketProperties.getAppReadBufSize(), socketProperties.getAppWriteBufSize(), socketProperties.getDirectBuffer()); //根据是否是HTTPS协议返回不同的channel的包装类 if (isSSLEnabled()) { channel = new SecureNioChannel(socket, bufhandler, selectorPool, this); } else { channel = new NioChannel(socket, bufhandler); } } else { channel.setIOChannel(socket); channel.reset(); } NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, this); channel.setSocketWrapper(socketWrapper); socketWrapper.setReadTimeout(getConnectionTimeout()); socketWrapper.setWriteTimeout(getConnectionTimeout()); socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests()); socketWrapper.setSecure(isSSLEnabled()); poller.register(channel, socketWrapper); return true; } catch (Throwable t) { ExceptionUtils.handleThrowable(t); try { log.error(sm.getString("endpoint.socketOptionsError"), t); } catch (Throwable tt) { ExceptionUtils.handleThrowable(tt); } } // Tell to close the socket return false; } }
Poller和PollerEvent
public class Poller implements Runnable
,而且每个poller都持有一个selector。那PollerEvent呢?这个类的run方法中进行socketChannel.register(selector,OP_XX)。Poller的run方法中调用events()方法,这个方法就是遍历一个元素的PollerEvent的队列,然后依次run,即依次register。这样每个poller上就注册了全部socketChannel,任何一个socketChannel发来请求的时候,poller都能通过select获得。根据之前JAVA NIO的那一节,select结束阻塞后,可以通过Set keys = selector.SelectedKeys获得selector上所有selectionkey,每个key都可以获得selector和channel。换而言之,poller可以通过select获得所有因为请求而触发的事件,而每个事件都对应着一个NioSocketChannel,这个channel可以读取请求的数据(因为持有SocketWrapper引用)。下一步就是交给SocketProcessor。
SocketProcessor和ConnectionHandler
- 这个小组件主要任务是吧NioSocket这个Channel的socket交给ConnectionHandler这个组件去处理。ConnectionHandler主要是解析http协议,并封装成request和response对象交给CoyoteAdapter。
小结
- 9.0.21版本IO模型的特点为,acceptor和poller都是单线程的,早期版本acceptor是有数组的,9.x早期版本poller也是有数组的,但是9.0.21都换成了单线程,且不交给线程池运行,这两个线程是从EndPoint.startInternal开始就创建并运行的。Acceptor组件负责以阻塞的方式监听连接,监听到后交给NioEndpoint封装NioSocketChannel和SocketWrapper,poller负责创建PointEvent实例(NioSocketChannel是核心)并加入events这个队列中。P.S.虽然PointEvent也是Runnable的实现类,但是他从来没有当做线程执行过,而是在poller的select前,遍历events并全部run一遍来保证注册所有事件。select后,遍历所有的SelectionKey,然后找到对应的NioSocketChannel,并取出socket,交给SocketProcessor。SocketProcessor先尝试从缓存中拿,缓存中不够的话会new一个,缓存的目的是为了避免反复的生成对象和GC,然后交给线程池去执行。这个线程池的阻塞队列是TaskQueue extends LinkedBlockingQueue,核心池为10,最大池为200。但是值得注意的是,虽然acceptor是单线程的,但是一个service有多个connector,因此用于接收客户端连接还是有多个NIO线程,不同的connector的SocketProcessor应该是交给不同线程池去运行,因为无论是executor还是acceptor都是属于某一个connector组件的。
- socketprocessor交给connectionHandler的process方法,处理socket。代码中是socketprocessor的run方法中调用doRun方法,doRun方法中先getHandler,然后connectionHandler.process()->AbstractProcessorLight.process(socketWrapper)->Http11Processor.service()
- reactor模式:事件驱动,有多个并发请求,有一个(或多个)Selector响应请求,并分发给其他线程处理。
Container
-
概览
-
书接上回,SocketProcessor.doRun()->ConnectionHandler.process()->Http11Processor.process()->Http11Processor.service(socketWrapper)->Adapter.service(request,)。根据这个执行链,到了CoytoteAdapter,此时已经封装好了request和response,并且作为参数传到了adapter的service中。这里的request和response还不是servlet中的,但是有转换关系,具体来说,adapter中解析出来的request和response比servlet中的更强
-
PipeLine之间使用valve连接,valve之间又通过单链表的形式组织而成。
PipeLine之间就是职责链,通过父子节点的方式连接。这里有个疑问,父节点如何找到自己的子节点呢?Tomcat的解决方案是,Engine与Host之间被一个Valve连接,这个Valve用于指明下一个子节点是谁,Engine有StrandEngineVavle,关系为:
上图所有的Value都拼错了,应该是Valve。这些Valve可以用户自己写,然后配置在xml中即可,相当于起到了拦截器的作用。Valve的组织形式是单链表。
(1)每个Pipeline都有特定的Valve,而且是在管道的最后一个执行,这个Valve叫做BasicValve,BasicValve是不可删除的;(2)在上层容器的管道的BasicValve中会调用下层容器的管道。
-
每个Valve的作用:
- StandardEngineValve:传递到Host容器中
- AccessLogValve:Host容器1号阀门,用于记录日志
- ErrorReportValve:Host容器2号阀门用于记录异常并封装到response中
- StandardHostValve:从request中找到映射的context容器(说明1个host有多个context),更新session的访问时间
- StandardContextValve:禁止对WEB-INF/META-INF目录下资源的重定向,从request中获取Wrapper(Mapper组件将request映射到正确的servlet)
- StandardServerWrapperValve:调用StandardWrapper的loadServlet方法生成servlet,调用ApplicationFilterFactory生成filter链条。
-
在Wrapper组件中,每个wrapper对应这样一个servlet和一条filterChain。每个请求会通过Mapper组件映射到一个Wrapper中,然后这个请求进入StandardWrapperValve中,先用filterChain进行处理,然后执行service方法。
Mapper
- Mapper组件由service管理,具有MappedHost,MappedContext,MappedWrapper。我们熟知的servlet的映射匹配方式,比如精确匹配,通配符匹配,扩展名匹配就是通过wrapperContext中不同的数组实现的。
- CoyoteAdapter中,之前说到用service方法处理request和response,在9.0.21版本中,service()方法会调用postParseRequest()方法,在此方法中,
connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData());
来找到对应的host,context和wrapper,用于之后BasicValve向下一层容器派发,这些都是保存在request中的。