Jabber 是著名的Linux即时通讯服务服务器,它是一个自由开源软件,能让用户自己架即时通讯服务器,可以在Internet上应用,也可以在局域网中应用。Jabber最有优势的就是其通信协议, XMPP的技术来自于Jabber,其实它是 Jabber的核心协定,所以XMPP有时被误称为Jabber协议。 因特网工程工作小组(IETF)已经将Jabber的核心XML串流协定以XMPP之名,正式列为认可的即时通讯及Presence技术。而XMPP的技术规格已被出版为RFC3920及RFC3921。
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='[email protected]'
to='[email protected]'
xml:lang='en'>
C: <body>Art thou not Romeo, and a Montague?</body>
C: </message>
S: <message from='[email protected]'
to='[email protected]'
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'> <mechanism>DIGEST-MD5</mechanism> <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(Info Query)查询服务器资源。
<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>[email protected]/someresource</jid> </bind> </iq>
1.4 XML短语(Stanzas)
有三种短语——<message>(消息)、<presence>(在线状态)和<iq>(信息查询),三种短语都有共同属性,如下。
2. 基本语义
2.1 Message语义
message短语可视为一种推送机制——某个实体向另一个实体推送信息。所有message短语必须携带to属性,以标明接收者。服务器收到message短语后,或路由或投递给接收者。
2.2 Presence语义
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语法
type是message短语必要的属性,分类如下:
message包含<subject/>、<body/>和<thread/>子元素。
3.1.1 Subject
<subject/>元素包含说明消息标题的可视字符。
3.1.2 Body
<body/>元素包含消息文字内容的可视字符。
3.1.3 Thread
<thread/>元素用于跟踪会话线索,包含标示线索的不可视字符。
3.2 Presence语法
<presence>用于表述一个实体的网络状态(在线及子状态,离线),并将该实体状态广播给其它实体。<presence>也用于协商和管理其他实体的网络状态。
<presence>的type属性可选,客户端发送一个没有type属性的<presence>到服务器仅表示在线,可以通信。如果<presence>包括type属性,则表述:1)不在;2)请求订阅其它实体的网络状态;3)查询其他实体的网络状态;4)错误。
type取值范围如下:
3.2.1 Show
<show>包含不可视字符以表述一个实体(或资源)的可用状态。<show>取值如下:
3.2.2 Status
<status>一般配合<show>使用,包含可视字符详细说明实体的网络状态。
3.2.3 Priority
<priority>包含不可视字符以说明资源的优先级。
3.3 IQ语法
XMPP IM规范对IQ做了两个扩展——好友管理和块通信(Blocking Communication)。
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'/>
<iq from='[email protected]/balcony' type='get' id='roster_1'> <query xmlns='jabber:iq:roster'/> </iq>
第2步:服务器返回列表
<iq to='[email protected]/balcony' type='result' id='roster_1'> <query xmlns='jabber:iq:roster'> <item jid='[email protected]' name='Romeo' subscription='both'> <group>Friends</group> </item> <item jid='[email protected]' name='Mercutio' subscription='from'> <group>Friends</group> </item> <item jid='[email protected]' name='Benvolio' subscription='both'> <group>Friends</group> </item> </query> </iq>
<iq from='[email protected]/balcony' type='set' id='roster_2'> <query xmlns='jabber:iq:roster'> <item jid='[email protected]' name='Nurse'> <group>Servants</group> </item> </query> </iq>
<iq from='[email protected]/chamber' type='set' id='roster_3'> <query xmlns='jabber:iq:roster'> <item jid='[email protected]' name='Romeo' subscription='both'> <group>Friends</group> <group>Lovers</group> </item> </query> </iq>
<iq from='[email protected]/balcony' type='set' id='roster_4'> <query xmlns='jabber:iq:roster'> <item jid='[email protected]' subscription='remove'/> </query> </iq>
用“关注”可能比“订阅”更能精确表述Subscribe概念。在XMPP中,联系人和好友的区别在于是否关注。被关注联系人的状态能够同步过来,反之亦然。
以下例子展示如何增加一个联系人:
第1步: 客户端发起一个新增联系人请求:
<iq type='set' id='set1'>
<iq type='set' id='set1'> <query xmlns='jabber:iq:roster'> <item jid='[email protected]' name='MyContact'> <group>MyBuddies</group> </item> </query> </iq>
<iq type='result' id='set1'/>
<presence to='[email protected]' type='subscribe'/>
<presence from='[email protected]' to='[email protected]' type='subscribe'/>
<iq type='set' id='set2'> <query xmlns='jabber:iq:roster'> <item jid='[email protected]' name='SomeUser'> <group>SomeGroup</group> </item> </query> </iq> <presence to='[email protected]' type='subscribed'/>
<presence from='[email protected]' to='[email protected]' type='subscribed'/>