开始
在本文中我将展示如何使用各种不同的 Java 技术构建一些简单的 Comet 风格的 Web 应用程序读者对 Java ServletAjax 和 JavaScript 应该有一定的了解我们将考察 Tomcat 和 Jetty 中一些支持 Comet 的特性因此需要使用这两个产品的最新版本本文使用 Tomcat 和 Jetty 另外还需要一个支持 Java 或更高版本的 JDK本文使用 JDK 此外还需要看看 Jetty 的预发布版因为它实现了 Servlet 规范我们将在本文中研究该规范
理解 Comet
您可能已经听说过 Comet因为它最近受到了一定的关注Comet 有时也称反向 Ajax 或服务器端推技术(serverside push)其思想很简单将数据直接从服务器推到浏览器而不必等到浏览器请求数据听起来简单但是如果熟悉 Web 应用程序尤其是 HTTP 协议那么您就会知道这绝不简单实现 Comet 风格的 Web 应用程序同时保证在浏览器和服务器上的可伸缩性这只是在最近几年才成为可能在本文的后面我们将看看一些流行的 Java Web 服务器如何支持可伸缩的 Comet 架构但首先我们来看看为什么要创建 Comet 应用程序以及用于实现它们的常见设计模式
使用 Comet 的动机
HTTP 协议的成功毋庸置疑它是 Internet 上大部分信息交换的基础然而它也有一些局限性特别是它是无状态单向的协议请求被发送到 Web 服务器服务器处理请求并发回一个响应 — 仅此而已请求必须由客户机发出而服务器则只能在对请求的响应中发送数据这至少会影响很多类型的 Web 应用程序的实用性典型的例子就是聊天程序另外还有一些例子例如比赛的比分股票行情或电子邮件程序
HTTP 的这些局限性也是它取得一定成功的原因请求/响应周期使它成为了经典的模型即每个连接使用一个线程只要能够快速为请求提供服务这种方法就有巨大的可伸缩性每秒钟可以处理大量的请求只需使用少量的服务器就可以处理很大数量的用户对于很多经典的 Web 应用程序例如内容管理系统搜索应用程序和电子商务站点等等而言这非常适合在以上任何一种 Web 应用程序中服务器提供用户请求的数据然后关闭连接并释放那个线程使之可以为其他请求服务如果提供初始数据之后仍可能存在交互那么将连接保持为打开状态因此线程就不能释放出来服务器也就不能为很多用户服务
但是如果想在对请求做出响应并发送初始数据之后仍然保持与用户的交互呢?在 Web 早期这一点常使用 meta 刷新实现这将自动指示浏览器在指定秒数之后重新装载页面从而支持简陋的轮询(polling)这不仅是一种糟糕的用户体验而且通常效率非常低下如果没有新的数据要显示在页面上呢?这时不得不重新呈现同样的页面如果对页面的更改很少并且页面的大部分没有变化呢?同样不管是否有必要都得重新请求和获取页面上的一切内容
Ajax 的发明和流行改变了上述状况现在服务器可以异步通信因此不必重新请求整个页面现在可以进行增量式的更新只需使用 XMLHttpRequest 轮询服务器这项技术通常被称作 Comet这项技术存在一些变体每种变体具有不同的性能和可伸缩性我们来看看这些不同风格的 Comet
Comet 风格
Ajax 的出现使 Comet 成为可能HTTP 的单向性质可以有效地加以规避实际上有一些不同的方法可以绕过这一点您可能已经猜到支持 Comet 的最容易的方式是轮询(poll)使用 XMLHttpRequest 向服务器发出调用返回后等待一段固定的时间(通常使用 JavaScript 的 setTimeout 函数)然后再次调用这是一项非常常见的技术例如大多数 webmail 应用程序就是通过这种技术在电子邮件到达时显示电子邮件的
这项技术有优点也有缺点在这种情况下您期望快速返回响应就像任何其他 Ajax 请求一样在请求之间必须有一段暂停否则连续不断的请求会沖垮服务器并且这种情况下显然不具有可伸缩性这段暂停使应用程序产生一个延时暂停的时间越长服务器上的新数据就需要越多的时间才能到达客户机如果缩短暂停时间又将重新面临沖垮服务器的风险但是另一方面这显然是最简单的实现 Comet 的方式
现在应该指出很多人认为轮询并不属于 Comet相反他们认为 Comet 是对轮询的局限性的一个解决方案最常见的 真正的 Comet 技术是轮询的一种变体即长轮询(long polling)轮询与长轮询之间的主要区别在于服务器花多长的时间作出响应长轮询通常将连接保持一段较长的时间 — 通常是数秒钟但是也可能是一分钟甚至更长当服务器上发生某个事件时响应被发送并随即关闭轮询立即重新开始
长轮询相对于一般轮询的优点在于数据一旦可用便立即从服务器发送到客户机请求可能等待较长的时间期间没有任何数据返回但是一旦有了新的数据它将立即被发送到客户机因此没有延时如果您使用过基于 Web 的聊天程序或者声称 实时 的任何程序那么它很可能就是使用了这种技术
长轮询有一种变体这是第三种风格的 Comet这通常被称为流(streaming)按照这种风格服务器将数据推回客户机但是不关闭连接连接将一直保持开启直到过期并导致重新发出请求XMLHttpRequest 规范表明可以检查 readyState 的值是否为 或 Receiving(而不是 或 Loaded)并获取正从服务器 流出 的数据和长轮询一样这种方式也没有延时当服务器上的数据就绪时该数据被发送到客户机这种方式的另一个优点是可以大大减少发送到服务器的请求从而避免了与设置服务器连接相关的开销和延时不幸的是XMLHttpRequest 在不同的浏览器中有很多不同的实现这项技术只能在较新版本的 Mozilla Firefox 中可靠地使用对于 Internet Explorer 或 Safari仍需使用长轮询
至此您可能会想长轮询和流都有一个很大的问题请求需要在服务器上存在一段较长的时间这打破了每个请求使用一个线程的模型因为用于一个请求的线程一直没有被释放更糟糕的是除非要发回数据否则该线程一直处于空闲状态这显然不具有可伸缩性幸运的是现代 Java Web 服务器有很多方式可以解决这个问题
Java 中的 Comet
现在有很多 Web 服务器是用 Java 构建的一个原因是 Java 有一个丰富的本地线程模型因此实现典型的每个连接一个线程的模型便非常简单该模型对于 Comet 不大适用但是Java 对此同样有解决的办法为了有效地处理 Comet需要非阻塞 IOJava 通过它的 NIO 库提供非阻塞 IO两种最流行的开源服务器 Apache Tomcat 和 Jetty 都利用 NIO 增加非阻塞 IO从而支持 Comet然而这两种服务器中的实现却各不相同我们来看看 Tomcat 和 Jetty 对 Comet 的支持
Tomcat 和 Comet
对于 Apache Tomcat要使用 Comet主要需要做两件事首先需要对 Tomcat 的配置文件 serverXML 稍作修改默认情况下启用的是更典型的同步 IO 连接器现在只需将它切换成异步版本如清单 所示
清单 修改 Tomcat 的 serverxml
<!ThisistheusualConnectorcommentitoutandaddtheNIOone>
<!ConnectorURIEncoding=utfconnectionTimeout=port=
protocol=HTTP/redirectPort=/>
<ConnectorconnectionTimeout=port=protocol=orgapache
coyoteredirectPort=/>
Servlet这显然是 Tomcat 特有的一个接口清单 显示了一个这样的例子
清单 Tomcat Comet servlet
publicclassTomcatWeatherServletextendsHttpServletimplementsCometProcessor{
privateMessageSendermessageSender=null;
privatestaticfinalIntegerTIMEOUT=*;
@Override
publicvoiddestroy(){
messageSenderstop();
messageSender=null;
}
@Override
publicvoidinit()throwsServletException{
messageSender=newMessageSender();
ThreadmessageSenderThread=
newThread(messageSenderMessageSender[+getServletContext()
getContextPath()+]);
messageSenderThreadsetDaemon(true);
messageSenderThreadstart();
}
publicvoidevent(finalCometEventevent)throwsIOExceptionServletException{
HttpServletRequestrequest=eventgetHttpServletRequest();
HttpServletResponseresponse=eventgetHttpServletResponse();
if(eventgetEventType()==CometEventEventTypeBEGIN){
requestsetAttribute(oettimeoutTIMEOUT);
log(Beginforsession:+requestgetSession(true)getId());
messageSendersetConnection(response);
Weathermanweatherman=newWeatherman();
newThread(weatherman)start();
}elseif(eventgetEventType()==CometEventEventTypeERROR){
log(Errorforsession:+requestgetSession(true)getId());
eventclose();
}elseif(eventgetEventType()==CometEventEventTypeEND){
log(Endforsession:+requestgetSession(true)getId());
eventclose();
}elseif(eventgetEventType()==CometEventEventTypeREAD){
thrownewUnsupportedOperationException(Thisservletdoesnotaccept
data);
}
}
}