xmpp通信过程分析

XMPP消息格式

Jabber/XMPP系统使用XML流在不同实体之间相互传输数据。在两个实体的连接期间,XML流将从一个实体传送到另一个实体。在实体间,有三个顶层的XML元素:,和。每一个都包含属性和子节点。下面将分别描述这些元素。

1)消息(message)元素:

一个即时消息系统最基本的功能就是能够在两个用户之间实时交换消息,元素就提供了这个功能。每条消息都有一个或多个属性和子元素。属性“from”和“to”分别表示了消息发送者和接收者的地址。也可以包含一个“type”属性,这给接收者一个提示,这个消息是什么样的消息。表3-1给出了“type”属性的可能取值。中也可以包含“id”属性,用来唯一的标识一个输出消息的响应。

2)状态(presence)元素:

元素用来传递一个用户的存在状态的感知信息。用户可以是“available”,要么是“unavailable”,“Hide”等。当用户连接到即时消息服务器后,好友发给他的消息就立即被传递。如果用户没有连接到服务器,好友发给他的消息将被服务器存储起来直到用户连接到服务器。用户通过即时消息客户端自己控制可用性。但是,如果用户断开了同服务器的连接,服务器将发送给订阅了这个用户的存在信息的用户通知他们用户已经不可用。还包含了两个子元素:和。包含了一个对的文本描述。

3)IQ(Info<Query)元素

IQ元素是Jabber/XMPP消息协议的第三个顶层元素。IQ代表”Info/Query”,用来发送和获取实体之间的信息。IQ消息是通过“请求/响应”机制在实体间进行交换的。IQ元素用于不同的目的,它们之间通过不同的命名空间来加以区分。在Jabber/XMPP消息协议里有许多的命名空间,但最常用的命名空间是:”jabber:iq:register”,”jabber:iq:auth”,”jabber:iq:roster”。

上面描述了Jabber协议的三个顶层节点。通过这种格式Jabber消息不仅可以是简单的文本(text),而且可以携带复杂的数据和各种格式的文件,也就是说Jabber不仅可以用在人与人之间的交流,而且可以实现软件与软件或软件与人之间的交流。Jabber的这种功能大大扩展了即时通信的应用范围。

XMPP通信过程

XMPP被设计为能够异步并能快速交换短文的协议,为此客户端和服务器之间存在两条XML流,用于异步通信,流传输的是XML文档。TCP是XMPP的默认承载协议,在客户端到服务器的通信模型下,需要一条TCP链路;在服务器到服务器的通信模式下,需要两条TCP链路以传输两个对端数据。

XMPP流以标记开始,以结束。XMPP流可以视为一个well-form的XML文档,每次传输的是文档的片段,而片段则是well-form的XML标签。

stream标记的属性如下:
to只能用于从客户端到服务器的XML流中。
from只能用于从服务器到客户端的XML流中。
id只能用于从接收实体到发送实体的XML流中,id必须唯一,用于标记会话。
xml:lang只能用于发起方,用于约定语言。如发起方没有携带xml:lang属性,接收方应使用默认语言。
version至少在“1.0”以上。

可以看到客户端和服务器双方都以标记开始会话,即双方都在传输一个完整的XML文档。以发起会话后,双方建立TLS安全链路,然后用SASL(稍后说明)认证对方身份,最后绑定资源并开始传输消息报文。

TLS是SSL的后继者,TLS1.0和SSL3.0非常相似。TLS建立在传输层上,通信双方先用不对称加密算法传输对称加密算法的密钥,然后用对称加密算法加密传输内容。

TLS

第1步: 客户端发起连接:

1
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='maimaijun.com' version='1.0'>

第2步: 服务器向客户端返回一个标记:

1
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='c2s_123' from='maimaijun.com' version='1.0'>

第3步: 服务器向客户端发送STARTTLS扩展,并携带认证机制和流特性:

1
<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给服务器:

1
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>

第5步: 服务器提示客户端可以继续:

1
<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>

第6步: 客户端和服务器尝试在已有的TCP链路上完成TLS握手过程。
第7步: 如果TLS握手成功,客户端向服务器发起一个新流:

1
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='maimaijun.com' version='1.0'>

第8步: 服务器以一个stream头响应,同时携带可能的流特性:

1
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="maimaijun.com" id="3f857b69" xml:lang="en" version="1.0"><stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"></starttls><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>CRAM-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features>

第9步: 客户端继续SASL握手。

SASL(Simple Authentication and Security Layer protocol)

第1步: 客户端向服务器发起流请求:

1
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='maimaijun.com' version='1.0'>

第2步: 服务器向客户端响应stream标记:

1
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="maimaijun.com" id="9a7eced8" xml:lang="en" version="1.0">

第3步: 服务器向客户端提示可用的认证方法:

1
<stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"></starttls><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>CRAM-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features>

第4步: 客户端选择一种认证方法:

1
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>

第5步: 服务器发送以BASE64编码的验证码给客户端:

1
2
3
4
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg==</challenge>
验证码解码后如下:
realm="somerealm",nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8,algorithm=md5-sess

第6步: 客户端发送以BASE64编码的响应码:

1
2
3
4
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>dXNlcm5hbWU9InNvbWVub2RlIixyZWFsbT0ic29tZXJlYWxtIixub25jZT0iT0E2TUc5dEVRR20yaGgiLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLG5jPTAwMDAwMDAxLHFvcD1hdXRoLGRpZ2VzdC11cmk9InhtcHAvZXhhbXBsZS5jb20iLHJlc3BvbnNlPWQzODhkYWQ5MGQ0YmJkNzYwYTE1MjMyMWYyMTQzYWY3LGNoYXJzZXQ9dXRmLTgK</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编码的验证码给客户端:

1
2
3
4
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZAo=</challenge>
解码后的验证码:
rspauth=ea40f60335c427b5527b84dbabcdfffd

第8步: 客户端响应验证码:

1
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

第9步: 服务器提示客户端认证通过:

1
<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

第10步: 客户端向服务器发起新连接:

1
2
3
4
5
<stream:stream
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'
to='example.com'
version='1.0'>

第11步: 服务器响应一个stream头,可能携带特性:

1
2
3
4
5
6
7
8
9
<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>

绑定资源

客户端登陆
SENT:

1
<iq id="1a58416" type="get"><query xmlns="jabber:iq:auth"><username>mbed</username></query></iq>

RECV:

1
<iq id='5573507c' type='result'><query xmlns='jabber:iq:auth'><username>mbed</username><digest/><password/><resource/></query></iq>

注意中间的ID,这个ID是服务器端返回给客户端的验证信息,验证信息一般是以该ID号+用户密码通过SHA1(RFC3174)算法进行操作的。也就是说客户端得到该ID和密码经过SHA1算法加密后返回给服务器。

SENT(客户端提交字段内容进行验证):
文本格式,非加密模式

1
2
3
4
5
6
7
<iq type='set' id='auth2'>
<query xmlns='jabber:iq:auth'>
<username>mbed</username>
<password>mirror</password>
<resource>flash</resource>
</query>
</iq>

加密模式

1
2
3
<iq id="ee17dc3f" type="set"><query xmlns="jabber:iq:auth"><username>mbed</username><resource>javaScript</resource><digest>459154dc8ea4a74ba5f692f79393e1c7e9de9fda</digest></query></iq>
或者
<iq id="ee17dc3f" type="set"><query xmlns="jabber:iq:auth"><username>mbed</username><resource>flash</resource><digest>459154dc8ea4a74ba5f692f79393e1c7e9de9fda</digest></query></iq>

这个digest就是上面经过SHA1算法得出的结果字段;
如果客户端发送的字段包括了用户名和IQ-GET的字段,服务器不应该返回错误消息(因为需要服务器判断当前用户名是否在使用),如果服务器不支持可插入的简单认证及密码模块,那么必须返回一个的错误;如果客户端企图使用SASL认证但是失败,服务器必须返回错误信息
在认证过程中,jabber:iq:auth命名、用户名和资源是必须要求客户端提供的,而服务器返回的XML流中也必须提供这2个元素。

RECV:

1
2
3
4
<iq id='jcl_104' type='result'/>
<presence><c node="http://exodus.jabberstudio.org/caps" ver="0.9.1.0" xmlns="http://jabber.org/protocol/caps"/><status>Available</status><priority>1</priority></presence>
<iq type='set' id='1a58416'><query xmlns='jabber:iq:auth'><username>mbed</username><password>mirror</password><resource>flash_as3</resource></query></iq>
<iq xmlns='jabber:client' type="get" to="muc.maimaijun.com"><query xmlns='jabber:iq:mucAnonyRoom' actionType='getRoomIndex'><item><customerIndex>1</customerIndex></item></query></iq>

登录结果
成功

1
<iq type='result' id='auth2'/>

失败 – 认证失败,可能是用户名密码不匹配或数字验证错误

1
2
3
4
5
<iq type='error' id='auth2'>
<error code='401' type='auth'>
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</iq>

失败 – 资源冲突/错误

1
2
3
4
5
<iq type='error' id='auth2'>
<error code='409' type='cancel'>
<conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</iq>

失败 – 没有提供需要验证的字段

1
2
3
4
5
<iq type='error' id='auth2'>
<error code='406' type='modify'>
<not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</iq>

客户端连接成功后使用iq(Info Query)查询服务器资源。

1
2
3
4
<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>

XML短语(Stanzas)

有三种短语——(消息)、(在线状态)和(信息查询),三种短语都有共同属性,如下。

  1. Message语义
    message短语可视为一种推送机制——某个实体向另一个实体推送信息。所有message短语必须携带to属性,以标明接收者。服务器收到message短语后,或路由或投递给接收者。
  2. Presence语义
    presence可被视为一个基本的广播或“发布-订阅”机制,多个实体接收他们订阅的关于某个实体的信息。
  3. IQ语义
    Info/Query,或IQ,是一种”请求-响应“机制,与HTTP类似。IQ使一个实体能够向另一个实体发起请求,请求/响应报文以id属性标示。IQ交互通常以get/result和set/result模式执行。
  4. 基于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扩展而得。