Openfire XMPP Smack RTC IM 即时通讯 聊天 MD
| Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
|---|---|---|---|---|
| MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
Openfire XMPP Smack RTC IM 即時通訊 聊天 MD
目錄
目錄簡介
Openfire 簡介
相關(guān)的幾個名詞
Smack
Spark
JID
XMPP
Openfire 安裝配置
Stanza 節(jié)
共同屬性
Presence 在線狀態(tài)
Message 傳遞消息
IQ 請求響應(yīng)
測試代碼
connect 過程
login 過程
獲取通訊錄
告訴服務(wù)器在線狀態(tài)
判斷是否在線
發(fā)送消息
測試案例代碼
項目結(jié)構(gòu)
MainActivity
常用功能封裝的工具欄
簡介
Demo地址:https://github.com/baiqiantao/OpenFireTest.git
官網(wǎng)
官方文檔
OpenFire下載
Openfire 簡介
- Openfire是一個根據(jù)開源Apache許可證授權(quán)的
實時協(xié)作服務(wù)器real time collaboration (RTC)。它使用唯一廣泛采用的即時消息開放協(xié)議XMPP(Jabber)。 Openfire非常容易設(shè)置和管理,但提供堅如磐石的安全性和性能。 - Openfire是一個
功能豐富的即時消息和跨平臺實時協(xié)作服務(wù)器,使用XMPP協(xié)議提供全面的群聊和即時消息服務(wù)。 - OpenFire是采用Java編程語言開發(fā)的實時協(xié)作服務(wù)器,可以輕易的構(gòu)建高效率的即時通信服務(wù)器,安裝和使用簡單,利用 Web 進行管理,單臺服務(wù)器可支持上萬并發(fā)用戶
相關(guān)的幾個名詞
簡單說,OpenFire 是服務(wù)器,XMPP 是協(xié)議,Smack 是類庫,Spark 是客戶端。
Smack
GitHub
Flowdalic/asmack
- Smack 是一個
基于 XMPP 協(xié)議的 Java 實現(xiàn),提供一套可擴展的API,與 OpenFire 進行通信。 - Smack 是一個開源,易于使用的
XMPP 客戶端類庫,可以實現(xiàn)即時通訊和聊天。 - Smack 是Spark項目的核心。
優(yōu)點:
- 簡單,功能強大,只需短短幾行代碼就可以向用戶發(fā)送文本消息;
- 不像其他類庫那樣強制你進行包級別的編碼,Smack提供了智能的、更高級的構(gòu)造,像Chat和Roster類,可以讓你進行更高效的編程;
- 你不需要熟悉 XMPP XML 格式,甚至不需要熟悉XML;
- 提供了簡單的機器到機器通訊,允許在每個消息中設(shè)置任意數(shù)量的屬性,包括java對象;
- Apache許可下的開源類庫,這意味著使用者可以將Smack整合進商業(yè)的或者非商業(yè)的應(yīng)用中。
缺點是其API并非為大量并發(fā)用戶設(shè)計,每個客戶要1個線程,占用資源大。
Spark
- Spark 相當與電腦版QQ,通過 smack 與 openfire 進行通信。
- Spark 是一個 XMPP 協(xié)議通信聊天的CS端的IM軟件,它可以通過 openfire 進行聊天對話。
<message from="admin@myopenfire.com" to="bqt@myopenfire.com">消息內(nèi)容</message> JID
- 基于歷史原因, 一個XMPP實體的地址稱為
Jabber Identifier或JID,它用來標示XMPP網(wǎng)絡(luò)中的各個XMPP實體。 - 鑒于協(xié)議的分布式特征, JID 應(yīng)包含
聯(lián)系到用戶所需的所有信息。 - 個人認為可以把JID理解為Email地址,就比較好理解了。
- 一個合法的JID包括節(jié)點名user、域名domain、資源名resource,其中 user 和 resource 是可有可無的,domain 是必須的。domain和user部分是不分大小寫的,但是resource區(qū)分大小寫。
- domainpart 通常指網(wǎng)絡(luò)中的
網(wǎng)關(guān)或者服務(wù)器 - localpart(user、node) 通常表示一個向服務(wù)器或網(wǎng)關(guān)請求和使用網(wǎng)絡(luò)服務(wù)的
實體(比如一個客戶端),當然它也能夠表示其他的實體(比如在多用戶聊天系統(tǒng)中的一個房間)。 - resourcepart:通常表示一個特定的會話(與某個設(shè)備),連接(與某個地址),或者一個附屬于某個節(jié)點ID實體相關(guān)實體的對象(比如多用戶聊天室中的一個參加者)。
- domainpart 通常指網(wǎng)絡(luò)中的
- JID的格式為:
jid = [ localpart "@" ] domainpart [ "/" resourcepart ],例如:- stpeter@jabber.org:表示
服務(wù)器jabber.org上的用戶stpeter。 - room@service:一個用來提供多用戶聊天服務(wù)的特定的聊天室。這里 room 是
聊天室的名字,service 是多用戶聊天服務(wù)的主機名。 - room@service/nick:加入了聊天室的用戶nick的地址。這里 nick 是用戶在聊天室的
昵稱。
- stpeter@jabber.org:表示
XMPP
Extensible Messaging and Presence Protocol,可擴展通訊和表示協(xié)議
- XMPP 是基于 XML 的協(xié)議,這表明 XMPP 是可擴展的。
- XMPP 包含了針對服務(wù)器端的軟件協(xié)議,用于即時消息以及在線現(xiàn)場探測。
- XMPP 的前身是Jabber(1998 年),一個開源形式組織產(chǎn)生的網(wǎng)絡(luò)即時通信協(xié)議。
- XMPP 是一個由IETF標準化的開放協(xié)議,由XMPP標準基金會支持和擴展。
XMPP是一種基于標準通用標記語言的子集XML的協(xié)議,它繼承了在XML環(huán)境中靈活的發(fā)展性。因此,基于XMPP的應(yīng)用具有超強的可擴展性。經(jīng)過擴展以后的XMPP可以通過發(fā)送擴展的信息來處理用戶的需求,以及在XMPP的頂端建立如內(nèi)容發(fā)布系統(tǒng)和基于地址的服務(wù)等應(yīng)用程序。而且,XMPP包含了針對服務(wù)器端的軟件協(xié)議,使之能與另一個進行通話,這使得開發(fā)者更容易建立客戶應(yīng)用程序或給一個配好系統(tǒng)添加功能。
優(yōu)點:開放、可擴展、標準、證實可用、分散、安全
缺點 :數(shù)據(jù)負載過重,沒有二進制傳輸
基本網(wǎng)絡(luò)結(jié)構(gòu)
- XMPP中定義了三個角色,
客戶端,服務(wù)器,網(wǎng)關(guān),通信能夠在這三者的任意兩個之間雙向發(fā)生。 服務(wù)器同時承擔了客戶端信息記錄,連接管理和信息的路由功能。網(wǎng)關(guān)承擔著與異構(gòu)即時通信系統(tǒng)的互聯(lián)互通,異構(gòu)系統(tǒng)可以包括SMS,MSN,ICQ等。- 基本的網(wǎng)絡(luò)形式是
單客戶端通過TCP/IP連接到單服務(wù)器,然后在之上傳輸XML。
XMPP 工作流程
- 節(jié)點連接到服務(wù)器
- 服務(wù)器利用本地目錄系統(tǒng)中的證書對其認證
- 節(jié)點指定目標地址,讓服務(wù)器告知目標狀態(tài)
- 服務(wù)器查找、連接并進行相互認證
- 節(jié)點之間進行交互
XMPP核心協(xié)議通信的基本模式就是先建立一個stream,然后協(xié)商一堆安全之類的東西,中間通信過程就是客戶端發(fā)送XML Stanza(節(jié)點),一個接一個的。服務(wù)器根據(jù)客戶端發(fā)送的信息以及程序的邏輯,發(fā)送XML Stanza給客戶端。但是這個過程并不是一問一答的,任何時候都有可能從一方發(fā)信給另外一方。通信的最后階段是</stream>關(guān)閉流,關(guān)閉TCP/IP連接。
傳輸?shù)膬?nèi)容
傳輸?shù)氖桥c即時通訊相關(guān)的指令。在以前這些命令要么用2進制的形式發(fā)送(比如QQ),要么用純文本指令加空格加參數(shù)加換行符的方式發(fā)送(比如MSN)。而XMPP傳輸?shù)募磿r通訊指令的邏輯與以往相仿,只是協(xié)議的形式變成了XML格式的純文本。這不但使得解析容易了,人也容易閱讀了,方便了開發(fā)和查錯。
XMPP 的核心部分就是一個在網(wǎng)絡(luò)上分片段發(fā)送 XML 的流協(xié)議。這個流協(xié)議是 XMPP 的即時通訊指令的傳遞基礎(chǔ),可以說 XMPP 用 TCP 傳的是 XML 流。
真實通訊案例
Xmpp協(xié)議是建立在xml的基礎(chǔ)上的,所以,看起來,xmpp協(xié)議就像一個xml。
客戶端 8049a646c63e65e8 發(fā)出去的消息:
<message from='8049a646c63e65e8@oatest.dgcb.com.cn/phone' id='5U6Mk-5' to='903e652d2334628a@oatest.dgcb.com.cn' type='chat'><body>{"fromId":"8049a646c63e65e8","fromName":"韓大東","messageType":1,"secret":false,"textContent":"你好","toName":"鄭西風","toUserID":"903e652d2334628a"}</body><request xmlns='urn:xmpp:receipts'/>
</message> 客戶端 8049a646c63e65e8 接收到的消息:
<message from="903e652d2334628a@oatest.dgcb.com.cn/phone" id="Bw4c9-4" to="8049a646c63e65e8@oatest.dgcb.com.cn" type="chat"><body>{"fromId":"903e652d2334628a","fromName":"鄭西風","messageType":1,"secret":false,"textContent":"你好"}</body><request xmlns="urn:xmpp:receipts"/><send time="2018-10-19 16:08:21:999" xmlns="icitic:msg:single"/>
</message> 其實 XMPP 是一種很類似于http協(xié)議的一種數(shù)據(jù)傳輸協(xié)議,用戶只需要明白它接收的類型,并理解它返回的類型,就可以很好的利用xmpp來進行數(shù)據(jù)通訊。
目前不少IM應(yīng)用系統(tǒng)如Google公司的Google Talk以及Jive Messenger等開源應(yīng)用,都是遵循XMPP協(xié)議集而設(shè)計實現(xiàn)的,這些應(yīng)用具有很好的互通性。
Openfire 安裝配置
安裝時除了修改一下安裝路徑,其他一路Next就Ok了。
安裝完畢后會自動啟動Openfire服務(wù)并自動打開 配置頁面 (可能需要手動刷新一下)。也可以通過雙擊 \Openfire\bin\openfire.exe 或 \Openfire\bin\openfired.exe 啟動Openfire服務(wù)后手動打開配置頁面。
然后按照指引設(shè)置 Openfire 服務(wù)器:
- 選擇語言:中文簡體
配置服務(wù)器域名【127.0.0.1】
選擇數(shù)據(jù)庫
選擇特性配置,默認即可
設(shè)置管理員帳戶【0909082401@163.com】【123456a】
提示安裝完成,點擊登錄管理員控制臺頁面【admin】【123456a】
進入后可以看到服務(wù)器名稱等信息【127.0.0.1】
創(chuàng)建用戶【admin】【baiqiantao】【bqt】【test】
安裝spark客戶端,這個spark僅僅是拿來測試用的。
至此代碼以外的環(huán)境已經(jīng)配置好了。
Stanza 節(jié)
Xml是由節(jié)點構(gòu)成的,而基于xml的xmpp協(xié)議中與通信有關(guān)三個最核心的節(jié)(Stanza)是:<message>、<presence>、<iq>,可以通過組織不同的節(jié)來達到各式各樣不同的通訊目的。接下來就對這些Stanza做一個大致的了解。
共同屬性
每個節(jié)都有其屬性,雖然不同的節(jié)其屬性各有不同,但是一些基本的屬性是這些所有的節(jié)所共同的以下這些是他們的共同屬性。
from
表示Stanza的發(fā)送方,在發(fā)送Stanza時,一般來說不推薦設(shè)定,服務(wù)器會自動設(shè)定正確的值,如果你設(shè)定了不正確的值,服務(wù)器將會拒收你的Stanza信息。to
表示Stanza的接收方。這個節(jié)點一般是自己設(shè)置的,若到達服務(wù)器的數(shù)據(jù)中沒收設(shè)置該屬性,則服務(wù)器會認為這條信息是發(fā)送給自己的。type
指定Stanza的類型。
這個節(jié)與前兩個不同,設(shè)置的值不可以統(tǒng)一而論,不同的節(jié)有不同的設(shè)定值,每種Stanza都有固定的幾種可能的設(shè)定值。
雖然不同節(jié)點的type屬性各有不同,但是都有一個error類型,表示這是一條錯誤信息,服務(wù)器接收到這種類型的信息的時候不需要作出任何的回應(yīng)。id
用于標志唯一的一條特定信息,表示一個特定的請求。
在節(jié)中,這個屬性是必須要指定的,但是在其他兩個Stanza中是一個可選屬性。
Presence 在線狀態(tài)
Presence Stanza 用來控制和表示實體的在線狀態(tài),可以展示離線、在線、離開、不能打擾等復雜狀態(tài),另外,還能被用來建立和結(jié)束在線狀態(tài)的訂閱。
除了類型信息外,Presence還包含其他一些可選的屬性:
- Status: 用于表示用戶狀態(tài)的
自定義文本,例如:外出吃飯 - Priority: 一個表示發(fā)送者資源
優(yōu)先級的非負數(shù) - Mode: 表示五種狀態(tài)之一
案例 <presence/> 設(shè)定用戶狀態(tài)為在線 <presence type="unavailable"/> 設(shè)定用戶狀態(tài)為離線
<presence><show>away</show><status>at the ball</status>
</presence> 用于顯示用戶狀態(tài)的詳細信息。上面的例子表明用戶因為at the ball在離開狀態(tài)。
<show>標簽在presence節(jié)點中最多出現(xiàn)一次,取值可以為Presence.Mode中的某一個。<status>標簽用于顯示額外信息
<presence><status>touring the countryside</status><priority>10</priority>
</presence> 在這個節(jié)中,出現(xiàn)了一個<priority>標簽,表示現(xiàn)在連接的優(yōu)先級。每個連接可以設(shè)置從-128到127的優(yōu)先級,默認是設(shè)置為0,用戶可以在這個標簽里修改相應(yīng)的優(yōu)先級。
在線狀態(tài)預定
首先我們來看一個例子:
<presencefrom="william_duan@jabber.org"to="test_account@jabber.org"type="subscribe"/> <presencefrom="test_account@jabber.org"to="william_duan@jabber.org"type="subscribed"/> 通過上述交互,william_duan 就能看到 test_account 的在線狀態(tài),并能接收到 test_account 的在線狀態(tài)通知了(例如上線提醒功能)。
Presence.Type
package org.jivesoftware.smack.packet;
public enum Presence.Type {available, //【在線,可接收消息】The user is available to receive messages (default).unavailable,//【離線,不可接收消息】The user is unavailable to receive messages.subscribe,//【申請?zhí)砑訉Ψ綖楹糜选縍equest subscription to recipient's presence.subscribed, //【同意對方添加自己為好友】Grant subscription to sender's presence.unsubscribe, //【刪除好友的申請】Request removal of subscription to sender's presence.unsubscribed,//【拒絕添加對方為好友】Grant removal of subscription to sender's presence.error,//【錯誤】The presence stanza(/packet) contains an error message.probe,;//【賬號是否存在】A presence probe as defined in section 4.3 of RFC 6121public static Type fromString(String string) {return Type.valueOf(string.toLowerCase(Locale.US));}
} Presence.Mode
package org.jivesoftware.smack.packet;
public enum Presence.Mode {chat, //【交談中】,Free to chat.available, //【在線】Available (the default).away, //【離開】,Away.xa, //【離開一段時間】,Away for an extended period of time.dnd; //【請勿打擾】,Do not disturb.public static Presence.Mode fromString(String string) {return Presence.Mode.valueOf(string.toLowerCase(Locale.US));}
} Message 傳遞消息
用于在用戶之間傳遞信息,這消息可以是單純的聊天信息,也可以某種格式化的信息。
message節(jié)點信息是傳遞之后就被忘記的。當消息被送出之后,發(fā)送者是不管這個消息是否已經(jīng)送出或者什么時候被接收到。但是通過擴展協(xié)議,可以改變這樣一種狀況。
案例
私人聊天信息:
<messagefrom="william_duan@jabber.org"to="test_account@jabber.org"type="chat"><body>Come on</body><thread>23sdfewtr234weasdf</thread>
</message> 多人聊天信息:
<messagefrom="test_account@jabber.org"to="william_duan@jabber.org"type="groupchat"><body>welcome</body>
</message> 上面的兩個例子都包含了一個<type>標簽,這個標簽表明了消息的類型,可以取 Message.Type 中的任一值。
<body>標簽里面是具體的消息內(nèi)容。
Message.Type
package org.jivesoftware.smack.packet;
public enum Message.Type {normal,//【廣播】(Default) a normal text message used in email like interface.chat,//【單聊】Typically short text message used in line-by-line chat interfaces.groupchat,//【群聊】Chat message sent to a groupchat server for group chats.headline,//【通知,不需要回應(yīng)】Text message to be displayed in scrolling marquee滾動選框 displays.error;//【錯誤】indicates a messaging error. error消息為系統(tǒng)自動發(fā)送的,往往是由于錯誤發(fā)送消息public static Type fromString(String string) {return Type.valueOf(string.toLowerCase(Locale.US));}
} IQ 請求響應(yīng)
- IQ Stanza 主要是用于
Info/Query模式的消息請求,他和Http協(xié)議比較相似。 - IQ節(jié)點
需要有回應(yīng)。 - 可以發(fā)出
get以及set請求,就如同http中的GET以及POST。 - 有
result以及error兩種回應(yīng)。
案例
william_duan 請求自己的聯(lián)系人列表:
<iqfrom="william_duan@jabber.org/study"id="roster1"type="get"><query xmlns="jabber:iq:roster"/>
</iq> 請求發(fā)生錯誤:
<iqid="roster1"to="william_duan@jabber.org/study"type="error"><query xmlns="jabber:iq:roster"/><error type="cancel"><feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/></error>
</iq> 請求成功,返回 william_duan 的聯(lián)系人列表。每一個<item>標簽代表了一個聯(lián)系人信息:
<iqid="roster1"to="william_duan@jabber.org/study"type="error"><query xmlns="jabber:iq:roster"/><itemname="one"jid="account_one@jabber.org"/><itemname="two"jid="account_two@jabber.org"/>
</iq> IQ.Type
public enum IQ.Type {get, //【請求消息】The IQ stanza requests information, inquires about what data is needed in order to complete further operations, etc.set, //【設(shè)置消息】The IQ stanza provides data that is needed for an operation to be completed, sets new values, replaces existing values, etc.result, //【成功】The IQ stanza is a response to a successful get or set request.error,; //【失敗】The IQ stanza reports an error that has occurred regarding processing or delivery of a get or set request.public static IQ.Type fromString(String string) {return IQ.Type.valueOf(string.toLowerCase(Locale.US));}
} 測試代碼
Demo地址:https://github.com/baiqiantao/OpenFireTest.git
XMPPConnection的連接需要首先通過XMPPTCPConnectionConfiguration.builder()配置你在Openfire設(shè)置的配置,然后根據(jù)配置構(gòu)造一個 XMPPTCPConnection ,以后所有操作基本都需要用到這個 XMPPTCPConnection 。
connection = new XMPPTCPConnection(configuration); 通過了上面的配置后,咱們可以登錄Openfire系統(tǒng)了,相當簡單:
XMPPUtils.getConnection().login(username, password); 下面我們重點分析下登錄過程的報文內(nèi)容以及一些最常用的API。
connect 過程
在建立了Socket后,client會向服務(wù)器發(fā)出一條xml:
<stream:stream xmlns:stream='http://etherx.jabber.org/streams'from='8049a646c63e65e8@oatest.dgcb.com.cn'to='oatest.dgcb.com.cn'version='1.0'xmlns='jabber:client'xml:lang='en'> 服務(wù)器解析到上面的指令后,會返回用于告訴client可選的SASL方式
<?xml version='1.0' encoding='UTF-8'?>
<stream:stream xmlns:stream="http://etherx.jabber.org/streams"from="oatest.dgcb.com.cn"id="36ebm4blnf"version="1.0"xmlns="jabber:client"xml:lang="en"><stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"></starttls><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism><mechanism>SCRAM-SHA-1</mechanism><mechanism>CRAM-MD5</mechanism><mechanism>DIGEST-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><ver xmlns="urn:xmpp:features:rosterver"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features> 至此,connect 算是完成了,此時會回調(diào) ConnectionListener 的 connected 方法。
login 過程
XMPPUtils.getConnection().login(username, password); 1、客戶端選擇PLAIN認證方式
<auth mechanism='PLAIN'xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>ADgwNDlhNjQ2YzYzZTY1ZTgAQkRFNEM3QzBGMzdENEZGRTlENDlGNDcwMTdFNUJCRjc=
</auth> 服務(wù)器通過計算加密后的密碼后,服務(wù)器將返回
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/> 2、當客戶端收到以上命令后,將首次發(fā)起連接的id發(fā)送到服務(wù)器
<stream:stream xmlns:stream='http://etherx.jabber.org/streams'from='8049a646c63e65e8@oatest.dgcb.com.cn'id='36ebm4blnf'to='oatest.dgcb.com.cn'version='1.0'xmlns='jabber:client'xml:lang='en'> 這時服務(wù)器會返回如下內(nèi)容說明此時已經(jīng)成功綁定了當前的Socket
<?xml version='1.0' encoding='UTF-8'?>
<stream:stream xmlns:stream="http://etherx.jabber.org/streams"from="oatest.dgcb.com.cn"id="36ebm4blnf"version="1.0"xmlns="jabber:client"xml:lang="en"><stream:features><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><ver xmlns="urn:xmpp:features:rosterver"/><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"><optional/></session><sm xmlns='urn:xmpp:sm:2'/><sm xmlns='urn:xmpp:sm:3'/></stream:features> 3、壓縮
3.1、客戶端在接收到如上的內(nèi)容后會告訴服務(wù)器開啟壓縮
項目中沒有使用壓縮,所以下面的過程不存在,以下為參考別人的案例
<compress xmlns='http://jabber.org/protocol/compress'><method>zlib</method></compress> 服務(wù)器返回
<compressed xmlns='http://jabber.org/protocol/compress'/> 3.2、客戶端收到服務(wù)器的響應(yīng)命令后,重新建立一個Socket,發(fā)送指令
<stream:stream xmlns='jabber:client' to='server domain' xmlns:stream='http://etherx.jabber.org/streams' version='1.0' from='username@server domain' id='c997c3a8' xml:lang='en'> 服務(wù)器將返回,不知道你有沒有發(fā)現(xiàn),這里的id還是那個id
<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="im" id="c997c3a8" xml:lang="en" version="1.0"><stream:features><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>JIVE-SHAREDSECRET</mechanism></mechanisms><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features> 4、客戶端發(fā)送綁定Socket的指令:
<iqid='SG6jR-3'type='set'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>phone</resource></bind>
</iq> 服務(wù)器返回綁定了具有指定 JID 的客戶端
<iqid="SG6jR-3"to="oatest.dgcb.com.cn/36ebm4blnf"type="result"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>8049a646c63e65e8@oatest.dgcb.com.cn/phone</jid></bind>
</iq> 5、開啟一個session
項目中沒有開啟一個session的邏輯,所以下面的過程不存在,以下為參考別人的案例
<iq id='b86j8-6' type='set'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq> 這時服務(wù)器返回
<iq type="result" id="b86j8-6" to="c997c3a8@im/c997c3a8"/> 6、認證
因為項目中沒有開啟認證,所以這里沒有報文通訊,只有如下日志:
至此,客戶端的登錄過程算是完成了。
注意,connect 和 login 都是同步操作,所以在
login(username, password)方法調(diào)用以后,如果沒有報異常,就是登陸成功了。
獲取通訊錄
登陸以后接著會自動發(fā)送一條獲取通訊錄的指令,并會將通訊錄緩存起來,所以以后再獲取通訊錄時,并不需要訪問網(wǎng)絡(luò)。
<iqid='gZYnq-5'type='get'><query xmlns='jabber:iq:roster'></query>
</iq> 服務(wù)器將返回
<iqid="SG6jR-5"to="8049a646c63e65e8@oatest.dgcb.com.cn/phone"type="result"><query ver="-491295515"xmlns="jabber:iq:roster"><itemname="李**"jid="0347a8a25e9074b0@oatest.dgcb.com.cn"subscription="to"/><itemjid="903e652d2334628a@oatest.dgcb.com.cn"subscription="from"/><itemask="subscribe"jid="28af56d053cbbf3e@oatest.dgcb.com.cn"subscription="none"/></query>
</iq> 此過程完成以后會回調(diào) RosterListener 的 entriesAdded 方法。
告訴服務(wù)器在線狀態(tài)
雖然已經(jīng)登錄了,但是還需要告訴服務(wù)器自己的狀態(tài),否則服務(wù)器不會認為你是在線狀態(tài),這時你可能就收不到其他好友發(fā)來的消息(我們我們項目中有集成離線推送功能,如果沒有告訴服務(wù)器你在笑,服務(wù)器會走離線消息推送的邏輯。)
XMPPUtils.getConnection().sendStanza(presence); 客戶端發(fā)送 presence 消息告訴服務(wù)器自己在線:
<presencefrom='8049a646c63e65e8@oatest.dgcb.com.cn/phone'id='91kqC-27'><status>IchatMM</status><priority>0</priority><c hash='sha-1'node='http://www.igniterealtime.org/projects/smack'ver='NfJ3flI83zSdUDzCEICtbypursw='xmlns='http://jabber.org/protocol/caps'/>
</presence> 服務(wù)器響應(yīng):
<presencefrom="c53706e24ce32f72@oatest.dgcb.com.cn/pc"to="8049a646c63e65e8@oatest.dgcb.com.cn/phone"><priority>0</priority><c hash="sha-1"node="http://camaya.net/gloox"ver="9ZtEa+bYQasYo2pVBGT9ShIT+Yc="xmlns="http://jabber.org/protocol/caps"></c>
</presence> 收到響應(yīng)后,會回調(diào) RosterListener 的 presenceChanged 方法,此后,就可以愉快的玩耍了。
判斷是否在線
服務(wù)器會定時(默認3分鐘)主動發(fā)送一條 ping 消息,以確定客戶端是否在線:
PingManager.getInstanceFor(connection).setPingInterval(60);//ping消息間隔 <iqfrom="oatest.dgcb.com.cn"id="553-595"to="8049a646c63e65e8@oatest.dgcb.com.cn/phone"type="get"><ping xmlns="urn:xmpp:ping"/>
</iq> 客戶端響應(yīng):
<iqid='553-595'to='oatest.dgcb.com.cn'type='result'></iq> 到此,整個登錄流程已經(jīng)成功了,接下來可以做一些用戶信息的獲取等操作。
發(fā)送消息
//發(fā)送方式一,簡單的發(fā)送文本消息
ChatManager.getInstanceFor(XMPPUtils.getConnection()).createChat(to).sendMessage(text);
//發(fā)送方式二,發(fā)送一個Message對象,可包含一些信息,一般使用這種方式
XMPPUtils.getConnection().sendStanza(msg); 除去消息內(nèi)容后的日志:
14:51:02.365 客戶端A I/bqt: 【chatCreated】
14:51:02.366 客戶端A D/SMACK: SENT (0)
14:51:02.399 客戶端A D/SMACK: RECV (0)
14:51:02.400 客戶端A I/bqt: 【processPacket】
14:51:02.402 客戶端A I/bqt: 【processMessage】
14:51:02.404 客戶端A D/SMACK: RECV (0)
14:51:02.404 客戶端B D/SMACK: RECV (0)
14:51:02.407 客戶端A I/bqt: 【processPacket】
14:51:02.407 客戶端A I/bqt: 【processMessage】
14:51:02.409 客戶端B I/bqt: 【processPacket】
14:51:02.410 客戶端B I/bqt: 【chatCreated】
14:51:02.411 客戶端B I/bqt: 【processMessage】
14:51:02.412 客戶端B I/bqt: 消息類型:chat 1、客戶端A發(fā)送消息:
<messagefrom='8049a646c63e65e8@oatest.dgcb.com.cn/phone'id='nCRIE-44'to='903e652d2334628a@oatest.dgcb.com.cn/phone'type='chat'><body>你好,我是包青天</body><thread>6828a752-cfae-4149-9d4d-c8fb83a17175</thread>
</message> 客戶端收到服務(wù)器的回執(zhí)(msgId相同):
<messagefrom="903e652d2334628a@oatest.dgcb.com.cn/phone"to="8049a646c63e65e8@oatest.dgcb.com.cn/phone"><received msgId="nCRIE-44"status="1"time="2018-10-20 14:50:16:566"xmlns="urn:xmpp:receipts"/>
</message> 2、然后,客戶端B會收到客戶端A發(fā)送的消息(id相同):
<messagefrom="8049a646c63e65e8@oatest.dgcb.com.cn/phone"id="nCRIE-44"to="903e652d2334628a@oatest.dgcb.com.cn/phone"type="chat"><body>你好,我是包青天</body><thread>6828a752-cfae-4149-9d4d-c8fb83a17175</thread><send time="2018-10-20 14:50:16:572"xmlns="icitic:msg:single"/>
</message> 客戶端A也會收到的回執(zhí)消息(id后面拼接了mutisingle):
<messagefrom="8049a646c63e65e8@oatest.dgcb.com.cn"id="nCRIE-44mutisingle"to="8049a646c63e65e8@oatest.dgcb.com.cn"type="chat"><subject>903e652d2334628a@oatest.dgcb.com.cn</subject><body>你好,我是包青天</body><send time="2018-10-20 14:50:16:571"xmlns="icitic:msg:single"/>
</message> 測試案例代碼
項目結(jié)構(gòu)
implementation 'org.igniterealtime.smack:smack-android:4.1.4'
implementation 'org.igniterealtime.smack:smack-tcp:4.1.4'
implementation 'org.igniterealtime.smack:smack-im:4.1.4'
implementation 'org.igniterealtime.smack:smack-extensions:4.1.4' MainActivity
public class MainActivity extends ListActivity {private boolean switchUser = false;private EditText etAccount, etPassword, etChat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);String[] array = {"初始化","登錄","發(fā)送在線狀態(tài)消息","發(fā)消息","獲取好友信息","創(chuàng)建聊天室","加入聊天室","邀請好友進入聊天室","注銷登錄","",};setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, Arrays.asList(array)));etAccount = new EditText(this);etPassword = new EditText(this);etChat = new EditText(this);etAccount.setText(switchUser ? "8049a646c63e65e8" : "903e652d2334628a");etPassword.setText(switchUser ? "1E6210BB50614D978F4758B2DC9D76C9" : "40C61DE3492C41B1846281833434D997");etChat.setText(switchUser ? "903e652d2334628a@oatest.dgcb.com.cn/phone" : "8049a646c63e65e8@oatest.dgcb.com.cn/phone");getListView().addFooterView(etAccount);getListView().addFooterView(etPassword);getListView().addFooterView(etChat);//要聊天的用戶的ID}@Overrideprotected void onListItemClick(ListView l, View v, int position, long id) {String account = etAccount.getText().toString();String password = etPassword.getText().toString();String jid = etChat.getText().toString();new Thread(() -> testApi(position, account, password, jid)).start();}private void testApi(int position, String account, String password, String jid) {switch (position) {case 0:XMPPUtils.init(account, password);//初始化break;case 1:XMPPUtils.login(account, password);//登錄break;case 2:XMPPUtils.setOnLineStatus();//在線break;case 3:XMPPUtils.sendMessage(account + "@oatest.dgcb.com.cn/phone", jid, "你好,我是包青天");//發(fā)消息break;case 4:XMPPUtils.getMyFriends();//獲取好友信息break;case 5:XMPPUtils.createMucRoom(jid, "包青天");//創(chuàng)建聊天室break;case 6:XMPPUtils.joinChatRoom(jid, account);//加入聊天室break;case 7:XMPPUtils.inviteToTalkRoom(jid, account, password, "快來參加第二十八屆英雄大會");//邀請好友進入聊天室break;case 8:XMPPUtils.logout();//注銷登錄break;default:break;}}
} 常用功能封裝的工具欄
public class XMPPUtils {private static XMPPTCPConnection connection;/*** 初始化*/public static synchronized void init(CharSequence username, String password) {if (connection == null) {//初始化XMPPTCPConnection相關(guān)配置XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder().setUsernameAndPassword(username, password)//設(shè)置登錄openfire的用戶名和密碼.setServiceName("oatest.dgcb.com.cn")//設(shè)置服務(wù)器名稱.setHost("oatest.dgcb.com.cn")//設(shè)置主機地址.setPort(25222)//設(shè)置端口號.setResource("phone") //默認為Smack.setDebuggerEnabled(true)//是否查看debug日志//********************************************** 以下為進階配置 *************************************************.setConnectTimeout(10 * 1000)//設(shè)置連接超時的最大時間.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)//設(shè)置安全模式,關(guān)閉安全模式.setCompressionEnabled(false) //開啟通訊壓縮,開啟后傳輸?shù)牧髁繉⒐?jié)省90%.setSendPresence(false).setCustomSSLContext(getSSLContext()) //自定義的TLS登錄.setHostnameVerifier((hostname, session) -> true).build();connection = new XMPPTCPConnection(configuration);connection.setFromMode(XMPPConnection.FromMode.USER);connection.addConnectionListener(new MyConnectionListener()); //監(jiān)聽connect狀態(tài)connection.addAsyncStanzaListener(new MyStanzaListener(), StanzaTypeFilter.MESSAGE);// 注冊包的監(jiān)聽器PingManager.getInstanceFor(connection).setPingInterval(60);//ping消息間隔//SASL認證SASLAuthentication.blacklistSASLMechanism("SCRAM-SHA-1");SASLAuthentication.blacklistSASLMechanism(SASLPlainMechanism.DIGESTMD5);SASLAuthentication.registerSASLMechanism(new SASLPlainMechanism());Roster.getInstanceFor(connection).addRosterListener(new MyRosterListener());ChatManager.getInstanceFor(connection).addChatListener(new MyChatManagerListener()); //監(jiān)聽與聊天相關(guān)的事件MultiUserChatManager.getInstanceFor(connection).addInvitationListener(new MyInvitationListener()); //被邀請監(jiān)聽}}private static SSLContext getSSLContext() {SSLContext context = null;try {context = SSLContext.getInstance("TLS");context.init(null, new TrustManager[]{new TLSUtils.AcceptAllTrustManager()}, new SecureRandom());} catch (NoSuchAlgorithmException e) {e.printStackTrace();} catch (KeyManagementException e) {e.printStackTrace();}return context;}public static XMPPTCPConnection getConnection() {return connection;}/*** 登錄*/public static void login(CharSequence username, String password) {try {if (!XMPPUtils.getConnection().isConnected()) {XMPPUtils.getConnection().connect();}if (XMPPUtils.getConnection().isConnected()) {Log.i("bqt", "開始登錄");XMPPUtils.getConnection().login(username, password);Log.i("bqt", "登錄成功");} else {Log.i("bqt", "登錄失敗");}} catch (SmackException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} catch (XMPPException e) {e.printStackTrace();}}/*** 告訴服務(wù)器登錄狀態(tài)*/public static void setOnLineStatus() {if (XMPPUtils.getConnection().isAuthenticated()) {try {Presence presence = new Presence(Presence.Type.available);presence.setStatus("IchatMM"); //顯示額外信息,內(nèi)容根據(jù)需求可隨意定制presence.setPriority(0); //連接的優(yōu)先級XMPPUtils.getConnection().sendStanza(presence);} catch (SmackException.NotConnectedException e) {e.printStackTrace();}}}/*** 注銷登錄*/public static void logout() {if (!XMPPUtils.getConnection().isConnected()) {XMPPUtils.getConnection().disconnect();}}/*** 發(fā)消息*/public static void sendMessage(String from, String to, String text) {try {ChatManager.getInstanceFor(XMPPUtils.getConnection()).createChat(to).sendMessage(text);//直接發(fā)送一條文本/*Message msg = new Message(to, Message.Type.chat);msg.setStanzaId(System.currentTimeMillis() + "");msg.setFrom(from);msg.setBody(text);XMPPUtils.getConnection().sendStanza(msg);//發(fā)送一個Message對象,可包含一些信息,一般使用后者*/} catch (SmackException.NotConnectedException e) {e.printStackTrace();}}/*** 獲取好友信息*/public static void getMyFriends() {//并不需要訪問網(wǎng)絡(luò),因為在登錄后已經(jīng)拿到用戶的通訊錄了,這里是直接從緩存中讀取的Set<RosterEntry> set = Roster.getInstanceFor(XMPPUtils.getConnection()).getEntries();for (RosterEntry entry : set) {Log.i("bqt", "JID:" + entry.getUser() + ",Name:" + entry.getName());}}/*** 創(chuàng)建聊天室*/public static void createMucRoom(String jid, String nickname) {try {MultiUserChat muc = MultiUserChatManager.getInstanceFor(XMPPUtils.getConnection()).getMultiUserChat(jid);muc.create(nickname);//昵稱Form form = muc.getConfigurationForm();Form submitForm = form.createAnswerForm();for (FormField field : form.getFields()) {if (!FormField.Type.hidden.equals(field.getType()) && field.getVariable() != null) {submitForm.setDefaultAnswer(field.getVariable());}}List<String> list = new ArrayList<>();list.add("20");List<String> owners = new ArrayList<>();owners.add("guochen@192.168.0.245");submitForm.setAnswer("muc#roomconfig_roomowners", owners);submitForm.setAnswer("muc#roomconfig_maxusers", list);submitForm.setAnswer("muc#roomconfig_roomname", "room01");submitForm.setAnswer("muc#roomconfig_persistentroom", true);submitForm.setAnswer("muc#roomconfig_membersonly", false);submitForm.setAnswer("muc#roomconfig_allowinvites", true);submitForm.setAnswer("muc#roomconfig_enablelogging", true);submitForm.setAnswer("x-muc#roomconfig_reservednick", true);submitForm.setAnswer("x-muc#roomconfig_canchangenick", false);submitForm.setAnswer("x-muc#roomconfig_registration", false);muc.sendConfigurationForm(submitForm);} catch (XMPPException.XMPPErrorException e) {e.printStackTrace();} catch (SmackException e) {e.printStackTrace();}}/*** 加入聊天室*/public static void joinChatRoom(String jid, String nickname) {try {MultiUserChat muc = MultiUserChatManager.getInstanceFor(XMPPUtils.getConnection()).getMultiUserChat(jid);muc.join(nickname);} catch (SmackException.NoResponseException e) {e.printStackTrace();} catch (XMPPException.XMPPErrorException e) {e.printStackTrace();} catch (SmackException.NotConnectedException e) {e.printStackTrace();}}/*** 邀請好友進入聊天室*/public static void inviteToTalkRoom(String jid, String nickname, String user, String reason) {try {MultiUserChat muc = MultiUserChatManager.getInstanceFor(XMPPUtils.getConnection()).getMultiUserChat(jid);muc.addInvitationRejectionListener((invitee, rejectReason) -> Log.i("bqt", "拒絕了," + invitee + "," + rejectReason));muc.join(nickname);muc.invite(user, reason);} catch (SmackException.NotConnectedException e) {e.printStackTrace();} catch (SmackException.NoResponseException e) {e.printStackTrace();} catch (XMPPException.XMPPErrorException e) {e.printStackTrace();}}
} 2018-10-19
轉(zhuǎn)載于:https://www.cnblogs.com/baiqiantao/p/9819242.html
總結(jié)
以上是生活随笔為你收集整理的Openfire XMPP Smack RTC IM 即时通讯 聊天 MD的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 洱海可以骑电动车环湖吗
- 下一篇: SAP Cloud for Custom