Openfire XMPP Smack RTC IM 即時通信 聊天 MD

Markdown版本筆記 個人GitHub首頁 個人博客 個人微信 個人郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

Openfire XMPP Smack RTC IM 即時通信 聊天 MDhtml


目錄

簡介

Demo地址:https://github.com/baiqiantao/OpenFireTest.git
官網
官方文檔
OpenFire下載
markjava

Openfire 簡介

  • Openfire是一個根據開源Apache許可證受權的實時協做服務器 real time collaboration (RTC)。它使用惟一普遍採用的即時消息開放協議XMPP(Jabber)。 Openfire很是容易設置和管理,但提供堅如磐石的安全性和性能。
  • Openfire是一個功能豐富即時消息和跨平臺實時協做服務器,使用XMPP協議提供全面的羣聊和即時消息服務
  • OpenFire是採用Java編程語言開發的實時協做服務器,能夠輕易的構建高效率的即時通訊服務器,安裝和使用簡單,利用 Web 進行管理,單臺服務器可支持上萬併發用戶

相關的幾個名詞

簡單說,OpenFire 是服務器,XMPP 是協議,Smack 是類庫,Spark 是客戶端。node

Smack

GitHub
Flowdalic/asmackandroid

  • Smack 是一個基於 XMPP 協議的 Java 實現,提供一套可擴展的API,與 OpenFire 進行通訊。
  • Smack 是一個開源,易於使用的 XMPP 客戶端類庫,能夠實現即時通信和聊天。
  • Smack 是Spark項目的核心。

優勢:git

  • 簡單,功能強大,只需短短几行代碼就能夠向用戶發送文本消息;
  • 不像其餘類庫那樣強制你進行包級別的編碼,Smack提供了智能的、更高級的構造,像Chat和Roster類,可讓你進行更高效的編程;
  • 你不須要熟悉 XMPP XML 格式,甚至不須要熟悉XML;
  • 提供了簡單的機器到機器通信,容許在每一個消息中設置任意數量的屬性,包括java對象;
  • Apache許可下的開源類庫,這意味着使用者能夠將Smack整合進商業的或者非商業的應用中。

缺點是其API並不是爲大量併發用戶設計,每一個客戶要1個線程,佔用資源大。github

Spark

  • Spark 至關與電腦版QQ,經過 smack 與 openfire 進行通訊。
  • Spark 是一個 XMPP 協議通訊聊天的CS端的IM軟件,它能夠經過 openfire 進行聊天對話。
<message from="admin@myopenfire.com" to="bqt@myopenfire.com">消息內容</message>

JID

  • 基於歷史緣由, 一個XMPP實體的地址稱爲Jabber IdentifierJID,它用來標示XMPP網絡中的各個XMPP實體。
  • 鑑於協議的分佈式特徵, JID 應包含聯繫到用戶所需的全部信息。
  • 我的認爲能夠把JID理解爲Email地址,就比較好理解了。
  • 一個合法的JID包括節點名user、域名domain、資源名resource,其中 user 和 resource 是無關緊要的,domain 是必須的。domain和user部分是不分大小寫的,可是resource區分大小寫。
    • domainpart 一般指網絡中的網關或者服務器
    • localpart(user、node) 一般表示一個向服務器或網關請求和使用網絡服務的實體(好比一個客戶端),固然它也可以表示其餘的實體(好比在多用戶聊天系統中的一個房間)。
    • resourcepart:一般表示一個特定的會話(與某個設備),鏈接(與某個地址),或者一個附屬於某個節點ID實體相關實體的對象(好比多用戶聊天室中的一個參加者)。
  • JID的格式爲:jid = [ localpart "@" ] domainpart [ "/" resourcepart ],例如:
    • stpeter@jabber.org:表示服務器jabber.org上的用戶stpeter
    • room@service:一個用來提供多用戶聊天服務的特定的聊天室。這裏 room 是聊天室的名字,service 是多用戶聊天服務的主機名
    • room@service/nick:加入了聊天室的用戶nick的地址。這裏 nick 是用戶在聊天室的暱稱

XMPP

Extensible Messaging and Presence Protocol,可擴展通信和表示協議數據庫

  • XMPP 是基於 XML 的協議,這代表 XMPP 是可擴展的。
  • XMPP 包含了針對服務器端的軟件協議,用於即時消息以及在線現場探測。
  • XMPP 的前身是Jabber(1998 年),一個開源形式組織產生的網絡即時通訊協議。
  • XMPP 是一個由IETF標準化的開放協議,由XMPP標準基金會支持和擴展。

XMPP是一種基於標準通用標記語言的子集XML的協議,它繼承了在XML環境中靈活的發展性。所以,基於XMPP的應用具備超強的可擴展性。通過擴展之後的XMPP能夠經過發送擴展的信息來處理用戶的需求,以及在XMPP的頂端創建如內容發佈系統和基於地址的服務等應用程序。並且,XMPP包含了針對服務器端的軟件協議,使之能與另外一個進行通話,這使得開發者更容易創建客戶應用程序或給一個配好系統添加功能。編程

優勢:開放、可擴展、標準、證明可用、分散、安全
缺點 :數據負載太重,沒有二進制傳輸 c#

基本網絡結構緩存

  • 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>

mark

客戶端 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>

mark

其實 XMPP 是一種很相似於http協議的一種數據傳輸協議,用戶只須要明白它接收的類型,並理解它返回的類型,就能夠很好的利用xmpp來進行數據通信。

目前很多IM應用系統如Google公司的Google Talk以及Jive Messenger等開源應用,都是遵循XMPP協議集而設計實現的,這些應用具備很好的互通性。

Openfire 安裝配置

安裝時除了修改一下安裝路徑,其餘一路Next就Ok了。

安裝完畢後會自動啓動Openfire服務並自動打開 配置頁面 (可能須要手動刷新一下)。也能夠經過雙擊 \Openfire\bin\openfire.exe\Openfire\bin\openfired.exe 啓動Openfire服務後手動打開配置頁面。
mark

而後按照指引設置 Openfire 服務器:

  • 選擇語言:中文簡體
  • 配置服務器域名【127.0.0.1】
    mark

  • 選擇數據庫
    mark

  • 選擇特性配置,默認便可

  • 設置管理員賬戶【0909082401@163.com】【123456a】
    mark

  • 提示安裝完成,點擊登陸管理員控制檯頁面【admin】【123456a】

  • 進入後能夠看到服務器名稱等信息【127.0.0.1】
    mark

  • 建立用戶【admin】【baiqiantao】【bqt】【test】
    mark

  • 安裝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,用戶能夠在這個標籤裏修改相應的優先級。

在線狀態預約
首先咱們來看一個例子:

<presence
    from="william_duan@jabber.org"
    to="test_account@jabber.org"
    type="subscribe"/>
<presence
    from="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 6121
    public 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節點信息是傳遞以後就被忘記的。當消息被送出以後,發送者是無論這個消息是否已經送出或者何時被接收到。可是經過擴展協議,能夠改變這樣一種情況。

案例
私人聊天信息:

<message
    from="william_duan@jabber.org"
    to="test_account@jabber.org"
    type="chat">
    <body>Come on</body>
    <thread>23sdfewtr234weasdf</thread>
</message>

多人聊天信息:

<message
    from="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 請求本身的聯繫人列表:

<iq
    from="william_duan@jabber.org/study"
    id="roster1"
    type="get">
    <query xmlns="jabber:iq:roster"/>
</iq>

請求發生錯誤:

<iq
    id="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>標籤表明了一個聯繫人信息:

<iq
    id="roster1"
    to="william_duan@jabber.org/study"
    type="error">
    <query xmlns="jabber:iq:roster"/>
    <item
        name="one"
        jid="account_one@jabber.org"/>
    <item
        name="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:
mark

<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方式
mark

<?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 算是完成了,此時會回調 ConnectionListenerconnected 方法。

login 過程

XMPPUtils.getConnection().login(username, password);

一、客戶端選擇PLAIN認證方式
mark

<auth mechanism='PLAIN'
      xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>ADgwNDlhNjQ2YzYzZTY1ZTgAQkRFNEM3QzBGMzdENEZGRTlENDlGNDcwMTdFNUJCRjc=
</auth>

服務器經過計算加密後的密碼後,服務器將返回
mark

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

二、當客戶端收到以上命令後,將首次發起鏈接的id發送到服務器
mark

<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
mark

<?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.一、客戶端在接收到如上的內容後會告訴服務器開啓壓縮

項目中沒有使用壓縮,因此下面的過程不存在,如下爲參考別人的案例

<compress xmlns='http://jabber.org/protocol/compress'><method>zlib</method></compress>

服務器返回

<compressed xmlns='http://jabber.org/protocol/compress'/>

3.二、客戶端收到服務器的響應命令後,從新創建一個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>

四、客戶端發送綁定Socket的指令:
mark

<iq
    id='SG6jR-3'
    type='set'>
    <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
        <resource>phone</resource>
    </bind>
</iq>

服務器返回綁定了具備指定 JID 的客戶端
mark

<iq
    id="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>

五、開啓一個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"/>

六、認證
由於項目中沒有開啓認證,因此這裏沒有報文通信,只有以下日誌:
mark

至此,客戶端的登陸過程算是完成了。

注意,connect 和 login 都是同步操做,因此在 login(username, password) 方法調用之後,若是沒有報異常,就是登錄成功了。

獲取通信錄

登錄之後接着會自動發送一條獲取通信錄的指令,並會將通信錄緩存起來,因此之後再獲取通信錄時,並不須要訪問網絡。
mark

<iq
    id='gZYnq-5'
    type='get'>
    <query xmlns='jabber:iq:roster'></query>
</iq>

服務器將返回
mark

<iq
    id="SG6jR-5"
    to="8049a646c63e65e8@oatest.dgcb.com.cn/phone"
    type="result">
    <query ver="-491295515"
           xmlns="jabber:iq:roster">
        <item
            name="李**"
            jid="0347a8a25e9074b0@oatest.dgcb.com.cn"
            subscription="to"/>
        <item
            jid="903e652d2334628a@oatest.dgcb.com.cn"
            subscription="from"/>
        <item
            ask="subscribe"
            jid="28af56d053cbbf3e@oatest.dgcb.com.cn"
            subscription="none"/>
    </query>
</iq>

此過程完成之後會回調 RosterListenerentriesAdded 方法。

告訴服務器在線狀態

雖然已經登陸了,可是還須要告訴服務器本身的狀態,不然服務器不會認爲你是在線狀態,這時你可能就收不到其餘好友發來的消息(咱們咱們項目中有集成離線推送功能,若是沒有告訴服務器你在笑,服務器會走離線消息推送的邏輯。)

XMPPUtils.getConnection().sendStanza(presence);

客戶端發送 presence 消息告訴服務器本身在線:

<presence
    from='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>

服務器響應:

<presence
    from="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>

收到響應後,會回調 RosterListenerpresenceChanged 方法,此後,就能夠愉快的玩耍了。

判斷是否在線

服務器會定時(默認3分鐘)主動發送一條 ping 消息,以肯定客戶端是否在線:

PingManager.getInstanceFor(connection).setPingInterval(60);//ping消息間隔

mark

<iq
    from="oatest.dgcb.com.cn"
    id="553-595"
    to="8049a646c63e65e8@oatest.dgcb.com.cn/phone"
    type="get">
    <ping xmlns="urn:xmpp:ping"/>
</iq>

客戶端響應:

<iq
    id='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

mark

一、客戶端A發送消息:

<message
    from='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相同):

<message
    from="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>

二、而後,客戶端B會收到客戶端A發送的消息(id相同):

<message
    from="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):

<message
    from="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>

測試案例代碼

項目結構

mark

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;

    @Override
    protected 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
    }

    @Override
    protected 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

相關文章
相關標籤/搜索