基于XMPP的即时通信系统的建立(四)— 协议详解
Presence
在XMPP协议中,我们使用presence来获取用户是否已经上线以及是否可以通信的状态。
为了能够知道自己联系人的状态以及让联系人知道自己的状态,用户上线后需要订阅联系人的状态,联系人也同样需要订阅用户的状态。
通过下面的消息订阅联系人的状态:
<presence from="alice@wonderland.lit" to="sister@realworld.lit" type="subscribe"/>
当联系人接收/拒绝订阅时,会发送消息的消息体(sucribed/unsubscribe)回应。
通常客户端是自动回应这些消息的,当我们订阅了联系人的状态之后,也会受到联系人的状态变更信息。
还可以通过嵌入<show/>(chat/away/xa/dnd)和<status/>元素表示更加丰富的信息。
<presence> <show>away</show> <status>Having a spot of tea</status> </presence>
需要注意的是presence节是占用带宽最多的节,任何对该节内容的扩展都需要慎重。
另外,presence还提供priority属性用来标识资源的优先级(-127-128),负数优先级的资源将无法接收消息,除非显式指定,这个特性通常是由服务器实现的。
另外,我们可以通过发送directed presence到其他用户,来避免订阅对方信息,非常适合应用于网络而上的简短交流。
可以通过发送<presence type=”unavailable” />发送下线通知,服务器会完成消息保存,联系人通知等一系列操作。
presence也有其富文本形式,可以包含更多信息,但不建议在presence中使用(资源和带宽)。Publish-Subscribe[XEP-0060]和Personal Eventing Protocal[XEP-0163]提供了类似功能,但presence是针对整个花名册广播的。
服务器返回的花名册中还可以包含更加丰富的信息,包括用户组以及订阅情况。
用户对花名册的修改也可以通过发送IQ-set(rost-push)同步到服务器及其他客户端上(用户可能有手机/pad等)。这种只推送变更的机制可以简化客户端编程并节省流量。
Instant Messaging
消息发送流程
- user1向user2发送消息,user1位于domain1,user2位于domain2
- user1的client1向server1发送消息节,server1设置from属性
- server1投递消息给server2(直接通信)
- server2收到消息并检查user2是否在线并投递
- normal
message消息类型
独立消息,将会马上投递或者缓存,是默认消息类型
- chat
用户聊天,通过session建立,通常处理一系列消息
- groupchat
多人聊天,通常此类型会指定一个组件或者模块处理多人聊天,该模块会为每个参与者发送消息
- headline
全体通知,不会缓存,立即投递
- error
错误信息节,反馈错误信息
延时投递
如果用户不在线,则消息被缓存,用户登录后,消息推送给用户,并携带消息原始产生时间,用于客户端对消息进行排序。
可以参考Delayed Delivery[XEP-0203]
Chat session
chat session用于用户频繁交流的情况,这类似于现实情况中的聊天,其建立过程为:双方用户在消息交互中知道了对方的Full JID,因此可以直接通信,响应的机制称为chat session。
状态通知
类似于QQ的“正在输入”功能,让交互双方了解即时状态。
该功能扩展由XEP-0085定义。如果用户不希望对方看到自己的状态,可以选择不响应<active/>节。
已经定义好的状态有:Starting,Active,Composing,Paused,Inactive,Gone
消息类似于:
<message from=you@yourdomain.tld/work to=daughter@yourdomain.tld type="chat"> <body>Hi honey!</body> <active xmlns="http://jabber.org/protocol/chatstates"/> </message>
格式化消息
可以在message消息中加入XHTML用于富文本展示。
协议可以在[XEP-0071]中找到。
<message from=you@yourdomain.tld/home to=friend@theirdomain.tld type="chat"> <body>I love this movie I saw last night, it's awesome!</body> <html xmlns="http://jabber.org/protocol/xhtml-im"> <body xmlns="http://www.w3.org/1999/xhtml"> <p> I <em>love</em>, this new movie I saw last night, it's <strong>awesome</strong>! </p> </body> </html> </message>
很容易注意到,消息中包含一个不包含格式的文本,以及XHTML格式的文本,是因为考虑到客户端如果不支持HTML,也可以正常展示。
HTML节中不可以包含HEAD中的信息,也不能包含脚本(安全性考虑)。
vCards
也就是虚拟名片,用户在聊天时可以通过虚拟名片查看相关信息。
参考[XEP-0054]
虚拟名片服务通过发送IQ请求查看和更新,但需要注意的是,更新虚拟名片时需要发送完整的信息而不是只有要更新的部分。
查询请求:
<iq from=mouse@wonderland.lit/pool id="pw91nf84" to=alice@wonderland.lit type="get"> <vCard xmlns="vcard-temp"/> </iq>
返回的消息:
<iq from=alice@wonderland.lit id="pw91nf84" to=mouse@wonderland.lit/pool type="result"> <vCard xmlns="vcard-temp"> <N> <GIVEN>Alice</GIVEN> </N> <URL>http://wonderland.lit/~alice/</URL> <PHOTO> <EXTVAL>http://www.cs.cmu.edu/~rgs/alice03a.gif</EXTVAL> </PHOTO> </vCard> </iq>
更新名片:
<iq from=alice@wonderland.lit/pda id="w0s1nd97" to=alice@wonderland.lit type="set"> <vCard xmlns="vcard-temp"> <N> <GIVEN>Alice</GIVEN> </N> <URL>http://wonderland.lit/~alice/</URL> <PHOTO>
<EXTVAL>http://www.cs.cmu.edu/~rgs/alice03a.gif</EXTVAL> </PHOTO> <EMAIL> <USERID>alice@wonderland.lit</USERID> </EMAIL> </vCard> </iq>
阻塞和过滤通信
类似于QQ可黑名单机制,可以对某人隐身,不看某人的消息或屏蔽某个域的消息等等;当然也包括对名单的修改。
该功能可支持的粒度非常细,参考标准[XEP-0016],[XEP-0191]。
更多消息扩展
- Extended Stanza Addressing[XEP-0033]
发送一条消息给多个接受者而不通过聊天室
- Advanced Message Processing[XEP-0079]
控制消息过期,避免消息被本地存储以及延时投递等
- Message Receipt[XEP-00184]
客户端层面确认消息是否已经送达
- Message Archiving[XEP-0136]
在服务器上存储消息,而不是在客户端机器上存储
Service Discovery
我们需要知道系统中有哪些实体,以及该实体支持哪些服务,为了完成这些操作,引入了实体发现和服务发现的概念。
Items and Info
XMPP 服务发现协议[XEP-0030]定义了两个基本的发现方法。首先发现disco#items,disco#info。
- 向服务器发送发现实体请求
<iq from=hatter@wonderland.lit/home id="xl391n47" to="wonderland.lit" type="get"> <query xmlns="http://jabber.org/protocol/disco#items"/> </iq>
- 服务器响应发回实体列表
<iq from="wonderland.lit" id="xl391n47" to=hatter@wonderland.lit/home type="result"> <query xmlns="http://jabber.org/protocol/disco#items"> <item jid="conference.wonderland.lit"/> <item jid="notify.wonderland.lit"/> </query> </iq>
- 查询实体支持的功能
<iq from=hatter@wonderland.lit/home id="gq02kb71" to="conference.wonderland.lit" type="get"> <query xmlns="http://jabber.org/protocol/disco#info"/> </iq>
- 返回实体支持功能结果列表
<iq from="conference.wonderland.lit" id="gq02kb71" to=hatter@wonderland.lit/home type="result"> <query xmlns="http://jabber.org/protocol/disco#info"> <identity category="conference" type="text" name="Chatrooms"/> <feature var="http://jabber.org/protocol/muc"/> <feature var="jabber:iq:register"/> <feature var="vcard-temp"/> </query> </iq>
Using Service Discovery with Servers and Services
服务发现同样适用iq-get的disco#items/disco#info这两个查询操作,只是将查询是服务而非实体。
具体的查询步骤较为复杂。
Using Service Discovery with Clients
Explicit Service Discovery
这种场景应用于用户的花名册向用户返回是否在线的信息,这种判断是否在线的信息带有Full JID,因此可以通过disco#info/disco#item来查询。
但是这种查询可能返回某个Full JID携带的全部信息,导致数据量过大,因此引入了下面的方式。
Entity Capabilites: Service Discovery Shorthand
是对上面方法的改进,通过将实体支持的特性HASH为一组特征吗,客户端接收该特征码后与本地存储进行比较,如果已有该特征码,则可以获得支持的特性列表;如果客户端没有缓冲该特征码,则重新发送disco#info消息获取,并缓存。
采用此种方法可以节省响应的资源。
并且通过presence节就可以获取客户端支持的功能了。
Data Forms
类似于HTML的表单,有工作流的特征,可以实现用户验证码输入和确认等功能。
由[XEP-0004]定义。
Multi-Party Interaction
MCU 基础
- 多人聊天最初被称为groupchat,后来的迭代版本改进为Multi-User Chat(UMC)[XEP-0045]。
- MCU的基本思想是用户加入到一个聊天室,而聊天室会组播消息,聊天室起到消息反射器的作用
- 聊天室有如下特征
3.1 消息在所有的参与者中共享
3.2 所有的参与者都有一个room roster
3.3 参与者都使用其nickname标识,而不是实际的JabberID
3.4 房间共享参与信息
3.5 参与者不仅限于人,也可以是服务等
- 聊天室有其自己的JID,且该JID是服务器的一个组件,因此具有不同的域,如服务器的域称为:wonderland.lit,组件的域为conference.wonderland.lit,实现MCU需要相应的组件,服务器根据域的不同将消息路由到对应的组件上处理。
- 加入聊天流程
5.1 用户发送presence消息
5.2 聊天室向成员广播该presence
5.3 聊天室向用户发送成员的presence
5.4 聊天室向用户发送一些历史消息好让用户参与讨论,消息数目可配置,且消息带有时间戳
<delay xmlns="urn:xmpp:delay" stamp="2008-11-07T18:42:03Z"/>
5.5 之后的聊天消息不再携带时间戳
5.6 聊天室之间的消息往来,消息类型为groupchat
- 如果用户向聊天室的成员发送消息,消息类型为chat,但消息实际上使用用户发送给聊天室的,聊天室会改写from/to字段为实际接受者的JID。
- 如果离开聊天室则会发送退席消息
<presence from=dormouse@wonderland.lit/sleepspace to=teaparty@conference.wonderland.lit/Dormouse type="unavailable"/>
成员管理
群组中有多重角色,不同的角色拥有不同的权限,可以将用户临时踢出,或加入黑名单等。
具体有:outcast,visitor,participant,member,moderator,admin,owner。
另外房间也有不同的类型,有指定名单的,有临时的,有隐藏的,有固定的等。
昵称
用户可以设置其在聊天室内的昵称,参考In-Band Registration[XEP-0077]。
配置
多人聊天可以进行配置,有非常多的可配置项,列出如下。
配置项 |
作用 |
allowinvites |
是否允许普通成员邀请 |
changesubject |
是否非管理员能够更改聊天室主题 |
enablelogging |
是否开启记录归档 |
getmemberlist |
是否能够获取成员列表 |
lang |
语言 |
maxuser |
最大参与者数量 |
membersonly |
是否仅会员可加入(适用于member类型聊天室) |
persistentroom |
房间是否为永久(所有成员退出也不会删除) |
presencebroadcast |
是否广播presence消息,对大room有用 |
publicroom |
该room是否可被发现 |
roomadmin |
设置room管理员 |
roomdesc |
设置room描述 |
roomname |
设置room名称 |
roomowner |
设置room 所有者 |
whois |
控制是否匿名等 |
数据传输
除了文字之外,MCU还可以传输地理信息,文件和进行远程调用等。
Publish/Subscribe
实际上就是消息系统中的推模式,主要分三个步骤完成:首先订阅一个主题;其次发布一个消息;最后消息被推送到订阅客户端。[XEP-0060]
Subscriptions
订阅者需要订阅一个源,发布者也将消息发布到这个源。
流程:
- 用户发送订阅请求
- 服务器响应该订阅请求
如果成功则可以接收消息了,如果失败,则有可能是要求更多的配置信息
- 订阅者对订阅进行配置(可选)
- 订阅者请求配置项
- 源返回订阅配置表单
- 用户解除订阅
- 服务器返回解除订阅响应
Publishing and Receiving Notifications
主要分为两个部分。
发布方发布消息到源;服务器将源的消息通过message的形式投递给订阅方。
需要注意的是:发布方只负责将消息推送至源,投递的逻辑由订阅方的服务器完成。
Payloads
用户可以选择是否在通知中携带payloads,如通知中包含头像信息时,只需要metadata,无需具体的数据。
是否启用payload可以通过配置项deliver_payloads来实现。
Items
是否保存items也是可以配置的,可以通过persist_items配置。
保存/不保存数据的node分别称为persistent nodes/transient node。
Discovering Nodes
nodes及其服务可以通过disco#info/disco#item来查询
返回的结果可以通过以上两个命令进一步查询,直到找到想要订阅的node。
Personal Eventing
如果用户想订阅某个好友的动态,可以使用用户的JID作为datanode,相应的简化协议称为PEP[XEP-0163](Personal Eventing Protocal)。
用户可以应用PEP协议,在自己的Presence消息中包含自己的爱好信息,服务器收到该presence后,将会根据此爱好向用户推送好友的动态。
包括User Tune,User Location,User Activity,User Mood,User Nickname,User Avatar等。
Design Decision
- 尽量不要修改XMPP的核心协议,而是应该试着通过新的namespace去扩展它
- 由于presence占用了大约90%的数据流量,需要控制presence节中的数据
- 需要尽量简化XMPP客户端的设计,尤其是需要减少对服务器端的压力
- 尽量重用已有协议,而不是重新设计一个