Jetty和DirectWebRem.doc_第1页
Jetty和DirectWebRem.doc_第2页
Jetty和DirectWebRem.doc_第3页
Jetty和DirectWebRem.doc_第4页
Jetty和DirectWebRem.doc_第5页
已阅读5页,还剩11页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Jetty 和 Direct Web Remotin简介:受异步服务器端事件驱动的Ajax应用程序实现较为困难,并且难于扩展。Philip McCarthy在其广受欢迎的系列文章中介绍了一种行之有效的方法:结合使用Comet模式(将数据推到客户机)和Jetty 6的Continuations API(将Comet应用程序扩展到大量客户机中)。您可以方便地在Direct Web Remoting(DWR)2中将Comet和Continuations与Reverse Ajax技术结合使用。查看本系列更多内容标记本文!原文地址:发布日期:2007年8月02日级别:中级访问情况69次浏览建议:0(添加评论)平均分(共1个评分)作为一种广泛使用的Web应用程序开发技术,Ajax牢固确立了自己的地位,随之而来的是一些通用Ajax使用模式。例如,Ajax经常用于对用户输入作出响应,然后使用从服务器获得的新数据修改页面的部分内容。但是,有时Web应用程序的用户界面需要进行更新以响应服务器端发生的异步事件,而不需要用户操作-例如,显示到达Ajax聊天应用程序的新消息,或者在文本编辑器中显示来自另一个用户的改变。由于只能由浏览器建立Web浏览器和服务器之间的HTTP连接,服务器无法在改动发生时将变化推送给浏览器。Ajax应用程序可以使用两种基本的方法解决这一问题:一种方法是浏览器每隔若干秒时间向服务器发出轮询以进行更新,另一种方法是服务器始终打开与浏览器的连接并在数据可用时发送给浏览器。长期连接技术被称为Comet(请参阅参考资料)。本文将展示如何结合使用Jetty servlet引擎和DWR简捷有效地实现一个Comet Web应用程序。为什么使用Comet?轮询方法的主要缺点是:当扩展到更多客户机时,将生成大量的通信量。每个客户机必须定期访问服务器以检查更新,这为服务器资源添加了更多负荷。最坏的一种情况是对不频繁发生更新的应用程序使用轮询,例如一种Ajax邮件Inbox。在这种情况下,相当数量的客户机轮询是没有必要的,服务器对这些轮询的回答只会是没有产生新数据。虽然可以通过增加轮询的时间间隔来减轻服务器负荷,但是这种方法会产生不良后果,即延迟客户机对服务器事件的感知。当然,很多应用程序可以实现某种权衡,从而获得可接受的轮询方法。尽管如此,吸引人们使用Comet策略的其中一个优点是其显而易见的高效性。客户机不会像使用轮询方法那样生成烦人的通信量,并且事件发生后可立即发布给客户机。但是保持长期连接处于打开状态也会消耗服务器资源。当等待状态的servlet持有一个持久性请求时,该servlet会独占一个线程。这将限制Comet对传统servlet引擎的可伸缩性,因为客户机的数量会很快超过服务器栈能有效处理的线程数量。回页首Jetty 6有何不同Jetty 6的目的是扩展大量同步连接,使用Java语言的非阻塞I/O(java.nio)库并使用一个经过优化的输出缓冲架构(参阅参考资料)。Jetty还为处理长期连接提供了一些技巧:该特性称为Continuations。我将使用一个简单的servlet对Continuations进行演示,这个servlet将接受请求,等待处理,然后发送响应。接下来,我将展示当客户机数量超过服务器提供的处理线程后发生的状况。最后,我将使用Continuations重新实现servlet,您将了解Continuations在其中扮演的角色。为了便于理解下面的示例,我将把Jetty servlet引擎限制在一个单请求处理线程。清单1展示了jetty.xml中的相关配置。我实际上需要在ThreadPool使用三个线程:Jetty服务器本身使用一个线程,另一线程运行HTTP连接器,侦听到来的请求。第三个线程执行servlet代码。清单1.单个servlet线程的Jetty配置?xml version=1.0?!DOCTYPE Configure PUBLIC-/Mort Bay Consulting/DTD Configure/ENConfigure id=Serverclass=org.mortbay.jetty.ServerSet name=ThreadPoolNew class=org.mortbay.thread.BoundedThreadPoolSet name=minThreads3/Set Set name=lowThreads0/Set Set name=maxThreads3/Set/New/Set/Configure接下来,为了模拟对异步事件的等待,清单2展示了BlockingServlet的service()方法,该方法将使用Thread.sleep()调用在线程结束之前暂停2000毫秒的时间。它还在执行开始和结束时输出系统时间。为了区别输出和不同的请求,还将作为标识符的请求参数记录在日志中。清单2.BlockingServlet public class BlockingServlet extends HttpServletpublic void service(HttpServletRequest req,HttpServletResponse res)throws java.io.IOExceptionString reqId=req.getParameter(id);res.setContentType(text/plain);res.getWriter().println(Request:+reqId+tstart:t+new Date();res.getWriter().flush();tryThread.sleep(2000);catch(Exception e)res.getWriter().println(Request:+reqId+tend:t+new Date();现在可以观察到servlet响应一些同步请求的行为。清单3展示了控制台输出,五个使用lynx的并行请求。命令行启动五个lynx进程,将标识序号附加在请求URL的后面。清单3.对BlockingServlet并发请求的输出$for iinseq 15;do lynx-dump localhost:8080/blocking?id=$i&done Request:1 start:Sun Jul 01 12:32:29 BST 2007 Request:1 end:Sun Jul 01 12:32:31 BST 2007 Request:2 start:Sun Jul 01 12:32:31 BST 2007 Request:2 end:Sun Jul 01 12:32:33 BST 2007 Request:3 start:Sun Jul 01 12:32:33 BST 2007 Request:3 end:Sun Jul 01 12:32:35 BST 2007 Request:4 start:Sun Jul 01 12:32:35 BST 2007 Request:4 end:Sun Jul 01 12:32:37 BST 2007 Request:5 start:Sun Jul 01 12:32:37 BST 2007 Request:5 end:Sun Jul 01 12:32:39 BST 2007清单3中的输出和预期一样。因为Jetty只可以使用一个线程执行servlet的service()方法。Jetty对请求进行排列,并按顺序提供服务。当针对某请求发出响应后将立即显示时间戳(一个end消息),servlet接着处理下一个请求(后续的start消息)。因此即使同时发出五个请求,其中一个请求必须等待8秒钟的时间才能接受servlet处理。请注意,当servlet被阻塞时,执行任何操作都无济于事。这段代码模拟了请求等待来自应用程序不同部分的异步事件。这里使用的服务器既不是CPU密集型也不是I/O密集型:只有线程池耗尽之后才会对请求进行排队。现在,查看Jetty 6的Continuations特性如何为这类情形提供帮助。清单4展示了清单2中使用Continuations API重写后的BlockingServlet。我将稍后解释这些代码。清单4.ContinuationServlet public class ContinuationServlet extends HttpServletpublic void service(HttpServletRequest req,HttpServletResponse res)throws java.io.IOExceptionString reqId=req.getParameter(id);Continuation cc=ContinuationSupport.getContinuation(req,null);res.setContentType(text/plain);res.getWriter().println(Request:+reqId+tstart:t+new Date();res.getWriter().flush();cc.suspend(2000);res.getWriter().println(Request:+reqId+tend:t+new Date();清单5展示了对ContinuationServlet的五个同步请求的输出;请与清单3进行比较。清单5.对ContinuationServlet的五个并发请求的输出$for iinseq 15;do lynx-dump localhost:8080/continuation?id=$i&done Request:1 start:Sun Jul 01 13:37:37 BST 2007 Request:1 start:Sun Jul 01 13:37:39 BST 2007 Request:1 end:Sun Jul 01 13:37:39 BST 2007 Request:3 start:Sun Jul 01 13:37:37 BST 2007 Request:3 start:Sun Jul 01 13:37:39 BST 2007 Request:3 end:Sun Jul 01 13:37:39 BST 2007 Request:2 start:Sun Jul 01 13:37:37 BST 2007 Request:2 start:Sun Jul 01 13:37:39 BST 2007 Request:2 end:Sun Jul 01 13:37:39 BST 2007 Request:5 start:Sun Jul 01 13:37:37 BST 2007 Request:5 start:Sun Jul 01 13:37:39 BST 2007 Request:5 end:Sun Jul 01 13:37:39 BST 2007 Request:4 start:Sun Jul 01 13:37:37 BST 2007 Request:4 start:Sun Jul 01 13:37:39 BST 2007 Request:4 end:Sun Jul 01 13:37:39 BST 2007清单5中有两处需要重点注意。首先,每个start消息出现两次;先不要着急。其次,更重要的一点,请求现在不需排队就能够并发处理,注意所有start和end消息的时间戳是相同的。因此,每个请求的处理时间不会超过两秒,即使只运行一个servlet线程。回页首Jetty Continuations机制原理理解了Jetty Continuations机制的实现原理,您就能够解释清单5中的现象。要使用Continuations,必须对Jetty进行配置,以使用其SelectChannelConnector处理请求。这个连接器构建在java.nio API之上,因此使它能够不用消耗每个连接的线程就可以持有开放的连接。当使用SelectChannelConnector时,ContinuationSupport.getContinuation()将提供一个SelectChannelConnector.RetryContinuation实例。(然而,您应该只针对Continuation接口进行编码;请参阅Portability and the Continuations API。)当对RetryContinuation调用suspend()时,它将抛出一个特殊的运行时异常-RetryRequest-该异常将传播到servlet以外并通过过滤器链传回,并由SelectChannelConnector捕获。但是发生该异常之后并没有将响应发送给客户机,请求被放到处于等待状态的Continuation队列中,而HTTP连接仍然保持打开状态。此时,为该请求提供服务的线程将返回ThreadPool,用以为其他请求提供服务。可移植性和Continuations API我提到过应该使用Jetty的SelectChannelConnector来启用Continuations功能。然而,Continuations API仍然可用于传统的SocketConnector,这种情况下Jetty将回退到不同的Continuation实现,该实现使用wait()/notify()方法。您的代码仍然可以编译和运行,但是却失去了非阻塞Continuations的优点。如果您希望继续使用非Jetty服务器,您应该考虑编写自己的Continuation包装器,在运行时期使用反射检查Jetty Continuations库是否可用。DWR就使用了这种策略。暂停的请求将一直保持在等待状态的Continuation队列,直到超出指定的时限,或者当对resume()方法的Continuation调用resume()时(稍后将详细介绍)。出现上述任意一种条件时,请求将被重新提交到servlet(通过过滤器链)。事实上,整个请求被重新进行处理,直到首次调用suspend()。当执行第二次发生suspend()调用时,RetryRequest异常不会被抛出,执行照常进行。现在应该可以解释清单5中的输出了。每个请求依次进入servlet的service()方法后,将发送start消息进行响应,Continuation的suspend()方法引发servlet异常,将释放线程使其处理下一个请求。所有五个请求快速通过service()方法的第一部分,并进入等待状态,并且所有start消息将在几毫秒内输出。两秒后,当超过suspend()的时限后,将从等待队列中检索第一个请求,并将其重新提交给ContinuationServlet。第二次输出start消息,立即返回对suspend()的第二次调用,并且发送end消息进行响应。然后将在此执行servlet代码来处理队列中的下一个请求,以此类推。因此,在BlockingServlet和ContinuationServlet两种情况中,请求被放入队列中以访问单个servlet线程。然而,虽然servlet线程执行期间BlockingServlet发生两秒暂停,SelectChannelConnector中的ContinuationServlet的暂停发生在servlet之外。ContinuationServlet的总吞吐量更高一些,因为servlet线程没有将大部分时间用在sleep()调用中。回页首使Continuations变得有用现在您已经了解到Continuations能够不消耗线程就可以暂停servlet请求,我需要进一步解释Continuations API以向您展示如何在实际应用中使用。resume()方法生成一对suspend()。可以将它们视为标准的Object wait()/notify()机制的Continuations等价体。就是说,suspend()使Continuation(因此也包括当前方法的执行)处于暂停状态,直到超出时限,或者另一个线程调用resume()。suspend()/resume()对于实现真正使用Continuations的Comet风格的服务非常关键。其基本模式是:从当前请求获得Continuation,调用suspend(),等待异步事件的到来。然后调用resume()并生成一个响应。然而,与Scheme这种语言中真正的语言级别的continuations或者是Java语言的wait()/notify()范例不同的是,对Jetty Continuation调用resume()并不意味着代码会从中断的地方继续执行。正如您刚刚看到的,实际上和Continuation相关的请求被重新处理。这会产生两个问题:重新执行清单4中的ContinuationServlet代码,以及丢失状态:即调用suspend()时丢失作用域内所有内容。第一个问题的解决方法是使用isPending()方法。如果isPending()返回值为true,这意味着之前已经调用过一次suspend(),而重新执行请求时还没有发生第二次suspend()调用。换言之,根据isPending()条件在执行suspend()调用之前运行代码,这样将确保对每个请求只执行一次。在suspend()调用具有等幂性之前,最好先对应用程序进行设计,这样即使调用两次也不会出现问题,但是某些情况下无法使用isPending()方法。Continuation也提供了一种简单的机制来保持状态:putObject(Object)和getObject()方法。在Continuation发生暂停时,使用这两种方法可以保持上下文对象以及需要保存的状态。您还可以使用这种机制作为在线程之间传递事件数据的方式,稍后将演示这种方法。回页首编写基于Continuations的应用程序作为实际示例场景,我将开发一个基本的GPS坐标跟踪Web应用程序。它将在不规则的时间间隔内生成随机的经纬度值对。发挥一下想象力,生成的坐标值可能就是临近的一个公共车站、随身携带着GPS设备的马拉松选手、汽车拉力赛中的汽车或者运输中的包裹。令人感兴趣的是我将如何告诉浏览器这个坐标。图1展示了这个简单的GPS跟踪器应用程序的类图:图1.显示GPS跟踪器应用程序主要组件的类图首先,应用程序需要某种方法来生成坐标。这将由RandomWalkGenerator完成。从一对初始坐标对开始,每次调用它的私有generateNextCoord()方法时,将从该位置移动随机指定的距离,并将新的位置作为GpsCoord对象返回。初始化完成后,RandomWalkGenerator将生成一个线程,该线程以随机的时间间隔调用generateNextCoord()方法并将生成的坐标发送给任何注册了addListener()的CoordListener实例。清单6展示了RandomWalkGenerator循环的逻辑:清单6.RandomWalkGenerators run()方法public void run()trywhile(true)int sleepMillis=5000+(int)(Math.random()*8000d);Thread.sleep(sleepMillis);dispatchUpdate(generateNextCoord();catch(Exception e)throw new RuntimeException(e);CoordListener是一个回调接口,仅仅定义onCoord(GpsCoord coord)方法。在本例中,ContinuationBasedTracker类实现CoordListener。ContinuationBasedTracker的另一个公有方法是getNextPosition(Continuation,int)。清单7展示了这些方法的实现:清单7.ContinuationBasedTracker结构public GpsCoord getNextPosition(Continuation continuation,int timeoutSecs)synchronized(this)if(!continuation.isPending()pendingContinuations.add(continuation);/Wait for next update continuation.suspend(timeoutSecs*1000);return(GpsCoord)continuation.getObject();public void onCoord(GpsCoord gpsCoord)synchronized(this)for(Continuation continuation:pendingContinuations)continuation.setObject(gpsCoord);continuation.resume();pendingContinuations.clear();当客户机使用Continuation调用getNextPosition()时,isPending方法将检查此时的请求是否是第二次执行,然后将它添加到等待坐标的Continuation集合中。然后该Continuation被暂停。同时,onCoord-生成新坐标时将被调用-循环遍历所有处于等待状态的Continuation,对它们设置GPS坐标,并重新使用它们。之后,每个再次执行的请求完成getNextPosition()执行,从Continuation检索GpsCoord并将其返回给调用者。注意此处的同步需求,是为了保护pendingContinuations集合中的实例状态不会改变,并确保新增的Continuation在暂停之前没有被处理过。最后一个难点是servlet代码本身,如清单8所示:清单8.GPSTrackerServlet实现public class GpsTrackerServlet extends HttpServletprivate static final int TIMEOUT_SECS=60;private ContinuationBasedTracker tracker=new ContinuationBasedTracker();public void service(HttpServletRequest req,HttpServletResponse res)throws java.io.IOExceptionContinuation c=ContinuationSupport.getContinuation(req,null);GpsCoord position=tracker.getNextPosition(c,TIMEOUT_SECS);String json=new Jsonifier().toJson(position);res.getWriter().print(json);如您所见,servlet只执行了很少的工作。它仅仅获取了请求的Continuation,调用getNextPosition(),将GPSCoord转换成JavaScript Object Notation(JSON),然后输出。这里不需要防止重新执行,因此我不必检查isPending()。清单9展示了调用GpsTrackerServlet的输出,同样,有五个同步请求而服务器只有一个可用线程:Listing 9.Output of GPSTrackerServlet$for iinseq 15;do lynx-dump localhost:8080/tracker&donecoord:lat:51.51122,lng:-0.08103112coord:lat:51.51122,lng:-0.08103112coord:lat:51.51122,lng:-0.08103112coord:lat:51.51122,lng:-0.08103112coord:lat:51.51122,lng:-0.08103112这个示例并不引人注意,但是提供了概念证明。发出请求后,它们将一直保持打开的连接直至生成坐标,此时将快速生成响应。这是Comet模式的基本原理,Jetty使用这种原理在一个线程内处理5个并发请求,这都是Continuations的功劳。回页首创建一个Comet客户机现在您已经了解了如何使用Continuations在理论上创建非阻塞Web服务,您可能想知道如何创建客户端代码来使用这种功能。一个Comet客户机需要完成以下功能:保持打开XMLHttpRequest连接,直到收到响应。将响应发送到合适的JavaScript处理程序。立即建立新的连接。更高级的Comet设置将使用一个连接将数据从不同服务推入浏览器,并且客户机和服务器配有相应的路由机制。一种可行的方法是根据一种JavaScript库,例如Dojo,编写客户端代码,这将提供基于Comet的请求机制,其形式为etd。然而,如果服务器使用Java语言,使用DWR 2可以同时在客户机和服务器上获得Comet高级支持,这是一种不错的方法(参阅参考资料)。如果您并不了解DWR的话,请参阅本系列第3部分结合Direct Web Remoting使用Ajax。DWR透明地提供了一种HTTP-RPC传输层,将您的Java对象公开给网络中JavaScript代码的调用。DWR生成客户端代理,将自动封送和解除封送数据,处理安全问题,提供方便的客户端实用工具库,并可以在所有主要浏览器上工作。回页首DWR 2:Reverse Ajax DWR 2最新引入了Reverse Ajax概念。这种机制可以将服务器端事件推入到客户机。客户端DWR代码透明地处理已建立的连接并解析响应,因此从开发人员的角度来看,事件是从服务器端Java代码轻松地发布到客户机中。DWR经过配置之后可以使用Reverse Ajax的三种不同机制。第一种就是较为熟悉的轮询方法。第二种称为piggyback,这种机制并不创建任何到服务器的连接,相反,将一直等待直至发生另一个DWR服务,piggybacks使事件等待该请求的响应。这使它具有较高的效率,但也意味着客户机事件通知被延迟到直到发生另一个不相关的客户机调用。最后一种机制使用长期的、Comet风格的连接。最妙的是,当运行在Jetty下时,DWR能够自动检测并切换为使用Contiuations,实现非阻塞Comet。我将在GPS示例中结合使用Reverse Ajax和DWR 2。通过这种演示,您将对Reverse Ajax的工作原理有更多的了解。此时不再需要使用servlet。DWR提供了一个控制器servlet,它将在Java对象之上直接转交客户机请求。同样也不需要显式地处理Continuations,因为DWR将在内部进行处理。因此我只需要一个新的CoordListener实现,将坐标更新发布到到任何客户机浏览器上。ServerContext接口提供了DWR的Reverse Ajax功能。ServerContext可以察觉到当前查看给定页面的所有Web客户机,并提供一个ScriptSession进行相互通信。ScriptSession用于从Java代码将JavaScript片段推入到客户机。清单10展示了ReverseAjaxTracker响应坐标通知的方式,并使用它们生成对客户端updateCoordinate()函数的调用。注意对DWR ScriptBuffer对象调用appendData()将自动把Java对象封送给JSON(如果使用合适的转换器)。清单10.ReverseAjaxTracker中的通知回调方法public void onCoord(GpsCoord gpsCoord)/Generate JavaScript code to call client-side/function with coord data ScriptBuffer script=new ScriptBuffer();script.appendScript(updateCoordinate().appendData(gpsCoord).appendScript(););/Push script out to clients viewing the page Collection ScriptSession sessions=sctx.getScriptSessionsByPage(pageUrl);for(ScriptSession session:sessions)session.addScript(script);接下来,必须对DWR进行配置以感知ReverseAjaxTracker的存在。在大型应用程序中,可以使用DWR的Spring集成提供Spring生成的bean。但是,在本例中,我仅使用DWR创建了一个ReverseAjaxTracker新实例并将其放到application范围中。所有后续请求将访问这个实例。我还需告诉DWR如何将数据从GpsCoord beans封送到JSON。由于GpsCoord是一个简单对象,DWR的基于反射的BeanConverter就可以完成此功能。清单11展示了ReverseAjaxTracker的配置:清单11.ReverseAjaxTracker的DWR配置dwr allow create creator=newjavascript=Trackerscope=applicationparam name=classvalue=developerworks.jetty6.gpstracker.ReverseAjaxTracker/create convert converter=beanmatch=developerworks.jetty6.gpstracker.GpsCoord/allow/dwr create元素的javascript属性指定了DWR用于将跟踪器公开为JavaScript对象的名字,在本例中,我的客户端代码没有使用该属性,而是将数据从跟踪器推入到其中。同样,还需对web.xml进行额外的配置,以针对Reverse Ajax配置DWR,如清单12所示:清单12.DwrServlet的web.xml配置servlet servlet-name dwr-invoker/servlet-name servlet-class org.directwebremoting.servlet.DwrServlet/servlet-class init-param param-name activeReverseAjaxEnabled/param-name param-value true/param-value/init-param init-param param-name initApplicationScopeCreatorsAtStartup/param-name param-value true/param-value/init-param load-on-startup 1/load-on-startup/servlet第一个servlet init-param,activeReverseAjaxEnabled将激活轮询和Comet功能。第二个initApplicationScopeCreatorsAtStartup通知DWR在应用程序启动时初始化ReverseAjaxTracker。这将在对bean生成第一个请求时改写延迟初始化(lazy initialization)的常规行为-在本例中这是必须的,因为客户机不会主动对ReverseAjaxTracker调用方法。最后,我需要实现调用自DWR的客户端JavaScript函数。将向回调函数-updateCoordinate()-传递GpsCoord Java bean的JSON表示,由DWR的BeanConverter自动序列化。该函数将从坐标中提取longitude和latitude字段,并通过调用Document Object Model(DOM)将它们附加到列表中。清单13展示了这一过程,以及页面的onload函数。onload包含对dwr.engine.setActiveReverseAjax(true)的调用,将通知DWR打开与服务器的持久连接并等待

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论