骏马金龙 (新博客:www.junmajinlong.com)

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!

Tomcat(三):tomcat处理连接的详细过程

Tomcat系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html


tomcat可以处理静态资源的请求,也可以通过servlet处理动态资源的请求。处理jsp动态资源时,先通过jasper组件(具体的是JspServlet)将jsp翻译成java源代码并编译成class后运行。需要知道的是,静态资源也一样是通过servlet处理的,只不过它使用的servlet是定义在$catalina_home/conf/web.xml中默认的servlet。本文将详细分析tomcat如何处理客户端请求(并发)以及如何处理动、静态资源。

1.Tomcat组件体系结构

如下两图:上面的图是tomcat组件体系的简图,下面的图是Service组件细化后的图。

其中:

  1. server组件是管理tomcat实例的组件,可以监听一个端口,从此端口上可以远程向该实例发送shutdown关闭命令。
  2. service组件是一个逻辑组件,用于绑定connector和container,有了service表示可以向外提供服务,就像是一般的daemon类服务的service。可以认为一个service就启动一个JVM,更严格地说,一个engine组件才对应一个JVM(定义负载均衡时,jvmRoute就定义在Engine组件上用来标识这个JVM),只不过connector也工作在JVM中。
  3. connector组件是监听组件,它有四个作用:
    • (1).开启监听套接字,监听外界请求,并建立TCP连接;
    • (2).使用protocolHandler解析请求中的协议和端口等信息,如http协议、AJP协议;
    • (3).根据解析到的信息,使用processer将请求数据转发给绑定的Engine;
    • (4).接收响应数据并返回给客户端。
  4. container是容器,它是一类组件,在配置文件(如server.xml)中没有体现出来。它包含4个容器类组件:engine容器、host容器、context容器和wrapper容器。
  5. engine容器用于从connector组件处接收转发过来的请求,然后按照分析的结果将相关参数传递给匹配出的虚拟主机。engine还用于指定默认的虚拟主机。
  6. host容器定义虚拟主机,由于tomcat主要是作为servlet容器的,所以为每个webapp指定了它们的根目录appBase。
  7. context容器主要是根据path和docBase获取一些信息,将结果交给其内的wrapper组件进行处理(它提供wrapper运行的环境,所以它叫上下文context)。一般来说,都采用默认的标准wrapper类,因此在context容器中几乎不会出现wrapper组件。
  8. wrapper容器对应servlet的处理过程。它开启servlet的生命周期,根据context给出的信息以及解析web.xml中的映射关系,负责装载相关的类,初始化servlet对象init()、执行servlet代码service()以及服务结束时servlet对象的销毁destory()。
  9. executor组件为每个Service组件提供线程池,使得Engine可以从线程池中获取线程处理请求,从而实现tomcat的并发处理能力。一定要注意,Executor的线程池大小是为Engine组件设置,而不是为Connector设置的,Connector的线程数量由Connector组件的acceptorThreadCount属性来设置。如果要在配置文件中设置该组件,则必须设置在Connector组件的前面,以便在Connector组件中使用executor属性来引用配置好的Executor组件。如果不显式设置,则采用Connector组件上的默认配置,默认配置如下:
    • (1).maxThreads:最大线程数,默认值200。
    • (2).minSpareThreads:最小空闲线程数,默认值25。
    • (3).maxIdleTime:空闲线程的线程空闲多长时间才会销毁,默认值60000即1分钟。
    • (4).prestartminSpareThreads:是否启动executor时就直接创建等于最小空闲线程数的线程,默认值为false,即只在有连接请求进入时才会创建。

根据上面描述的tomcat组件体系结构,处理请求的大致过程其实很容易推导出来:

Client(request)-->Connector-->Engine-->Host-->Context-->Wrapper(response data)-->Connector(response header)-->Client

2.Tomcat和httpd/nginx在监听和处理请求上的区别

在监听和处理请求上,tomcat和httpd/nginx等服务程序不一样,而且是巨大的区别。因此,在理解处理请求时,万万不可将httpd/nginx的处理模式套在tomcat上。

关于httpd/nginx等服务程序处理连接时的过程,此处仅简单说明以体现它们和tomcat的不同之处,详细内容可参见我另一篇文章:不可不知的socket和TCP连接过程

(1).httpd/nginx等都是监听进程/线程负责监听,当监听到连接请求时,将生成一个新的已连接套接字放进一个称为已连接队列中,然后监听进程/线程继续回去监听。而负责处理请求的工作进程/线程则从该队列中获取已连接套接字并与客户端建立TCP连接,然后与客户端进行通信,包括接收客户端的资源请求数据、构建和响应数据给客户端。

(2).tomcat虽然也将监听和处理请求的工作分别使用不同的组件进行处理,但connector线程监听到请求就直接建立TCP连接,并一直与客户端保持该连接。connector线程会分析请求并将结果转发给与之绑定的Engine组件,Engine线程负责处理请求以及构建响应数据,但Engine组件不会和客户端建立任何连接。Engine的一切数据来源都是Connector,客户端任何一次资源请求都会发送到connector上,并从connector转发给Engine。Engine构建响应后,再次将响应数据转发给Connector,并由Connector做一些处理(如加上首部字段)回复给客户端。

只要明确一点即可推导出tomcat的连接和请求处理机制:任何一次从外界流入的请求都必将经过connector,任何一次从本地流出的响应数据也都必将经过connector。这正是连接器的意义所在──连接客户端和服务端servlet。

2.1 tomcat如何处理并发请求

connector组件支持4种IO协议类型:同步阻塞BIO、同步非阻塞NIO、异步非阻塞NIO2、apache基金会提供的IO模型APR(IO模型只是APR类库的其中一种功能模块)。它们的区别如下图所示,除了BIO,其他IO模型在接受新请求上都是非阻塞的,因此这里不考虑BIO,而且现在也不会有人将connector设置成BIO模式。

该表中,最需要关注的是"Wait for next Request"行,NIO/NIO2/APR都是Non Blocking,这表示正在处理某个请求时不会被阻塞,可以接收额外的请求,这是tomcat实现并发处理请求的关键。

再来看connector组件和并发数量有关的设置选项:

acceptorThreadCount:用于接收连接请求的线程数。默认值为1。多核CPU系统应该增大该值,另外由于长连接的存在,也应该考虑增大该值。
maxThreads:线程池中最多允许存在多少线程用于处理请求。默认值为200。它是最大并发处理的数量,但不影响接收线程接收更多的连接。
maxConnections:服务端允许接收和处理的最大连接数。当达到该值后,操作系统还能继续接收额外acceptCount个的连接请求,但这些连接暂时不会被处理。当Connector类型为BIO模型时的默认值等于maxThread的值,当为NIO/NIO2模型时的默认值为10000,当APR时默认长度为8192。
acceptCount:当所有请求处理线程都处于忙碌状态时,连接请求将进入等待队列,该值设置等待队列的长度。当达到队列最大值后,如果还有新连接请求进入,则会被拒绝。默认队列长度为100。

从上面几个属性的意义来分析并发机制:

  • (1).connector中最多有acceptorThreadCount个专门负责监听、接收连接请求并建立TCP连接的线程,这些线程是非阻塞的(不考虑BIO)。当和某客户端建立TCP连接后,可以继续去监听或者将Engine返回的数据发送给客户端或者处理其它事情。
  • (2).线程池中的最大线程数maxThreads决定了某一刻允许处理的最大并发请求数,这是专门负责处理connector转发过来的请求的线程,可以认为这些线程专门是为Engine组件服务的(因此我将其称之为Engine线程)。注意,maxThreads决定的是某一刻的最大并发处理能力,但不意味着maxThreads数量的线程只能处理maxThreads数量的请求,因为这些Engine线程也是非阻塞的,当处理某个请求时出现IO等待时,它不会阻塞,而是继续处理其它请求。也就是说,每个请求都占用一个Engine线程直到该客户端的所有请求处理完毕,但每个Engine线程可以处理多个请求。同时还能推测出,每个connector线程可以和多个Engine线程绑定(connector线程的数量远少于Engine线程的数量)。
  • (3).当并发请求数量逐渐增多,tomcat处理能力的极限由maxConnector决定,这个值是由maxThreads和acceptorThreadCount以及非阻塞特性同时决定的。由于非阻塞特性,无论是connector线程还是Engine线程,都能不断接收、处理新请求。它的默认值看上去很大(10000或8192),但分配到每个线程上的数量并不大。假设不考虑监听线程对数量的影响,仅从处理线程上来看,10000个连接分配给200个处理线程,每个处理线程可以轮询处理50个请求。和nginx默认的一个worker线程允许1024个连接相比,已经很少了,当然,因为架构模型不一样,它们没有可比性。
  • (4).当并发请求数量继续增大,tomcat还能继续接收acceptCount个请求,但不会去建立连接,所以也不会去处理。实际上,这些请求不是tomcat接收的,而是操作系统接收的,接收后放入到由Connector创建的队列中,当tomcat有线程可以处理新的请求了再去队列中取出并处理。

再来细分一下tomcat和httpd/nginx的不同点:

  • (1).httpd/nginx的监听者只负责监听和产生已连接套接字,不会和客户端直接建立TCP连接。而tomcat的监听者connector线程不仅会监听,还会直接建立TCP连接,且一直处于ESTABLISHED状态直到close
  • (2).httpd/nginx的工作进程/线程首先从已连接套接字队列中获取已连接套接字,并与客户端建立TCP连接,然后和客户端通信兵处理请求、响应数据。而tomcat的工作线程(Engine线程)只接受来自connector转发过来的请求,处理完毕后还会将响应数据转发回connector线程,由connector将响应数据传输给客户端(和客户端的所有通信数据都必须经过连接器connector来传输)
  • (3).不难推断出,一个Connector线程可以和多个客户端建立TCP连接,也可以和多个Engine线程建立绑定关系,而一个Engine线程可以处理多个请求。如果不理解并发处理机制,这一点很容易被"Connector组件和Engine组件绑定在一起组成Service组件"这句话误导。这句话的意思并不是要求它们1:1对应,就像httpd/nginx也一样,一个监听者可能对应多个工作者。

因此,tomcat处理连接的过程如下图所示,其中我把Engine线程处理请求的过程用"Engine+N"来表示,例如Engine线程1下的Engine1表示该Engine线程处理的某个请求,Engine2表示该线程处理的另一个请求。

3.Tomcat处理jsp动态资源的过程

假设tomcat的配置如下,其中项目名称为"xiaofang"。

<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>

<Engine name="Catalina" defaultHost="localhost">
    <Host name="www.xiaofang.com"  appBase="webapps/xiaofang"
          unpackWARs="true" autoDeploy="true">
        <Context path="" docBase="" reloadable="true" />
        <Context path="/xuexi" docBase="xuexi" reloadable="true" />
    </Host>

    <Host name="localhost"  appBase="webapps"
          unpackWARs="true" autoDeploy="true">
    </Host>
</Engine>

当客户端访问http://www.xiaofang.com:8080/xuexi/abc.jsp时,其请求的是$CATALINA_HOME/webapps/xiaofang/xuexi/abc.jsp文件。

(1).Connector组件扮演的角色。

Connector组件首先监听到该请求,于是建立TCP连接,并分析该请求。Connector分析请求的内容包括请求的协议、端口、参数等。因为这里没考虑集群问题,因此只可能是http协议而不可能是ajp协议的请求。分析后,将请求和相关参数转发给关联的Engine组件进行处理。

(2).Engine组件扮演的角色。

Engine组件主要用于将请求分配到匹配成功的虚拟主机上,如果没有能匹配成功的,则分配到默认虚拟主机上。对于上面的请求,很显然将分配到虚拟主机www.xiaofang.com上。

(3).Host组件扮演的角色。

Host组件收到Engine传递过来的请求参数后,将对请求中的uri与Context中的path进行匹配,如果和某个Context匹配成功,则将请求交给该Context处理。如果匹配失败,则交给path=""对应的Context来处理。所以,根据匹配结果,上面的请求将交给<Context path="/xuexi" docBase="xuexi" />进行处理。

注意,这次的uri匹配是根据path进行的匹配,它是目录匹配,不是文件匹配也就是说,只匹配到uri中的xuexi就结束匹配。之所以要明确说明这一点,是因为后面还有一次文件匹配,用于决定交给哪个Servlet来处理。

(4).Context和Wrapper组件扮演的角色。

到了这里,就算真正到了Servlet程序运行的地方了,相比于前面几个组件,这里的过程也更复杂一些。

请求http://www.xiaofang.com:8080/xuexi/abc.jsp经过Host的uri匹配后,分配给<Context path="/xuexi" docBase="xuexi" />进行处理,此时已经匹配了url中的目录,剩下的是abc.jsp。abc.jsp也需要匹配,但这个匹配是根据web.xml中的配置进行匹配的。

首先,从项目名为xiaofang的私有web.xml中进行查找,即webapps/xiaofang/WEB-INF/web.xml。由于此处仅为简单测试,因此并没有该文件。

于是从全局web.xml即$CATALINA_HOME/conf/web.xml中匹配abc.jsp。以下是web.xml中能匹配到该文件名的配置部分。

<!-- The mappings for the JSP servlet -->
<servlet-mapping>
    <servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
</servlet>

首先根据<servlet-mapping>中的url-pattern进行文件匹配,发现该url匹配的是servlet-name为"jsp"的servlet,然后再找到与该名称对应的<servlet>标签段,发现处理该动态资源的类为org.apache.jasper.servlet.JspServlet,于是找到该类对应的class文件,该class文件归档在$catalina_home/lib/jasper.jar中。

JspServlet程序的作用是将jsp文件翻译成java源代码文件,并放在$catalina_home/work目录下。然后将该java源文件进行编译,编译后的class文件也放在work目录下。这个class文件就是abc.jsp最终要执行的servlet小程序。

[root@xuexi ~]# ls /usr/local/tomcat/work/Catalina/www.xiaofang.com/xuexi/org/apache/jsp/
index_jsp.class  index_jsp.java  new_

在翻译后的servlet小程序中,不仅会输出业务逻辑所需的数据,还会输出html/css代码,这样一来,客户端接收到的数据都将是排版好的。

4.Tomcat处理静态资源的过程

对于tomcat来说,无论是动态还是静态资源,都是经过servlet处理的。只不过处理静态资源的servlet是默认的servlet而已。

在$catalina_home/conf/web.xml中关于静态资源处理的配置如下。

<!-- The mapping for the default servlet -->
<servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
</servlet>

需要记住的是,web.xml中的url-pattern是文件匹配,而server.xml中的<Context path="URL-PATTERN" />是目录匹配。

上面web.xml中的<url-pattern>/</url-pattern>表示的是默认servlet。这意味着,当web.xml中没有servlet-mapping能匹配请求url中的路径时,将匹配servlet-name,即名为default的servlet。然后找到处理default的类为org.apache.catalina.servlets.DefaultServlet,该类的class文件归档在$catalina_home/lib/catalina.jar中。该servlet不像JspServlet会翻译jsp文件,它只有最基本的作用:原样输出请求文件中的内容给客户端。

例如,根据前面的配置,下面几个请求都将采用默认servlet进行处理,即当作静态资源处理。

http://www.xiaofang.com:8080/xuexi/index.html
http://www.xiaofang.com:8080/xuexi/abc.js
http://www.xiaofang.com:8080/xuexi/index
http://www.xiaofang.com:8080/xuexi/index.txt

http://www.xiaofang.com:8080/xuexi则不一定,因为tomcat中默认的index文件包含index.jsp和index.html,而index.jsp排在index.html的前面,只有不存在index.jsp时才请求index.html。

posted @ 2018-02-03 09:51  骏马金龙  阅读(13714)  评论(5编辑  收藏  举报