• 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方式。

  • 相关阅读:
    关于动态规划的问题494_LEETCODE_TARGET_SUM
    Python 关于二叉树生成、先序遍历、中序遍历、后序遍历、反转
    关于python引入文件路径的解决办法
    git一些笔记
    迪克斯特拉 算法(算最短距离)
    Python多线程编程中daemon属性的作用
    types.MethodType实例绑定方法
    Python之__getitem__、__getattr__、__setitem__ 、__setitem__ 的区别
    jenkins自动打包ios、安卓
    python socket编程tcp/udp俩连接
  • 原文地址:https://www.cnblogs.com/amei0/p/4444184.html
Copyright © 2020-2023  润新知