zoukankan      html  css  js  c++  java
  • tomcat 详解

    首先搞清楚几个概念:Servlet容器与web容器。Servlet容器的主要任务是管理servlet的生命周期,而web容器更准确的说应该叫web服务器,它是来管理和部署web应用的。还有一种服务器叫做应用服务器,它的功能比web服务器要强大的多,它可以部署EJB应用,可以实现容器管理的事务,一般的应用服务器有weblogic和websphere等,它们都是商业服务器,功能强大但都是收费的。web容器最典型的就是tomcat,apache了。
    Tomcat是一个免费的开源的Serlvet容器,也就是说它可以处理servlet请求。但它也不仅仅是一个servlet容器,它也是一个web容器,具有传统web服务器的功能,比如处理html页面等。但是它处理静态html,css,js的能力不如Apache,Apache只是web容器,不能用来处理jsp和servlet请求,但是它在对文字,图片,js,css等内容的请求有自己的一套管理方法,非常优秀,因此一般用来处理对这类静态资源的请求。因此,我们可以在部署应用的时候把tomcat和apache结合起来,让apache来负责处理静态资源的请求,而tomcat用来负责解析动态的jsp和servlet的请求。
    在 Tomcat中,应用程序的部署很简单,只要讲应用打成WAR包,放到Tomcat的webapp目录下,Tomcat会自动检测到这个文件,并将其解压。通常在浏览器中第一次访问某个应用的jsp时,速度都会比较慢,这是因为Tomcat要将jsp解析成servlet文件,然后进行编译。编译过一次之后,以后再次访问这个页面,速度就会很快了。另外 Tomcat也提供了一个应用:manager,访问这个应用需要用户名和密码,用户名和密码存储在一个xml文件中。通过这个应用,辅助于Ftp,可以在远程通过Web部署和撤销应用。当然本地也可以。


    ① Server 一个server代表了整个catalina servlet容器,其实就是BackGroud程序,在Tomcat里面的Server的用处是启动和监听服务端事件(诸如重启、关闭等命令)。
    ② Service Service是由一个或多个Connector与一个Engine的组合。这些Connector共享一个Engine来处理请求。
    ③ Connector Connector将在某个指定的端口上来监听客户的请求,把从socket传递过来的数据,封装成Request,传递给Engine来处理,并从Engine处获得响应并返回给客户。 Tomcat通常会用到两种Connector: a) Http Connector 在端口8080处侦听来自客户browser的http请求。 b) AJP Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求。
    ④ Engine 负责处理来自相关联的service的所有请求,处理后,将结果返回给service,而connector是作为service与engine的中间媒介出现的。 一个engine下可以配置多个虚拟主机,每个虚拟主机都有一个域名。当engine获得一个请求时,它把该请求匹配到某个虚拟主机(host)上,然后把请求交给该主机来处理。 Engine有一个默认主机,当请求无法匹配到任何一个虚拟主机时,将交给默认host来处理。
    ⑤ Host 代表一个虚拟主机,每个虚拟主机和某个网络域名(Domain Name)相匹配。 每个虚拟主机下都可以部署一个或多个web应用,每个web应用对应于一个context,有一个context path。 当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理匹配的方法是“最长匹配”,所以一个path==””的Context将成为该Host的默认Context所有无法和其它Context的路径名匹配的请求都将最终和该默认Context匹配。
    ⑥ Context 一个Context对应于一个Web应用,一个Web应用由一个或者多个Servlet组成Context在创建的时候将根据配置文件$CATALINA_HOME/conf/web.xml和$WEBAPP_HOME/WEB-INF/web.xml载入Servlet类。当Context获得请求时,将在自己的映射表(mapping table)中寻找相匹配的Servlet类,如果找到,则执行该类,获得请求的回应,并返回。
    在Container这层,包含了3种容器:Engine,Host,Context。容器里面又盛装着各种各样的组件,可以理解为提供各种各样的增值服务。 Manager:当一个容器里面装了manager组件后,这个容器就支持session管理了, 事实上在tomcat里面的session管理,就是靠的在context里面装的manager component。 Logger:当一个容器里面装了logger组件后,这个容器里所发生的事情,就被该组件记录下来了。我们通常会在logs/这个目录下看见catalina_log.time.txt以及localhost.time.txt和localhost_examples_log.time.txt。这就是因为我们分别为:engine,host以及context(examples)这三个容器安装了logger组件,这也是默认安装,又叫做标配。
    下面我们来了解一下tomcat中的主要的配置文件: ① Server.xml 这个文件描述了如何启动tomcat server。
    a) 在这个文件的开头我们就可以看到“<Server port="8005" shutdown="SHUTDOWN" debug="0">”,前面说到Tomcat里的Server的用处是启动和监听服务端事件,这里的“SHUTDOWN”就是server在监听服务端事件的时候所使用的命令字,server在端口8005处等待关闭命令,如果接收到”SHUTDOWN”字符串则关闭服务器。
    b) 下面看一下connector的配置:

    Java代码 
    1. <Connector port="8080" protocol="HTTP/1.1" URIEncoding="GBK" 
    2. maxThreads="150"     
    3.           minSpareThreads="25"     
    4.           maxSpareThreads="75"   
    5.           acceptCount="100" 
    6.           connectionTimeout="20000"  
    7.           redirectPort="8443" /> 
       	 <Connector port="8080" protocol="HTTP/1.1" URIEncoding="GBK"
    		   maxThreads="150"    
                   minSpareThreads="25"    
                   maxSpareThreads="75"  
                   acceptCount="100"
                   connectionTimeout="20000" 
                   redirectPort="8443" />
    

             port:表示在端口号8080处侦听来自客户browser的HTTP1.1请求。 minProcessors :该Connector先创建5个线程放入线程池中等待客户请求,每个客户请求由一个线程负责。 minSpareThreads:表示线程池中将始终保持有这么多个线程,即使没有人用也将开这么多空线程等待。 maxSpareThreads:表示线程池中最多可以保留的空闲线程数,例如maxSpareThreads=75,某时刻有80个请求在访问服务器,然后访问结束,这80个空闲的线程不会都保留下来,tomcat会关闭5个空闲线程,最多只保留75个空闲的线程。 maxThreads:线程池中的最大线程数量,表示服务器可以最多同时处理这么多个连接请求。当线程池中现有的线程不够服务客户请求时,若线程总数不足maxThreads,则创建新线程来处理请求。 acceptCount :当现有线程已经达到最大数maxThreads时,接下来的客户请求将进入请求队列中进行排队,当排队队列中请求数超过acceptCount时,后来的请求将对其返回Connection refused错误。 redirectport :当客户请求是https时,把该请求转发到端口8443去。
    c) Engine的配置 Engine用来处理Connector收到的http请求,它将请求匹配到自己的某个虚拟主机上,并把请求转交给对应的host来处理。默认的虚拟主机是localhost。

    Java代码 
    1. <Engine name="Catalina" defaultHost="localhost"> 
    <Engine name="Catalina" defaultHost="localhost">

    d) 虚拟主机host的配置

    Java代码 
    1. <Host name="localhost"  appBase="webapps" 
    2.       unpackWARs="true" autoDeploy="true" 
    3.       xmlValidation="false" xmlNamespaceAware="false"> 
          <Host name="localhost"  appBase="webapps"
                unpackWARs="true" autoDeploy="true"
                xmlValidation="false" xmlNamespaceAware="false">
    

    appBase表示了该虚拟主机localhost的根目录是webapps/,虚拟主机会将请求匹配到自己的context路径上,并把请求转交给对应的context来处理。
    e) Context的配置

    Java代码 
    1. <Context path="/adv" reloadable="false" docBase="E:workspaceaddataextract" /> 
    <Context path="/adv" reloadable="false" docBase="E:workspaceaddataextract" />

    一个Context就代表了一个web应用,path表示该web应用的路径,docBase指定了该应用的根目录所在。
    ② Web.xml 前面说过了一个Context对应于一个Web App,每个Web App是由一个或者多个servlet组成的,而web.xml就是web app的部署配置文件。当一个Web App被初始化的时候,它将用自己的ClassLoader对象载入web.xml中定义的每个servlet类。
    它首先载入在$CATALINA_HOME/conf/web.xml中部署的servlet类,然后载入在自己的Web App根目录下的WEB-INF/web.xml中部署的servlet类。
    web.xml文件有两部分:servlet类定义和servlet映射定义。每个被载入的servlet类都有一个名字,且被填入该Context的映射表(mapping table)中,和某种URL PATTERN对应。当该Context获得请求时,将查询mapping table,找到被请求的servlet,并执行以获得请求回应。
    下面我们来分析一下tomcat的$CATALINA_HOME/conf/web.xml,这个文件是所有web app的共用的部署配置文件。当部署一个web app时,这个文件将首先被读取处理,然后才是web app自己的在WEB-INF目录下的web.xml文件。

    Java代码 
    1.     <servlet> 
    2.         <servlet-name>default</servlet-name> 
    3.         <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class> 
    4.         <init-param> 
    5.             <param-name>debug</param-name> 
    6.             <param-value>0</param-value> 
    7.         </init-param> 
    8.         <init-param> 
    9.             <param-name>listings</param-name> 
    10.             <param-value>false</param-value> 
    11.         </init-param> 
    12.         <load-on-startup>1</load-on-startup> 
    13. </servlet> 
    14.     <servlet-mapping> 
    15.         <servlet-name>default</servlet-name> 
    16.         <url-pattern>/</url-pattern> 
    17.     </servlet-mapping> 
        <servlet>
            <servlet-name>default</servlet-name>
            <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
            <init-param>
                <param-name>debug</param-name>
                <param-value>0</param-value>
            </init-param>
            <init-param>
                <param-name>listings</param-name>
                <param-value>false</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
    </servlet>
        <servlet-mapping>
            <servlet-name>default</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    

    这个servlet是default servlet,也就是说当用户的Http请求无法匹配到任何一个servlet的时候,就执行该servlet。 这里面有个参数交listings,默认是设置为false的,因为tomcat6出于安全问题的考虑,默认是禁止目录浏览。之前版本的tomcat在访问某个目录的时候,例如test,tomcat会把test目录下的所有文件都列出来。但是这样一来的话,这个目录下的文件都变成对外可见的了。因此tomcat6默认是禁止这个功能的,当然,你可以把listings设置为true来开启这个功能。一般来说,在上到生产环境的时候,最好把listings设置为false。

    Java代码 
    1. <servlet> 
    2.     <servlet-name>jsp</servlet-name> 
    3.     <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> 
    4.     <init-param> 
    5.         <param-name>fork</param-name> 
    6.         <param-value>false</param-value> 
    7.     </init-param> 
    8.     <init-param> 
    9.         <param-name>xpoweredBy</param-name> 
    10.         <param-value>false</param-value> 
    11.     </init-param> 
    12.     <load-on-startup>3</load-on-startup> 
    13. </servlet> 
    14. <servlet-mapping> 
    15.     <servlet-name>jsp</servlet-name> 
    16.     <url-pattern>*.jsp</url-pattern> 
    17. </servlet-mapping> 
        <servlet>
            <servlet-name>jsp</servlet-name>
            <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
            <init-param>
                <param-name>fork</param-name>
                <param-value>false</param-value>
            </init-param>
            <init-param>
                <param-name>xpoweredBy</param-name>
                <param-value>false</param-value>
            </init-param>
            <load-on-startup>3</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>jsp</servlet-name>
            <url-pattern>*.jsp</url-pattern>
        </servlet-mapping>
    

    这个jsp servlet也是很重要的,表示当请求的是一个jsp页面的时候(即请求.jsp时),将调用该servlet,它是一个jsp编译器,用来将请求的jsp页面编译成servlet再执行。

    二 tomcat的启动

    对于engine, host, context来说,它们都属于容器,当接收到客户端请求的时候,请求会被传递到容器中,在一个容器中处理完毕之后,会被传递给下一个容器处理。因此,我们可以这样理解tomcat,总的来说,tomcat就是一种自上而下,一个容器里面又嵌套包含了另一个子容器的结构。所以,在tomcat启动的时候,我们也可以想象,它必定要先启动父容器,然后再启动子容器,在启动每一层容器的时候,还会启动容器中的一些相关组件,当所有的容器与组件都安装启动完毕,那么tomcat就启动完毕了。
    因此,很容易理解,tomcat 启动的第一步就是进行容器的装配,就是把父容器和子容器拼装起来,并且安装上相关的组件,这很像一个车间装配的过程。
    当一切装配齐全,机器已经在各个工人的手中完全组装好了,那么接下来的一步,我们只需要按下开关,机器就可以工作啦。多么方便哪!
    1、 一切事情的起点都源于org.apache.catalina.startup.Bootstrap的“引导”。Bootstrap负责对catalina的配置文件路径进行了一番指导,指定了三种类型的classLoader,接下来catalina就可以用这三种类型的classLoader来负责装配容器了。然后Bootstrap用反射机制调用了org.apache.catalina.startup.Catalina的process方法,引导catalina进行启动。
    2、 Catalina的工作首先是用digester来装配各个容器与组件(degester是Jakarta子项目Commons下的一个模块,支持基于规则的对任意XML文档的处理,提供了一种将XML与Java对象进行映射的方便方法),这个装配就像我们上面说的那样,就是把server下的service进行安装,然后依次把service下的engine,host,context这些容器以及容器中的各种组件按照父子关系一一拼装。这些配置文件的来源都是Bootstrap之间就已经告知了的。在这里它只负责组装。 接着,catalina会对server进行初始化工作,主要就是把service中配置的connector进行初始化(HTTP与AJP)。 然后调用server的start方法,启动tomcat server。 最后,为server注册一个hook程序,检测当server shutdown的时候,关闭tomcat的各个容器。
    3、 进入server的start方法。 启动server的容器的三个lifecycle事件:BEFORE_START_EVENT,START_EVENT,AFTER_START_EVENT。 启动server的子容器service。
    4、 进入service的start方法。启动engine与connector。
    5、 下面就开始进入engine了。 之前说过,engine, host与context都是容器,它们都继承自Container类。它们既然都是一种container,那么在处理手法上一定又很多类似的地方,因此,tomcat使用了ContainerBase这个类,把它作为engine, host与context的父类,让这些容器都可以通过super.start()方法来达到大部分主要逻辑的复用。
    那么,我们首先就来看一下这个ContainerBase中都做了些什么,也就可以知道容器大致都怎么处理请求的。 a) 触发启动前事件(BEFORE_START_EVENT)。 b) 设置标签,表示该容器已启动。 c) 启动容器中的各个组件,如loader, logger, manager, cluster, realm, resources等。 d) 启动当前容器的子容器。 e) 启动当前容器的管道pipeline*。 f) 触发启动中事件(START_EVENT)。 g) 触发启动后事件(AFTER_START_EVENT)。 *:pipeline:当一个容器要把从上一级传递过来的需求转交给子容器的时候,它就会把这个需求放进容器的管道(pipeline)里面去,这个管道里面呢有多个阀门机关(value),而需求在管道里面流动的时候,就会被管道里面的各个阀门拦截下来,只有满足了过关的要求,阀门才会放行。比如管道里面放了两个阀门,第一个阀门叫做“access_allow_vavle”,也就是说需求流过来的时候,它会看这个需求是哪个IP过来的,如果这个IP已经在黑名单里面了,OK,立马拦截,这个需求最远就只能走到这里了,不可能再往下走了!第二个阀门叫做“defaul_access_valve”,它会做例行的检查,如果通过的话,OK,把需求传递给当前容器的子容器。 就是通过这种方式, 需求就在各个容器里面传递,流动, 最后抵达目的地的了。
    以上就是ContainerBase中进行的一些处理。尽管大部分内容都是共用的,但每个容器还是有一些自己特别的处理的,这些各个容器特有的任务都会放在调用ContainerBase之前进行处理。在engine中的特别处理包括engine自己的log以及mbean的处理等等。
    6、 Host是engine的子容器,所以host也会调用ContainerBase的start()方法。 而host的特殊处理主要就是往pipeline里面安装了一个errorReportValue的阀门。这个errorReportValue的作用主要就是用来检查response的。需求在被Engine传递给Host后, 会继续传递给Context做具体的处理。 这里需求其实就是作为参数传递的Request, Response。所以在context把需求处理完后,通常会改动response。而这个org.apache.catalina.valves.ErrorReportValve的作用就是检察response是否包含错误, 如果有就做相应的处理。
    7、 终于到了Context了。Context的启动是从 StandardContext的start()开始的。下面我们一步一步来看StandardContext的start()中都做了些什么。
    a) 触发启动前事件(BEFORE_START_EVENT)。 b) 设置web app的具体目录webappResources。 c) 为context指定loader,Loader就是用来指定这个context会用到哪些类啊,哪些jar包啊这些什么的。 d) GetCharsetMapper(),得到字符编码格式,tomcat自己有一个默认的配置文件用来设置默认情况下的字符编码格式,如果用户没有自定义的话,就采用默认的配置,一般为为/org/apache/catalina/util/CharsetMapperDefault.properties。 e) postWorkDirectory (),创建临时文件目录。Tomcat下面有一个work目录,用来存放临时文件。这个步骤就是在那里创建一个目录,一般说来会在%CATALINA_HOME%/work/Standalonelocalhost这个地方生成一个目录。 f) Binding thread(),负责绑定当前线程与context。首先要转换class loader,因为之前需要的是tomcat下的所有class和lib,接下来需要的就是当前context,也就是web app的class和lib了,所以要重新设置当前的的contextClassLoader,同时要记录下旧的class loader。然后就要进行线程的绑定了。

    Java代码 
    1. threadBindings.put(Thread.currentThread(), context); 
    2. threadNameBindings.put(Thread.currentThread(), name); 
    threadBindings.put(Thread.currentThread(), context);
    threadNameBindings.put(Thread.currentThread(), name);
    

    threadBindings和threadNameBindings都是HashTable,这两步操作把当前线程与当前的这个context绑定起来。接下来这个线程就作为这个web app的主线程了。 g) 启动当前context的loader。 h) 重置logger并启动它。 i) 若存在子容器,启动子容器,并启动其管道pipeline。 j) 触发START_EVENT事件监听,

    Java代码 
    1. lifecycle.fireLifecycleEvent(START_EVENT, null); 
    lifecycle.fireLifecycleEvent(START_EVENT, null);

    在这个事件监听里面会启动ContextConfig的start()事件,ContextConfig是用来配置web.xml的。比如这个Context有多少Servlet,又有多少Filter,就是在这里给Context装上去的。ContextConfig主要做了这些工作:

    Java代码 
    1. defaultWebConfig();    //每个context要配置一个默认的web.xml,就是omcat/conf/web.xml,这样container servlet才能被加载。 
    2. applicationWebConfig();    //配置web app自己的web.xml 
    3. validateSecurityRoles();   //验证访问角色的安全性。就是web app的部署权限,通常我们会通过访问/admin 或者/manager来进入应用的部署界面,一般用户是admin或者manager才能访问。访问的用户以及可以访问的资源都是可以限制的,这些都可以通过权限验证来实现。 
    4. authenticatorConfig();   //配置自动认证 
    defaultWebConfig();    //每个context要配置一个默认的web.xml,就是omcat/conf/web.xml,这样container servlet才能被加载。
    applicationWebConfig();    //配置web app自己的web.xml
    validateSecurityRoles();   //验证访问角色的安全性。就是web app的部署权限,通常我们会通过访问/admin 或者/manager来进入应用的部署界面,一般用户是admin或者manager才能访问。访问的用户以及可以访问的资源都是可以限制的,这些都可以通过权限验证来实现。
    authenticatorConfig();   //配置自动认证
    

    k) 为context创建welcome files,通常是这三个启动文件:index.html、index.htm、index.jsp,它们就被默认地绑在了这个context上。 l) 触发AFTER_START_EVENT事件。 m) 配置listener。 n) 启动manager。Manager是用来管理session的。对于服务器来说,每个请求传递过来的时候,会在request里面加上一个叫做sessionId的属性,服务器就通过这个sessionId来知道这个请求到底是属于哪一个session的。 o) 启动context的后台主线程。 p) 配置filter。 q) 启动带有<load-on-startup>的Servlet。如<load-on-startup>1</load-on-startup>,启动的顺序从1开始按照数字从小到大,1, 2, 3 ……,最后才是0。 默认情况下,至少会启动如下3个的Servlet: org.apache.catalina.servlets.DefaultServlet 负责处理静态资源的Servlet,例如图片、html、css、js等等。 org.apache.catalina.servlets.InvokerServlet负责处理没有做Servlet Mapping的那些Servlet。 org.apache.jasper.servlet.JspServlet负责处理JSP文件。 r) 标识context已经启动完毕,如果在启动的时候发生错误,则stop server。 s) 注册JMX。registerJMX(); t) 关闭所有JAR,以免在启动的时候打开的文件数量总是很高。
    如果文字看不下来的话,可以看看下面的流程图,如果你坚持看完了上面一大段话的话,呃,也可以再看看下面的图。


    到这里tomcat就算启动完毕了,我们可以看到它的启动过程是一环套一环的过程,先是父容器,然后是子容器,一层层往下递进。

    三  请求处理

    在这第三部分里面我们主要看一下tomcat是如何接收客户端请求,并把这个请求一层一层的传递到子容器中,并最后交到应用程序中进行处理的。
    首先,我们来了解一下什么叫做NIO和BIO。 在前面的解读tomcat里面,我们已经说到过了线程池。线程池,顾名思义,里面存放了一定数量的线程,这些线程用来处理用户请求。现在我们要讨论的NIO和BIO就是如何分配线程池中的线程来处理用户请求的方式。
    BIO(Block IO):阻塞式IO。在tomcat6之前一直都是采用的这种方式。当一个客户端的连接请求到达的时候,ServerSocket.accept负责接收连接,然后会从线程池中取出一个空闲的线程,在该线程读取InputStream并解析HTTP协议,然后把输入流中的内容封装成Request和Response,在线程中执行这个请求的Servlet ,最后把Response的内容发送到客户端连接并关闭本次连接。这就完成了一个客户端的请求过程。我们要注意的是在阻塞式IO中,tomcat是直接从线程池中取出一个线程来处理客户端请求的,那么如果这些处理线程在执行网络操作期间发生了阻塞的话,那么线程将一直阻塞,导致新的连接一直无法分配到空闲线程,得不到响应。

    NIO(Non-blocking IO):tomcat中的非阻塞式IO与阻塞式的不同,它采用了一个主线程来读取InputStream。也就是说当一个客户端请求到达的时候,这个主线程会负责从网络中读取字节流,把读入的字节流放入channel中。然后这个主线程就会到线程池中去找有没有空闲的线程,如果找到了,那么就会由空闲线程来负责从channel中取出字节,然后解析Http,转换成request和response,进行处理。当处理线程把要返回给客户端的内容放在Response之后,处理线程就可以把处理结束的字节流也放入channel中,最后主线程会给这个channel加个标识,表示现在需要操作系统去进行io操作,要把这个channel中的内容返回给客户端。这样的话,线程池中的处理线程的任务就集中在如何处理用户请求上了,而把与网络有交互的操作都交给主线程去处理。

    对于这个非阻塞式IO,anne我想了一个很有趣的比喻,就好像去饭店吃饭点菜一样。餐馆的接待员就好像我们的操作系统,客人来了,他要负责记下客人的点菜,然后传达给厨房,厨房里面有好几位烧菜厨师(处理线程),主厨(主线程)呢比较懒,不下厨,只负责分配厨师的工作。现在来了一个客人,跟接待员说要吃宫宝鸡丁,然后接待员就写了张纸条,上面写了1号桌客人要点宫宝鸡丁,从厨房柜台上的一摞盘子里面拿了一个空的,把点菜单放在盘子里面。然后主厨就时刻关注这这些盘子,看到有盘子里面有点菜单的,就从厨房里面喊一个空闲的厨子说,你来把这菜给烧一下,这个厨子就从这个盘子里面拿出点菜单去烧菜了,好了这下这个盘子又空了,如果这时候还有客人来,那么接待员还可以把点菜单放到这个盘子里面去。等厨师烧好了菜,他就从柜台上找一个空盘子,把菜盛在里面,贴上纸条说这个是1号桌客人点的宫宝鸡丁。然后主厨看看,嗯,烧的还不错,可以上菜了,就在盘子上贴个字条说这盘菜烧好了,上菜去吧。最后接待员就来了,他一直在这候着呢,看到终于有菜可以上了,赶紧端去。嗯,自我感觉挺形象的,你们说呢?
    因此,我们可以分析出tomcat中的阻塞式IO与非阻塞式IO的主要区别就是这个主线程。Tomcat6之前的版本都是只采用的阻塞式IO的方式,服务器接收了客户端连接之后,马上分配处理线程来处理这个连接;tomcat6中既提供了阻塞式的IO,也提供了非阻塞式IO处理方式,非阻塞式IO把接收连接的工作都交给主线程处理,处理线程只关心具体的如何处理请求。
    好了,我们现在知道了tomcat是采用非阻塞式IO来分配请求的了。那么接下来我们就可以从发出一个请求开始看看tomcat是怎么把它传递到我们自己的应用程序中的。
    程序员最爱看类图了,所以anne画了个类图,我们来照着类图,一个一个类来看。

    我们首先从NioEndPoint开始,这个类是实际处理tcp连接的类,它里面包括一个操作线程池,socket接收线程(acceptorThread),socket轮询线程(pollerThread)。
    首先我们看到的是start()方法,在这个方法里面我们可以看到启动了线程池,acceptorThread和pollerThread。然后,在这个类中还定义了一些子类,包括SocketProcessor,Acceptor,Poller,Worker,NioBufferHandler等等。SocketProcessor,Acceptor,Poller和Worker都实现了Runnable接口。 我想还是按照接收请求的调用顺序来讲会比较清楚,所以我们从Acceptor开始。
    1、Acceptor负责接收socket,一旦得到一个tcp连接,它就会尝试去从nioChannels中去取出一个空闲的nioChannel,然后把这个连接的socket交给它,接着它会告诉轮询线程poller,我这里有个channel已经准备好了,你注意着点,可能不久之后就要有数据过来啦。下面的事情它就不管了,接着等待下一个tcp连接的到来。 我们可以看一下它是怎么把socket交给channel的:

    Java代码 
    1.    protected boolean setSocketOptions(SocketChannel socket) { 
    2.        try { 
    3.            ... //ignore 
    4.         //从nioChannels中取出一个channel 
    5.            NioChannel channel = nioChannels.poll(); 
    6.         //若没有可用的channel,根据不同情况新建一个channel 
    7.            if ( channel == null ) { 
    8.                if (sslContext != null) { 
    9.                    ...//ignore 
    10.                    channel = new SecureNioChannel(...); 
    11.                } else { 
    12.                    ...// normal tcp setup 
    13.                    channel = new NioChannel(...); 
    14.                } 
    15.            } else {                 
    16.                channel.setIOChannel(socket); 
    17.            ...//根据channel的类型做相应reset 
    18.            } 
    19.            getPoller0().register(channel); // 把channel交给poller 
    20.        } catch (Throwable t) { 
    21.            try {             
    22.            return false;   // 返回false,关闭socket 
    23.        } 
    24.        return true; 
        protected boolean setSocketOptions(SocketChannel socket) {
            try {
                ... //ignore
    			//从nioChannels中取出一个channel
                NioChannel channel = nioChannels.poll();
    			//若没有可用的channel,根据不同情况新建一个channel
                if ( channel == null ) {
                    if (sslContext != null) {
                        ...//ignore
                        channel = new SecureNioChannel(...);
                    } else {
                        ...// normal tcp setup
                        channel = new NioChannel(...);
                    }
                } else {                
                    channel.setIOChannel(socket);
    			   ...//根据channel的类型做相应reset
                }
                getPoller0().register(channel); // 把channel交给poller
            } catch (Throwable t) {
                try {            
                return false;   // 返回false,关闭socket
            }
            return true;
     }
    

    要说明的是,Acceptor这个类在BIO的endpoint类中也是存在的。对于BIO来说acceptor就是用来接收请求,然后给这个请求分配一个空闲的线程来处理它,所以是起到了一个连接请求与处理线程的作用。现在在NIO中,我们可以看到Acceptor.run()里面是把processSocket(socket);给注释掉了(processSocket这个方法就是分配线程来处理socket的方法,这个anne打算在后面讲)。


    2、Poller这个类其实就是我们在前面说到的nio的主线程。它里面也有一个run()方法,在这里我们就会轮询channel啦。看下面的代码:

    Java代码 
    1. Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null; 
    2. while (iterator != null && iterator.hasNext()) { 
    3.             SelectionKey sk = (SelectionKey) iterator.next(); 
    4.          KeyAttachment attachment = (KeyAttachment)sk.attachment(); 
    5.          attachment.access(); 
    6.          iterator.remove(); 
    7.          processKey(sk, attachment); 
    Iterator iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
    while (iterator != null && iterator.hasNext()) {
        		SelectionKey sk = (SelectionKey) iterator.next();
             KeyAttachment attachment = (KeyAttachment)sk.attachment();
             attachment.access();
             iterator.remove();
             processKey(sk, attachment);
     }
    

    我们可以看到,程序遍历了所有selectedKeys,这个SelectionKey就是一种可以用来读取channel的钥匙。这个KeyAttachment又是个什么类型的对象呢?其实它记录了包括channel信息在内的又与这个channel息息相关的一些附加信息。MS很长的一句话,这么说吧,它里面有channel对象,还有lastAccess(最近一次访问时间),error(错误信息),sendfileData(发送的文件数据)等等。然后在processKey这个方法里面我们就可以把channel里面的字节流交给处理线程去处理了。 然后我们来看一下这个processKey方法:

    Java代码 
    1. protected boolean processKey(SelectionKey sk, KeyAttachment attachment) { 
    2.             boolean result = true; 
    3.             try { 
    4.                 if ( close ) { 
    5.                     cancelledKey(sk, SocketStatus.STOP, false); 
    6.                 } else if ( sk.isValid() && attachment != null ) { 
    7.                     attachment.access(); 
    8.                     sk.attach(attachment); 
    9.                     NioChannel channel = attachment.getChannel(); 
    10.             ①        if (sk.isReadable() || sk.isWritable() ) { 
    11.             ②            if ( attachment.getSendfileData() != null ) { 
    12.                             processSendfile(sk,attachment,true); 
    13.                         } else if ( attachment.getComet() ) { 
    14.                             if ( isWorkerAvailable() ) { 
    15.                                 reg(sk, attachment, 0); 
    16.                                 if (sk.isReadable()) { 
    17.                                     if (!processSocket(channel, SocketStatus.OPEN)) 
    18.                                         processSocket(channel, SocketStatus.DISCONNECT); 
    19.                                 } else { 
    20.                                     if (!processSocket(channel, SocketStatus.OPEN)) 
    21.                                         processSocket(channel, SocketStatus.DISCONNECT); 
    22.                                 } 
    23.                             } else { 
    24.                                 result = false; 
    25.                             } 
    26.                         } else { 
    27.                             if ( isWorkerAvailable() ) { 
    28.                                 unreg(sk, attachment,sk.readyOps()); 
    29.      ③                           boolean close = (!processSocket(channel)); 
    30.                                 if (close) { 
    31.                                     cancelledKey(sk,SocketStatus.DISCONNECT,false); 
    32.                                 } 
    33.                             } else { 
    34.                                 result = false; 
    35.                             } 
    36.                         }                      } 
    37.                     }  
    38.                 } else { 
    39.                     //invalid key 
    40.                     cancelledKey(sk, SocketStatus.ERROR,false); 
    41.                 } 
    42.             } catch ( CancelledKeyException ckx ) { 
    43.                 cancelledKey(sk, SocketStatus.ERROR,false); 
    44.             } catch (Throwable t) { 
    45.                 log.error("",t); 
    46.             } 
    47.             return result; 
    protected boolean processKey(SelectionKey sk, KeyAttachment attachment) {
                boolean result = true;
                try {
                    if ( close ) {
                        cancelledKey(sk, SocketStatus.STOP, false);
                    } else if ( sk.isValid() && attachment != null ) {
                        attachment.access();
                        sk.attach(attachment);
                        NioChannel channel = attachment.getChannel();
                ①        if (sk.isReadable() || sk.isWritable() ) {
                ②            if ( attachment.getSendfileData() != null ) {
                                processSendfile(sk,attachment,true);
                            } else if ( attachment.getComet() ) {
                                if ( isWorkerAvailable() ) {
                                    reg(sk, attachment, 0);
                                    if (sk.isReadable()) {
                                        if (!processSocket(channel, SocketStatus.OPEN))
                                            processSocket(channel, SocketStatus.DISCONNECT);
                                    } else {
                                        if (!processSocket(channel, SocketStatus.OPEN))
                                            processSocket(channel, SocketStatus.DISCONNECT);
                                    }
                                } else {
                                    result = false;
                                }
                            } else {
                                if ( isWorkerAvailable() ) {
                                    unreg(sk, attachment,sk.readyOps());
         ③                           boolean close = (!processSocket(channel));
                                    if (close) {
                                        cancelledKey(sk,SocketStatus.DISCONNECT,false);
                                    }
                                } else {
                                    result = false;
                                }
                            }                      }
                        } 
                    } else {
                        //invalid key
                        cancelledKey(sk, SocketStatus.ERROR,false);
                    }
                } catch ( CancelledKeyException ckx ) {
                    cancelledKey(sk, SocketStatus.ERROR,false);
                } catch (Throwable t) {
                    log.error("",t);
                }
                return result;
    }
    

    首先是判断一下这个selection key是否可用,没有超时,然后从sk中取出channel备用。然后看一下这个sk的状态是否是可读的,或者可写的,代码①处。代码②处是返回阶段,要往客户端写数据时候的路径,程序会判断是否有要发送的数据,这部分我们后面再看,先往下看request进来的情况。然后我们就可以在下面看到开始进行真正的处理socket的工作了,代码③处,进入processSocket()方法了。

    Java代码 
    1. protected boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) { 
    2.     try { 
    3.         KeyAttachment attachment = (KeyAttachment)socket.getAttachment(false); 
    4.         attachment.setCometNotify(false);  
    5.         if (executor == null) { 
    6.        ④     getWorkerThread().assign(socket, status); 
    7.         } else { 
    8.             SocketProcessor sc = processorCache.poll(); 
    9.             if ( sc == null ) sc = new SocketProcessor(socket,status); 
    10.             else sc.reset(socket,status); 
    11.             if ( dispatch ) executor.execute(sc); 
    12.             else sc.run(); 
    13.         } 
    14.     } catch (Throwable t) { 
    15.         return false; 
    16.     } 
    17.     return true; 
        protected boolean processSocket(NioChannel socket, SocketStatus status, boolean dispatch) {
            try {
                KeyAttachment attachment = (KeyAttachment)socket.getAttachment(false);
                attachment.setCometNotify(false); 
                if (executor == null) {
               ④     getWorkerThread().assign(socket, status);
                } else {
                    SocketProcessor sc = processorCache.poll();
                    if ( sc == null ) sc = new SocketProcessor(socket,status);
                    else sc.reset(socket,status);
                    if ( dispatch ) executor.execute(sc);
                    else sc.run();
                }
            } catch (Throwable t) {
                return false;
            }
            return true;
    }
    

    从④处可以看到明显是取了线程池中的一个线程来操作这个channel,也就是说在这个方法里面我们就开始进入线程池了。那么executor呢?executor可以算是一个配置项,如果使用了executor,那么线程池就使用java自带的线程池,如果不使用executor的话,就使用tomcat的线程池WorkerStack,这个WrokerStack我在后面有专门写它,现在先跳过。我们可以看到在start()方法里面,是这样写的:

    Java代码 
    1.       if (getUseExecutor()) { 
    2.            if ( executor == null ) { 
    3.                executor = new ThreadPoolExecutor(...); 
    4.            } 
    5.        } else if ( executor == null ) { 
    6.            workers = new WorkerStack(maxThreads); 
               if (getUseExecutor()) {
                    if ( executor == null ) {
                        executor = new ThreadPoolExecutor(...);
                    }
                } else if ( executor == null ) {
                    workers = new WorkerStack(maxThreads);
         }
    

    好了,现在回到processSocket(),我们先来看有executor的情况,就是使用java自己的线程池。首先从processorCache中取出一个线程socketProcessor,然后把channel交给这个线程,启动线程的run方法。于是我们终于脱离主线程,进入了SocketProcessor的run方法啦! 

    3、SocketProcessor:这个类是真正处理用户请求的类。 我们只看最重要的一步,如何处理channel:

    Java代码 
    1. boolean closed = (status==null)?(handler.process(socket)==Handler.SocketState.CLOSED) : (handler.event(socket,status)==Handler.SocketState.CLOSED); 
    boolean closed = (status==null)?(handler.process(socket)==Handler.SocketState.CLOSED) : (handler.event(socket,status)==Handler.SocketState.CLOSED);
    

    这里的handler是Http11NioProtocal里面的一个子类Http11ConnectionHandler。在这里对于这个socket有没有状态是分开处理的,还记得前面的processKey()方法里面,有好几处的if-else,有三个分支处都调用了processSocket(),有的是有status的,有的只有一个socket传进去,分别就在此处。其实大体都差不多,所以,我们就着重来看看handler.process()吧。
    不管怎么样,我们首先都要得到一个processor对象,这个processor也是从一个可回收的数组中取出来的,然后主要的是调用processor的process方法。

    Java代码 
    1. SocketState state = processor.process(socket); 
    SocketState state = processor.process(socket);

    在Http11NioProcessor的process方法就是用来从channel里面把字节流读出来,然后把它转换成容器需要的request和response对象(通过inputBuffer的parseRequestLine, parseRequest等方法来实现这个功能的)。然后,封装好了request,并且也创建好了response之后,会用adapter.service(request, response);来把request和response传递进context。

    Java代码 
    1. public SocketState process(NioChannel socket){ 
    2. if (!error) { 
    3.      try { 
    4. adapter.service(request, response); 
    5. ... 
    6.          } 
    7.      } 
    public SocketState process(NioChannel socket){
    if (!error) {
         try {
    adapter.service(request, response);
    ...
             }
         }
    }
    

    这个service方法中我们只关注下面一段。

    Java代码 
    1. req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName()); 
    2.       if (postParseRequest(req, request, res, response)) { 
    3.           connector.getContainer().getPipeline().getFirst().invoke(request, response); 
          req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
                if (postParseRequest(req, request, res, response)) {
                    connector.getContainer().getPipeline().getFirst().invoke(request, response);
    }
    

    好了,我们看到,第一步就是给这个request设置处理它的线程。第二步,在这个里面会对request进行一些处理,譬如说sessionId就是在这里处理的,通过parstSessionId把sessionId设置到request里面。第三步,就开始调用container了,接下来的过程anne我打算后面慢慢分解。
    4、在NioEndpoint类中还有个子类叫做WorkerStack,这是一个存放Worker的堆栈。 前面在讲到调用processSocket方法的时候,说从这里开始要取线程池中的线程了,如果使用了executor,那么线程池就使用java自带的线程池,如果不使用executor的话,就使用tomcat的线程池WorkerStack。

    Java代码 
    1. public class WorkerStack { 
    2.  
    3.         protected Worker[] workers = null; 
    4.         protected int end = 0; 
    5.         public WorkerStack(int size) { 
    6.             workers = new Worker[size]; 
    7.         } 
    8.         public void push(Worker worker) { 
    9.             workers[end++] = worker; 
    10.         } 
    11.         public Worker pop() { 
    12.             if (end > 0) { 
    13.                 return workers[--end]; 
    14.             } 
    15.             return null; 
    16.         } 
    17. ... 
    public class WorkerStack {
    
            protected Worker[] workers = null;
            protected int end = 0;
            public WorkerStack(int size) {
                workers = new Worker[size];
            }
            public void push(Worker worker) {
                workers[end++] = worker;
            }
            public Worker pop() {
                if (end > 0) {
                    return workers[--end];
                }
                return null;
            }
    ...
    }
    

    Workers[]当然不用说,是一个Worker对象的数组,end则是数组中Worker的个数。这都非常好理解。现在的问题就是Worker。Worker是用来处理socket的工具。 首先我们要通过getWorkerThread()来得到一个Worker对象,怎么的到呢,先看看workerStack里面有没有空闲的Worker啊,有的话最好,直接就拿出来了,没有的话,就新建一个呗,万一这么倒霉,线程数已经到顶了,不能新建了,那就请先稍微等等,等到有了空闲的worker,就唤醒getWorkerThread()方法。代码如下:

    Java代码 
    1. protected Worker getWorkerThread() { 
    2.     Worker workerThread = createWorkerThread(); 
    3.     while (workerThread == null) { 
    4.         try { 
    5.             synchronized (workers) { 
    6.                 workerThread = createWorkerThread(); 
    7.                 if ( workerThread == null ) workers.wait(); 
    8.             } 
    9.         } catch (InterruptedException e) { 
    10.             // Ignore 
    11.         } 
    12.         if ( workerThread == null ) workerThread = createWorkerThread(); 
    13.     } 
    14.     return workerThread; 
        protected Worker getWorkerThread() {
            Worker workerThread = createWorkerThread();
            while (workerThread == null) {
                try {
                    synchronized (workers) {
                        workerThread = createWorkerThread();
                        if ( workerThread == null ) workers.wait();
                    }
                } catch (InterruptedException e) {
                    // Ignore
                }
                if ( workerThread == null ) workerThread = createWorkerThread();
            }
            return workerThread;
        }
    

    顺便看一下如何唤醒等待worker的线程。

    Java代码 
    1. protected void recycleWorkerThread(Worker workerThread) { 
    2.      synchronized (workers) { 
    3.          workers.push(workerThread); 
    4.          curThreadsBusy--; 
    5.          workers.notify(); 
    6.      } 
        protected void recycleWorkerThread(Worker workerThread) {
            synchronized (workers) {
                workers.push(workerThread);
                curThreadsBusy--;
                workers.notify();
            }
       }
    

    好了,到这里客户端请求的接收就讲完了,当然,anne没有忘记这个过程只到进入container,但大体上我们已经知道inputStream已经变成request了,接下来就要经过engine, host, context的洗礼,最后目标就是servlet。 呃,我还画了一个时序图,虽然比较简陋,但图文并茂的话看起来比较省力。

    现在我们脑补一下,假设这个请求servlet已经处理结束了,现在我们要把这个response返回给客户端了!
    之前在讲到Http11NioProcessor的process方法的时候,我们知道就是在process里面调用了adapter.service(),这个方法很明显是request进入容器的入口,那么出口是不是也在这里呢,我们在process方法里面往下找找,就可以看到:

    Java代码 
    1. if (sendfileData != null && !error) { 
    2.           KeyAttachment ka = (KeyAttachment)socket.getAttachment(false); 
    3.        ka.setSendfileData(sendfileData); 
    4.        sendfileData.keepAlive = keepAlive; 
    5.        SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector()); 
    6.        openSocket = socket.getPoller().processSendfile(key,ka,true); 
    7.        break; 
        if (sendfileData != null && !error) {
        	      KeyAttachment ka = (KeyAttachment)socket.getAttachment(false);
               ka.setSendfileData(sendfileData);
               sendfileData.keepAlive = keepAlive;
               SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
               openSocket = socket.getPoller().processSendfile(key,ka,true);
               break;
    }
    

    最后的break我们不管,关键是我们看到从key attachement中取出了senfileData,然后调用了Poller的processSendfile()。我们已经知道了,Poller就是NIO中的主线程,现在又跑到主线程中去了,它要做什么呢?

    Java代码 
    1.   public boolean processSendfile(SelectionKey sk, KeyAttachment attachment, boolean reg) { 
    2.       try { 
    3.           SendfileData sd = attachment.getSendfileData(); 
    4. .//ignore 
    5.           if ( sd.length <= 0 ) { 
    6.               attachment.setSendfileData(null); 
    7.               if ( sd.keepAlive ) { 
    8. ①                if (reg) reg(sk,attachment,SelectionKey.OP_READ); 
    9.               } else { 
    10.                   cancelledKey(sk,SocketStatus.STOP,false); 
    11.               } 
    12.           } else if ( attachment.interestOps() == 0 && reg ) { 
    13. ②              reg(sk,attachment,SelectionKey.OP_WRITE); 
    14.           } 
    15.       }catch ( IOException x ) { 
    16.           cancelledKey(sk,SocketStatus.ERROR,false); 
    17.           return false; 
    18.       }catch ( Throwable t ) { 
    19.           cancelledKey(sk, SocketStatus.ERROR, false); 
    20.           return false; 
    21.       } 
    22.       return true; 
    23.   } 
            public boolean processSendfile(SelectionKey sk, KeyAttachment attachment, boolean reg) {
                try {
                    SendfileData sd = attachment.getSendfileData();
    				...//ignore
                    if ( sd.length <= 0 ) {
                        attachment.setSendfileData(null);
                        if ( sd.keepAlive ) {
          ①                if (reg) reg(sk,attachment,SelectionKey.OP_READ);
                        } else {
                            cancelledKey(sk,SocketStatus.STOP,false);
                        }
                    } else if ( attachment.interestOps() == 0 && reg ) {
          ②              reg(sk,attachment,SelectionKey.OP_WRITE);
                    }
                }catch ( IOException x ) {
                    cancelledKey(sk,SocketStatus.ERROR,false);
                    return false;
                }catch ( Throwable t ) {
                    cancelledKey(sk, SocketStatus.ERROR, false);
                    return false;
                }
                return true;
            }
    

    我们可以看到在代码①处和②处(不同的分支),都又调用reg方法,这个意思是告诉操作系统要对channel进行读(写)操作了。
    关于对channel的读写操作,NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要观察的socket channel告诉Selector,那么当有我们感兴趣的事件发生时,selector就会通知我们,并且传回一组SelectionKey,通过读取这些selection key,我们就可以的到刚才注册过的channel,进而得到channel中的字节流了。Selector的内部原理实际上是一直在对注册的channel进行轮询访问,一旦轮询到一个channel有我们感兴趣的事情发生(比如数据来了),selector就会报告返回这个channel的selection key,让我们通过这个selection key来读取对应channel的内容。
    我们已经知道了selection key是对应channel的一把钥匙,之间的代码中我们有看到selection key有isReadalbe和isWriteable两种状态,这个状态是针对主线程(也就是poller)来说的,它告诉主线程现在这个channel是可读的,或者可写的。而出现在代码①处和②处这里的OP_READ和OP_WRITE则是用来告诉给操作系统要做的操作。SelectionKey中的operation有四种:OP_READ, OP_WRITE, OP_CONNECT, OP_ACCEPT。这些状态是由主线程告诉给操作系统要进行操作了。例如reg(sk,attachment,SelectionKey.OP_READ),这个意思就是告诉操作系统要去socket读取数据了,把读入的数据放入到channel中;reg(sk,attachment,SelectionKey.OP_WRITE),就是告诉操作系统现在channel中的数据都已经准备好了,现在可以往客户端写了;同理,OP_CONNECT和OP_ACCEPT分别表示结束连接和接受连接。  

    Java代码 
    1.     public static final int OP_READ = 1 << 0; 
    2.     public static final int OP_WRITE = 1 << 2;  
    3.     public static final int OP_CONNECT = 1 << 3;  
    4. public static final int OP_ACCEPT = 1 << 4; 
    5.  
    6.     public final boolean isReadable() { 
    7.     return (readyOps() & OP_READ) != 0; 
    8.  
    9.     public final boolean isWritable() { 
    10.     return (readyOps() & OP_WRITE) != 0; 
        public static final int OP_READ = 1 << 0;
        public static final int OP_WRITE = 1 << 2; 
        public static final int OP_CONNECT = 1 << 3; 
    public static final int OP_ACCEPT = 1 << 4;
    
        public final boolean isReadable() {
    	return (readyOps() & OP_READ) != 0;
    }
    
        public final boolean isWritable() {
    	return (readyOps() & OP_WRITE) != 0;
    }
    

    上面的代码是从SelectionKey类中节选的,我们可以看到OP_READ, OP_WRITE, OP_CONNECT, OP_ACCEPT与isReadable()和isWritable()有着密切的联系。从四个操作的定义我们不难看出,ms这四个操作分别代表了一个字节中的四个位置,一个字节中有8个bit,00000001代表read,00000100代表write,00001000代表connect,00010000代表accept。 拿read来举例,假如这时候我们打算让某个channel去读取客户端数据,那么我们就给这个channel注册OP_READ事件,reg(sk,attachment,SelectionKey.OP_READ)。

    Java代码 
    1. protected void reg(SelectionKey sk, KeyAttachment attachment, int intops) { 
    2.      sk.interestOps(intops);  
    3.      attachment.interestOps(intops); 
    protected void reg(SelectionKey sk, KeyAttachment attachment, int intops) {
         sk.interestOps(intops); 
         attachment.interestOps(intops);
    }
    

    现在这个channel的interestOps就是00000001。 readyOps()表示的是这个channel当前准备好的状态。假如操作系统还没有给这个channel进行读操作,那么readyOps()当然在代表read的那一位是0了,假如操作系统已经把这个channel中填充了客户端来的数据了,那么就把read这位置为1,这个时候readyOps()就变成了00000001了,那么(readyOps() & OP_READ)就=1啦,表示这个channel现在是isReadable的,所以接下来主线程就可以从这个channel中读取数据了。 我们可以看一下下面的图示。

    现在我们从connector.getContainer().getPipeline().getFirst().invoke(request, response)开始进入容器...
    前面说到容器的时候,anne一直都只有说三个容器,engine, host, context。其实在context之下,还有一个容器,叫做wrapper,每个wapper包含了一个servlet,因此前文没有接触到servlet的时候,就暂时省略了。好了,现在我们知道了有这四个级别的容器。这四个容器素由上至下逐渐细分,形成树状结构,构成了tomcat容器结构的主体,它们都位于org.apache.catalina包内。 

     

    之前在第二部分tomcat的启动的时候,我们就看到了pipeline,pipeline是一种上级容器和下级容器进行沟通的管道,当上级容器种的request和response要被传递到下一个容器中去时,就必须通过这个管道,而value就像管道中的一个个阀门一样,给传递的request和response把把关,只有过了所有的阀门,才能被正确的传递到下一级容器中去。Tomcat中的pipeline/valve是标准的责任链模式,每个级别的容器中pipeline下都有配置value,每种类型的value专门负责做一项工作,比如验证Request的有效性、写日志等等。请求先到达第一个value,value会对其做一些工作,当工作做完后,将请求传递给下一个value。每个pipeline的最后都会有一个BasicValue(比如Engine的StandardEngineValue、Host的StanadrdHostValue),它负责寻找下一级容器的pipeline,并且将请求传递给下一级容器的pipeline中的value,这样一直传递下去直到真正的servlet。
    在tomcat中,这个责任链真是最最标准,最最基础的责任链了,我们来看一下StandardPipeline中是怎样为pipeline添加value的:

    Java代码 
    1. public void addValve(Valve valve) {     
    2.     if (valve instanceof Contained) 
    3.         ((Contained) valve).setContainer(this.container); 
    4.  
    5.     ...// Start the new component if necessary 
    6.  
    7.     // Add this Valve to the set associated with this Pipeline 
    8.    if (first == null) { 
    9.         first = valve; 
    10.         valve.setNext(basic); 
    11.     } else { 
    12.         Valve current = first; 
    13.         while (current != null) { 
    14. if (current.getNext() == basic) { 
    15.     current.setNext(valve); 
    16.     valve.setNext(basic); 
    17.     break; 
    18. current = current.getNext(); 
    19.  
    20.     } 
        public void addValve(Valve valve) {    
            if (valve instanceof Contained)
                ((Contained) valve).setContainer(this.container);
    
            ...// Start the new component if necessary
    
            // Add this Valve to the set associated with this Pipeline
    ①      if (first == null) {
            	first = valve;
            	valve.setNext(basic);
            } else {
                Valve current = first;
                while (current != null) {
    				if (current.getNext() == basic) {
    					current.setNext(valve);
    					valve.setNext(basic);
    					break;
    				}
    				current = current.getNext();
    			}
            }
    }
    

    First(代码①处)记录了这个pipeline关联着的第一个value,basic则是pipeline关联的最后一个value,执行完basic,就进入到下一级的容器中去了。 从上面的代码,我们可以看到,pipeline用了一种最基本的方法来维持这个value的链条,每个value都保持了一个下个value的引用。于是,我们就看到connector.getContainer().getPipeline().getFirst()就得到了engine中的第一个value。为什么呢?因为connector.getContainer()得到的是connector父节点(也就是service)中的engine容器,进而getPipeline()得到engine的pipeline,getFirst()得到engine的pipeline中的第一个value。
    好了,下面我们来看看具体进入到了value,都做了些什么,我们来看一个AccessLogValue,很明显,是一个用来写日志的value。

    Java代码 
    1. public void invoke(Request request, Response response) throws IOException, ServletException { 
    2.     if (started && getEnabled()) {                 
    3.         long t1 = System.currentTimeMillis(); 
    4.  
    5.        getNext().invoke(request, response); 
    6.  
    7.         long t2 = System.currentTimeMillis(); 
    8.         long time = t2 - t1;     
    9.         StringBuffer result = new StringBuffer(); 
    10.     ...    //add log info into result 
    11.         log(result.toString()); 
    12.     } else 
    13.         getNext().invoke(request, response);        
        public void invoke(Request request, Response response) throws IOException, ServletException {
            if (started && getEnabled()) {                
                long t1 = System.currentTimeMillis();
        
      ②        getNext().invoke(request, response);
        
                long t2 = System.currentTimeMillis();
                long time = t2 - t1;    
                StringBuffer result = new StringBuffer();
      			...    //add log info into result
                log(result.toString());
            } else
                getNext().invoke(request, response);       
        }
    

    OK,很明显,value是通过getNext()方法来得到下一个责任链上的value的(代码②处)。那么当这个责任链到头了,进入到了最后一个value的话是怎么处理的呢?前面说了,每个容器的pipeline的责任链的末端都会有一个特殊的value,Engine的StandardEngineValue、Host的StanadrdHostValue,Context的StanadrdContextValue,Wrapper的StanadrdWrapperValue,这些叫做basicValue,对于容器来说,这些basicValue是一定会有的。我们就看一个StandardEngineValue:

    Java代码 
    1.     public final void invoke(Request request, Response response) 
    2.         throws IOException, ServletException { 
    3.         // Select the Host to be used for this Request 
    4.         Host host = request.getHost(); 
    5.         if (host == null) { 
    6.             response.sendError 
    7.                 (HttpServletResponse.SC_BAD_REQUEST, 
    8.                  sm.getString("standardEngine.noHost",  
    9.                               request.getServerName())); 
    10.             return; 
    11.         } 
    12.  
    13.         // Ask this Host to process this request 
    14. ③      host.getPipeline().getFirst().invoke(request, response); 
    15.     } 
        public final void invoke(Request request, Response response)
            throws IOException, ServletException {
            // Select the Host to be used for this Request
            Host host = request.getHost();
            if (host == null) {
                response.sendError
                    (HttpServletResponse.SC_BAD_REQUEST,
                     sm.getString("standardEngine.noHost", 
                                  request.getServerName()));
                return;
            }
    
            // Ask this Host to process this request
    ③      host.getPipeline().getFirst().invoke(request, response);
        }
    

    我们可以看到在basicValue中得到了下一级容器,并且调用了下级容器的pipeline中的first value(代码③处)。对于StanadrdHostValue,StanadrdContextValue和StanadrdWrapperValue来说,也都是类似的。BasicValue放在org.apache.catalina.core下,而其他的value都放在org.apache.catalina.value下面。

    Ok,大家继续脑补一下,现在已经进入到StanadrdWrapperValue了。那根据我们对tomcat的了解下面应该做什么了呢?对了,接下来我们就要穿越过层层filter,进入servlet了。 我们看一下StanadrdWrapperValuede的invoke方法,为了看起来方便一点,anne就大刀阔斧的只截取了一点点我们需要关注的内容。

    Java代码 
    1. public final void invoke(Request request, Response response) 
    2.     throws IOException, ServletException { 
    3.      ... 
    4.     servlet = wrapper.allocate(); 
    5.  
    6.     // 下面开始创建filter链啦 
    7.    ApplicationFilterFactory factory = 
    8.         ApplicationFilterFactory.getInstance(); 
    9.     ApplicationFilterChain filterChain = 
    10.         factory.createFilterChain(request, wrapper, servlet); 
    11.     ... 
    12.  
    13.     // 调用filter链 
    14.     filterChain.doFilter(request.getRequest(),response.getResponse()); 
    15.  
    16.     if (servlet != null) { 
    17.        wrapper.deallocate(servlet); 
    18.     } 
        public final void invoke(Request request, Response response)
            throws IOException, ServletException {
             ...
       ④	servlet = wrapper.allocate();
    
            // 下面开始创建filter链啦
       ⑤   ApplicationFilterFactory factory =
                ApplicationFilterFactory.getInstance();
            ApplicationFilterChain filterChain =
                factory.createFilterChain(request, wrapper, servlet);
            ...
    
            // 调用filter链
            filterChain.doFilter(request.getRequest(),response.getResponse());
    
            if (servlet != null) {
               wrapper.deallocate(servlet);
            }
         }
    

    因为一个wrapper是对应与一个servlet的,所以wrapper.allocate()就是得到它负责封装的那个servlet(代码④处)。 下面在代码⑤处,我们就要来创建filter chain了,anne照样把createFilterChain中的方法稍微提取了一下:

    Java代码 
    1.   public ApplicationFilterChain createFilterChain 
    2.         (ServletRequest request, Wrapper wrapper, Servlet servlet) { 
    3.     ApplicationFilterChain filterChain = new ApplicationFilterChain(); 
    4.     filterChain.setServlet(servlet);  
    5.  
    6.     // 得到context的filter mapping 
    7.     StandardContext context = (StandardContext) wrapper.getParent(); 
    8.     FilterMap filterMaps[] = context.findFilterMaps(); 
    9.  
    10.     // 遍历filterMaps,如果有符合这个servlet的filter就把它加到filter chain中 
    11.     for (int i = 0; i < filterMaps.length; i++) { 
    12.         if (!matchDispatcher(filterMaps[i] ,dispatcher)) { 
    13.           continue; 
    14.         } 
    15.         if (!matchFiltersURL(filterMaps[i], requestPath)) 
    16.             continue; 
    17.  
    18.         ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) 
    19.                 context.findFilterConfig(filterMaps[i].getFilterName()); 
    20.         if (filterConfig == null) { 
    21.             continue; 
    22.         } 
    23.         filterChain.addFilter(filterConfig); 
    24.      } 
    25.     return (filterChain); 
      public ApplicationFilterChain createFilterChain
            (ServletRequest request, Wrapper wrapper, Servlet servlet) {
        ApplicationFilterChain filterChain = new ApplicationFilterChain();
        filterChain.setServlet(servlet); 
    
        // 得到context的filter mapping
        StandardContext context = (StandardContext) wrapper.getParent();
        FilterMap filterMaps[] = context.findFilterMaps();
    
        // 遍历filterMaps,如果有符合这个servlet的filter就把它加到filter chain中
        for (int i = 0; i < filterMaps.length; i++) {
            if (!matchDispatcher(filterMaps[i] ,dispatcher)) {
              continue;
            }
            if (!matchFiltersURL(filterMaps[i], requestPath))
                continue;
    
            ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
                    context.findFilterConfig(filterMaps[i].getFilterName());
            if (filterConfig == null) {
                continue;
            }
            filterChain.addFilter(filterConfig);
         }
        return (filterChain);
    }
    

    我们都知道filter chain也是采用的责任链模式,前面我们说到pipeline中的value也是采用的责任链模式,每个value都持有了下一个value的引用。我们可以看看ahuaxuan的《请问责任链真的是一种设计模式吗》这篇文章(http://ahuaxuan.iteye.com/blog/105825),这里面谈到了三种责任链的实现方式,filter chain就是这第三种潇洒版责任链。 前面pipeline的value链是通过引用的方式来形成一条隐形的链条,而这里,filterChain是真是存在的。我们只需要把这个链条上面的一个一个关节通过filterChain.addFilter()装上即可。这个filterChain中的每个关节都是一个FilterConfig对象,这个对象中包含了filter,context,initParameter等等。
    链条组装完毕! 启动filterChain.doFilter()! doFilter方法主要调用了internalDoFilter()。

    Java代码 
    1.     public void doFilter(ServletRequest request, ServletResponse response) 
    2.         throws IOException, ServletException { 
    3.         ... 
    4.         internalDoFilter(request,response); 
    5.     } 
    6.  
    7.     private void internalDoFilter(ServletRequest request,  
    8.                                   ServletResponse response) 
    9.         throws IOException, ServletException {  
    10.   ⑥    if (pos < n) { 
    11.             ApplicationFilterConfig filterConfig = filters[pos++]; 
    12.             Filter filter = null;  
    13.             filter = filterConfig.getFilter(); 
    14. ... 
    15.             filter.doFilter(request, response, this); 
    16.         } 
    17.  
    18.    ⑦       if ((request instanceof HttpServletRequest) && 
    19.                 (response instanceof HttpServletResponse)) { 
    20.                     servlet.service((HttpServletRequest) request, 
    21.                                     (HttpServletResponse) response); 
    22.                 } 
    23.             } else { 
    24.                 servlet.service(request, response); 
    25.             } 
    26.     } 
        public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
            ...
            internalDoFilter(request,response);
        }
    
        private void internalDoFilter(ServletRequest request, 
                                      ServletResponse response)
            throws IOException, ServletException { 
      ⑥    if (pos < n) {
                ApplicationFilterConfig filterConfig = filters[pos++];
                Filter filter = null; 
                filter = filterConfig.getFilter();
    ...
                filter.doFilter(request, response, this);
    		}
    
       ⑦       if ((request instanceof HttpServletRequest) &&
                    (response instanceof HttpServletResponse)) {
                        servlet.service((HttpServletRequest) request,
                                        (HttpServletResponse) response);
                    }
                } else {
                    servlet.service(request, response);
                }
        }
    

    代码⑥处的pos是filter chain上的一个标识位,表示现在执行的是哪一个filter。然后调用了filter的doFilter方法。我们来看一个例子,一个用来记录执行时间的filter。

    Java代码 
    1. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
    2. rows IOException, ServletException { 
    3. if (attribute != null) 
    4.     request.setAttribute(attribute, this); 
    5.  
    6. long startTime = System.currentTimeMillis(); 
    7. ⑧     chain.doFilter(request, response); 
    8. long stopTime = System.currentTimeMillis(); 
    9. filterConfig.getServletContext().log 
    10.     (this.toString() + ": " + (stopTime - startTime) + 
    11.      " milliseconds"); 
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    	throws IOException, ServletException {
    	  if (attribute != null)
    	      request.setAttribute(attribute, this);
    
    	  long startTime = System.currentTimeMillis();
       ⑧     chain.doFilter(request, response);
    	  long stopTime = System.currentTimeMillis();
    	  filterConfig.getServletContext().log
    	      (this.toString() + ": " + (stopTime - startTime) +
    	       " milliseconds");
        }
    

    首先把当前正在执行的这个filter作为一个attribute放到request中去,接下来我们可以看到在两个time之间,调用了chain.doFilter()(代码⑧处),Chain就是filter chain。这下又要回到internalDoFilter,pos又加了1,就变成执行filterChain的下一个filter了。 如果这个filter chain已经到头了(pos=n),那就进入代码⑦处,就表示request和response已经突破filter的重重阻拦,可以进入servlet了。因此,我们就可以调用wrapper内的servlet的service()方法了。自此进入servlet。这下我们知道了filter原来是这样执行的,它是一层包着一层,一直不断的向内层进发,当进入到最内层,就是servlet了。

    好了,我们现在终于进入servlet的service方法了。

    Java代码 
    1.     protected void service(HttpServletRequest req, HttpServletResponse resp) 
    2.         throws ServletException, IOException { 
    3.  
    4.         String method = req.getMethod(); 
    5.  
    6.         if (method.equals(METHOD_GET)) { 
    7. ... 
    8.             doGet(req, resp); 
    9.         } else if (method.equals(METHOD_HEAD)) { 
    10.             doHead(req, resp); 
    11.         } else if (method.equals(METHOD_POST)) { 
    12.             doPost(req, resp);             
    13.         } else if (method.equals(METHOD_PUT)) { 
    14.             doPut(req, resp);                     
    15.         } else if (method.equals(METHOD_DELETE)) { 
    16.             doDelete(req, resp);             
    17.         } else if (method.equals(METHOD_OPTIONS)) { 
    18.             doOptions(req,resp);             
    19.         } else if (method.equals(METHOD_TRACE)) { 
    20.             doTrace(req,resp);             
    21.         } else { 
    22.             ...// NO servlet supports 
    23.         } 
    24.     } 
        protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
    
            String method = req.getMethod();
    
            if (method.equals(METHOD_GET)) {
    ...
                doGet(req, resp);
            } else if (method.equals(METHOD_HEAD)) {
                doHead(req, resp);
            } else if (method.equals(METHOD_POST)) {
                doPost(req, resp);            
            } else if (method.equals(METHOD_PUT)) {
                doPut(req, resp);                    
            } else if (method.equals(METHOD_DELETE)) {
                doDelete(req, resp);            
            } else if (method.equals(METHOD_OPTIONS)) {
                doOptions(req,resp);            
            } else if (method.equals(METHOD_TRACE)) {
                doTrace(req,resp);            
            } else {
                ...// NO servlet supports
            }
        }
    

    Service方法里面其实很简单,就是根据不同的请求,调用不同的do***()方法。Http的请求类型一共有如上七种。 一个HttpServlet的子类必须至少覆写以下方法中的一个。 1) doGet()方法,适用于HTTP GET请求。自动支持一个HTTP HEAD请求。当覆写doGet()时,首先读取请求数据,写入响应的head,然后获得响应的writer或输出流对象,最后写入响应数据。 2) doPost()方法,适用于HTTP POST请求。覆写该方法与doGet()类似。 3) doPut()方法,适用于HTTP PUT请求。PUT操作允许客户好像使用FTP一样把文件放置到服务器。 4) doDelete()方法,适用于HTTP DELETE请求。DELETE操作允许客户从服务器中删除一个文档或网页。 5) init()和destroy()方法,管理Servlet生命周期中的资源。 6) getServletInfo()方法,提供Servlet本身的信息。
    另外还有不需要覆写的方法: 7) doHead()方法,适用于HTTP HEAD请求。当客户端只需要知道响应头,比如Content-Type或者Content-Length,客户端只需要发送一个HEAD请求。HTTP HEAD会准确地计算输出的字节数来设定Content-Length。如果覆写该方法,可以避免计算响应的BODY,而只需设置响应头以改善性能。 8) doOptions()方法,适用于OPTIONS请求。OPTIONS操作决定服务器支持哪种HTTP方法,并返回一个适当的头信息。例如,如果一个servlet覆写了doGet()方法,doOptions()方法将会返回如下头信息:Allow: GET, HEAD, TRACE, OPTIONS。 9) doTrace()方法,适用于TRACE请求。该方法用于程序调试,无需被覆写。
    尽管说有这么多do***()方法,可是我们常用的就只有doGet()和doPost()。
    Tomcat6讲到这里就大概差不多了,它的组成,启动,消息处理都过了一遍。不过消息处理我只写了nio的处理方式,下篇看看要不写下传统的bio方式。

  • 相关阅读:
    Unique Binary Search Trees——LeetCode
    Binary Tree Inorder Traversal ——LeetCode
    Maximum Product Subarray——LeetCode
    Remove Linked List Elements——LeetCode
    Maximum Subarray——LeetCode
    Validate Binary Search Tree——LeetCode
    Swap Nodes in Pairs——LeetCode
    Find Minimum in Rotated Sorted Array——LeetCode
    Linked List Cycle——LeetCode
    VR AR MR
  • 原文地址:https://www.cnblogs.com/amei0/p/4444184.html
Copyright © 2011-2022 走看看