深入了解 Dojo 的服务器推送技术(转)
简介:服务器推送技术已经出来一段时间了,业界上也有不少基于这种技术(应该说是设计模式)的开源实现,但是要移植或者说应用到自己的项目上都比较麻烦。Dojo这样一个大型的Web2.0开发框架提供了一套封装好的基于服务端推送技术的具体实现(包括服务端Java和客户端Web和JavaScript),它基于Bayeux协议,提供了一些简单而且强大的接口可以让你快速构建自己的服务端推送功能。客户端实现即Dojo的Cometd前端组件,它封装了建立连接、消息订阅等等接口。服务端基于Jetty和annotation,组建消息推送机制,同样也封装了比较简单但实用的消息推送接口,与前端Dojox的Cometd接口协同工作。这篇文章将重点介绍Dojo的服务端推送机制是如何运作的,以及我们应该如何基于Dojo的Cometd工具包构建自己的服务端推送功能。
服务器推送技术和Bayeux协议简介
服务器推送技术的基础思想是将浏览器主动查询信息改为服务器主动发送信息。服务器发送一批数据,浏览器显示这些数据,同时保证与服务器的连接。当服务器需要再次发送一批数据时,浏览器显示数据并保持连接。以后,服务器仍然可以发送批量数据,浏览器继续显示数据,依次类推。基于这种思想,这里我们要引出Bayeux协议。
Bayeux是一套基于Publish/Subscribe模式,以JSON格式在浏览器与服务器之间传输事件的通信协议。该协议规定了浏览器与服务器之问的双向通信机制,克服了传统Web通信模式的缺点。
Bayeux协议主要基于HTTP来传输低延迟的、异步的事件消息。这些消息通过频道(Channels)来投递,能够实现从服务器端到客户端、从客户端到服务器端或者通过服务器从一个客户端到另一个客户端的传送。Bayeux协议的主要目的是为使用了Ajax和Comet技术的Web客户端实现高响应的用户交互。Bayeux协议旨在通过允许执行者更容易的实现互操作性,来降低开发Comet应用程序的复杂性。它解决了共同的消息发布和路由问题,并提供了渐进式的改进和扩展机制。
一般情况下,在HTTP协议中,Client要想获得Server的消息,必须先自己发送一个Request,然后Server才会给予Response。而Bayeux协议改变了这个情况,它允许Server端异步Push自己的消息到Client端。从而实现了Client和Server之间的双向操作模式。
回页首
服务器推送技术的一个简单实现
基于Bayeux协议实现服务器推送技术的方式有很多,可以通过Flex或者Java的Applet。基于这两种技术,我们可以建立在客户端建立服务套接字接口,“双向操作模式”自然很容易实现,但是这些方式需要除浏览器以外的运行环境的支持。这里我们希望能采用一种纯脚本的方式,这种方式是不可能建立服务套接字接口的,那如何实现基于Bayeux协议的服务器推送呢?其实是可以模拟实现的,主要有两种方式:
1.基于HTTP的长轮询来进行消息通信(基于Ajax的长轮询(long-polling)方式)。
2.基于Iframe及htmlfile的流(streaming)方式。
这里我们采用第一种方式实现,即:客户端先向服务器端发送一个HTTPRequest,服务器端接收到后,阻塞在那边,等服务器有消息的时候,则返回一个HTTPResponse给客户端,客户端收到后,断开连接,紧接着再发第二个HTTPRequest,以此反复进行,保持这个“长轮询”。期间,如果连接超时,那么会断开重连,以保持连接。
基于以上的思想,我们来看一下一个简单的实现,这个简单实现是基于PHP的。示例很简单,即便没用过PHP也能够很容易看明白,而且我们会在后面一一作出解释。
这个示例主要实现这样一个功能:
我们在浏览器里面分别打开三个窗口,并访问同一张页面。修改其中一个页面上的内容,另外两个页面上的内容也随即发生变化(注意:这里不用刷新页面)。这就会给我们一种:数据是服务器推送过来的感觉。
图1.简单服务器推送示例--内容修改前
我们修改其中第一个窗口(左上)的内容(输入“222”,点击“Send”按钮,发送到后台)。此时不仅第一个窗口的内容变化了,其余两个窗口的内容也随即变化。
图2.简单服务器推送示例--内容修改
接下来我们来看看示例代码吧:
清单1.简单服务器推送--前端代码HTML
<formaction=""method="get"
onsubmit="comet.doRequest($('word').value);$('word').value='';returnfalse;">
<inputtype="text"name="word"id="word"value=""/>
<inputtype="submit"name="submit"value="Send"/>
</form>
这个是我们所看到的输入框和提交按钮,大家可以注意一下它的“onsubmit”方法:当我们输入内容并点击提交时,它会执行“comet.doRequest($('word').value)”方法向后端发起请求(其实在这之前我们就已经建立了与服务端的长轮询并可随时开始服务器推送数据)。接下来我们来看看这个“comet”是什么样子的以及他的Request的具体实现:
清单2.简单服务器推送--前端代码JavaScript
[javascript]viewplaincopy
varComet=Class.create();
Comet.prototype={
timestamp:0,
url:'./backend.php',
noerror:true,
initialize:function(){
},
connect:function(){
this.ajax=newAjax.Request(this.url,{
method:'get',
parameters:{
'timestamp':this.timestamp
},
onSuccess:function(transport){
varresponse=transport.responseText.evalJSON();
this.comet.timestamp=response['timestamp'];
this.comet.handleResponse(response);
this.comet.noerror=true;
},
onComplete:function(transport){
if(!this.comet.noerror)setTimeout(function(){
comet.connect()
},5000);
else
this.comet.connect();
this.comet.noerror=false;
}
});
this.ajax.comet=this;
},
handleResponse:function(response){
$('content').innerHTML+='<div>'+response['msg']+'</div>';
},
doRequest:function(request){
newAjax.Request(this.url,{
method:'get',
parameters:{
'msg':request
}
});
}
}
varcomet=newComet();
comet.connect();
我们先看最后两段代码,这里是页面初始化时会执行的代码,其实在这里,我们就建立了一服务端的长轮询,我们来看看“connect”方法的实现吧:
“connect”方法这里是发了一个Ajax请求,然后分别设定了成功时(onSuccess)的返回处理和请求完成时(onComplete)的处理(注意onComplete不论成功失败都会执行)。我们要挂住这里的onComplete方法。可以看到,当请求完成时,如果连接有问题,它会过5秒重新连接,;如果没有问题,他会立即重新连接。
相信大家看到这里应该会有点眉目了,这里其实没有什么所谓的恒定不断的连接(类似TCP方式),它的真正实现是通过不断的Ajax请求实现的。
所以,当我们开启3个窗口时,其实我们打开了3个模拟的不间断的客户端与服务端的连接,所以他们会即时解到服务端的信息,不需要刷新页面。
我们再来看看服务端的实现,看看他是如何推送的:
清单3.简单服务器推送--后端代码PHP
[php]viewplaincopy
$filename=dirname(__FILE__).'/data.txt';
//将新消息存入文件中
$msg=isset($_GET['msg'])?$_GET['msg']:'';
if($msg!='')
{
file_put_contents($filename,$msg);
die();
}
//这是一个无限循环,一旦发现文件被修改,便会跳出循环并返回文件修改数据。如果文件一直没有修改,则会一
//直处于循环检测状态,此时的Ajax连接也会一直保留,直到文件被修改为止,这就是所谓的“长轮询”。
$lastmodif=isset($_GET['timestamp'])?$_GET['timestamp']:0;
$currentmodif=filemtime($filename);
while($currentmodif<=$lastmodif)//检测文件是否被修改
{
usleep(10000);//sleep10mstounloadtheCPU
clearstatcache();
$currentmodif=filemtime($filename);
}
//返回JSON数组
$response=array();
$response['msg']=file_get_contents($filename);
$response['timestamp']=$currentmodif;
echojson_encode($response);
flush();
我们可以参照上面的注释理解该代码,其实并不需要多少PHP的知识。服务端推送技术不是一个开发用的控件库,而是一个思想。这里的while循环便说明了服务端推送是如何保留所谓的“长轮询”的。
现在大家应该明白为什么三个窗口会同步变化了。其主要的核心思想就是服务端“握住”长轮询,然后在适当的时候“放手”。
回页首
Dojo的Cometd工具包简介
之前我们是基于JavaScript自己实现了一个简单的Cometd应用,我们花了大量的代码来建立一个Cometd框架,真正用于处理我们自己的业务逻辑的代码其实就是“handleResponse”里面的那一行。我们能不能吧这些通用的代码省掉呢?答案是肯定的。Dojo已经对Cometd做了封装,基于Dojo的Cometd包,我们不用再浪费大量的代码在搭建Cometd框架上。对于前端脚本代码,我们只需要加上一个Cometd包的简单接口代码,便可以开始加入我们自己的业务逻辑代码了。
当然,Dojo的Cometd包还包括后端的代码,可以在Dojo的官网下载中找到,它不与Dojo包一起发布,是一个单独的服务端开源代码,基于Java和Jetty的,有兴趣的读者可以下载下来研究一下。
通过Dojo的这两部分代码,我们便可以迅速地搭建我们的Cometd框架,我们剩下需要做的就是加入我们的业务逻辑。
回页首
Dojo的Cometd工具包之前端
接下来我们来看看Dojo的Cometd工具包的前端封装:
清单4.Cometd前端初始化
[javascript]viewplaincopy
dojox.cometd.init("http://www.xxx.com/cometd");
这个接口用于建立并初始化与服务端的握手连接(Bayeuxhandshake,初始化了“Bayeuxcommunication”消息通讯)。建立这个连接是基于Bayeux协议的,它主要有两个任务:
客户端与服务端协商传输的消息类型。
如果协商成功,服务端会通知客户端具体的请求参数配置。
如果协商失败,客户端重新发起协商流程。
我们深入Dojo的init方法内部可以看到握手连接的具体实现过程,它的实现也是不间断的重复发送客户端的Ajax请求,与我们之前的自制案类似,有兴趣的同学可以参考如下代码(摘取部分):
清单5.Cometd内部机制
[javascript]viewplaincopy
this.init=function(...){
............
varbindArgs={
url:this.url,
handleAs:this.handleAs,
content:{"message":dojo.toJson([props])},
load:dojo.hitch(this,function(msg){
this._backon();
this._finishInit(msg);
}),
error:dojo.hitch(this,function(e){
this._backoff();
this._finishInit(e);
}),
timeout:this.expectedNetworkDelay
};
..............
if(this._isXD){
r=dojo.io.script.get(bindArgs);
}else{
r=dojo.xhrPost(bindArgs);
}
..............
}
this._finishInit=function(data){
..................
if(successful){
........
//ajaxrequestinside
this.tunnelInit=transport.tunnelInit&&dojo.hitch(transport,
"tunnelInit");
this.tunnelCollapse=transport.tunnelCollapse&&dojo.hitch(transport,
"tunnelCollapse");
transport.startup(data);
}else{
if(!this._advice||this._advice["reconnect"]!="none"){
setTimeout(dojo.hitch(this,"init",this.url,this._props),
this._interval());
}
}
....................
}
可见,它们的callback方法里面都带有对自己本身的调用,这里的”init“方法也不例外。细心的读者可能还会发现,其实从例子上可以看出:Dojo的Cometd也支持跨域,它的跨域是通过“script”的方式实现的。这里有一点需要大家了解,我们默认的服务端推送实现方式是长轮询(long-polling)模式,遇到跨域时,“long-polling”便不再适用,转为基于“script”的返回调用(callback-polling)模式。
接下来我们再来看看Cometd中关于消息推送的一些接口,这些消息通讯主要是基于渠道:
清单6.Cometd前端发布消息
[javascript]viewplaincopy
dojox.cometd.publish("/service/echo",{msg:msg});
这里的所谓“发布消息”就是向后端发送消息,用于前端主动向后端推送。
这里的第一个参数是发送消息的渠道标识(channel),这种“channel”共有三种类型:
1.元渠道(metachannels):示例“/meta/connect”(通常以“/meta/”为开头)。元渠道主要不是用来消息传输,而是用于客户端监听,如握手连接或者网络连接等等的错误。通常我们会在客户端调用“addListener()”来开启监听元渠道,它可以在握手连接的建立之前就开启监听,而且这种消息监听是同步的。
2.服务渠道(servicechannels):示例“/service/connect”(通常以“/service/”为开头)。它主要用于私有消息通讯,主要是一对一的通讯。通常我们会在客户端调用“subscribe()”来订阅服务渠道消息。服务渠道只有等握手连接建立好后才能开启,而且它是异步通讯的。
3.普通渠道(normalchannels):示例“/foo/bar”(无限制)。这种渠道没有什么限制,主要用于广播消息,即:多个客户端订阅了一个服务,该服务可以通过普通渠道进行消息广播。
渠道是通信的基础模式,我们可以根据需要选择相应的渠道模式。
第二个参数则是消息对象,这里的“msg”则是消息内容。
有一点要注意:这里的“publish”是基于Bayeux协议的,采用的异步消息传输机制,所以它是在服务端(Bayeux服务器)收到消息之前就返回的。所以publish的返回并不代表服务端收到你publish的消息了。
Dojo的Cometd还支持批量发送消息,通过这个接口可以有效地避免不必要的网络消息传输的浪费:
清单7.Cometd前端批量发布消息
[javascript]viewplaincopy
//方法1
cometd.batch(function()
{
cometd.publish('/channel1',{product:'foo'});
cometd.publish('/channel2',{notificationType:'all'});
cometd.publish('/channel3',{update:false});
});
//方法2
cometd.startBatch()
cometd.publish('/channel1',{product:'foo'});
cometd.publish('/channel2',{notificationType:'all'});
cometd.publish('/channel3',{update:false});
cometd.endBatch()
上述两种方案都可以实现消息的批量发送,推荐使用方法1。
接下来我们看看服务端的消息推送:
清单8.Cometd前端订阅消息
[javascript]viewplaincopy
dojox.cometd.subscribe("/service/echo",echoRpcReturn);
functionechoRpcReturn(msg){
dojo.byId("responses").innerHTML+=msg;
}
这里所谓的“订阅消息”,其实就是接收服务端推送的消息,是后端主动向前端推送。这也是服务端推送的精华所在,同样也是很简单的一行代码。
这里我们看到了一个熟悉的方法---“subscribe”,之前我们已经介绍过了,它主要用于订阅服务渠道私有消息,这里就是它用法的一个示例。对应的服务端Service向对应的前端订阅者推送消息,这里就是通过“echo”渠道向前端推送消息,他会回调“echoRpcReturn”方法,并传入推送的消息作为实参。对于后端的每次推送,都会调用前端的“echoRpcReturn”方法。
回页首
Dojo的Cometd工具包之后端
Dojo的Cometd工具包的后端实现是基于Java和Jetty组件的,通过Dojo的服务端Cometd组件,我们同样能极其迅速的构建Cometd框架。我们需要做的仅仅是加入我们的业务逻辑代码即可。
先来看看web.xml的配置参数:
清单9.基本配置参数(web.xml)
[html]viewplaincopy
<web-appxmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>
org.cometd.server.continuation.ContinuationCometdServlet
</servlet-class>
<init-param>
<param-name>timeout</param-name>
<param-value>60000</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>cross-origin</filter-name>
<filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cross-origin</filter-name>
<url-pattern>/cometd/*</url-pattern>
</filter-mapping>
</web-app>
这里我们先来看看“ContinuationCometdServlet”,这个Servlet主要用于解释Bayeux协议,所以关于它的配置是必须的。基于“ContinuationCometdServlet”的其他配置参数还有很多,如:
Timeout:长轮询的过期时间。如果超过这个时间还没有客户端消息,服务端会推送一个空消息。
Interval:轮询间隔时间。客户端结束前一个请求到发送下一个请求之间的间隔时间。
maxInterval:服务端最长等待时间。即:建立连接时,如果超过这个时间仍没有接到一个新的长轮询连接请求,服务端就会认为该客户端无效或者关闭了。
logLevel:日志级别。“0=warn,1=info,2=debug”。
以上是主要的配置参数,其余的配置参数还有很多,这里不一一介绍,有需要的读者可以查阅Dojo的帮助文档。另外,最后几行我们还配置了一个“cross-origin”,对应着“CrossOriginFilter”类,他用于支持跨域的JavaScript请求,如果您的项目中要支持跨域的服务器推送,请加入该配置。
接下来我们再来看看一些高级配置参数:
清单10.高级配置参数(web.xml)
[html]viewplaincopy
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.cometd.java.annotation.AnnotationCometdServlet</servlet-class>
<init-param>
<param-name>logLevel</param-name>
<param-value>1</param-value>
</init-param>
<init-param>
<param-name>services</param-name>
<param-value>org.cometd.examples.ChatService</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>cometdDemo</servlet-name>
<servlet-class>org.cometd.examples.CometdDemoServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
这里我们主要要注意三个地方:
1.“CometdDemoServlet”:它是用于启动服务端Cometd框架的Servlet,我们在后面会介绍。由于他配置了“load-on-startup”参数,所以在服务容器启动的时候,我们的Cometd服务端就已经搭建好了,之后我们会着重介绍他的“init”方法中的行为。
2.“AnnotationCometdServlet”:这个Servlet配置在这里表示了我们在服务端代码是基于annotation的。这是一个非常实用的Servlet,通过这个Servlet,你会发现,我们要做的事情仅仅是定义几个Service类,实现其中的几个方法即可。连很多调用Cometd框架API接口的代码都省去了。
3.“ChatService”:这里声明了一个Service类,他的用途是处理服务渠道的消息。这里声明的作用等同于代码中的“processor.process(newChatService())”。
配置完成后,我们接下来可以看看代码了。通过以上的配置之后,你会发现,我们接下来要写的代码非常简单精炼:
清单11.服务类初始化init
[java]viewplaincopy
publicvoidinit()throwsServletException
{
finalBayeuxServerImplbayeux=
(BayeuxServerImpl)getServletContext().getAttribute(BayeuxServer.ATTRIBUTE);
if(bayeux==null)
thrownewUnavailableException("NoBayeuxServer!");
.................
//创建扩展点
bayeux.addExtension(newTimesyncExtension());
bayeux.addExtension(newAcknowledgedMessagesExtension());
//设定握手连接权限
bayeux.getChannel(ServerChannel.META_HANDSHAKE).addAuthorizer(
GrantAuthorizer.GRANT_PUBLISH);
//启动服务渠道
ServerAnnotationProcessorprocessor=newServerAnnotationProcessor(bayeux);
processor.process(newEchoRPC());
processor.process(newMonitor());
//processor.process(newChatService());
bayeux.createIfAbsent("/foo/bar/baz",newConfigurableServerChannel.Initializer()
{
publicvoidconfigureChannel(ConfigurableServerChannelchannel)
{
channel.setPersistent(true);
}
});
if(bayeux.getLogger().isDebugEnabled())
System.err.println(bayeux.dump());
.................
}
这里我们介绍三个知识点:
1.Extension:Extension是一个函数,它会在消息发出之前或者收到之后被调用,专门用来修改消息内容,例如加入一些特殊属性(这些属性多在消息的ext属性中)。注意,这些属性大多是应用无关的,如记录长轮询的次数等等。这里的“TimesyncExtension”和“AcknowledgedMessagesExtension”是两个比较常用的Extension:
1)“TimesyncExtension”用于计算客户端事件和服务端时间的偏差。客户端需要同时引入“dojox.cometd.timesync”类,该Extension使得客户端和服务端在每次握手或者连接的时候能够互相交换各自的时钟信息,这也是的客户端可以很精确的计算出他与服务端时钟的偏移量。消息格式如下:
{ext:{timesync:{tc:12345567890,ts:1234567900,p:123,a:3},...},...}
TC:客户端发消息的时间(距离1970年1月号的时长,单位为毫秒)
TS:服务端收到消息的时间
2)“AcknowledgeExtension”用于提供可靠的顺序消息机制。一旦加入了“AcknowledgeExtension”,服务端会阻截非长轮询的客户端请求,这样会使你的服务器更加的高效。注意:客户端需要同时引入“dojox.cometd.ack”类与其协同工作。
2.Authorizer:设定握手连接权限,这里设定值为“GrantAuthorizer.GRANT_PUBLISH”,表示允许所有客户端建立握手连接。
3.ProcessService:启动服务渠道“processor.process(newEchoRPC())”。通过这些服务渠道类,我们可以启动服务渠道处理客户端请求。这是我们服务端推送技术的关键所在,我们的业务逻辑代码也是主要放在这些服务渠道类里面。
接下来我们来看看这些服务渠道类的具体实现:
清单12.EchoService实现
[java]viewplaincopy
@Service("echo")
publicstaticclassEchoRPC
{
@Session
privateServerSession_session;
@SuppressWarnings("unused")
@Configure("/service/echo")
privatevoidconfigureEcho(ConfigurableServerChannelchannel)
{
channel.addAuthorizer(GrantAuthorizer.GRANT_SUBSCRIBE_PUBLISH);
}
@Listener("/service/echo")
publicvoiddoEcho(ServerSessionsession,ServerMessagemessage)
{
Map<String,Object>data=message.getDataAsMap();
Log.info("ECHOfrom"+session+""+data);
for(inti=0;i<50;i++){
session.deliver(_session,message.getChannel(),data,null);
}
}
}
我们可以在“configureEcho”里面设定该服务渠道支持的权限。我们主要来看看“doEcho”方法,它被标识为“@Listener("/service/echo")”,所以它可以用于像客户端推送服务渠道为“echo”的消息,我们之前客户端代码示例里面的如下代码:“dojox.cometd.subscribe("/service/echo",echoRpcReturn)”就是专门用于处理这里服务渠道推送的消息,消息推送通过“deliver”方法,推送的消息信息放在“data”实参中。
再来看看Monitor类:
清单13.MonitorService实现
[java]viewplaincopy
@Service("monitor")
publicstaticclassMonitor
{
@Listener("/meta/subscribe")
publicvoidmonitorSubscribe(ServerSessionsession,ServerMessagemessage)
{
Log.info("MonitoredSubscribefrom"+session+"for"
+message.get(Message.SUBSCRIPTION_FIELD));
}
@Listener("/meta/unsubscribe")
publicvoidmonitorUnsubscribe(ServerSessionsession,ServerMessagemessage)
{
Log.info("MonitoredUnsubscribefrom"+session+"for"
+message.get(Message.SUBSCRIPTION_FIELD));
}
@Listener("/meta/*")
publicvoidmonitorMeta(ServerSessionsession,ServerMessagemessage)
{
if(Log.isDebugEnabled())
Log.debug(message.toString());
}
}
Monitor渠道类与之前的Echo服务渠道类比较类似,不过它主要用于处理meta渠道,与业务逻辑无关。
最后,我们来看看被注释掉的“ChatService”类,他也可以通过“processor.process(newChatService())”来启用,但是我们这里用了一个更为简单的方法:直接配置在web.xml文件中:
清单14.ChatService的配置
[html]viewplaincopy
<servlet>
...............
<init-param>
<param-name>services</param-name>
<param-value>org.cometd.examples.ChatService</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
细心的读者可能在之前的代码示例中已经看到,这里就是通过配置的方式加载服务渠道类。参考以下具体实现的代码:
清单15.ChatService实现
[java]viewplaincopy
@Service("chat")
publicclassChatService
{
..........................................
@Listener("/service/members")
publicvoidhandleMembership(ServerSessionclient,ServerMessagemessage)
{
Map<String,Object>data=message.getDataAsMap();
finalStringroom=((String)data.get("room")).substring("/chat/".length());
Map<String,String>roomMembers=_members.get(room);
if(roomMembers==null)
{
Map<String,String>new_room=newConcurrentHashMap<String,String>();
roomMembers=_members.putIfAbsent(room,new_room);
if(roomMembers==null)roomMembers=new_room;
}
finalMap<String,String>members=roomMembers;
StringuserName=(String)data.get("user");
members.put(userName,client.getId());
client.addListener(newServerSession.RemoveListener()
{
publicvoidremoved(ServerSessionsession,booleantimeout)
{
members.values().remove(session.getId());
broadcastMembers(room,members.keySet());
}
});
broadcastMembers(room,members.keySet());
}
privatevoidbroadcastMembers(Stringroom,Set<String>members)
{
//Broadcastthenewmemberslist
ClientSessionChannelchannel=
_session.getLocalSession().getChannel("/members/"+room);
channel.publish(members);
}
..........................................
@Listener("/service/privatechat")
protectedvoidprivateChat(ServerSessionclient,ServerMessagemessage)
{
Map<String,Object>data=message.getDataAsMap();
Stringroom=((String)data.get("room")).substring("/chat/".length());
Map<String,String>membersMap=_members.get(room);
if(membersMap==null)
{
Map<String,String>new_room=newConcurrentHashMap<String,String>();
membersMap=_members.putIfAbsent(room,new_room);
if(membersMap==null)
membersMap=new_room;
}
String[]peerNames=((String)data.get("peer")).split(",");
ArrayList<ServerSession>peers=newArrayList<ServerSession>(peerNames.length);
.................
}
}
以上是摘录部分ChatService实现代码,它主要是实现一个在线的聊天室,包括公开发言和私有(1对1)聊天等等功能,它的实现方式与之前的Echo和Monitor类似,这里不做详述,有兴趣的读者可以参考一下他的实现,来构造自己的服务器推送应用。
回页首
服务器推送技术之比较
其实有很多种方式实现服务器推送,它们各有各的优缺点:
传统轮询:此方法是利用HTML里面meta标签的刷新功能,在一定时间间隔后进行页面的转载,以此循环往复。它的最大缺点就是页面刷性给人带来的体验很差,而且服务器的压力也会比较大。
Ajax轮询:异步响应机制,即通过不间断的客户端Ajax请求,去发现服务端的变化。这种方式由于是客户端主动连接的,所以会有一定程度的延时,并且服务器的压力也不小。
长连接:这也是我们之前所介绍的一种方式。由于它是利用客户端的现有连接实现服务器主动向客户端推送信息,所以延时的情况很少,并且由于服务端的可操控性使得服务器的压力也迅速减小。其实这种技术还有其他的实现方式,通过Iframe,在页面上嵌入一个隐藏帧(Iframe),将其“src”属性指向一个长连接的请求,这样一来,服务端就能够源源不断的向客户端发送数据。这种方式的不足就在于:它会造成浏览器的进度栏一直显示没有加载完成,当然我们可以通过Google的一个称为“htmlfile”的ActiveX控件解决,但是毕竟他需要安装ActiveX控件,对于终端用户也是不合适的。
套接字:可以利用Flash的XMLSocket类或者Java的Applet来建立Socket连接,实现全双工的服务器推送,然后通过Flash或者Applet与JavaScript通信的接口来实现最终的数据推送。但是这种方式需要Flash或者JVM的支持,同样不太合适于终端用户。
HTML5的WebSocket:这种方式其实与套接字一样,但是这里需要单独强调一下:它是不需要用户而外安装任何插件的。HTML5提供了一个WebSocket的JavaScript接口,可以直接与服务端建立Socket连接,实现全双工通信,这种方式的服务器推送就是完全意义上的服务器推送了,没有半点模拟的成分,只是现阶段支持HTML5的浏览器并不多,而且一般老版本的各种浏览器基本都不支持。不过HTML5是一套非常好的标准,在将来,当HTML5流行起来以后将是我们实现服务器推送技术的不二选择。
回页首
结束语
这篇文章介绍了Dojo中的服务器推送Cometd工具包。基于服务器推送的理念,介绍了Bayeux协议的核心思想,并结合一个简单示例介绍了服务器推送的基本实现。随后,本着快速建立服务器推送框架的想法,介绍了Dojo的Cometd工具包,并分别从客户端接口和服务端接口两个方面分别介绍了Dojo的服务器推送框架的搭建和实现原理。最后,通过一些简单的示例展示了基于服务端推送的业务逻辑的具体实现。服务端推送技术具有很强的实用性,希望广大读者在开发自己的项目的过程中多关注一下,以尽可能多的完善自己的Web应用。
参考资料
学习
Dojo校园文档主页:Dojo中控件的比较完全的API文档主页,包括Dojo,Dijit,Dojox等等。
Dojo官方文档主页:Dojo官方的很多支持Ajax应用程序开发的组件的文档。
“在Ajax应用程序中实现实时数据推送”(developerWorks,2009年11月):一些特殊的应用场景会要求Web程序的客户端能够在服务器端数据发生变化时立即得到通知,这就要求应用程序具有“服务器推送”的特性。本文讲述了如何利用RIA技术来通过套接字来实现相应的功能,并介绍了具体的实现方法。
“使用HTML5WebSocket构建实时Web应用”(developerWorks,2011年12月):本文主要介绍了HTML5WebSocket的原理以及它给实时Web开发带来的革命性的创新,并通过一个WebSocket服务器和客户端的案例来充分展示WebSocket的强大和易用。
developerWorksWebdevelopment专区:通过专门关于Web技术的文章和教程,扩展您在网站开发方面的技能。
developerWorksAjax资源中心:这是有关Ajax编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki和新闻。任何Ajax的新信息都能在这里找到。
developerWorksWeb2.0资源中心,这是有关Web2.0相关信息的一站式中心,包括大量Web2.0技术文章、教程、下载和相关技术资源。您还可以通过Web2.0新手入门栏目,迅速了解Web2.0的相关概念。
查看HTML5专题,了解更多和HTML5相关的知识和动向。
原文转自:http://blog.csdn.net/dojotoolkit/article/details/7298417