CometProcessor 接口要求实现 event 方法这是用于 Comet 交互的一个生命周期方法Tomcat 将使用不同的 CometEvent 实例调用通过检查 CometEvent 的 eventType可以判断正处在生命周期的哪个阶段当请求第一次传入时即发生 BEGIN 事件READ 事件表明数据正在被发送只有当请求为 POST 时才需要该事件遇到 END 或 ERROR 事件时请求终止
在清单 的例子中Servlet 使用一个 MessageSender 类发送数据这个类的实例是在 servlet 的 init 方法中在其自身的线程中创建并在 servlet 的 destroy 方法中销毁的清单 显示了 MessageSender
清单 MessageSender
privateclassMessageSenderimplementsRunnable{
protectedbooleanrunning=true;
protectedfinalArrayListmessages=newArrayList();
privateServletResponseconnection;
privatesynchronizedvoidsetConnection(ServletResponseconnection){
nnection=connection;
notify();
}
publicvoidsend(Stringmessage){
synchronized(messages){
messagesadd(message);
log(Messageadded#messages=+messagessize());
messagesnotify();
}
}
publicvoidrun(){
while(running){
if(messagessize()==){
try{
synchronized(messages){
messageswait();
}
}catch(InterruptedExceptione){
//Ignore
}
}
String[]pendingMessages=null;
synchronized(messages){
pendingMessages=messagestoArray(newString[]);
messagesclear();
}
try{
if(connection==null){
try{
synchronized(this){
wait();
}
}catch(InterruptedExceptione){
//Ignore
}
}
PrintWriterwriter=connectiongetWriter();
for(intj=;j<pendingMessageslength;j++){
finalStringforecast=pendingMessages[j]+
;
writerprintln(forecast);
log(Writing:+forecast);
}
writerflush();
writerclose();
connection=null;
log(Closingconnection);
}catch(IOExceptione){
log(IOExeptionsendingmessagee);
}
}
}
}
这个类基本上是样板代码与 Comet 没有直接的关系但是有两点要注意这个类含有一个 ServletResponse 对象回头看看清单 中的 event 方法当事件为 BEGIN 时response 对象被传入到 MessageSender 中在 MessageSender 的 run 方法中它使用 ServletResponse 将数据发送回客户机注意一旦发送完所有排队等待的消息后它将关闭连接这样就实现了长轮询如果要实现流风格的 Comet那么需要使连接保持开启但是仍然刷新数据
回头看清单 可以发现其中创建了一个 Weatherman 类正是这个类使用 MessageSender 将数据发送回客户机这个类使用 Yahoo RSS feed 获得不同地区的天气信息并将该信息发送到客户机这是一个特别设计的例子用于模拟以异步方式发送数据的数据源清单 显示了它的代码
清单 Weatherman
privateclassWeathermanimplementsRunnable{
privatefinalListzipCodes;
privatefinalStringYAHOO_WEATHER=;
publicWeatherman(Integerzips){
zipCodes=newArrayList(zipslength);
for(Integerzip:zips){
try{
zipCodesadd(newURL(YAHOO_WEATHER+zip));
}catch(Exceptione){
//dontadditifitsucks
}
}
}
publicvoidrun(){
inti=;
while(i>=){
intj=i%zipCodessize();
SyndFeedInputinput=newSyndFeedInput();
try{
SyndFeedfeed=inputbuild(newInputStreamReader(zipCodesget(j)
openStream()));
SyndEntryentry=(SyndEntry)feedgetEntries()get();
messageSendersend(entryToHtml(entry));
Threadsleep(L);
}catch(Exceptione){
//justeatiteatit
}
i++;
}
}
privateStringentryToHtml(SyndEntryentry){
StringBuilderhtml=newStringBuilder(
);
htmlappend(entrygetTitle());
htmlappend(
);
htmlappend(entrygetDescription()getValue());
returnhtmltoString();
}
}
这个类使用 Project Rome 库解析来自 Yahoo Weather 的 RSS feed如果需要生成或使用 RSS 或 Atom feed这是一个非常有用的库此外这个代码中只有一个地方值得注意那就是它产生另一个线程用于每过 秒钟发送一次天气数据最后我们再看一个地方使用该 Servlet 的客户机代码在这种情况下一个简单的 JSP 加上少量的 JavaScript 就足够了清单 显示了该代码
清单 客户机 Comet 代码
<%@pagecontentType=text/htmlpageEncoding=UTF%>
<!DOCTYPEHTMLPUBLIC//WC//DTDHTMLTransitional//EN
>
<html>
<head>
<metahttpequiv=ContentTypecontent=text/html;charset=UTF>
<title>CometWeather</title>
<SCRIPTTYPE=text/Javascript>
functiongo(){
varurl=//localhost:/WeatherServer/Weather
varrequest=newXMLHttpRequest();
requestopen(GETurltrue);
requestsetRequestHeader(ContentTypeapplication/xjavascript;);
requestonreadystatechange=function(){
if(requestreadyState==){
if(requeststatus==){
if(requestresponseText){
documentgetElementById(forecasts)innerHTML=
requestresponseText;
}
}
go();
}
};
requestsend(null);
}
</SCRIPT>
</head>
<body>
<h>RapidFireWeather</h>
<inputtype=buttononclick=go()value=Go!></input>
<divid=forecasts></div>
</body>
</html>
该代码只是在用户单击 Go 按钮时开始长轮询注意它直接使用 XMLHttpRequest 对象所以这在 Internet Explorer 中将不能工作您可能需要使用一个 Ajax 库解决浏览器差异问题除此之外惟一需要注意的是回调函数或者为请求的 onreadystatechange 函数创建的闭包该函数粘贴来自服务器的新的数据然后重新调用 go 函数
现在我们看过了一个简单的 Comet 应用程序在 Tomcat 上是什么样的有两件与 Tomcat 密切相关的事情要做一是配置它的连接器二是在 Servlet 中实现一个特定于 Tomcat 的接口您可能想知道将该代码 移植 到 Jetty 有多大难度接下来我们就来看看这个问题
Jetty 和 Comet
Jetty 服务器使用稍微不同的技术来支持 Comet 的可伸缩的实现Jetty 支持被称作 continuations 的编程结构其思想很简单请求先被暂停然后在将来的某个时间点再继续规定时间到期或者某种有意义的事件发生都可能导致请求继续当请求被暂停时它的线程被释放
可以使用 Jetty 的 orgmortbayutilajaxContinuationSupport 类为任何 HttpServletRequest 创建 orgmortbayutilajaxContinuation 的一个实例这种方法与 Comet 有很大的不同但是continuations 可用于实现逻辑上等效的 Comet清单 显示清单 中的 weather servlet 移植 到 Jetty 后的代码
清单 Jetty Comet servlet
publicclassJettyWeatherServletextendsHttpServlet{
privateMessageSendermessageSender=null;
privatestaticfinalIntegerTIMEOUT=*;
publicvoidbegin(HttpServletRequestrequestHttpServletResponseresponse)
throwsIOExceptionServletException{
requestsetAttribute(oetBooleanTRUE);
requestsetAttribute(oettimeoutTIMEOUT);
messageSendersetConnection(response);
Weathermanweatherman=newWeatherman();
newThread(weatherman)start();
}
publicvoidend(HttpServletRequestrequestHttpServletResponseresponse)
throwsIOExceptionServletException{
synchronized(request){
requestremoveAttribute(oet);
Continuationcontinuation=ContinuationSupportgetContinuation
(requestrequest);
if(continuationisPending()){
continuationresume();
}
}
}
publicvoiderror(HttpServletRequestrequestHttpServletResponseresponse)
throwsIOExceptionServletException{
end(requestresponse);
}
publicbooleanread(HttpServletRequestrequestHttpServletResponseresponse)
throwsIOExceptionServletException{
thrownewUnsupportedOperationException();
}
@Override
protectedvoidservice(HttpServletRequestrequestHttpServletResponseresponse)
throwsIOExceptionServletException{
synchronized(request){
Continuationcontinuation=ContinuationSupportgetContinuation
(requestrequest);
if(!continuationisPending()){
begin(requestresponse);
}
Integertimeout=(Integer)requestgetAttribute
(oettimeout);
booleanresumed=continuationsuspend(timeout==null?:
timeoutintValue());
if(!resumed){
error(requestresponse);
}
}
}
publicvoidsetTimeout(HttpServletRequestrequestHttpServletResponseresponse
inttimeout)throwsIOExceptionServletException
UnsupportedOperationException{
requestsetAttribute(oettimeoutnewInteger(timeout));
}
}
这里最需要注意的是该结构与 Tomcat 版本的代码非常类似beginreadend 和 error 方法都与 Tomcat 中相同的事件匹配该 Servlet 的 service 方法被覆盖为在请求第一次进入时创建一个 continuation 并暂停该请求直到超时时间已到或者发生导致它重新开始的事件上面没有显示 init 和 destroy 方法因为它们与 Tomcat 版本是一样的该 servlet 使用与 Tomcat 相同的 MessageSender因此不需要修改注意 begin 方法如何创建 Weatherman 实例对这个类的使用与 Tomcat 版本中也是完全相同的甚至客户机代码也是一样的只有 servlet 有更改虽然 servlet 的变化比较大但是与 Tomcat 中的事件模型仍是一一对应的
希望这足以鼓舞人心虽然完全相同的代码不能同时在 Tomcat 和 Jetty 中运行但是它是非常相似的当然JavaEE 吸引人的一点是可移植性大多数在 Tomcat 中运行的代码无需修改就可以在 Jetty 中运行反之亦然因此毫不奇怪下一个版本的 Java Servlet 规范包括异步请求处理(即 Comet 背后的底层技术)的标准化 我们来看看这个规范Servlet 规范
Servlet 规范
在此我们不深究 Servlet 规范的全部细节只看看 Comet servlet 如果在 Servlet 容器中运行可能会是什么样子注意 可能 二字该规范已经发布公共预览版但在撰写本文之际还没有最终版因此清单 显示的是遵从公共预览规范的一个实现
清单 Servlet Comet
@WebServlet(asyncSupported=trueasyncTimeout=)
publicclassWeatherServletextendsHttpServlet{
privateMessageSendermessageSender;
//initanddestroyarethesameasother
@Override
protectedvoiddoGet(HttpServletRequestrequestHttpServletResponseresponse)
throwsServletExceptionIOException{
AsyncContextasync=requeststartAsync(requestresponse);
messageSendersetConnection(async);
Weathermanweatherman=newWeatherman();
asyncstart(weatherman);;
}
}
值得高兴的是这个版本要简单得多平心而论如果不遵从 Tomcat 的事件模型在 Jetty 中可以有类似的实现这种事件模型似乎比较合理很容易在 Tomcat 以外的容器(例如 Jetty)中实现只是没有相关的标准
回头看看清单 注意它的标注声明它支持异步处理并设置了超时时间startAsync 方法是 HttpServletRequest 上的一个新方法它返回新的 javaxservletAsyncContext 类的一个实例注意MessageSender 现在传递 AsynContext 的引用而不是 ServletResponse 的引用在这里不应该关闭响应而是调用 AsyncContext 实例上的 complete 方法还应注意Weatherman 被直接传递到 AsyncContext 实例的 start 方法这样将在当前 ServletContext 中开始一个新线程
而且尽管与 Tomcat 或 Jetty 相比都有较大的不同但是修改相同风格的编程来处理 Servlet 规范提议的 API 并不是太难还应注意Jetty 是为实现 Servlet 而设计的目前处于 beta 状态但是在撰写本文之际它还没有实现该规范的最新版本
结束语
Comet 风格的 Web 应用程序可以为 Web 带来全新的交互性它为大规模地实现这些特性带来一些复杂的挑战但是领先的 Java Web 服务器正在为实现 Comet 提供成熟稳定的技术在本文中您看到了 Tomcat 和 Jetty 上当前风格的 Comet 的不同点和相似点以及正在进行的 Servlet 规范的标准化Tomcat 和 Jetty 使如今构建可伸缩的 Comet 应用程序成为可能并且明确了未来面向 Servlet 标准化的升级路线