转载自:http://blog.csdn.net/yhawaii/article/details/7920680
1. 简介
XMPP来源于Jabber开源社区,基于XML,提供准实时的传递消息、在线状态和请求/响应服务。XMPP使用客户/服务模式,服务器之间能够相互连接,建立在面向连接的协议上,通常是TCP。下图概要描述了XMPP架构:
![](https://img-my.csdn.net/uploads/201208/29/1346227029_2457.gif)
以黑色线条连接的方框是XMPP的客户端和服务器,要注意的是XMPP客户端之间不能相互连接,这是一个严格的客户/服务器模型。客户端2和服务器1通过网关接入异构消息网络,异构消息网络指没有应用XMPP协议的网络,比如MSN、SMTP等。XMPP协议的开放性使它很容易接入同构和异构网络,把数据孤岛串联为一个网络。
XMPP允许通过身份验证的多个资源(resource)并发接入一台服务器,这些源以XMPP地址中的资源号来标示自己,比如node@domain/home和node@domain/work。服务客户端的周知端口是5222,服务器相互连接的端口是5269。
由于历史原因,XMPP节点被称为Jabber Identifier(或JID),格式为node@domain/resource。JID可以是domain,表示这是一个网关,或者是提供服务的一个子节点。node通常代表访问服务器的实体,比如及时信使客户端。resource通常代表一个特定的会话、连接(一个设备或位置)、属于实体的对象(多人聊天室的一个参与者)
XMPP被设计为能够异步并能快速交换短文的协议,为此客户端和服务器之间存在两条XML流,用于异步通信,流传输的是XML文档。TCP是XMPP的默认承载协议,在客户端到服务器的通信模型下,需要一条TCP链路;在服务器到服务器的通信模式下,需要两条TCP链路以传输两个对端数据。
XMPP流以<stream>标记开始,以</stream>结束。XMPP流可以视为一个well-form的XML文档,每次传输的是文档的片段,而片段则是well-form的XML标签。
stream标记的属性如下:
to只能用于从客户端到服务器的XML流中。
from只能用于从服务器到客户端的XML流中。
id只能用于从接收实体到发送实体的XML流中,id必须唯一,用于标记会话。
xml:lang只能用于发起方,用于约定语言。如发起方没有携带xml:lang属性,接收方应使用默认语言。
version至少在“1.0”以上。
XMPP Core对应的是RFC3920,从中摘录一段代码以简要说明通信过程。(S代表服务器,C代表客户端)
- C: <?xml version='1.0'?>
- <stream:stream
- to='example.com'
- xmlns='jabber:client'
- xmlns:stream='http://etherx.jabber.org/streams'
- version='1.0'>
- S: <?xml version='1.0'?>
- <stream:stream
- from='example.com'
- id='someid'
- xmlns='jabber:client'
- xmlns:stream='http://etherx.jabber.org/streams'
- version='1.0'>
- ... encryption, authentication, and resource binding ...
- C: <message from='juliet@example.com'
- to='romeo@example.net'
- xml:lang='en'>
- C: <body>Art thou not Romeo, and a Montague?</body>
- C: </message>
- S: <message from='romeo@example.net'
- to='juliet@example.com'
- xml:lang='en'>
- S: <body>Neither, fair saint, if either thee dislike.</body>
- S: </message>
- C: </stream:stream>
- S: </stream:stream>
可以看到客户端和服务器双方都以<stream>标记开始会话,即双方都在传输一个完整的XML文档。以<stream>发起会话后,双方建立TLS安全链路,然后用SASL(稍后说明)认证对方身份,最后绑定资源并开始传输消息报文。
TLS是SSL的后继者,TLS1.0和SSL3.0非常相似。TLS建立在传输层上,通信双方先用不对称加密算法传输对称加密算法的密钥,然后用对称加密算法加密传输内容。 1.1 TLS
(以下例子摘录自RFC3920)
第1步: 客户端发起连接:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='example.com' version='1.0'>
第2步: 服务器向客户端返回一个<stream>标记:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='c2s_123' from='example.com' version='1.0'>
第3步: 服务器向客户端发送STARTTLS扩展,并携带认证机制和流特性:
- <stream:features>
- <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
- <required/>
- </starttls>
- <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
- <mechanism>DIGEST-MD5</mechanism>
- <mechanism>PLAIN</mechanism>
- </mechanisms>
- </stream:features>
第4步: 客户端发送STARTTLS给服务器:
- <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
第5步: 服务器提示客户端可以继续:
- <proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
第6步: 客户端和服务器尝试在已有的TCP链路上完成TLS握手过程。
第7步: 如果TLS握手成功,客户端向服务器发起一个新流:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='example.com' version='1.0'>
第8步: 服务器以一个stream头响应,同时携带可能的流特性:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' from='example.com' id='c2s_234' version='1.0'>
- <stream:features>
- <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
- <mechanism>DIGEST-MD5</mechanism>
- <mechanism>PLAIN</mechanism>
- <mechanism>EXTERNAL</mechanism>
- </mechanisms>
- </stream:features>
第9步: 客户端继续SASL握手(见后)。
1.2 简单授权和安全传输层协议SASL(Simple Authentication and Security Layer protocol)
(以下例子摘自RFC3920)第1步: 客户端向服务器发起流请求:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='example.com' version='1.0'>
第2步: 服务器向客户端响应stream标记:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='c2s_234' from='example.com' version='1.0'>
第3步: 服务器向客户端提示可用的认证方法:
- <stream:features>
- <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
- <span style="white-space:pre"> </span><mechanism>DIGEST-MD5</mechanism>
- <span style="white-space:pre"> </span><mechanism>PLAIN</mechanism>
- </mechanisms>
- </stream:features>
第4步: 客户端选择一种认证方法:
- <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>
第5步: 服务器发送以BASE64编码的验证码给客户端:
- <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
- cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgi
- LGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg==
- </challenge>
验证码解码后如下:
realm="somerealm",nonce="OA6MG9tEQGm2hh",\
qop="auth",charset=utf-8,algorithm=md5-sess
第6步: 客户端发送以BASE64编码的响应码:
- <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
- dXNlcm5hbWU9InNvbWVub2RlIixyZWFsbT0ic29tZXJlYWxtIixub25jZT0i
- T0E2TUc5dEVRR20yaGgiLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLG5jPTAw
- MDAwMDAxLHFvcD1hdXRoLGRpZ2VzdC11cmk9InhtcHAvZXhhbXBsZS5jb20i
- LHJlc3BvbnNlPWQzODhkYWQ5MGQ0YmJkNzYwYTE1MjMyMWYyMTQzYWY3LGNo
- YXJzZXQ9dXRmLTgK
- </response>
解码后的响应码如下:
username="somenode",realm="somerealm",\
nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk",\
nc=00000001,qop=auth,digest-uri="xmpp/example.com",\
response=d388dad90d4bbd760a152321f2143af7,charset=utf-8
第7步: 服务器发送另一个以BASE64编码的验证码给客户端:
- <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
- cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZAo=
- </challenge>
解码后的验证码:
rspauth=ea40f60335c427b5527b84dbabcdfffd
第8步: 客户端响应验证码:
- <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
第9步: 服务器提示客户端认证通过:
- <success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
第10步: 客户端向服务器发起新连接:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='example.com' version='1.0'>
第11步: 服务器响应一个stream头,可能携带特性:
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='c2s_345' from='example.com' version='1.0'>
- <stream:features>
- <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
- <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
- </stream:features>
1.3 绑定资源
- <iq type='set' id='bind_2'> <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'> <resource>someresource</resource> </bind> </iq>
服务器确认客户端要绑定的资源后,必须返回一个iq标签。
- <iq type='result' id='bind_2'>
- <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
- <jid>somenode@example.com/someresource</jid>
- </bind>
- </iq>
1.4 XML短语(Stanzas)
2. 基本语义
2.1 Message语义
message短语可视为一种推送机制——某个实体向另一个实体推送信息。所有message短语必须携带to属性,以标明接收者。服务器收到message短语后,或路由或投递给接收者。
2.2 Presence语义
2.3 IQ语义
Info/Query,或IQ,是一种”请求-响应“机制,与HTTP类似。IQ使一个实体能够向另一个实体发起请求,请求/响应报文以id属性标示。IQ交互通常以get/result和set/result模式执行。
3. 基于XMPP的即时信使扩展
XMPP标准由XMPP Core(RFC3920)、XMPP IM(RFC3921)、XMPP CPIM(RFC3922)(映射XMPP到IETF的CPIM规范)、XMPP E2E(RFC3922)(端到端信号和对象加密)、XMPP URN(RFC4854)(基于XMPP扩展的Uniform Resource Name树)、XMPP ENUM(RFC4979)(在IANA注册的枚举服务)、XMPP URI(RFC5122)(对RFC4622的勘误)。本文只覆盖了XMPP Core和XMPP IM,其中XMPP IM由XMPP Core扩展而得。
3.1 Message语法
message包含<subject/>、<body/>和<thread/>子元素。
3.1.1 Subject
3.1.2 Body
3.1.3 Thread
<thread/>元素用于跟踪会话线索,包含标示线索的不可视字符。
chat。一对一聊天。
error。发送上一条消息所致错误。
groupchat。多人聊天(类似IRC)。
headline。该消息可能由提供投递或广播内容(新闻、体育、市场信息、RSS源等)的自动服务发出。
normal。这是一条与一对一或群聊无关的消息,发送者希望接收者能够回复该消息。
to属性表述接收者的JID。
from属性表述发送者的JID。
可选id属性,用于内部跟踪短语,比如用于<iq>短语。
type属性表述消息上下文(或用途)的详细信息。
当短语承载用于阅读的信息时,xml:lang表述语言类型,如果xml:lang不存在则应用stream的语言设定。
3.2 Presence语法
<presence>的type属性可选,客户端发送一个没有type属性的<presence>到服务器仅表示在线,可以通信。如果<presence>包括type属性,则表述:1)不在;2)请求订阅其它实体的网络状态;3)查询其他实体的网络状态;4)错误。
type取值范围如下:
不可用(unavailable)。
订阅。
退订。
已退订。表示先前订阅被拒绝(或取消)。
探测。请求另一个实体的状态,只能由服务器发出。
错误。
3.2.1 Show
away。
chat。
dnd (Do Not Disturb)。
xa (eXtended Away)。
3.2.2 Status
<status>一般配合<show>使用,包含可视字符详细说明实体的网络状态。
3.2.3 Priority
3.3 IQ语法
3.4 会话
大多数IM和在线状态应用构建在XMPP的客户端/服务器架构上,需要与服务器建立会话以传输消息和状态。支持会话的服务器在完成流认证后,向客户端发送<session>标签。
- <stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='c2s_345' from='example.com' version='1.0'>
- <stream:features>
- <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>
- <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>
- </stream:features>
客户端绑定资源完毕后,向服务器发起<iq>请求,要求创建会话。
第1步: 客户端向服务器发起会话请求:
<iq to='example.com' type='set' id='sess_1'> <session xmlns='urn:ietf:params:xml:ns:xmpp-session'/> </iq>
第2步: 服务器提示客户端会话已建立:
<iq from='example.com' type='result' id='sess_1'/>
3.5 联系人管理(Roster Management)
联系人列表通过<iq>标签获取。
3.5.1 获得当前联系人
第1步: 客户端向服务器发起查询请求:
<iq from='juliet@example.com/balcony' type='get' id='roster_1'> <query xmlns='jabber:iq:roster'/> </iq>
第2步:服务器返回列表
- <iq to='juliet@example.com/balcony' type='result' id='roster_1'>
- <query xmlns='jabber:iq:roster'>
- <item jid='romeo@example.net' name='Romeo' subscription='both'>
- <group>Friends</group>
- </item>
- <item jid='mercutio@example.org' name='Mercutio' subscription='from'>
- <group>Friends</group>
- </item>
- <item jid='benvolio@example.org' name='Benvolio' subscription='both'>
- <group>Friends</group>
- </item>
- </query>
- </iq>
3.5.2 增加联系人
- <iq from='juliet@example.com/balcony' type='set' id='roster_2'>
- <query xmlns='jabber:iq:roster'>
- <item jid='nurse@example.com' name='Nurse'>
- <group>Servants</group>
- </item>
- </query>
- </iq>
服务器收到请求后,首先更新本地存储资料,然后向关联实体发出<iq>设置指令,使所有实体处于同步状态。
更新与删除请求与增加类似,下面分别是更新和删除例子。
- <iq from='juliet@example.com/chamber' type='set' id='roster_3'>
- <query xmlns='jabber:iq:roster'>
- <item jid='romeo@example.net' name='Romeo' subscription='both'>
- <group>Friends</group>
- <group>Lovers</group>
- </item>
- </query>
- </iq>
- <iq from='juliet@example.com/balcony' type='set' id='roster_4'>
- <query xmlns='jabber:iq:roster'>
- <item jid='nurse@example.com' subscription='remove'/>
- </query>
- </iq>
3.6 关注(Subscribe)
以下例子展示如何增加一个联系人:
第1步: 客户端发起一个新增联系人请求:
- <iq type='set' id='set1'>
- <query xmlns='jabber:iq:roster'>
- <item jid='contact@example.org' name='MyContact'>
- <group>MyBuddies</group>
- </item>
- </query>
- </iq>
第2步: 服务器返回操作结果:
<iq type='result' id='set1'/>
第3步: 客户端发起关注请求:
<presence to='contact@example.org' type='subscribe'/>
第4步: 服务器向被关注实体服务器发出关注指令:
<presence from='user@example.com' to='contact@example.org' type='subscribe'/>
第5步: 被关注实体服务器发送关注请求给被关注实体,被关注实体返回确认指令:
- <iq type='set' id='set2'>
- <query xmlns='jabber:iq:roster'>
- <item jid='user@example.com' name='SomeUser'>
- <group>SomeGroup</group>
- </item>
- </query>
- </iq>
- <presence to='user@example.com' type='subscribed'/>
第6步: 用户服务器返回结果给关注发起者:
<presence from='contact@example.org' to='user@example.com' type='subscribed'/>