注:同样来源我本科的毕业设计,以cometd框架来实现的一个服务器推送机制。
协同设计涉及多个人之间的交互,需要一个实时可靠地消息传递机制,这通常是由服务器推送技术来支持的。前一文章介绍的web下常见的服务器推送技术中分析可知,目前实现服务器推送较好的两种方式是基于ajax的长轮询方法和HTML5的WebSocket方法,这两种方式都不需要安装插件。WebSocket方法最优,应优先考虑,但考虑到一些用户的浏览器可能并未支持WebSocket,所以也要实现ajax的长轮询方法。然后通过JavaScript去识别客户端是否支持WebSocket,如果不支持则转换为基于ajax的长轮询方法。本文使用了一个名为cometd框架[27]来搭建一个在线交流平台。
cometd介绍
CometD是由DOJO资金会提供支持的基于HTTP事件驱动的一套开源通讯解决方案,伸缩扩展性良好。DOJO资金会还定义了一个Bayeux协议[28],主要是为web服务器和web浏览器之间低延时、异步传输的消息推送而定义的。CometD是基于bayeux协议的多个平台的实现,目前已经可以支持java、python、perl、JavaScript等语言。
bayeux协议
在详细说明CometD之前,需要了解Bayeux协议的内容,因为CometD的实现是遵循Bayeux协议的。Bayeux协议时基于发布/订阅模式的,web服务器和web浏览器之间的数据交互都基于JSON格式[29]。JSON是一种轻量的数据交换格式,使用字符少,易于阅读和编写,优于XML数据。服务器和浏览器之间交换的数据也可以定义为一个事件消息,所以CometD是基于事件驱动的。Bayeux协议定义了一些专门术语和消息传递的规范,下面会详细讲解。
通道: 每个通道实际上是一个以“/”开头的字符串。消息的传递是基于通道传输的,Bayeux协议默认定义了3种类型的通道:
- meta通道: meta通道只能是Bayeux协议自己定义,用户无法进行修改或添加,以“/meta/”字符串开头。它是基础的消息通道,服务器和客户端的握手、连接、订阅、断开等操作都是通过meta通道来实现的,如”/meta/handshake”用来和服务器握手,”/meta/connect”用来和服务器进行连接。
- service通道: Service通道以”/service/”开头,不支持广播,也就只能用于服务器和客户端之间的单向通讯,和http的请求/响应模式类似。Service通道常常用于私聊,两个客户端之间,通过服务器来进行通讯。
- 广播通道: 广播通道只要求以”/”开头,但不能和meta、service通道相混淆,比如”/chat/hi”是合法的广播通道。前面说Bayeux协议基于发布/订阅模式的,在这里,只要订阅了某个广播通道,一旦有消息发布,该通道下所有的订阅者都可以接收到消息。
客户端id: 每个用户在和服务器握手成功后都会接收到一个客户端id,这也是客户端在服务器当中的区分不同用户的识别码。服务器可以通过客户端id给固定的用户发送消息。
事件消息: 一个事件消息通常有4个字段。Channel字段表示消息的发布通道;data字段表示消息的真实内容;clientid字段表示客户端id;id字段是指消息的id,由客户端指定。一个简单的例子是:
[{
"channel": "/chat/hi",
"clientId": "Un1q31d3nt1f13r",
"data": "how about this project?",
"id": "4"
}]
响应消息: 每个消息发送到服务器上通常都有一个响应消息回来,通常会包含有channel、successful、id、error、ext这些字段,successful字段的值是一个布尔类型,表示成功与否。
CometD框架
从高层视图来看,CometD基于发布/订阅模式,实现了一种中心辐射型拓扑结构,,如图3-5所示,中心点是指服务器,其它的客户端都是直接与服务器相连而形成一个中心辐射型的拓扑结构。当服务器接收到客户的发布消息,如果它是广播型的,就会把消息传递给其它订阅者,和辐射类似。如果是meta通道、service通道上的消息就会进行特殊处理。
从低层次的结构来看,如图3-6,ClentSessin和ServerSession之间通过一条线来进行通讯,在图中它们都是一个半圆,CometD称它为半对象。每个客户端和服务器发起连接的时候,服务端都会实例化出一个半对象来与客户端进行通讯。LocalSession和ServerSession也是类似的,只是LocalSession这个客户端是运行在服务器上的。
在CometD的消息传输过程中,每一部分的Session对应的Transport类来进行数据传输。如图3-7所示,ClientSession有对应的ClientTransport类,ServerSession有对应的ServerTransport类,和客户端不同的是,多个ServerSession共用一个ServerTransport。ServerTransport收到消息首先会交给BayeuxServer,BayeuxServer获取到消息后会识别出对应的通道,然后交给ServerSession来进行处理。如果同一个通道对应着多个ServerSession,就需要维护着一个ServerSession队列。
具体实现方案
本协同平台需要实现群组内聊天和私人聊天的功能,群组聊天允许用户同时对某一个项目进行讨论。群内聊天属于多人通信的内容,使用广播通道来进行消息传递。私人聊天如果同样采用广播类型的话,对服务器要求很大,会产生不必要的资源浪费,所以采用service通道进行消息传递。
通道的设立:
- 每个群组都有一个独立的广播通道,通道定义为“/chat/[项目名称]”的方式,群组内的成员更新也定义了一个的广播通道,通道定义为“/members/[项目名称]”的方式。也就是每个群组有两个独立的通道,一个用来传递交流的消息内容,一个用来传递用户状态(群成员的在线与否、当前在线成员的名称等信息)。
- 私聊采用service通道的形式,我定义了一个“/service/privatechat”的service通道,所有用户关于私聊的信息都发布到这个通道上去。服务器监听该通道,并提取其中的信息,转发给对应的用户。
- 本平台还定义了一个特殊的service通道“/service/members”,用来处理用户上下线的情况,并更新用户状态和通知好友。
- 定义一个“/private/person”通道,虽然属于一个广播通道,但是不允许任何人来订阅,它是一个服务器传递给客户端的专属通道。
数据维护:
CometD服务器默认会维护一个在线用户的名称和对应的id的表,用来识别用户和给用户发送消息。我另外定义了3个map类型,名称分别为_members、_chatfriends、_chatgroups。其中_members保存有每个群名称和它的当前在线用户的一个映射。_chatfriends保存着每个用户名称和它的好友的一个映射。_chatgroups保存着每个用户名和它所属的群的映射。
逻辑处理:
- 服务器和客户端连接的初始化:在本设计平台中,客户端是由JavaScript的org.cometd.CometD对象来实例化的。当客户端要和服务器发起连接时,客户端会向服务器发布一个”/meta/handshake”通道的消息,服务器接收到该消息,如果成功会生成一个客户端ID并返回给客户端。客户端接收到回复,会发起一个”/meta/connect”通道的连接。如果返回成功就会向”/service/members”发送消息,该消息包含有该用户的好友列表、所属的群列表和自己的名称。
- “/service/members”消息处理:当服务器接收到“/service/members”的消息时,它会处理4个事情:1.把该用户的好友列表添加到_chatfriends中,判断好友在不在线,如果在线则通知他们该用户已上线。2.把该用户所属的群列表添加到_chatgroups中,对于里面的每个群,更新_members中该群的用户列表,并发送一个”/members/[该群名称]”的消息,消息包含当前群在线成员列表。3.通过”/private/person”通道返回给当前用户他的在线好友名称列表。4.给当前的客户所对应的ServerSession添加一个用户离开时的监听事件,表明当用户离开后,通知他的好友他已经下线和把他所对应的群的在线好友列表中移除,并广播给群成员。
- 客户端对”/private/person”通道的监听事件:“private/person”通道是服务器发给客户端的专属通道,发布到这个通道的消息都有一个字段type,。Type等于1表示这是私聊的消息内容,通过该消息的其它内容,可以知道这条消息是和谁聊天的,并在相关的聊天窗口进行数据更新和提醒用户。Type等于0 表示有好友上线,找到好友列表中该用户的头像,把它改成在线状态。Type等于2表示有好友下线,把该好友的状态改为离线。
- 客户端对于”/chat/”通道的监听事件:客户端收到”/chat/”通道的消息后,提取出消息内容、发布者名称、对应的群名称,并根据提取内容更新聊天窗口内容和提醒用户。
- 客户端对于”/members/”通道的监听事件:客户端收到”/members/”通道的,提取出群名称、在线好友列表,找到该群对应的好友列表,更新它的UI。
错误处理
当有短暂的网络连接失败的时候,”/meta/connect”通道上的返回信息中的successful字段就会变为false。我在JavaScript端对”/meta/connect”通道进行监听,并记录下上一次的连通情况,如果两次连通情况发生改变,可以知道自己是掉线还是重新连接上了。对于短暂的网络连接失败,服务器将维持有客户端的状态不变,当网络再次连接的时候,就好像什么事情都没发生一样,而对于客户端来说,只需要在网页界面端只需要针对掉线、重新连接上两个动作进行一些响应即可。
当有长久一些的网络连接失败的时候,服务器和浏览器之间感觉不到“心跳”(心跳,就是客户端周期性地给Push Server发送一个数据包,避免该连接上长期没有数据传送[30]),服务器中有规定一个超时的时间,如果超时又没接收到“心跳”,服务器会把客户端相对应的ServerSession清除掉,并触发ServerSession的离开动作的监听事件(会通知群和好友说他掉线了)。
参考文献
[27] Simone Bordet. The CometD Reference Book. [EB/0L]http://docs.cometd.org/reference/. 2013-5-25
[28] Alex Russell, Greg Wilkins, David Davis, Mark Nesbitt. Bayeux Protocol – Bayeux 1.0.0. [EB/0L]http://svn.cometd.com/trunk/bayeux/bayeux.html. 2007.
[29] Crockford D. The application/json media type for JavaScript object notation (json)[J]. 2006.
[30] 张雷, 金德. 基于 Push 通道客户端的智能心跳机制研究与优化[J]. 工业控制计算机, 2013, 1: 039.