Openfire XMPP Smack RTC IM 即时通讯 聊天 MD
| Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
|---|---|---|---|---|
| MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
Openfire XMPP Smack RTC IM 即時通訊 聊天 MD
目錄
目錄簡介
Openfire 簡介
相關的幾個名詞
Smack
Spark
JID
XMPP
Openfire 安裝配置
Stanza 節
共同屬性
Presence 在線狀態
Message 傳遞消息
IQ 請求響應
測試代碼
connect 過程
login 過程
獲取通訊錄
告訴服務器在線狀態
判斷是否在線
發送消息
測試案例代碼
項目結構
MainActivity
常用功能封裝的工具欄
簡介
Demo地址:https://github.com/baiqiantao/OpenFireTest.git
官網
官方文檔
OpenFire下載
Openfire 簡介
- Openfire是一個根據開源Apache許可證授權的
實時協作服務器real time collaboration (RTC)。它使用唯一廣泛采用的即時消息開放協議XMPP(Jabber)。 Openfire非常容易設置和管理,但提供堅如磐石的安全性和性能。 - Openfire是一個
功能豐富的即時消息和跨平臺實時協作服務器,使用XMPP協議提供全面的群聊和即時消息服務。 - OpenFire是采用Java編程語言開發的實時協作服務器,可以輕易的構建高效率的即時通信服務器,安裝和使用簡單,利用 Web 進行管理,單臺服務器可支持上萬并發用戶
相關的幾個名詞
簡單說,OpenFire 是服務器,XMPP 是協議,Smack 是類庫,Spark 是客戶端。
Smack
GitHub
Flowdalic/asmack
- Smack 是一個
基于 XMPP 協議的 Java 實現,提供一套可擴展的API,與 OpenFire 進行通信。 - Smack 是一個開源,易于使用的
XMPP 客戶端類庫,可以實現即時通訊和聊天。 - Smack 是Spark項目的核心。
優點:
- 簡單,功能強大,只需短短幾行代碼就可以向用戶發送文本消息;
- 不像其他類庫那樣強制你進行包級別的編碼,Smack提供了智能的、更高級的構造,像Chat和Roster類,可以讓你進行更高效的編程;
- 你不需要熟悉 XMPP XML 格式,甚至不需要熟悉XML;
- 提供了簡單的機器到機器通訊,允許在每個消息中設置任意數量的屬性,包括java對象;
- Apache許可下的開源類庫,這意味著使用者可以將Smack整合進商業的或者非商業的應用中。
缺點是其API并非為大量并發用戶設計,每個客戶要1個線程,占用資源大。
Spark
- Spark 相當與電腦版QQ,通過 smack 與 openfire 進行通信。
- Spark 是一個 XMPP 協議通信聊天的CS端的IM軟件,它可以通過 openfire 進行聊天對話。
<message from="admin@myopenfire.com" to="bqt@myopenfire.com">消息內容</message> JID
- 基于歷史原因, 一個XMPP實體的地址稱為
Jabber Identifier或JID,它用來標示XMPP網絡中的各個XMPP實體。 - 鑒于協議的分布式特征, JID 應包含
聯系到用戶所需的所有信息。 - 個人認為可以把JID理解為Email地址,就比較好理解了。
- 一個合法的JID包括節點名user、域名domain、資源名resource,其中 user 和 resource 是可有可無的,domain 是必須的。domain和user部分是不分大小寫的,但是resource區分大小寫。
- domainpart 通常指網絡中的
網關或者服務器 - localpart(user、node) 通常表示一個向服務器或網關請求和使用網絡服務的
實體(比如一個客戶端),當然它也能夠表示其他的實體(比如在多用戶聊天系統中的一個房間)。 - resourcepart:通常表示一個特定的會話(與某個設備),連接(與某個地址),或者一個附屬于某個節點ID實體相關實體的對象(比如多用戶聊天室中的一個參加者)。
- domainpart 通常指網絡中的
- JID的格式為:
jid = [ localpart "@" ] domainpart [ "/" resourcepart ],例如:- stpeter@jabber.org:表示
服務器jabber.org上的用戶stpeter。 - room@service:一個用來提供多用戶聊天服務的特定的聊天室。這里 room 是
聊天室的名字,service 是多用戶聊天服務的主機名。 - room@service/nick:加入了聊天室的用戶nick的地址。這里 nick 是用戶在聊天室的
昵稱。
- stpeter@jabber.org:表示
XMPP
Extensible Messaging and Presence Protocol,可擴展通訊和表示協議
- XMPP 是基于 XML 的協議,這表明 XMPP 是可擴展的。
- XMPP 包含了針對服務器端的軟件協議,用于即時消息以及在線現場探測。
- XMPP 的前身是Jabber(1998 年),一個開源形式組織產生的網絡即時通信協議。
- XMPP 是一個由IETF標準化的開放協議,由XMPP標準基金會支持和擴展。
XMPP是一種基于標準通用標記語言的子集XML的協議,它繼承了在XML環境中靈活的發展性。因此,基于XMPP的應用具有超強的可擴展性。經過擴展以后的XMPP可以通過發送擴展的信息來處理用戶的需求,以及在XMPP的頂端建立如內容發布系統和基于地址的服務等應用程序。而且,XMPP包含了針對服務器端的軟件協議,使之能與另一個進行通話,這使得開發者更容易建立客戶應用程序或給一個配好系統添加功能。
優點:開放、可擴展、標準、證實可用、分散、安全
缺點 :數據負載過重,沒有二進制傳輸
基本網絡結構
- XMPP中定義了三個角色,
客戶端,服務器,網關,通信能夠在這三者的任意兩個之間雙向發生。 服務器同時承擔了客戶端信息記錄,連接管理和信息的路由功能。網關承擔著與異構即時通信系統的互聯互通,異構系統可以包括SMS,MSN,ICQ等。- 基本的網絡形式是
單客戶端通過TCP/IP連接到單服務器,然后在之上傳輸XML。
XMPP 工作流程
- 節點連接到服務器
- 服務器利用本地目錄系統中的證書對其認證
- 節點指定目標地址,讓服務器告知目標狀態
- 服務器查找、連接并進行相互認證
- 節點之間進行交互
XMPP核心協議通信的基本模式就是先建立一個stream,然后協商一堆安全之類的東西,中間通信過程就是客戶端發送XML Stanza(節點),一個接一個的。服務器根據客戶端發送的信息以及程序的邏輯,發送XML Stanza給客戶端。但是這個過程并不是一問一答的,任何時候都有可能從一方發信給另外一方。通信的最后階段是</stream>關閉流,關閉TCP/IP連接。
傳輸的內容
傳輸的是與即時通訊相關的指令。在以前這些命令要么用2進制的形式發送(比如QQ),要么用純文本指令加空格加參數加換行符的方式發送(比如MSN)。而XMPP傳輸的即時通訊指令的邏輯與以往相仿,只是協議的形式變成了XML格式的純文本。這不但使得解析容易了,人也容易閱讀了,方便了開發和查錯。
XMPP 的核心部分就是一個在網絡上分片段發送 XML 的流協議。這個流協議是 XMPP 的即時通訊指令的傳遞基礎,可以說 XMPP 用 TCP 傳的是 XML 流。
真實通訊案例
Xmpp協議是建立在xml的基礎上的,所以,看起來,xmpp協議就像一個xml。
客戶端 8049a646c63e65e8 發出去的消息:
<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協議的一種數據傳輸協議,用戶只需要明白它接收的類型,并理解它返回的類型,就可以很好的利用xmpp來進行數據通訊。
目前不少IM應用系統如Google公司的Google Talk以及Jive Messenger等開源應用,都是遵循XMPP協議集而設計實現的,這些應用具有很好的互通性。
Openfire 安裝配置
安裝時除了修改一下安裝路徑,其他一路Next就Ok了。
安裝完畢后會自動啟動Openfire服務并自動打開 配置頁面 (可能需要手動刷新一下)。也可以通過雙擊 \Openfire\bin\openfire.exe 或 \Openfire\bin\openfired.exe 啟動Openfire服務后手動打開配置頁面。
然后按照指引設置 Openfire 服務器:
- 選擇語言:中文簡體
配置服務器域名【127.0.0.1】
選擇數據庫
選擇特性配置,默認即可
設置管理員帳戶【0909082401@163.com】【123456a】
提示安裝完成,點擊登錄管理員控制臺頁面【admin】【123456a】
進入后可以看到服務器名稱等信息【127.0.0.1】
創建用戶【admin】【baiqiantao】【bqt】【test】
安裝spark客戶端,這個spark僅僅是拿來測試用的。
至此代碼以外的環境已經配置好了。
Stanza 節
Xml是由節點構成的,而基于xml的xmpp協議中與通信有關三個最核心的節(Stanza)是:<message>、<presence>、<iq>,可以通過組織不同的節來達到各式各樣不同的通訊目的。接下來就對這些Stanza做一個大致的了解。
共同屬性
每個節都有其屬性,雖然不同的節其屬性各有不同,但是一些基本的屬性是這些所有的節所共同的以下這些是他們的共同屬性。
from
表示Stanza的發送方,在發送Stanza時,一般來說不推薦設定,服務器會自動設定正確的值,如果你設定了不正確的值,服務器將會拒收你的Stanza信息。to
表示Stanza的接收方。這個節點一般是自己設置的,若到達服務器的數據中沒收設置該屬性,則服務器會認為這條信息是發送給自己的。type
指定Stanza的類型。
這個節與前兩個不同,設置的值不可以統一而論,不同的節有不同的設定值,每種Stanza都有固定的幾種可能的設定值。
雖然不同節點的type屬性各有不同,但是都有一個error類型,表示這是一條錯誤信息,服務器接收到這種類型的信息的時候不需要作出任何的回應。id
用于標志唯一的一條特定信息,表示一個特定的請求。
在節中,這個屬性是必須要指定的,但是在其他兩個Stanza中是一個可選屬性。
Presence 在線狀態
Presence Stanza 用來控制和表示實體的在線狀態,可以展示離線、在線、離開、不能打擾等復雜狀態,另外,還能被用來建立和結束在線狀態的訂閱。
除了類型信息外,Presence還包含其他一些可選的屬性:
- Status: 用于表示用戶狀態的
自定義文本,例如:外出吃飯 - Priority: 一個表示發送者資源
優先級的非負數 - Mode: 表示五種狀態之一
案例 <presence/> 設定用戶狀態為在線 <presence type="unavailable"/> 設定用戶狀態為離線
<presence><show>away</show><status>at the ball</status>
</presence> 用于顯示用戶狀態的詳細信息。上面的例子表明用戶因為at the ball在離開狀態。
<show>標簽在presence節點中最多出現一次,取值可以為Presence.Mode中的某一個。<status>標簽用于顯示額外信息
<presence><status>touring the countryside</status><priority>10</priority>
</presence> 在這個節中,出現了一個<priority>標簽,表示現在連接的優先級。每個連接可以設置從-128到127的優先級,默認是設置為0,用戶可以在這個標簽里修改相應的優先級。
在線狀態預定
首先我們來看一個例子:
<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 的在線狀態,并能接收到 test_account 的在線狀態通知了(例如上線提醒功能)。
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,//【申請添加對方為好友】Request 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節點信息是傳遞之后就被忘記的。當消息被送出之后,發送者是不管這個消息是否已經送出或者什么時候被接收到。但是通過擴展協議,可以改變這樣一種狀況。
案例
私人聊天信息:
<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>標簽里面是具體的消息內容。
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,//【通知,不需要回應】Text message to be displayed in scrolling marquee滾動選框 displays.error;//【錯誤】indicates a messaging error. error消息為系統自動發送的,往往是由于錯誤發送消息public static Type fromString(String string) {return Type.valueOf(string.toLowerCase(Locale.US));}
} IQ 請求響應
- IQ Stanza 主要是用于
Info/Query模式的消息請求,他和Http協議比較相似。 - IQ節點
需要有回應。 - 可以發出
get以及set請求,就如同http中的GET以及POST。 - 有
result以及error兩種回應。
案例
william_duan 請求自己的聯系人列表:
<iqfrom="william_duan@jabber.org/study"id="roster1"type="get"><query xmlns="jabber:iq:roster"/>
</iq> 請求發生錯誤:
<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 的聯系人列表。每一個<item>標簽代表了一個聯系人信息:
<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, //【設置消息】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設置的配置,然后根據配置構造一個 XMPPTCPConnection ,以后所有操作基本都需要用到這個 XMPPTCPConnection 。
connection = new XMPPTCPConnection(configuration); 通過了上面的配置后,咱們可以登錄Openfire系統了,相當簡單:
XMPPUtils.getConnection().login(username, password); 下面我們重點分析下登錄過程的報文內容以及一些最常用的API。
connect 過程
在建立了Socket后,client會向服務器發出一條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'> 服務器解析到上面的指令后,會返回用于告訴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 算是完成了,此時會回調 ConnectionListener 的 connected 方法。
login 過程
XMPPUtils.getConnection().login(username, password); 1、客戶端選擇PLAIN認證方式
<auth mechanism='PLAIN'xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>ADgwNDlhNjQ2YzYzZTY1ZTgAQkRFNEM3QzBGMzdENEZGRTlENDlGNDcwMTdFNUJCRjc=
</auth> 服務器通過計算加密后的密碼后,服務器將返回
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/> 2、當客戶端收到以上命令后,將首次發起連接的id發送到服務器
<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'> 這時服務器會返回如下內容說明此時已經成功綁定了當前的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、客戶端在接收到如上的內容后會告訴服務器開啟壓縮
項目中沒有使用壓縮,所以下面的過程不存在,以下為參考別人的案例
<compress xmlns='http://jabber.org/protocol/compress'><method>zlib</method></compress> 服務器返回
<compressed xmlns='http://jabber.org/protocol/compress'/> 3.2、客戶端收到服務器的響應命令后,重新建立一個Socket,發送指令
<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'> 服務器將返回,不知道你有沒有發現,這里的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、客戶端發送綁定Socket的指令:
<iqid='SG6jR-3'type='set'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>phone</resource></bind>
</iq> 服務器返回綁定了具有指定 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> 這時服務器返回
<iq type="result" id="b86j8-6" to="c997c3a8@im/c997c3a8"/> 6、認證
因為項目中沒有開啟認證,所以這里沒有報文通訊,只有如下日志:
至此,客戶端的登錄過程算是完成了。
注意,connect 和 login 都是同步操作,所以在
login(username, password)方法調用以后,如果沒有報異常,就是登陸成功了。
獲取通訊錄
登陸以后接著會自動發送一條獲取通訊錄的指令,并會將通訊錄緩存起來,所以以后再獲取通訊錄時,并不需要訪問網絡。
<iqid='gZYnq-5'type='get'><query xmlns='jabber:iq:roster'></query>
</iq> 服務器將返回
<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> 此過程完成以后會回調 RosterListener 的 entriesAdded 方法。
告訴服務器在線狀態
雖然已經登錄了,但是還需要告訴服務器自己的狀態,否則服務器不會認為你是在線狀態,這時你可能就收不到其他好友發來的消息(我們我們項目中有集成離線推送功能,如果沒有告訴服務器你在笑,服務器會走離線消息推送的邏輯。)
XMPPUtils.getConnection().sendStanza(presence); 客戶端發送 presence 消息告訴服務器自己在線:
<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> 服務器響應:
<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> 收到響應后,會回調 RosterListener 的 presenceChanged 方法,此后,就可以愉快的玩耍了。
判斷是否在線
服務器會定時(默認3分鐘)主動發送一條 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> 客戶端響應:
<iqid='553-595'to='oatest.dgcb.com.cn'type='result'></iq> 到此,整個登錄流程已經成功了,接下來可以做一些用戶信息的獲取等操作。
發送消息
//發送方式一,簡單的發送文本消息
ChatManager.getInstanceFor(XMPPUtils.getConnection()).createChat(to).sendMessage(text);
//發送方式二,發送一個Message對象,可包含一些信息,一般使用這種方式
XMPPUtils.getConnection().sendStanza(msg); 除去消息內容后的日志:
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發送消息:
<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> 客戶端收到服務器的回執(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發送的消息(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也會收到的回執消息(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> 測試案例代碼
項目結構
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 = {"初始化","登錄","發送在線狀態消息","發消息","獲取好友信息","創建聊天室","加入聊天室","邀請好友進入聊天室","注銷登錄","",};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, "你好,我是包青天");//發消息break;case 4:XMPPUtils.getMyFriends();//獲取好友信息break;case 5:XMPPUtils.createMucRoom(jid, "包青天");//創建聊天室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相關配置XMPPTCPConnectionConfiguration configuration = XMPPTCPConnectionConfiguration.builder().setUsernameAndPassword(username, password)//設置登錄openfire的用戶名和密碼.setServiceName("oatest.dgcb.com.cn")//設置服務器名稱.setHost("oatest.dgcb.com.cn")//設置主機地址.setPort(25222)//設置端口號.setResource("phone") //默認為Smack.setDebuggerEnabled(true)//是否查看debug日志//********************************************** 以下為進階配置 *************************************************.setConnectTimeout(10 * 1000)//設置連接超時的最大時間.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)//設置安全模式,關閉安全模式.setCompressionEnabled(false) //開啟通訊壓縮,開啟后傳輸的流量將節省90%.setSendPresence(false).setCustomSSLContext(getSSLContext()) //自定義的TLS登錄.setHostnameVerifier((hostname, session) -> true).build();connection = new XMPPTCPConnection(configuration);connection.setFromMode(XMPPConnection.FromMode.USER);connection.addConnectionListener(new MyConnectionListener()); //監聽connect狀態connection.addAsyncStanzaListener(new MyStanzaListener(), StanzaTypeFilter.MESSAGE);// 注冊包的監聽器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()); //監聽與聊天相關的事件MultiUserChatManager.getInstanceFor(connection).addInvitationListener(new MyInvitationListener()); //被邀請監聽}}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();}}/*** 告訴服務器登錄狀態*/public static void setOnLineStatus() {if (XMPPUtils.getConnection().isAuthenticated()) {try {Presence presence = new Presence(Presence.Type.available);presence.setStatus("IchatMM"); //顯示額外信息,內容根據需求可隨意定制presence.setPriority(0); //連接的優先級XMPPUtils.getConnection().sendStanza(presence);} catch (SmackException.NotConnectedException e) {e.printStackTrace();}}}/*** 注銷登錄*/public static void logout() {if (!XMPPUtils.getConnection().isConnected()) {XMPPUtils.getConnection().disconnect();}}/*** 發消息*/public static void sendMessage(String from, String to, String text) {try {ChatManager.getInstanceFor(XMPPUtils.getConnection()).createChat(to).sendMessage(text);//直接發送一條文本/*Message msg = new Message(to, Message.Type.chat);msg.setStanzaId(System.currentTimeMillis() + "");msg.setFrom(from);msg.setBody(text);XMPPUtils.getConnection().sendStanza(msg);//發送一個Message對象,可包含一些信息,一般使用后者*/} catch (SmackException.NotConnectedException e) {e.printStackTrace();}}/*** 獲取好友信息*/public static void getMyFriends() {//并不需要訪問網絡,因為在登錄后已經拿到用戶的通訊錄了,這里是直接從緩存中讀取的Set<RosterEntry> set = Roster.getInstanceFor(XMPPUtils.getConnection()).getEntries();for (RosterEntry entry : set) {Log.i("bqt", "JID:" + entry.getUser() + ",Name:" + entry.getName());}}/*** 創建聊天室*/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
轉載于:https://www.cnblogs.com/baiqiantao/p/9819242.html
總結
以上是生活随笔為你收集整理的Openfire XMPP Smack RTC IM 即时通讯 聊天 MD的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 洱海可以骑电动车环湖吗
- 下一篇: 求一个感觉心好累的个性签名