Jetty 的工作原理以及与 Tomcat 的比较
简介:Jetty应该是目前最活跃也是很有前景的一个Servlet引擎。本文将介绍Jetty基本架构与基本的工作原理:您将了解到Jetty的基本体系结构;Jetty的启动过程;Jetty如何接受和处理用户的请求。你还将了解到AJP的一些细节:Jetty如何基于AJP工作;以及Jetty如何集成到Jboss;最后我们将比较一下两个Servlet引擎:Tomcat和Jetty的优缺点。
.Jetty的基本架构
Jetty目前的是一个比较被看好的Servlet引擎,它的架构比较简单,也是一个可扩展性和非常灵活的应用服务器,它有一个基本数据模型,这个数据模型就是Handler,所有可以被扩展的组件都可以作为一个Handler,添加到Server中,Jetty就是帮你管理这些Handler。
Jetty的基本架构
下图是Jetty的基本架构图,整个Jetty的核心组件由Server和Connector两个组件构成,整个Server组件是基于Handler容器工作的,它类似与Tomcat的Container容器,Jetty与Tomcat的比较在后面详细介绍。Jetty中另外一个比不可少的组件是Connector,它负责接受客户端的连接请求,并将请求分配给一个处理队列去执行。
图1.Jetty的基本架构
Jetty中还有一些可有可无的组件,我们可以在它上做扩展。如JMX,我们可以定义一些Mbean把它加到Server中,当Server启动的时候,这些Bean就会一起工作。
图2.Jetty的主要组件的类图
从上图可以看出整个Jetty的核心是围绕着Server类来构建,Server类继承了Handler,关联了Connector和Container。Container是管理Mbean的容器。Jetty的Server的扩展主要是实现一个个Handler并将Handler加到Server中,Server中提供了调用这些Handler的访问规则。
整个Jetty的所有组件的生命周期管理是基于观察者模板设计,它和Tomcat的管理是类似的。下面是LifeCycle的类关系图
每个组件都会持有一个观察者(在这里是Listener类,这个类通常对应到观察者模式中常用的Observer角色,关于观察者模式可以参考《Tomcat系统架构与设计模式,第2部分:设计模式分析》一文中关于观察者模式的讲解)集合,当start、fail或stop等事件触发时,这些Listener将会被调用,这是最简单的一种设计方式,相比Tomcat的LifeCycle要简单的多。
Handler的体系结构
前面所述Jetty主要是基于Handler来设计的,Handler的体系结构影响着整个Jetty的方方面面。下面总结了一下Handler的种类及作用:
图3.Handler的体系结构(查看大图)
Jetty主要提供了两种Handler类型,一种是HandlerWrapper,它可以将一个Handler委托给另外一个类去执行,如我们要将一个Handler加到Jetty中,那么就必须将这个Handler委托给Server去调用。配合ScopeHandler类我们可以拦截Handler的执行,在调用Handler之前或之后,可以做一些另外的事情,类似于Tomcat中的Valve;另外一个Handler类型是HandlerCollection,这个Handler类可以将多个Handler组装在一起,构成一个Handler链,方便我们做扩展。
--------------------------------------------------------------------------------
回页首
Jetty的启动过程
Jetty的入口是Server类,Server类启动完成了,就代表Jetty能为你提供服务了。它到底能提供哪些服务,就要看Server类启动时都调用了其它组件的start方法。从Jetty的配置文件我们可以发现,配置Jetty的过程就是将那些类配置到Server的过程。下面是Jetty的启动时序图:
图4.Jetty的启动流程
因为Jetty中所有的组件都会继承LifeCycle,所以Server的start方法调用就会调用所有已经注册到Server的组件,Server启动其它组件的顺序是:首先启动设置到Server的Handler,通常这个Handler会有很多子Handler,这些Handler将组成一个Handler链。Server会依次启动这个链上的所有Handler。接着会启动注册在Server上JMX的Mbean,让Mbean也一起工作起来,最后会启动Connector,打开端口,接受客户端请求,启动逻辑非常简单。
--------------------------------------------------------------------------------
回页首
接受请求
Jetty作为一个独立的Servlet引擎可以独立提供Web服务,但是它也可以与其他Web应用服务器集成,所以它可以提供基于两种协议工作,一个是HTTP,一个是AJP协议。如果将Jetty集成到Jboss或者Apache,那么就可以让Jetty基于AJP模式工作。下面分别介绍Jetty如何基于这两种协议工作,并且它们如何建立连接和接受请求的。
基于HTTP协议工作
如果前端没有其它web服务器,那么Jetty应该是基于HTTP协议工作。也就是当Jetty接收到一个请求时,必须要按照HTTP协议解析请求和封装返回的数据。那么Jetty是如何接受一个连接又如何处理这个连接呢?
我们设置Jetty的Connector实现类为org.eclipse.jetty.server.bi.SocketConnector让Jetty以BIO的方式工作,Jetty在启动时将会创建BIO的工作环境,它会创建HttpConnection类用来解析和封装HTTP1.1的协议,ConnectorEndPoint类是以BIO的处理方式处理连接请求,ServerSocket是建立socket连接接受和传送数据,Executor是处理连接的线程池,它负责处理每一个请求队列中任务。acceptorThread是监听连接请求,一有socket连接,它将进入下面的处理流程。
当socket被真正执行时,HttpConnection将被调用,这里定义了如何将请求传递到servlet容器里,有如何将请求最终路由到目的servlet,关于这个细节可以参考《servlet工作原理解析》一文。
下图是Jetty启动创建建立连接的时序图:
图5.建立连接的时序图
Jetty创建接受连接环境需要三个步骤:
1.创建一个队列线程池,用于处理每个建立连接产生的任务,这个线程池可以由用户来指定,这个和Tomcat是类似的。
2.创建ServerSocket,用于准备接受客户端的socket请求,以及客户端用来包装这个socket的一些辅助类。
3.创建一个或多个监听线程,用来监听访问端口是否有连接进来。
相比Tomcat创建建立连接的环境,Jetty的逻辑更加简单,牵涉到的类更少,执行的代码量也更少了。
当建立连接的环境已经准备好了,就可以接受HTTP请求了,当Acceptor接受到socket连接后将转入下图所示流程执行:
图6.处理连接时序图
Accetptor线程将会为这个请求创建ConnectorEndPoint。HttpConnection用来表示这个连接是一个HTTP协议的连接,它会创建HttpParse类解析HTTP协议,并且会创建符合HTTP协议的Request和Response对象。接下去就是将这个线程交给队列线程池去执行了。
基于AJP工作
通常一个web服务站点的后端服务器不是将Java的应用服务器直接暴露给服务访问者,而是在应用服务器,如Jboss的前面在加一个web服务器,如Apache或者nginx,我想这个原因大家应该很容易理解,如做日志分析、负载均衡、权限控制、防止恶意请求以及静态资源预加载等等。
下图是通常的web服务端的架构图:
图7.Web服务端架构(查看大图)
这种架构下servlet引擎就不需要解析和封装返回的HTTP协议,因为HTTP协议的解析工作已经在Apache或Nginx服务器上完成了,Jboss只要基于更加简单的AJP协议工作就行了,这样能加快请求的响应速度。
对比HTTP协议的时序图可以发现,它们的逻辑几乎是相同的,不同的是替换了一个类Ajp13Parserer而不是HttpParser,它定义了如何处理AJP协议以及需要哪些类来配合。
实际上在AJP处理请求相比较HTTP时唯一的不同就是在读取到socket数据包时,如何来转换这个数据包,是按照HTTP协议的包格式来解析就是HttpParser,按照AJP协议来解析就是Ajp13Parserer。封装返回的数据也是如此。
让Jetty工作在AJP协议下,需要配置connector的实现类为Ajp13SocketConnector,这个类继承了SocketConnector类,覆盖了父类的newConnection方法,为的是创建Ajp13Connection对象而不是HttpConnection。如下图表示的是Jetty创建连接环境时序图:
与HTTP方式唯一不同的地方的就是将SocketConnector类替换成了Ajp13SocketConnector。改成Ajp13SocketConnector的目的就是可以创建Ajp13Connection类,表示当前这个连接使用的是AJP协议,所以需要用Ajp13Parser类解析AJP协议,处理连接的逻辑都是一样的。如下时序图所示:
基于NIO方式工作
前面所描述的Jetty建立客户端连接到处理客户端的连接都是基于BIO的方式,它也支持另外一种NIO的处理方式,其中Jetty的默认connector就是NIO方式。
关于NIO的工作原理可以参考developerworks上关于NIO的文章,通常NIO的工作原型如下:
Selectorselector=Selector.open();
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);
ServerSocketChannelss=(ServerSocketChannel)key.channel();
SocketChannelsc=ss.accept();
sc.configureBlocking(false);
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
SetselectedKeys=selector.selectedKeys();
创建一个Selector相当于一个观察者,打开一个Server端通道,把这个server通道注册到观察者上并且指定监听的事件。然后遍历这个观察者观察到事件,取出感兴趣的事件再处理。这里有个最核心的地方就是,我们不需要为每个被观察者创建一个线程来监控它随时发生的事件。而是把这些被观察者都注册一个地方统一管理,然后由它把触发的事件统一发送给感兴趣的程序模块。这里的核心是能够统一的管理每个被观察者的事件,所以我们就可以把服务端上每个建立的连接传送和接受数据作为一个事件统一管理,这样就不必要每个连接需要一个线程来维护了。
这里需要注意的地方时,很多人认为监听SelectionKey.OP_ACCEPT事件就已经是非阻塞方式了,其实Jetty仍然是用一个线程来监听客户端的连接请求,当接受到请求后,把这个请求再注册到Selector上,然后才是非阻塞方式执行。这个地方还有一个容易引起误解的地方是:认为Jetty以NIO方式工作只会有一个线程来处理所有的请求,甚至会认为不同用户会在服务端共享一个线程从而会导致基于ThreadLocal的程序会出现问题,其实从Jetty的源码中能够发现,真正共享一个线程的处理只是在监听不同连接的数据传送事件上,比如有多个连接已经建立,传统方式是当没有数据传输时,线程是阻塞的也就是一直在等待下一个数据的到来,而NIO的处理方式是只有一个线程在等待所有连接的数据的到来,而当某个连接数据到来时Jetty会把它分配给这个连接对应的处理线程去处理,所以不同连接的处理线程仍然是独立的。
Jetty的NIO处理方式和Tomcat的几乎一样,唯一不同的地方是在如何把监听到事件分配给对应的连接的处理方式。从测试效果来看Jetty的NIO处理方式更加高效。下面是Jetty的NIO处理时序图:
--------------------------------------------------------------------------------
回页首
处理请求
下面看一下Jetty是如何处理一个HTTP请求的。
实际上Jetty的工作方式非常简单,当Jetty接受到一个请求时,Jetty就把这个请求交给在Server中注册的代理Handler去执行,如何执行你注册的Handler,同样由你去规定,Jetty要做的就是调用你注册的第一个Handler的handle(Stringtarget,RequestbaseRequest,HttpServletRequestrequest,HttpServletResponseresponse)方法,接下去要怎么做,完全由你决定。
要能接受一个web请求访问,首先要创建一个ContextHandler,如下代码所示:
Serverserver=newServer(8080);
ContextHandlercontext=newContextHandler();
context.setContextPath("/");
context.setResourceBase(".");
context.setClassLoader(Thread.currentThread().getContextClassLoader());
server.setHandler(context);
context.setHandler(newHelloHandler());
server.start();
server.join();
当我们在浏览器里敲入http://localhost:8080时的请求将会代理到Server类的handle方法,Server的handle的方法将请求代理给ContextHandler的handle方法,ContextHandler又调用HelloHandler的handle方法。这个调用方式是不是和Servlet的工作方式类似,在启动之前初始化,然后创建对象后调用Servlet的service方法。在Servlet的API中我通常也只实现它的一个包装好的类,在Jetty中也是如此,虽然ContextHandler也只是一个Handler,但是这个Handler通常是由Jetty帮你实现了,我们一般只要实现一些我们具体要做的业务逻辑有关的Handler就好了,而一些流程性的或某些规范的Handler,我们直接用就好了,如下面的关于Jetty支持Servlet的规范的Handler就有多种实现,下面是一个简单的HTTP请求的流程。
访问一个Servlet的代码:
Serverserver=newServer();
Connectorconnector=newSelectChannelConnector();
connector.setPort(8080);
server.setConnectors(newConnector[]{connector});
ServletContextHandlerroot=new
ServletContextHandler(null,"/",ServletContextHandler.SESSIONS);
server.setHandler(root);
root.addServlet(newServletHolder(new
org.eclipse.jetty.embedded.HelloServlet("Hello")),"/");
server.start();
server.join();
创建一个ServletContextHandler并给这个Handler添加一个Servlet,这里的ServletHolder是Servlet的一个装饰类,它十分类似于Tomcat中的StandardWrapper。下面是请求这个Servlet的时序图:
图8.Jetty处理请求的时序图
上图可以看出Jetty处理请求的过程就是Handler链上handle方法的执行过程,在这里需要解释的一点是ScopeHandler的处理规则,ServletContextHandler、SessionHandler和ServletHandler都继承了ScopeHandler,那么这三个类组成一个Handler链,它们的执行规则是:ServletContextHandler.handleServletContextHandler.doScopeSessionHandler.doScopeServletHandler.doScopeServletContextHandler.doHandleSessionHandler.doHandleServletHandler.doHandle,它这种机制使得我们可以在doScope做一些额外工作。
--------------------------------------------------------------------------------
回页首
与Jboss集成
前面介绍了Jetty可以基于AJP协议工作,在正常的企业级应用中,Jetty作为一个Servlet引擎都是基于AJP协议工作的,所以它前面必然有一个服务器,通常情况下与Jboss集成的可能性非常大,这里介绍一下如何与Jboss集成。
Jboss是基于JMX的架构,那么只要符合JMX规范的系统或框架都可以作为一个组件加到Jboss中,扩展Jboss的功能。Jetty作为主要的Servlet引擎当然支持与Jboss集成。具体集成方式如下:
Jetty作为一个独立的Servlet引擎集成到Jboss需要继承Jboss的AbstractWebContainer类,这个类实现的是模板模式,其中有一个抽象方法需要子类去实现,它是getDeployer,可以指定创建web服务的Deployer。Jetty工程中有个jetty-jboss模块,编译这个模块就会产生一个SAR包,或者可以直接从官网下载一个SAR包。解压后如下图:
图9.jboss-jetty目录
在jboss-jetty-6.1.9目录下有一个webdefault.xml配置文件,这个文件是Jetty的默认web.xml配置,在META-INF目录发下发现jboss-service.xml文件,这个文件配置了MBean,如下:
<mbeancode="org.jboss.jetty.JettyService"
name="jboss.web:service=WebServer"xmbean-dd="META-INF/webserver-xmbean.xml">
同样这个org.jboss.jetty.JettyService类也是继成org.jboss.web.AbstractWebContainer类,覆盖了父类的startService方法,这个方法直接调用jetty.start启动Jetty。
--------------------------------------------------------------------------------
回页首
与Tomcat的比较
Tomcat和Jetty都是作为一个Servlet引擎应用的比较广泛,可以将它们比作为中国与美国的关系,虽然Jetty正常成长为一个优秀的Servlet引擎,但是目前的Tomcat的地位仍然难以撼动。相比较来看,它们都有各自的优点与缺点。
Tomcat经过长时间的发展,它已经广泛的被市场接受和认可,相对Jetty来说Tomcat还是比较稳定和成熟,尤其在企业级应用方面,Tomcat仍然是第一选择。但是随着Jetty的发展,Jetty的市场份额也在不断提高,至于原因就要归功与Jetty的很多优点了,而这些优点也是因为Jetty在技术上的优势体现出来的。
架构比较
从架构上来说,显然Jetty比Tomcat更加简单,如果你对Tomcat的架构还不是很了解的话,建议你先看一下《Tomcat系统架构与设计模式》这篇文章。
Jetty的架构从前面的分析可知,它的所有组件都是基于Handler来实现,当然它也支持JMX。但是主要的功能扩展都可以用Handler来实现。可以说Jetty是面向Handler的架构,就像Spring是面向Bean的架构,iBATIS是面向statement一样,而Tomcat是以多级容器构建起来的,它们的架构设计必然都有一个“元神”,所有以这个“元神“构建的其它组件都是肉身。
从设计模板角度来看Handler的设计实际上就是一个责任链模式,接口类HandlerCollection可以帮助开发者构建一个链,而另一个接口类ScopeHandler可以帮助你控制这个链的访问顺序。另外一个用到的设计模板就是观察者模式,用这个设计模式控制了整个Jetty的生命周期,只要继承了LifeCycle接口,你的对象就可以交给Jetty来统一管理了。所以扩展Jetty非常简单,也很容易让人理解,整体架构上的简单也带来了无比的好处,Jetty可以很容易被扩展和裁剪。
相比之下,Tomcat要臃肿很多,Tomcat的整体设计上很复杂,前面说了Tomcat的核心是它的容器的设计,从Server到Service再到engine等container容器。作为一个应用服务器这样设计无口厚非,容器的分层设计也是为了更好的扩展,这是这种扩展的方式是将应用服务器的内部结构暴露给外部使用者,使得如果想扩展Tomcat,开发人员必须要首先了解Tomcat的整体设计结构,然后才能知道如何按照它的规范来做扩展。这样无形就增加了对Tomcat的学习成本。不仅仅是容器,实际上Tomcat也有基于责任链的设计方式,像串联Pipeline的Vavle设计也是与Jetty的Handler类似的方式。要自己实现一个Vavle与写一个Handler的难度不相上下。表面上看,Tomcat的功能要比Jetty强大,因为Tomcat已经帮你做了很多工作了,而Jetty只告诉,你能怎么做,如何做,有你去实现。
打个比方,就像小孩子学数学,Tomcat告诉你1+1=2,1+2=3,2+2=4这个结果,然后你可以根据这个方式得出1+1+2=4,你要计算其它数必须根据它给你的公式才能计算,而Jetty是告诉你加减乘除的算法规则,然后你就可以根据这个规则自己做运算了。所以你一旦掌握了Jetty,Jetty将变得异常强大。
性能比较
单纯比较Tomcat与Jetty的性能意义不是很大,只能说在某种使用场景下,它表现的各有差异。因为它们面向的使用场景不尽相同。从架构上来看Tomcat在处理少数非常繁忙的连接上更有优势,也就是说连接的生命周期如果短的话,Tomcat的总体性能更高。
而Jetty刚好相反,Jetty可以同时处理大量连接而且可以长时间保持这些连接。例如像一些web聊天应用非常适合用Jetty做服务器,像淘宝的web旺旺就是用Jetty作为Servlet引擎。
另外由于Jetty的架构非常简单,作为服务器它可以按需加载组件,这样不需要的组件可以去掉,这样无形可以减少服务器本身的内存开销,处理一次请求也是可以减少产生的临时对象,这样性能也会提高。另外Jetty默认使用的是NIO技术在处理I/O请求上更占优势,Tomcat默认使用的是BIO,在处理静态资源时,Tomcat的性能不如Jetty。
特性比较
作为一个标准的Servlet引擎,它们都支持标准的Servlet规范,还有JavaEE的规范也都支持,由于Tomcat的使用的更加广泛,它对这些支持的更加全面一些,有很多特性Tomcat都直接集成进来了。但是Jetty的应变更加快速,这一方面是因为Jetty的开发社区更加活跃,另一方面也是因为Jetty的修改更加简单,它只要把相应的组件替换就好了,而Tomcat的整体结构上要复杂很多,修改功能比较缓慢。所以Tomcat对最新的Servlet规范的支持总是要比人们预期的要晚。
--------------------------------------------------------------------------------
回页首
总结
本文介绍了目前Java服务端中一个比较流行应用服务器Jetty,介绍了它的基本架构和工作原理以及如何和Jboss工作,最后与Tomcat做了比较。在看这篇文章的时候最好是结合我前面写的两篇文章《Tomcat系统架构与设计模式》和《Servlet工作原理解析》以及这些系统的源代码,耐心的都看一下会让你对Java服务端有个总体的了解。