一、Xmpp資源綁定
??????XMPP協議設計中引入了一個抽象的資源綁定過程,何為資源,如何綁定?
??????首先這得從JID的格式設計說起,JID是XMPP前身Jabber協議ID的簡寫,用于唯一標識一個客戶身份。一個合法的 JID 包括一組排列好的元素,包括域名(domain identifier),節點名(node identifier),和資源名(resource identifier),如下:
jid = [ node "@" ] domain [ "/" resource ] ,所有 JID 都是基于上述的結構,類似 { user@host/resource }這種結構。
- node:是對用戶的抽象,既可以代表一個真實的用戶,也能表示一個虛擬用戶如一個聊天室等。
- domain:表達了客戶所連接的服務器,在實踐中通常表示一個特定的集群,由同一domain來表示。
- resource:它通常表示一個特定的會話,連接。對于服務器和和其他客戶端來說,資源名是不透明的。
??????資源名的獲得需要經歷一個資源綁定的過程,這個過程按照XMPP協議約定是在SASL握手完成后,由客戶端重新發起初始化流請求后。
服務器向客戶端聲明資源綁定特性,過程如下:
<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'/>
</stream:features>
客戶端發起資源綁定請求,并指定一個綁定的資源名
<iq type='set' id='bind_2'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<resource>pc-win-someone</resource>
</bind>
</iq>
服務端響應資源綁定請求,并返回綁定后的Full JID名
<iq type='result' id='bind_2'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<jid>somenode@example.com/pc-win-someone-server-gen-random-string</jid>
</bind>
</iq>
??????以上過程即完成了資源綁定,那么資源綁定有什么作用呢,注意查看協議xml中客戶端端請求綁定資源名為pc-win-someone,通常實現中可考慮用客戶端的平臺相關標識,例如 pc-win標示pc下的windows平臺等,標識連接客戶端的平臺和自身名稱,但XMPP協議約定resource由服務端按照每客戶端生成隨機值,用于唯一標識一個客戶端一次連接會話。因此服務端的實現在客戶端請求資源名后添加了隨機生成的唯一后綴,用于區分不同的客戶端連接。
那么如此設計的目的何在?
??????主要考慮方便同賬號用戶的多點登陸(手機、pad、pc端等多點同時在線),通過resource區分同一用戶的不同接入點,由node+domain+resource組成唯一的用戶在線標識。通過用戶ID形成一對多的用戶接入映射,方便獲得同一賬號的多個接入信息,可靈活的設計多點登陸時用戶的自選策略(是否踢下其他登陸、或選擇最近登陸接收消息等)。
二、Xmpp安全機制
??????XMPP(Extensible Messaging and Presence Protocol)是一個應用于實時通信的開放協議,定義了有關即時消息通信的各方面內容,本文主要是關于XMPP安全機制的介紹以及設計實現思考。
??????XMPP包含一個保證流安全的方法來防止篡改和偷聽,包括兩個層次的安全機制,分別是TLS(Tansport Layer Security)和 SASL(Simple Authentication Security Layer)。
TLS主要用于保證傳輸通道安全,SASL用于用戶鑒權認證,協商流程如下:
- 客戶端發起,流初始化(建立TCP連接,發送如下格式數據)
<stream:stream
from='juliet@im.example.com'
to='im.example.com'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
- 服務端應答流初始化
<stream:stream
from='im.example.com'
id='t7AMCin9zjMNwQKDnplntZPIDEI='
to='juliet@im.example.com'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
- 服務端發送TLS流特征說明
<stream:features>
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
<required/>
</starttls>
</stream:features>
- 客戶端發起TLS握手
<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
- 服務端應答
-- 握手成功,繼續
<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
-- 握手失敗,結束流,關閉TCP連接
<failure xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
</stream:stream>
- 握手成功后,客戶端重新初始化加密流,并采用安全加密傳輸(通常由SSL實現),注意:這一步之后的交互數據全部經過加密傳輸TLS協商完成。
<stream:stream
from='juliet@im.example.com'
to='im.example.com'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
- 服務端應答加密流初始化
<stream:stream
from='im.example.com'
id='vgKi/bkYME8OAj4rlXMkpucAqe4='
to='juliet@im.example.com'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
- 服務端發送SASL特征說明,mechanism指出了服務端支持的認證機制,有關SASL認證的機制可參考RFC4422[html] [view plain]
<stream:features>
<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
<mechanism>EXTERNAL</mechanism>
<mechanism>SCRAM-SHA-1-PLUS</mechanism>
<mechanism>SCRAM-SHA-1</mechanism>
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>
- 客戶端選擇認證機制
<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AGp1bGlldAByMG0zMG15cjBtMzA=</auth>
??????以下認證過程根據選擇的認證機制有所不同,實踐中真正的實現一般就具體采用一種認證,依賴具體的用戶權限系統進行設計。
這里說說其中一種常見Challenge-Response認證機制
??????客戶端在發送<auth>請求認證時,如上xml片段所示,<auth>元素中包含了一段BASE64編碼的字符串,可以是用戶ID(UID)向服務端表明身份id。
??????服務端接收到認證請求后,發回挑戰碼,挑戰碼由服務器每次隨機生成(挑戰碼也經過BASE64編碼)
<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
cmVhbG09InNvbWVyZWFsbSIsbm9uY2U9Ik9BNk1HOXRFUUdtMmhoIixxb3A9ImF1dGgi
LGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNzCg==
</challenge>
??????客戶端接收到挑戰碼后,根據用戶輸入的密碼(原文)按注冊用戶時保存密碼采用Hash算法進行同樣的計算,得到與服務后端數據庫存儲的密碼Hash同樣的值,再以此為種子對挑戰碼進行特定算法計算。
具體算法可以用對稱加密、二次加鹽hash等,經過計算后的挑戰碼作為響應發回給服務端,如下:
<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
dXNlcm5hbWU9InNvbWVub2RlIixyZWFsbT0ic29tZXJlYWxtIixub25jZT0i
T0E2TUc5dEVRR20yaGgiLGNub25jZT0iT0E2TUhYaDZWcVRyUmsiLG5jPTAw
MDAwMDAxLHFvcD1hdXRoLGRpZ2VzdC11cmk9InhtcHAvZXhhbXBsZS5jb20i
LHJlc3BvbnNlPWQzODhkYWQ5MGQ0YmJkNzYwYTE1MjMyMWYyMTQzYWY3LGNo
YXJzZXQ9dXRmLTgK
</response>
服務端根據之前提供的UID獲取用戶保存的密碼hash值,對響應碼進行相同的算法計算后與客戶端傳遞的挑戰碼響應進行碰撞認證
-- 成功,返回
<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
-- 失敗,返回
<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
</stream:stream>
結束流,并關閉TCP連接
三、Xmpp消息格式
??????交換消息是XMPP的一個基本用途并且隨之而來的是一個用戶生成一個發給另一個實體的消息節。
XMPP定義的消息節語法完整格式如下:
<message
from='juliet@example.com/balcony'
to='romeo@example.net'
type='chat'
xml:lang='en'>
<subject>I implore you!</subject>
<subject xml:lang='cs'>
úpěnlivě prosím!
</subject>
<body>Wherefore art thou, Romeo?</body>
<body xml:lang='cs'>
Pro?e? jsi ty, Romeo?
</body>
<thread parent='e0ffe42b28561960c6b12b944a092794b9683a38'>
0e3141cd80894871a68e6fe6b1ec56fa
</thread>
</message>
- from屬性:
??????設置消息發送方自身的Full JID(node@domain/resource) - to屬性:
??????設置消息接收方的Bare JID(node@domain),通常第一次發送方無法確知接收方的Full JID,通過服務器中轉路由時由服務器根據Base JID映射接收方的Full JID。 但如果這個消息是在回復之前接收到的消息,則to屬性應該包含對方完整的Full JID。
??????如此設計的好處在于:當to屬性設定為Full JID時可以幫助服務器省卻了接收者資源定位(接入定位),在一個IM服務集群環境中這種定位通常意味著一次分布式緩存讀取操作。 - type屬性:XMPP約定了type的枚舉值,包括:
??????chat: 表明在一個點對點會話環境中的聊天消息。
??????groupchat:表明在一個多人會話環境中的聊天消息。
??????headline: 通常一些系統通知、警告、實時數據更新采用此類型,這類消息不期待客戶端回復或響應,具有很高的實時性,不需要離線存儲。
??????normal: 默認的消息類型(缺乏type屬性時),通常表達一種要求接收方必須確認的消息,一般用于系統提示強制用戶確認或取消等。
??????error: 表示一個錯誤消息,可能由服務端發送給客戶端,也可能是另一個客戶接收端回應給客戶發送端,此類消息也不需要離線存儲。 - <subject>子元素:
??????表明一個消息主題,通常客戶端實現顯示在聊天窗口標題欄處 - <body>子元素:
消息內容部分 - <subject>和<subject>都允許包含多個元素標簽,不同的標簽根據xml:lang表達了不同的語言(XMPP可是一個國際化協議)
- <thread>子元素:
??????用于跟蹤一個會話, 該元素的作用主要在于方便客戶端實現消息展示(例如:消息歷史查詢時按每次會話折疊顯示消息),每次會話產生一個唯一的thread id,xmpp推薦采用uuid算法,具體用法可參考XEP-0201擴展協議和RFC6121。
還有一種情況是離線消息,它與正常消息的格式和處理機制又有所不同,格式如下所示:
<message from='romeo@montague.net/orchard' to='juliet@capulet.com'>
<body>
O blessed, blessed night! I am afeard.
Being in night, all this is but a dream,
Too flattering-sweet to be substantial.
</body>
<delay xmlns='urn:xmpp:delay'
from='capulet.com'
stamp='2002-09-10T23:08:25Z'>Offline Storage</delay>
</message>
??????離線消息中包含了一個<delay>的子元素,<delay>子元素的from記錄了延遲消息的最后來源方,如上例中from為capulet.com指接收離線消息人連接的服務器,離線消息最終由該服務器發出stamp屬性記錄了離線消息的存儲時間,客戶端實現應顯示該時間而非接收到的時間。
四、XMPP多用戶文本聊天協議(MUC:Multi User Chat)
????? XMPP在其XEP-0045擴展中定義了一個用于多用戶文本會議(群聊)的協議,類似于聊天室、QQ群等。由于它作為一個標準協議在定義模型上力求完備,涵蓋了現實中的絕大部分IM產品模型,而現實中的IM產品基本都只實現了XMPP定義的模型中的一個子集。
XMPP定義的一些基本概念:
- 房間:房間的JID標識 room@service (例如, jdev@conference.jabber.org), 這里 "room" 是房間的名稱而 "service" 是多用戶聊天服務運行所在的主機名
- 房客:房客的JID標識<room@service/nick>,nick是房客在房間的昵稱
- 崗位:表達了用戶和房間的長期關系。XMPP定義的崗位有:所有者(owner)、管理者(admin)、成員(member)、排斥者(outcast)
- 角色:表達了用戶和房間的臨時聯系,它只存在與一次訪問期間。XMPP定義的角色有:主持人(moderator)、與會者(paticipant)、游客(visitor)
有關崗位、角色及其權限詳細描述,參考協議規范描述(角色、崗位和權限)
XMPP MUC協議擴展定義了一個廣泛的用例集合,下面提取一些典型的核心場景來簡要分析說明并輔助實現。
- MUC服務發現
主要用于客戶端向服務器咨詢是否支持MUC,協議交互細節詳見:MUC Discovering - 新建房間
從房間創建的視角來看,本質上有2種類型的房間:
instant room 臨時房間(類似于臨時會話),適用于那些臨時選取多個用戶進行會話的場景
reserverd room 永久房間(類似于固定群) - 銷毀房間
銷毀房間通常僅限于房間的所有者,臨時房間通常是在房間所有用戶都離開后自動銷毀 - 加入房間
加入房間可以有2種方式,申請和邀請 - 發言
在房間內發言方式從使用場景的角度看通常有3種:
- 向房間內所有人發言,發言者發送一個消息類型為groupchat的消息,由房間服務轉發給所有與會者。
- 向部分人發言,這個場景發言者實際創建了一個臨時房間,在該臨時房間內進行群發。
- 向某一個人發送似有消息,這個場景退化為了一對一的單獨聊天。
- 退出房間
主動退出、管理員(主持人)踢出房間
關于XMPP多用戶文本聊天協議的完整用例集合,請參考協議規范。