技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> 源码分析 --> Tomcat 5源码分析

Tomcat 5源码分析

浏览:4332次  出处信息

    老话题了,不过经典代码分析总是能学到很多东西。 PS:图都存在google drive里的,如果你看不到,说明你已经被HX!!过些日子把所有的图都移过来~^ ^

目录

代码准备与DEBUG调试配置

总体结构

容器与生命周期管理思想

HTTP(TCP)连接管理详细

Deployer模块详细

资源请求与响应

servlet请求

servlet 请求响应

servlet 加载与管理

静态资源请求

SSL详细

思考与问题

Tomcat5中的设计模式

facade

chain of responsibility

factory

observer

问题

magic number

double check

zombie control

REFERENCES

代码准备与DEBUG调试配置

官方下载地址:http://archive.apache.org/dist/tomcat/tomcat-5/v5.0.28/src/

毕竟太老了(04年的东西),很多jar依赖都下不下来了。建议使用我修正后的source,下载后直接根目录ant即可完成build。为了方便跟踪与调试,bin目录下新建一个debug,后面加上:

set JAVA_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8787

即开启JDPA,全称Java

     Platform Debugger Architecture(关于JPDA,这里还有篇论文可以看)同时等待DEBUGER连接后再继续(如果你使用的是我提供的,直接运行debug.bat就可以了)。Eclipse里新建Remote Debug,端口号8787。这样就可以在Eclipse里调试Tomcat 5了。

总体结构

    从下至上:

  • server:代表着整个Catalina(Tomcat内核代号)系统,系统基础,启动入口,全局资源,下属service。注意这里的port是管理用的(比如shutdown什么的),不是http监听端口。
  • service:代表一种服务,比如HTTP服务、JMX、JPDA,其本质上就是一组connector,接收相应的请求后递交给上层container进行处理。一个server可以有多个service,但一个service只对应一个container。
  • engine、host、context、wrapper:Tomcat的容器思想,每个容器实现Container接口,一个容拥有自己的资源配置、装载与事件分发机制。在实际中,事件的分发一般通过“观察者”模式,而上层数据则通过pipeline(后再详说)。
  • (图3 注:成员未全部列出)

    容器与生命周期管理思想

        容器是tomcat 5中的重要思想。一个容器是负责处理加工来自外部的请求并将结果返回的模块。所有的容器都实现了Container这个接口,其采用了典型的Compositor模式。成员中其中较重要的是pipeline与resource。一个请求是通过invoke方法到达的,Host、Wrapper、Context都继承ContainerBase。前者依次被后者包含,Tomcat

         5 对这几个的默认实现均称为StandardXXX,如StandardEngine。 Invoke方法在ContainerBase中的实现即简单的调用pipeline的invoke,如下:

        

        

         通过执行pipeline的valve,执行相应的操作。上层数据的递交就是通过pipeline实现的。容器还采用了Observer模式来分发消息,每当容器发生图3所示事件时,都会通知listener。其实不仅是容器,这种设计思想还贯穿于整个设计中。

        

         再来说说生命周期管理。在启动tomcat的时候,我们能从控制台上明显看出,启动分两个步骤:init与start。Tomcat 模块生命周期管理都实现了Lifecycle这个接口,其定义了start()与stop()方法,不过奇怪的是init()在lifecycle中却没有定义,但几本上实现了lifecycle的类都有init()方法(所以我觉得这个也应该算在lifecycle里面的)。因此,当启动时,只要递归的调用init()与start()方法,就可以完成启动,注销时则递归地调用stop()即可。与容器的事件设计思想相同,生命周期的每步也会触发事件消息,只要订阅对应的事件,就可以非常容易地知晓其某阶段的进展。非常方便、简洁!

        

    HTTP(TCP)连接管理详细

        HTTP使用的是TCP连接。TCP是网联层协议,是端到端的。Tomcat使用的是最原始的Socket进行连接监听,即java.net.ServerSocket.ServerSocket(int, int)。早些时候Tomcat使用的是一个线程监听,多个工作线程的模式:即一个线程专门负责响应连接请求,再递交给工作线程进行处理。Tomcat

         5 后开始使用线程池取代了原有的方案。如下图所示:

        

        

        早期的方式

        

        

         新方式

        那么单从结构来说,新的方式相对了老方法而言,消减了线程之间不必须的关联关系,使单个请求的处理流程完全独立。同时我们看到,在老方式里,一旦listener线程挂掉,那么整个tomcat也就完完了,这是非常危险的事。

        

         从总体来看TCP连接结构管理涉及这几个类:

        

         TcpWorkerThread 是PoolTcpEndPoint的内联类,继承了ThreadWithAttribute接口。可以理解成一个Runable的类。PoolTcpEndPoint里的ThreadPool执行的就是TcpWorkThread任务。启动时,PoolTcpEndPoint会从ThreadPool里取一个线程来监听端口,一旦有连接进来,它就会从ThreadPool里再取一个继续监听。当一个连接进来并最终到达Container(也就是StandardEngine)来进行对应的处理。先来看看整个请求调用的栈:

        

        

         大概有这么几个步骤:

  • 连接进入后,一直执行serverSocket.accept方法并处于block状态的TcpWorkThread会返回socket对象,如果设置了SSL,会进行SSL握手,之后将socket包装成TcpConection递交给Http11ConnectionHandler继续进行处理;
  • Http11ConectionHandler主要是从socket从提取出InputStream与OutputStream,然后调用prosessor.process()交给Http11Processor来解析http请求,也就是说Http11Processor才是真正读取HTTP请求体的单元;
  • Http11Processor主要任务就是解析HTTP请求,它会把InputStream与OutputStream包装成InternalInputBuffer与InternalOutputBuffer,它们主要提供了HTTP头解析的功能。之后设置好maxKeepAlive、timeout(比如上传超时)等socket相关属性,再有就是HTTP一些属性,如是GET请求还是POST等。最后就是对请做一些检查与限制了,如什么样的agent

         直接deny之类的,然后调用adapter.service()交给CoyoteAdapter处理,之后便进入了Container,对应不同的业务处理逻辑。

  • Deployer模块详细

        当start(),init()生命周完成后,便到了deployer起作用的时候了。deploy是由org.apache.catalina.Deployer及其实现完成的。

         最关键的是install方法,URL指一war包的地址,String指的是一个context path。这里顺便说一下tomcat对整个URL请求部分的命名,如下图:

        

         Deployer的实现org.apache.catalina.core.StandardHostDeployer,支持jar包、war包及文件夹的形式对,然后就是一些验校,之后便new一个context,加到host里就OK啦~

    资源请求与响应

    servlet请求

    servelt 请求响应

         先来看看整个调用的栈情况,接着HTTP(TCP)请求之后,继续跟踪:

        

         需要说明的是,index_jsp这个类是根据jsp页面自动生成的,所以上看去有些古怪^_^

        

  • 前面我们说到Http11processor会调用CoyoteAdapter以将数据传给上层Container。顾名意义,CoyoteAdapter使用了Adapter模式,即适配上层容器,实际作用就是将请求转发给上层Container。为了适应Container的参数要求,其会对请求再次做一些处理。主要是集中在“postParseRequest()”方法中,关键代码如下:
  • Container的invoke()实际上就是简的调用其pipeline的invoke()方法,pipeline里的basicValve为StandardHostValve,它会调用StandardHost的pipeline,然后是StandardContextValve,再调用自己的pipeline,再是StandardWrapperValve,调用pipeline,最后便到了StandardWrapper这里。同样wrapper也是调用自己的pipeline,即StandardWrapperValve,它负责分配servlet(如果此servlet工作在SingleThreadMode(STM)下,则每次都需要分配新的serlvet,后详。
  • 同样StandardWrapperValve也是调用自己的pipeline,即StandardWrapperValve,它负责分配servlet(如果此servlet工作在SingleThreadMode(STM)下,则每次都需要分配新的serlvet,后详。然根据servelt与Request创建FilterChain,这里的filter就是我们在tomcat里配置的filter,ApplicationFilterChain调用servlet.service((HttpServletRequest)

         request,(HttpServletResponse) response);终到达servlet。

  •     

    servlet 加载与管理

        分为STM(SingleThreadModel)与非STM,STM使用实例栈来管理。负责生成Servlet实例的类为StandardWrapper,对应的方法为allocate()。代码不长,执行流程图如下:

        

        从上面我们可以比较明确的看出,servlet的加载分两条路线,一条是STM,即一个servlet对应一个线程,类似Spring里的prototype,另一种则是非STM,有点像Singleton,即单例。这里要说的是,Tomcat的servlet加载是支持自定义classLoader的,这里的classLoader来自于Container,前面说过了,Container的作用是管理自己的生态圈,如资源的加载、类的加载以及生命周期管理,所以自定义loader放在那里再适合不过了。

         当一次servlet请求完成以后,StandardWrapperValve就会调用ServletWrapper的dellocate来销毁一个servlet,其过程大致就是上面过程的反向,还是分为STM与非STM,非STM只会减少引用量,instance不会销毁,STM就是将serlvet归还给instancePool。

    静态资源请求

        上面说的是动态servlet的请求过程,那么静态资源又是怎么样响应的呢?那我们知道,HTTP请求实质上就是处理GET,POST,PUT,DELETE四种请求,所以静态资源本质上也是由一个servlet来处理的,当然这个servlet不同于JSP的,他是一种HTTP

         Servlet(DefaultServlet.java)。

         资源的加载逻辑在org.apache.catalina.servlets.DefaultServlet.getResources()方法中。其主要逻辑就是先试着在servlet context(即你应用的根,如:E:\\jakarta-tomcat-5.0.28-src\\jakarta-tomcat-5\\build\\webapps\\ROOT)里寻找资源,如果没有再在JDNI里寻找,如果仍然失败了,就返回null。

        随便来看看Servlet的总的继承结构:

        

         正如前面所说的,所有的jsp页面都会被编译成一个jsp类。以XXX_jsp进行命名。当然这里要列举了一些其它类型的servlet,有兴趣的同学可以去看看。

         现在我们来请求http://localhost:8080/tomcat.gif

         来看看其调用栈:

         开始仍然是一路请求到wrapper容器,之后到defaultServelt,再到getResource。总之,从下往上看,非常清楚的。DefaultServer获取资源的方式有根据JNDI与servletContext两种。

    SSL详细

        SSL即“安全套接字层(Scure Sockets Layer)”,其是这种端到端的加密技术,是“非对称”与“对称加密”的结合(因为对称加密运算少,后详),属于“传输层安全协议”即TLS(Transport Layer Secure),现在普遍使用的是SSL

         3.0版本。建立SSL需要一个handshake过程,总的说来有这么几个步骤:

        

         更详细的信息可以参考wikipediaRFC

         6101。现在我们来看看tomcat里是怎么处理SSL连接请求的。

         要使用SSL首先Tomcat需要配置成支持SSL,详见这里,这样在创建ServerSocket的时候便创建的是com.sun.net.ssl.internal.ssl.SSLSocketImpl

         而不是前说的普通的java.net.ServerSocket.ServerSocket(int, int),之后便用在TcpWorkerThread处理请求时调用其handshake()方法进行上图所描述的协议握手。SSL密文的加解密也是在SSLSocketImpl里进行的,上图已经说明,SSL在session数据传输时使用的是对称加密,不过jsse包并不开源,所以我们无法得知其具体使用的是什么样的对称加密算法(AEStwofish?),不过现在有openJDK,有兴趣的同学的可以去研究一下。之后便到了Http11Processor,这里会根据配置生成一个org.apache.tomcat.util.net.SSLSupport用于向request里注入一些SSL信息,一般不会用。这篇文章主要讲的是tomcat

         5,所以想要更深入的理解SSL,可以参考其它文章。

        

    思考与问题

    Tomcat5中的设计模式

    facade

        facade的作用是为复杂的功能集提供一个统一的入口,还要可以将一个object 隐藏起来,只暴露需要的功能。这里我们举例:org.apache.catalina.core.ApplicationContextFacade,其就是对

         org.apache.catalina.core.ApplicationContext的mask,比如org.apache.catalina.core.ApplicationContext.getServletContextName()方法,ApplicationContextFacade里就是没有的。另外要说的就是facade里对context的调用全采用的是反射,这样确实可以减少耦合性,但本人觉得是没有什么必须的,因为反射会增加代码的复杂程度(当然facade在另一个项目里就另说了)。

    chain of responsibility

        体现责任链模式的设计最明显的当局各Container容器中的pipeline了,每个valve都是一个负责结点,当此任务完成以后,它又会传递给一个结点。为了使用pipeline的执行能够线程安全,负责valve移动的实质上是org.apache.catalina.ValveContext。其负责记录本次pipeline当前执行结点、总结点数及上下文变量。不过比较古怪的是每个pipeline有个名为basic的valve,当pipeline的valves为空时,这个basic

         valve就会执行。

    factory

        工厂模式是个很简单的模式,到处可见。比如org.apache.tomcat.util.net.ServerSocketFactory,此工厂负责生产java.net.ServerSocket。为了方便,工厂本身又是一个单例,所以这里又会涉及一些同步问题,一般有double

         check,staic initialization之类的方便,不过tomcat里却用的是最简单当然也最有效的方法,就是synchronized method :)

        

    observer

        这个在前面我们就见得很多了,很多容器的事件钩子都是这样实现的。比如:StandardServer.java。观察者都在listener数组中,通知事件时publisher挨个进行通知,当然这里会有一些同步问题,如下:

        

        我们看到其在递归通知对listerner进行了clone那是因为不这样做可以会出现IndexOutOfBoundry问题,即使采用安全的iterator,也会出现ConcurrentModificationException.

        

    问题

        当然金无赤足,人无完人。再好的代码也会有问题,下面就来看看:

    magic number

        这个是老问题了,其实如果要求不是很严格的话,这个也可以不算个问题。比如:org.apache.coyote.http11.Http11Processor.process(InputStream, OutputStream) 第 775 行与第 787 行。

         显然这里如果将400改为HttpStatusEnum.BAD_REQUEST要好很多,但开发者却没有这么做,估计是太懒了吧 ^_^

    double check

        standardservice 478行

        

        严格来说,只有使用的是1.5JDK及以后的版本,同时将container标记为volatile才能完全的线程安全,不然可能获取到未初始化完全的对象(见《Concurrency In Practice》16.2.4.Double Checked Locking 一节)。

    zombie control

        这个问题子来源于 http11processor 第745行:

        

         这个就太明显了,就不多说了。

        

    REFERENCES

  • Java Concurrency In PracticeBrianGöetz, TimPeierls, etc.
  • http://www.sunchis.com/html/java/javaweb/2010/0314/71.html

         Tomcat SSL配置及Tomcat CA证书安装

  •     

    建议继续学习:

    1. tomcat catalina.out日志切割每天生成一个文件    (阅读:8094)
    2. Tomcat内存溢出的原因    (阅读:3377)
    3. tomcat的虚拟目录    (阅读:2780)
    4. 在tomcat应用中获得原始IP    (阅读:2016)
    QQ技术交流群:445447336,欢迎加入!
    扫一扫订阅我的微信号:IT技术博客大学习
    © 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

    京ICP备15002552号-1