老话题了,不过经典代码分析总是能学到很多东西。
代码准备与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-srcjakarta-tomcat-5buildwebappsROOT)里寻找资源,如果没有再在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过程,总的说来有这么几个步骤:
更详细的信息可以参考wikipedia或RFC
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包并不开源,所以我们无法得知其具体使用的是什么样的对称加密算法(AES、twofish?),不过现在有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证书安装