1.tomcat结构图
2.结构描述
2.1 Server:一个catalina服务器
2.2 Service:服务,负责处理所有Connector所获得的客户请求。由多个Connector和一个Engine组成。
2.3 Connector:连接器,负责在指定端口上监听请求,并将获得的请求交给Engine来处理,从Engine处获得回应并返回客户。Tomcat默认有两个连接器:1.监听http请求的连接器;2.监听ajp请求的连接器。
2.4 Engine:顶层容器。Engine下可以配置多个虚拟主机Virtual Host,每个虚拟主机都有一个域名。
当Engine获得一个请求时,它把该请求匹配到某个Host上,然后把该请求交给该Host来处理。
Engine有一个默认虚拟主机,当请求无法匹配到任何一个Host上的时候,将交给该默认Host来处理。
2.5 Host:它是Engine的子容器,代表一个Virtual Host,虚拟主机,每个虚拟主机和某个网络域名Domain Name相匹配。
每个虚拟主机下都可以部署(deploy)一个或者多个Web App,每个Web App对应于一个Context,有一个Context path。
当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理。
匹配的方法是“最长匹配”,所以一个path==""的Context将成为该Host的默认Context。
所有无法和其它Context的路径名匹配的请求都将最终和该默认Context匹配。
2.6 Context:它是Host的子容器,一个Context对应于一个Web Application,一个Web Application由一个或者多个Servlet组成。
Context在创建的时候将根据配置文件$CATALINA_HOME$/conf/web.xml和$WEBAPP_HOME$/WEB-INF/web.xml载入Servlet类。
当Context获得请求时,将在自己的映射表(mapping table)中寻找相匹配的Servlet类。
如果找到,则执行该类,获得请求的回应,并返回。
2.7 Wrapper:它是针对每个Servlet的Container,是Context的子容器。每个Servlet都有相应的Wrapper来管理。
综上,可以看出Server、Service、Connector、Container、Engine、Host、Context和Wrapper这些核心组件的作用范围是逐层递减,并逐层包含。
3.启动过程
3.1 设置Catalina (Tomcat Servlet 容器的代号)的路径:CATALINA_HOME 和CATALINA_BASE
3.2 初始化Tomcat 的类加载器体系
3.3 创建org.apache.catalina.startup.Catalina 对象(启动阶段剩余的工作由Catalina类 完成)
3.4 初始化Container和Connector。其实就是解析server.xml并将其中对象都实例化起来。
3.5 启动Container和Connector。
跟初始化的顺序一样,先启动所有的Container,然后启动Connector。 Tomcat默认Connector有两个:Http11Protocol和AjpProtocol。
这两种默认Connector都使用JIoEndpoint处理传入的TCP连接,是代码里写死的。
所以如果想使用NioEndpoint,就要在server.xml配置时使用Http11NioProtocol或AjpNioProtocol.(Connector属性protocol=”类全名”即可)
无论使用哪种Endpoint,其启动时都主要是启动这三个线程:Acceptor、Executor和AsyncTimeout线程。
3.5.1 Connector的start方法的最后,会根据已经加载完毕的容器的结构关系,构造出Mapper。供以后请求来临时的定位使用。
(由于先启动了Acceptor,后构造的Mapper,所以在构造出Mapper之前,Acceptor启动了也没意义,因为请求来了也找不到对应的context,不过这倒也没关系,等启动全完了再发请求就行了)
3.6 至此,启动完毕,处于等待请求的状态。
4.请求过程
4.1 浏览器发起请求
4.2 Acceptor接收到请求。
4.3 如果是jioEndpoint处理:
4.3.1 放到线程池执行。getExecutor().execute(new SocketProcessor(wrapper));
4.3.2 执行内容根据当前Connector协议的不同而不同。如果是http11则执行Http11Processor,Ajp则执行AjpProcessor。
4.3.3 如果是Http11Processor处理:
4.3.3.1 首先,获取请求方法,请求的URI和协议名称及版本。
4.3.3.2 调用adapter.service。这里的adapter是Connector初始化时set进来的。默认的也是代码写死的CoyoteAdapter。如果使用自己定义的连接器,就可以重写初始化方法,从而使用另外的adapter了。
4.3.3.3 CoyoteAdapter的service方法中,先构造并填充request对象,然后根据mapper的内容,对request所指向的请求context和wrapper进行定位。
4.3.3.4 定位之后,调用容器的invoke方法,这个容器就是StandardEngine,把request传给容器,交给容器处理了。
4.3.3.5 request传给容器,由于容器是个层次结构,所以应该先由最顶层容器开始,这样才能够准确找到并按正确顺序执行处理。
4.3.3.6 设置响应头信息并把response输出给客户端的方法是: AbstractHttp11Processor.endRequest()。
5.部署流程
5.1 首先,热加载线程ContainerBackgroundProcessor一直在运行着,监控着
5.1.1 StandardHost会一直发出一个代号为periodic的事件,该事件处理中调用HostConfig.check方法。所以这里就是周期性的check
5.2 HostConfig.check方法内容:
5.2.1 check所有已部署的应用。当check出一个context不存在了时,就调用host.removeChild(context)。
5.2.1.1 逐层的stop:context.stop,wrapper.stop。调用context.stop时,会触发删除其所有的wrapper.stop。
5.2.1.2 mapperlistener会接收到removeChild事件。从而与context(及其wrapper)解除绑定。
5.2.2 热加载未部署的应用。当检查出有一个新的context.xml需要deploy时,会new一个DeployDescriptor放到线程池执行。
该线程会调用HostConfig.deployDescriptor:
5.2.2.1 利用digester对context.xml进行解析,得到context实例。
5.2.2.2 把xml和最后修改时间标记到deployedApp.redeployResources
5.2.2.3 host.addChild(context);——方法中会调用该context的start,从而启动其所有的wrapper。
5.2.2.4 mapperlistener也会接收到addChild事件。从而与context(及其wrapper)绑定。
Q&A:
问题1:启动时如何部署的?
答:在启动时就开始运行上面的热加载线程了,是从StandardEngine的start开始运行的。
问题2:为什么在把request传给容器时先调用engine的阀?
答:关于把request传给容器执行时,为何用这种阀嵌套的方式调用。我想这就是像栈的结构,先进后出:先调用engine的阀,然后最后一个engine的阀再调用host阀,逐渐深入到调用servlet返回response,先返回最后调用的,然后逐层返回直到回到调用engine的阀的位置。
把阀分成不同级别,不同级别所处高度不同,做的事肯定不同。
默认的Host级别的阀有这两个:
输出HTML错误页面的阀:ErrorReportValve
记录连接日志的阀:AccessLogValve
假设把日志的阀挪到一个context的管道中去,那么别的context就不能用这个阀记录日志了。
也就是说,这个阀就是在host这个层面的公共处理,当然不能放到别的层面。
问题3:AsyncTimeout线程的作用?
答:只有bio方式下有这个线程。主要是找出当前任务队列中已经超时的request,加上timeout状态标记,交给Executor处理。
问题4:nio与bio的区别?
答:
BIO情况下,获取ServerSocket.accept获取Socket,对每次连接都单独启动一个线程。无论连接是否有真正数据请求,都需要独占一个线程。one connection one thread
NIO情况下,主要是Poller通过Selector对非阻塞的Channel进行select,有数据则表示有请求,然后对请求创建一个线程。这就是one request one thread。这种方式在server端需要支持大量连接但这些连接同时发送请求的峰值不会很多的时候十分有效。
我对nio运行模式的体会:
在第一次建立连接时,是Acceptor线程接收到的,并注册到队列。同时,Poller线程有个Selector一直在对队列中的Channel进行select。所以这个连接以后的请求就都不需要通过Acceptor了,而是Selector在不停的select。
具体点说:nio会启动这两个线程:Acceptor、Poller线程,它们之间通过PollerEvent Queue通信。这是一个生产者消费者的模式。Acceptor是events queue的生产者,接收到请求SocketChannel封装到PollerEvent Queue中,Poller是events queue的消费者,从Queue中取出event对象然后找到socket,再生成任务给Worker做。
注意一点,Acceptor和Poller这块只是建立连接,并不会读取数据。读取数据是在Worker的线程里。
bio直接获取socket,然后不管有没有请求,都new一个线程处理,并且线程一直占着。
nio获取channel,并监听之,一旦来数据了,再new线程处理。
channel设为非阻塞的是为了在Poller对channel进行select时不会阻塞住,读不到数据就能出来。
Poller会一直轮询队列中的channel,直到有了io事件,这时从线程池中拿出一个来运行。
好处,大量连接时,不一定请求频繁,bio模式会一直每个连接不管有没有请求都阻塞着等待请求。而nio模式会只由一个Poller在轮询,有io事件时才启动线程运行之。
举个简单例子:
1000个连接、10个请求。
bio下创建1个acceptor线程和1000个worker线程在阻塞着等待请求数据。
nio下创建1个acceptor、1个poller线程和10个worker线程即可。
问题6:对session的管理?
答:
创建session
方法是这个:ManagerBase.createSession
Http11Processor会从request中解析出“jsessionid”,具体的解析过程为先从request的URL中解析,如果解析不出来,再从cookie中解析相应的jsessionid。
servlet会根据刚才解析得到的jsessionid,从session池中获取相应的session对象。如果jsessionid为空,可以创建一个新的,放到session池中。
然后把jsessionid写入cookie通过响应头的方式发送给客户端。
注销session
主动注销:用户可以调用Session的invalidate方法主动注销。该方法中调用实际的注销方法expire()。
超时注销:后台ContainerBackgroundProcessor线程检查session有效性。该线程会周期性地执行session的processExpires方法,检查session有效性,如果超时,就注销。具体代码为:对每个session调用isValid方法。
持久化及启动加载session
这块就是在关闭服务器时,把session持久化到硬盘;在启动服务器时,把session从硬盘加载到内存。
对应的StandardManager中的方法为doLoad和doUnload
问题5:研究mapperListener和jmx相关处理。
答:tomcat中使用jmx是为了可以监控和动态管理其中的对象。
要管理的对象已经都继承了LifecycleMBeanBase类,当初始化时,会注册到Mbean服务器,stop时,又会从Mbean服务器unregister
关于请求在mapper中找到对应的servlet这个过程中如何用到jmx,还需要再研究。因为始终没看到在如下这方法里有用到jmx:
根据mapper内容定位servlet的方法:connector.getMapper().map(serverName, decodedURI, version,request.getMappingData());
mapper中数据怎么来的呢?向mapper中addContext的堆栈如下所示:
集群的配置
配置成功了apache+tomcat集群。
根据这个文章http://www.iteye.com/topic/1017961
这个文章写的真的非常准确和有效。我概括一下:
1.首先要准备好tomcat、apache和jk插件
2.然后在Apache安装目录的conf下修改httpd.conf文件,增加一个include一个新文件mod_jk.conf。
这个mod_jk.conf就是jk插件相关的,这里面指定了插件文件名和插件的properties文件名,即workers.properties
这个workers.properties里面配置的就是集群的tomcat节点的信息,以及负载均衡器的几个参数。
3.这两个tomcat节点本身的安装目录中的server.xml也要改一下。具体就不说了。
4.这是最容易遗漏的重点,没有它就不能成功——部署的应用的web.xml中要增加标签。表示是分布式的意思。没有它就不好使。
最后说一下配置原理:就是tomcat本身配置一下ajp通信的端口和自身标识。然后apache中配置与其对应上即可。