1. 程式人生 > >Openfire XMPP Smack RTC IM 即時通訊 聊天

Openfire XMPP Smack RTC IM 即時通訊 聊天

ray 控制臺 cati 探測 ica strong ttext 包括 login

Openfire XMPP Smack RTC IM 即時通訊 聊天


目錄

目錄
簡介
Openfire 簡介
相關的幾個名詞
Smack
Spark
JID
XMPP
Openfire 安裝配置
測試代碼
初始化
登錄服務器
登錄底層報文通訊簡要解析
登錄底層報文通訊簡要解析
服務器判斷客戶端是否在線
發送消息
測試案例代碼
項目結構
MainActivity
常用功能封裝的工具欄

簡介

Demo地址:https://github.com/baiqiantao/OpenFireTest.git
官網
官方文檔
OpenFire下載
技術分享圖片

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="[email protected]" to="[email protected]">消息內容</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 ],例如:
    • [email protected]:表示服務器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包含了針對服務器端的軟件協議,使之能與另一個進行通話,這使得開發者更容易建立客戶應用程序或給一個配好系統添加功能。

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

基本網絡結構
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=‘[email protected]/phone‘ id=‘5U6Mk-5‘ to=‘[email protected]‘ type=‘chat‘>
    <body>{"fromId":"8049a646c63e65e8","fromName":"韓大東","messageType":1,"secret":false,"textContent":"你好","toName":"鄭西風","toUserID":"903e652d2334628a"}</body>
    <request xmlns=‘urn:xmpp:receipts‘/>
</message>

技術分享圖片

客戶端 8049a646c63e65e8 接收到的消息:

<message from="[email protected]/phone" id="Bw4c9-4" to="[email protected]" 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】
    技術分享圖片

  • 選擇數據庫
    技術分享圖片

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

  • 設置管理員帳戶【[email protected]】【123456a】
    技術分享圖片

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

  • 進入後可以看到服務器名稱等信息【127.0.0.1】
    技術分享圖片

  • 創建用戶【admin】【baiqiantao】【bqt】【test】
    技術分享圖片

  • 安裝spark客戶端,這個spark僅僅是拿來測試用的。

至此代碼以外的環境已經配置好了。

測試代碼

Demo地址:https://github.com/baiqiantao/OpenFireTest.git

初始化

XMPPConnection的連接需要通過XMPPTCPConnectionConfiguration.builder()配置你在Openfire設置的配置,代碼如下:

/**
 * 初始化
 */
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.addConnectionListener(new MyConnectionListener()); //監聽connect狀態

            //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()); //被邀請監聽
      }
}

登錄服務器

通過了上面的配置後,咱們可以登錄Openfire系統了,相當簡單:

/**
 * 登錄
 */
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();
      }
}

登錄底層報文通訊簡要解析

1、在建立了Socket後,client會向服務器發出一條xml:
技術分享圖片

<stream:stream xmlns:stream=‘http://etherx.jabber.org/streams‘
               from=‘[email protected]‘
               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>

2、客戶端選擇PLAIN認證方式
技術分享圖片

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

服務器通過計算加密後的密碼後,服務器將返回
技術分享圖片

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

3、當客戶端收到以上命令後,將首次發起連接的id發送到服務器
技術分享圖片

<stream:stream xmlns:stream=‘http://etherx.jabber.org/streams‘
               from=‘[email protected]‘
               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>

4、壓縮
4.1、客戶端在接收到如上的內容後會告訴服務器開啟壓縮

項目中沒有使用壓縮,所以下面的過程不存在,以下為參考別人的案例

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

服務器返回

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

4.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>

實際上到這裏客戶端的登錄已經完成了,但是還沒算成功,接下來可以開始做綁定Socket的操作了

登錄底層報文通訊簡要解析

1、客戶端發送綁定Socket的指令:
技術分享圖片

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

服務器返回綁定了具有指定 JID 的客戶端
技術分享圖片

<iq
    id="SG6jR-3"
    to="oatest.dgcb.com.cn/36ebm4blnf"
    type="result">
    <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
        <jid>[email protected]/phone</jid>
    </bind>
</iq>

2、開啟一個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"/>

3、接著會自動發送一條獲取通訊錄的指令
技術分享圖片

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

服務器將返回
技術分享圖片

<iq
    id="SG6jR-5"
    to="[email protected]/phone"
    type="result">
    <query ver="-491295515"
           xmlns="jabber:iq:roster">
        <item
            name="李**"
            jid="[email protected]"
            subscription="to"/>
        <item
            jid="[email protected]"
            subscription="from"/>
        <item
            ask="subscribe"
            jid="[email protected]"
            subscription="none"/>
    </query>
</iq>

服務器判斷客戶端是否在線

服務器會定時(3分鐘)主動發送一條 ping 消息,以確定客戶端是否在線:
技術分享圖片

<iq
    from="oatest.dgcb.com.cn"
    id="553-595"
    to="[email protected]/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
    from=‘[email protected]/phone‘
    id=‘WRULf-15‘
    to=‘[email protected]/phone‘
    type=‘chat‘>
    <body>你好,我是包青天</body>
    <thread>a86ee445-0028-4058-8d08-98803c9b6fdb</thread>
</message>

發送方式二:

XMPPUtils.getConnection().sendStanza(msg);//發送一個Message對象,可包含一些信息,一般使用後者
<message
    from=‘[email protected]/phone‘
    id=‘1539957065416‘
    to=‘[email protected]/phone‘
    type=‘chat‘>
    <body>你好,我是包青天</body>
</message>

服務器回執:

<message
    from="[email protected]/phone"
    to="[email protected]/phone">
    <received msgId="WRULf-15"
              status="1"
              time="2018-10-19 21:50:23:848"
              xmlns="urn:xmpp:receipts"/>
</message>

由於項目中有集成離線推送功能,而通過Demo登錄時會認為沒有正常登錄,所以實現對方收不到消息,這個以後有空再走走流程。

推送的消息:
技術分享圖片

測試案例代碼

項目結構

技術分享圖片

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 ? "BDE4C7C0F37D4FFE9D49F47017E5BBF7" : "40C61DE3492C41B1846281833434D997");
        etChat.setText(switchUser ? "[email protected]/phone" : "[email protected]/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.logout();//註銷登錄
                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;
            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);// 註冊包的監聽器

            //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 logout() {
        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("[email protected]");
            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

Openfire XMPP Smack RTC IM 即時通訊 聊天