iOS - XMPP 的使用


1、XMPP

  • XMPP 是一個基於 Socket 通信的即時通訊的協議,它規范了即時通信在網絡上數據的傳輸格式,比如登錄,獲取好友列表等等的格式。XMPP 在網絡傳輸的數據是 XML 格式。

  • 開發架構:

    IM3

  • iOS 框架:XMPPFramework

  • 服務器:Openfire

  • 數據庫:MySQL

2、XMPPFramework 框架簡介

2.1 XMPPFramework 簡介

  • XMPPFramework 是一個 OS X/iOS 平台的開源項目,使用 Objective-C 實現了 XMPP 協議(RFC-3920),同時還提供了用於讀寫 XML 的工具,大大簡化了基於 XMPP 的通信應用的開發。

2.2 XMPPFramework 結構

  • 1、XMPPFramework 的目錄結構如下:

    IM5

    目錄 說明
    Authentication 授權,與授權驗證相關,如用戶名密碼等
    Categories 分類,XMPP 自己寫的一些分類,尤其是 NSXMLElement+XMPP 擴展是必備的
    Core 核心,這里是 XMPP 的核心文件目錄,我們最主要的目光還是要放在這個目錄上
    Extensions 擴展,XMPP 的擴展模塊,用於擴展各種協議和各種獨立的功能,其下每個子目錄都是對應的一個單獨的子功能
    Utilities 工具,都是輔助類,我們開發者不用關心這里
    Vendor 第三方庫,這個目錄是 XMPP 所引用的第三方類庫,我們也不用關心這里
    • 雖然這里有很多個目錄,但是我們在開發中基本只關心 Core 和 Extensions 這兩個目錄下的類。

    • 在 Core 中:

      說明
      PElement
      PIQ
      PMessage
      PPresence
      PStream
    • 在 Extensions 中:

      說明
      eDataStorage
      onnect
      ter
      temInputActivityMonitor
    • 在 Vendor 中:

      說明
      oaAsyncSocket 異步 Socket
      oaLumberjack ⽇志相關
      sXML XML 解析
  • 2、XMPPFramework 中常用的類:

    說明
    XMPPStream XMPP 基礎服務類
    XMPPRoster 好友列表類
    XMPPUserCoreDataStorageObject 管理用戶的類
    XMPPRosterCoreDataStorage 好友列表(用戶賬號)在 core data 中的操作類
    XMPPvCardCoreDataStorage 好友名片(昵稱,簽名,性別,年齡等信息)在 core data 中的操作類
    XMPPvCardTemp 好友名片實體類,從數據庫里取出來的都是它
    xmppvCardAvatarModule 好友頭像
    XMPPReconnect 如果失去連接,自動重連
    XMPPRoom 提供多用戶聊天支持
    XMPPPubSub 發布訂閱
    XMPPMessageArchiving 其中有數據表
    XMPPMessageArchiving_Message_CoreDataObject 取出當前信息的類
  • 3、XMPPFramework 幾個常用到的擴展協議:

    協議 協議簡介
    XEP-0006 使能與網絡上某個 XMPP 實體間的通信
    XEP-0009 在兩個 XMPP 實體間傳輸 XML-RPC 編碼請求和響應
    XEP-0012 最后的活動(判斷上線,離開斷開)
    XEP-0045 多人聊天相關協議
    XEP-0054 名片格式的標准文檔,個人信息設置
    XEP-0060 提供通用公共訂閱功能
    XEP-0065 兩個 XMPP 用戶之間建立一個帶外流,主要用於文件傳輸,sockets5 字節流
    XEP-0066 二進制數據傳輸(特殊信息的發送)
    XEP-0082 日期和時間信息的標准化表示
    XEP-0085 聊天對話中通知用戶狀態,聊天狀態通知
    XEP-0100 表述了 XMPP 客戶端與提供傳統的 IM 服務的代理網關之間交換的最佳實踐
    XEP-0115 廣播和動態發現客戶端、設備、或一般實體能力
    XEP-0136 為服務端備份和檢索 XMPP 消息定義機制和偏好設置,聊天記錄歸檔
    XEP-0153 用於交換用戶頭像,基於名片的頭像
    XEP-0184 消息送達回執協議
    XEP-0199 XMPP ping 協議(用來 ping 服務器和 ping 自己)
    XEP-0202 用於交換實體間的本地時間信息
    XEP-0203 用於延遲發送
    XEP-0224 引起另一個用戶注意的協議
    XEP-0335 JSON 容器(可能以后某些信息傳輸將用 JSON 格式)
    • XMPP 的擴展協議 Jingle 使得其支持語音和視頻,目前 iOS 尚不支持。

      • iOS 發送附件(圖片,語音,文檔…)時比較麻煩,XMPP 框架沒有提供發送附件的功能,需要自己實現。

      • iOS 發送附件實現方法:

        • 1、將獲取到的圖片/音頻文件通過 base64 加密,直接通過 xmpp 的消息體發送過去,然后解碼。

        • 2、通過 http 請求的方式將圖片/音頻文件上傳到服務器,然后將圖片/音頻文件的下載地址通過 xmpp 消息體發送過去,另外一個客戶端下載。

        • 音頻文件建議轉碼為 amr,這種格式的音頻文件比較小。

2.3 XMPPJID 類

  • 登錄需要到賬號,而所謂的賬號其實就是用戶唯一標識符(JID),在 XMPP 中使用 XMPPJID 類來表示。

  • JID 一般由三部分構成:用戶名,域名和資源名,格式為 user@domain/resource,例如:test@example.com/Anthony。對應於 XMPPJID 類中的三個屬性 user、domain、resource。

  • 如果沒有設置主機名(HOST),則使用 JID 的域名(domain)作為主機名,而端口號是可選的,默認是 5222,一般也沒有必要改動它。

2.4 XMPPStream 類

  • 我們要與服務器連接,就必須通過 XMPPStream 類了,它提供了很多的 API 和屬性設置,通過 socket 來實現的。Verdor 目錄包含了 CocoaAsyncSocket 這個非常有名的 socket 編程庫。XMPPStream 類還遵守並實現了 GCDAsyncSocketDelegate 代理,用於客戶端與服務器交互。

    	@interface XMPPStream : NSObject <GCDAsyncSocketDelegate>
    
  • 當我們創建 XMPPStream 對象后,我們需要設置代理,才能回調我們的代理方法,這個是支持 multicast delegate,也就是說對於一個 XMPPStream 對象,可以設置多個代理對象,其中協議是XMPPStreamDelegate。

    	- (void)addDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
    
  • 而當我們不希望某個 XMPPStream 對象繼續接收到代理回調時,我們通過這樣的方式來移除代理。

    	- (void)removeDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue;
    	- (void)removeDelegate:(id)delegate;
    
  • 接下來,我們要設置主機和端口,通過設置這兩個屬性。

    	// 主機,可選設置,如果沒有設置默認會使用 domain
    	@property (readwrite, copy) NSString *hostName;
    	
    	// 端口號,默認為 5222
    	@property (readwrite, assign) UInt16 hostPort;
    
  • XMPPStream 有 XMPPJID 類對象作為屬性,標識用戶,因為我們后續很多操作都需要到 myJID。

    	@property (readwrite, copy) XMPPJID *myJID;
    
  • 而管理用戶在線狀態的就交由 XMPPPresence 類了,它同樣被作為 XMPPStream 的屬性,組合到 XMPPStream 中,后續很多關於用戶的操作是需要到處理用戶狀態的。

    	@property (strong, readonly) XMPPPresence *myPresence;
    

2.5 XMPPStreamDelegate

  • 這個協議是非常關鍵的,我們的很多主要操作都集中在這個協議的代理回調上。它分為好幾種類型的代理 API,比如授權的、注冊的、安全的等。

    	@protocol XMPPStreamDelegate
    	@optional
    	
    	// 將要與服務器連接
    	- (void)xmppStreamWillConnect:(XMPPStream *)sender;
    	
    	// 已經與服務器連接,
    	// 當 TCP Socket 已經與遠程主機連接上時會回調此方法
    	// 若 App 要求在后台運行,需要設置 XMPPStream's enableBackgroundingOnSocket 屬性
    	- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket;
    	
    	// 當 TCP 與服務器建立連接后會回調此方法
    	- (void)xmppStreamDidStartNegotiation:(XMPPStream *)sender;
    	
    	// TLS 傳輸層協議在將要驗證安全設置時會回調
    	// 參數 settings 會被傳到 startTLS,此方法可以不實現的
    	// 若服務端使用自簽名的證書,需要在 settings 中添加 GCDAsyncSocketManuallyEvaluateTrust = YES
    	- (void)xmppStream:(XMPPStream *)sender willSecureWithSettings:(NSMutableDictionary *)settings;
    	
    	// 上面的方法執行后,下一步就會執行這個代理回調
    	// 用於在 TCP 握手時手動驗證是否受信任
    	- (void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust
    	                                      completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler;
    	
    	// 當 stream 通過了 SSL/TLS 的安全驗證時,會回調此代理方法
    	- (void)xmppStreamDidSecure:(XMPPStream *)sender;
    	
    	// 當 XML 流已經完全打開時(也就是與服務器的連接完成時)會回調此代理方法。此時可以安全地與服務器通信了
    	- (void)xmppStreamDidConnect:(XMPPStream *)sender;
    	
    	// 注冊新用戶成功時的回調
    	- (void)xmppStreamDidRegister:(XMPPStream *)sender;
    	
    	// 注冊新用戶失敗時的回調
    	- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error;
    	
    	// 授權通過時的回調,也就是登錄成功的回調
    	- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender;
    	
    	// 授權失敗時的回調,也就是登錄失敗時的回調
    	- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error;
    	
    	// 將要綁定 JID resource 時的回調,這是授權程序的標准部分
    	// 當驗證 JID 用戶名通過時,下一步就驗證 resource。若使用標准綁定處理,return nil 或者不要實現此方法
    	- (id <XMPPCustomBinding>)xmppStreamWillBind:(XMPPStream *)sender;
    	
    	// 如果服務器出現 resouce 沖突而導致不允許 resource 選擇時,會回調此代理方法
    	// 返回指定的 resource 或者返回 nil 讓服務器自動幫助我們來選擇。一般不用實現它
    	- (NSString *)xmppStream:(XMPPStream *)sender alternativeResourceForConflictingResource:(NSString *)conflictingResource;
    	
    	// 將要接收 IQ(消息查詢)時的回調
    	- (XMPPIQ *)xmppStream:(XMPPStream *)sender willReceiveIQ:(XMPPIQ *)iq;
    	
    	// 將要接收到消息時的回調
    	- (XMPPMessage *)xmppStream:(XMPPStream *)sender willReceiveMessage:(XMPPMessage *)message;
    	
    	// 將要接收到用戶在線狀態時的回調
    	- (XMPPPresence *)xmppStream:(XMPPStream *)sender willReceivePresence:(XMPPPresence *)presence;
    	
    	// 通過實現此代理方法,可以知道被過濾的原因,有一定的幫助
    	// 當 xmppStream:willReceiveX: (也就是前面這三個 API 回調后),過濾了 stanza,會回調此代理方法
    	- (void)xmppStreamDidFilterStanza:(XMPPStream *)sender;
    	
    	// 在接收了 IQ(消息查詢后)會回調此代理方法
    	- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq;
    
    	// 在接收了消息后會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message;
    
    	// 在接收了用戶在線狀態消息后會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence;
    	
    	// 在接收 IQ/messag、presence 出錯時,會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didReceiveError:(NSXMLElement *)error;
    	
    	// 將要發送 IQ(消息查詢時)時會回調此代理方法
    	- (XMPPIQ *)xmppStream:(XMPPStream *)sender willSendIQ:(XMPPIQ *)iq;
    
    	// 在將要發送消息時,會回調此代理方法
    	- (XMPPMessage *)xmppStream:(XMPPStream *)sender willSendMessage:(XMPPMessage *)message;
    
    	// 在將要發送用戶在線狀態信息時,會回調此方法
    	- (XMPPPresence *)xmppStream:(XMPPStream *)sender willSendPresence:(XMPPPresence *)presence;
    	
    	// 在發送 IQ(消息查詢)成功后會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didSendIQ:(XMPPIQ *)iq;
    
    	// 在發送消息成功后,會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message;
    
    	// 在發送用戶在線狀態信息成功后,會回調此方法
    	- (void)xmppStream:(XMPPStream *)sender didSendPresence:(XMPPPresence *)presence;
    	
    	// 在發送 IQ(消息查詢)失敗后會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didFailToSendIQ:(XMPPIQ *)iq error:(NSError *)error;
    
    	// 在發送消息失敗后,會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error;
    
    	// 在發送用戶在線狀態失敗信息后,會回調此方法
    	- (void)xmppStream:(XMPPStream *)sender didFailToSendPresence:(XMPPPresence *)presence error:(NSError *)error;
    	
    	// 當修改了 JID 信息時,會回調此代理方法
    	- (void)xmppStreamDidChangeMyJID:(XMPPStream *)xmppStream;
    	
    	// 當 Stream 被告知與服務器斷開連接時會回調此代理方法
    	- (void)xmppStreamWasToldToDisconnect:(XMPPStream *)sender;
    	
    	// 當發送了 </stream:stream> 節點時,會回調此代理方法
    	- (void)xmppStreamDidSendClosingStreamStanza:(XMPPStream *)sender;
    	
    	// 連接超時時會回調此代理方法
    	- (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender;
    	
    	// 當與服務器斷開連接后,會回調此代理方法
    	- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error;
    	
    	// P2P 類型相關的
    	- (void)xmppStream:(XMPPStream *)sender didReceiveP2PFeatures:(NSXMLElement *)streamFeatures;
    	- (void)xmppStream:(XMPPStream *)sender willSendP2PFeatures:(NSXMLElement *)streamFeatures;
    	
    	- (void)xmppStream:(XMPPStream *)sender didRegisterModule:(id)module;
    	- (void)xmppStream:(XMPPStream *)sender willUnregisterModule:(id)module;
    	
    	// 當發送非 XMPP 元素節點時,會回調此代理方法
    	// 也就是說,如果發送的 element 不是 <iq>, <message> 或者 <presence>,那么就會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didSendCustomElement:(NSXMLElement *)element;
    
    	// 當接收到非 XMPP 元素節點時,會回調此代理方法
    	// 也就是說,如果接收的element不是 <iq>, <message> 或者 <presence>,那么就會回調此代理方法
    	- (void)xmppStream:(XMPPStream *)sender didReceiveCustomElement:(NSXMLElement *)element;
    

2.6 XMPPIQ 類

  • 消息查詢(IQ)就是通過此類來處理的了。XMPP 給我們提供了 IQ 方便創建的類,用於快速生成 XML 數據。

    	@interface XMPPIQ : XMPPElement
    	
    	// 生成 IQ
    	
    	+ (XMPPIQ *)iq;
    	+ (XMPPIQ *)iqWithType:(NSString *)type;
    	+ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid;
    	+ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    	+ (XMPPIQ *)iqWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	+ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid;
    	+ (XMPPIQ *)iqWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	+ (XMPPIQ *)iqWithType:(NSString *)type child:(NSXMLElement *)childElement;
    	
    	- (id)init;
    	- (id)initWithType:(NSString *)type;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)jid;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	- (id)initWithType:(NSString *)type elementID:(NSString *)eid;
    	- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement;
    	
    	// IQ 類型
    	- (NSString *)type;
    	
    	// 判斷 type 類型
    	
    	- (BOOL)isGetIQ;
    	- (BOOL)isSetIQ;
    	- (BOOL)isResultIQ;
    	- (BOOL)isErrorIQ;
    	
    	// 當 type 為 get 或者 set 時,這個 API 是很有用的,用於指定是否要求有響應
    	- (BOOL)requiresResponse;
    	
    	- (NSXMLElement *)childElement;
    	- (NSXMLElement *)childErrorElement;
    	
    	@end
    
  • IQ 是一種請求/響應機制,從一個實體發送請求,另外一個實體接受請求並進行響應。例如,Client 在 stream 的上下文中插入一個元素,向 Server 請求得到自己的好友列表,Server 返回一個,里面是請求的結果。

  • <type></type> 有以下類別(可選設置如:<type>get</type>

    type 說明
    get 獲取當前域值。類似於 http get 方法
    set 設置或替換 get 查詢的值。類似於 http put 方法
    result 說明成功的響應了先前的查詢。類似於 http 狀態碼 200
    error 查詢和響應中出現的錯誤
  • 下面是一個 IQ 例子:

    	<iqfrom="huangyibiao@welcome.com/ios"  
    	    id="xxxxxxx" 
    	    to="biaoge@welcome.com/ios"  
    	    type="get"> 
    	  <queryxmlns="jabber:iq:roster"/> 
    	</iq> 
    

2.7 XMPPPresence 類

  • 這個類代表節點,我們通過此類提供的方法來生成 XML 數據。presence 它代表用戶在線狀態。

    	@interface XMPPPresence : XMPPElement
    	
    	// Converts an NSXMLElement to an XMPPPresence element in place (no memory allocations or copying)
    	+ (XMPPPresence *)presenceFromElement:(NSXMLElement *)element;
    	
    	+ (XMPPPresence *)presence;
    	+ (XMPPPresence *)presenceWithType:(NSString *)type;
    
    	// type:用戶在線狀態,to:接收方的 JID
    	+ (XMPPPresence *)presenceWithType:(NSString *)type to:(XMPPJID *)to;
    	
    	- (id)init;
    	- (id)initWithType:(NSString *)type;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)to;
    	
    	- (NSString *)type;
    	
    	- (NSString *)show;
    	- (NSString *)status;
    	
    	- (int)priority;
    	
    	- (int)intShow;
    	
    	- (BOOL)isErrorPresence;
    	
    	@end
    
  • presence 用來表明用戶的狀態,如:online、offline、away、dnd (請勿打擾) 等。當改變自己的狀態時,就會在 stream 的上下文中插入一個 Presence 元素,來表明自身的狀態。要想接受 presence 消息,必須經過一個叫做 presence subscription 的授權過程。

  • <type></type> 有以下類別(可選設置如:<type>subscribe</type>):

    type 說明
    available 上線
    unavailable 下線
    away 離開
    do not disturb 忙碌
    subscribe 訂閱其他用戶的狀態
    probe 請求獲取其他用戶的狀態
    unavailable 不可用,離線(offline)狀態
  • <show></show> 節點有以下類別,如 <show>dnd</show>

    show 說明
    chat 聊天中
    away 暫時離開
    xa eXtend Away,長時間離開
    dnd 勿打擾
  • <status></status> 節點

    • 這個節點表示狀態信息,內容比較自由,幾乎可以是所有類型的內容。常用來表示用戶當前心情,活動,聽的歌曲,看的視頻,所在的聊天室,訪問的網頁,玩的游戲等等。
  • <priority></priority> 節點

    • 范圍 -128~127。高優先級的 resource 能接受發送到 bare JID 的消息,低優先級的 resource 不能。優先級為負數的 resource 不能收到發送到 bare JID 的消息。
  • 發送一個用戶在線狀態的例子:

    	<presencefrom="alice@wonderland.lit/pda"> 
    	  <show>dnd</show> 
    	  <status>瀏覽器搜索</status> 
    	</presence> 
    

2.8 XMPPMessage 類

  • XMPPMessage 是 XMPP 框架給我們提供的,方便用於生成 XML 消息的數據。

    	@interface XMPPMessage : XMPPElement
    	
    	// Converts an NSXMLElement to an XMPPMessage element in place (no memory allocations or copying)
    	+ (XMPPMessage *)messageFromElement:(NSXMLElement *)element;
    	
    	+ (XMPPMessage *)message;
    	+ (XMPPMessage *)messageWithType:(NSString *)type;
    	+ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)to;
    	+ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    	+ (XMPPMessage *)messageWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	+ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid;
    	+ (XMPPMessage *)messageWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	+ (XMPPMessage *)messageWithType:(NSString *)type child:(NSXMLElement *)childElement;
    	
    	- (id)init;
    	- (id)initWithType:(NSString *)type;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)to;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid;
    	- (id)initWithType:(NSString *)type to:(XMPPJID *)jid elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	- (id)initWithType:(NSString *)type elementID:(NSString *)eid;
    	- (id)initWithType:(NSString *)type elementID:(NSString *)eid child:(NSXMLElement *)childElement;
    	- (id)initWithType:(NSString *)type child:(NSXMLElement *)childElement;
    	
    	- (NSString *)type;
    	- (NSString *)subject;
    	- (NSString *)body;
    	- (NSString *)bodyForLanguage:(NSString *)language;
    	- (NSString *)thread;
    	
    	- (void)addSubject:(NSString *)subject;
    	- (void)addBody:(NSString *)body;
    	- (void)addBody:(NSString *)body withLanguage:(NSString *)language;
    	- (void)addThread:(NSString *)thread;
    	
    	- (BOOL)isChatMessage;
    	- (BOOL)isChatMessageWithBody;
    	- (BOOL)isErrorMessage;
    	- (BOOL)isMessageWithBody;
    	
    	- (NSError *)errorMessage;
    	
    	@end
    
  • message 是一種基本 推送 消息方法,它不要求響應。主要用於 IM、groupChat、alert 和 notification 之類的應用中。

  • <type></type> 有以下類別(可選設置如:<type>chat</type>):

    type 說明
    normal 類似於 email,主要特點是不要求響應
    chat 類似於 qq 里的好友即時聊天,主要特點是實時通訊
    groupchat 類似於聊天室里的群聊
    headline 用於發送 alert 和 notification
    error 如果發送 message 出錯,發現錯誤的實體會用這個類別來通知發送者出錯了
  • <body></body> 節點

    • 所要發送的內容就放在 body 節點下
  • 消息節點的例子:

    	<messageto="lily@jabber.org/contact" type="chat"> 
    	    <body>您好?</body>
    	</message> 
    

3、XMPPFramework 框架使用

3.1 CocoaPods 導入框架

  • 1、通過 CocoaPods 導入第三方框架 XMPPFramework。

    • 在 Podfile 文件中加入如下代碼,在終端中,使用命令 pod install 下載添加 XMPPFramework 框架。

      	platform :ios, '8.0'
      	
      	target 'XMPPDemo' do
      	
      	    use_frameworks!
      	    pod 'XMPPFramework', '~> 3.7.0'
      	
      	end
      
  • 2、在需要使用 XMPPFramework 的文件中導入以下頭文件。

    	#import <XMPPFramework/XMPPFramework.h>
    

3.2 導入框架過程中問題解決

  • 1、用 Cocoapods 集成 XMPPFramework 遇 Module 'KissXML' not found 等問題解決方法。

    • 一般來說,通過 Coacopods 集成集成第三方框架,不會再有依賴庫方面的問題,所以需要檢查導入方式是否正確,最終找到原因,仔細看 githup 上導入說明

      	Install
      	
      	The minimum deployment target is iOS 8.0 / macOS 10.8.
      	
      	The easiest way to install XMPPFramework is using CocoaPods. Remember to add to the top of your Podfile the 
      	use_frameworks! line (even if you are not using swift):
      
    • 因此,Podfile 里必須寫入這一句

      	use_frameworks!
      
  • 2、Xcode8 之后 XMPP 重定義 Redefinition of module 'dnssd' 問題解決方法。

    • 在升級 Xcode 到 8 之后,原來的關於 XMPP 的項目運行報錯,錯誤信息為: Redefinition of module 'dnssd'。系統和XMPP框架同時用到了 'dnssd',大概就是錯誤的原因。

    • 解決方案:

      	# The version pushed to CocoaPods is very out of date, use master branch for now 
      	pod 'XMPPFramework', :git => "https://github.com/robbiehanson/XMPPFramework.git", :branch => 'master'
      
      	大概意思是需要更新 XMPP 框架,需要把 Podfile 文件中的 
      		pod 'XMPPFramework', '~> 3.6.6' 
      	用 
      		pod 'XMPPFramework', :git => "https://github.com/robbiehanson/XMPPFramework.git", :branch => 'master'
      	來替換
      	
      	或者直接改成 
      		pod 'XMPPFramework', '~> 3.7.0'
      
  • 3、在 pod update 的過程中有的童鞋會遇到下面這樣的錯誤。

    IM6

    • 這個是因為更新的 XMPP 框架中支持的最低版本為 iOS 8.0 / macOS 10.8。The minimum deployment target is iOS 8.0 / macOS 10.8.

    • 把 Podfile 文件中

      	platform:ios, '7.0' 
      
      • 的 7.0 改為 8.0 或以上。
  • 4、pod 更新完成了,出現下面這樣的錯誤。

    IM7

    • 到報錯的工程里面搜一下

      	Enable Strict Checking of objc_msgSend Calls
      
      • 改成相反的值就行了,別改沒有報錯的工程。

4、XMPPFramework 實現簡單聊天

  • 聊天實現的原理就是,一個客戶端通過 XMPP 協議把信息傳給服務器,服務器再發消息發給另一個客戶端。

4.1 用戶注冊

  • 初始化

    	/// 包含頭文件
    	#import <XMPPFramework/XMPPFramework.h>
    	
    	/// 遵守協議
    	<XMPPStreamDelegate>
    	
    	/// 定義 XMPP 服務器相關信息
    	#define HOST_DOMAIN     @"jhq0228-macbookair.local"
    	#define HOST_NAME       @"jhq0228-macbookair.local"
    	#define HOST_PORT       5222
    	
    	/// 注冊的賬號
    	@property (nonatomic, copy) NSString *registerUserName;
    	
    	/// 注冊的密碼
    	@property (nonatomic, copy) NSString *registerPassWord;
    	
    	/// XMPP 流
    	@property (nonatomic, strong) XMPPStream *stream;
    	
    	/// 初始化
    	self.stream = [[XMPPStream alloc] init];
    	self.stream.hostName = HOST_NAME;
    	self.stream.hostPort = HOST_PORT;
    	[self.stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
    
  • 與服務器建立鏈接

    	/// 與服務器建立鏈接
    	[self connectToSercerWithUserName:self.registerUserName resource:nil];
    	
    	#pragma mark 與服務器連接通信
    	
    		/// 與服務器建立鏈接,自定義方法
    		- (void)connectToSercerWithUserName:(NSString *)userName resource:(NSString *)resource {
    		    
    		    if ([self.stream isConnected]) {
    		        [self disconnectWithServer];
    		    }
    		    
    		    // jid
    		    self.stream.myJID = [XMPPJID jidWithUser:userName domain:HOST_DOMAIN resource:resource];
    		    
    		    NSError *error = nil;
    		    
    		    // 進行鏈接
    		    [self.stream connectWithTimeout:30.0 error:&error];
    		    
    		    if (error != nil) {
    		        NSLog(@"連接出現問題");
    		    }
    		}
    
  • 進行注冊

    	#pragma mark XMPPStreamDelegate 協議方法
    	
    		/// 與服務器連接成功
    		- (void)xmppStreamDidConnect:(XMPPStream *)sender {
    		    
    		    NSError *error1 = nil;
    		    
    		    // 進行注冊
    		    [self.stream registerWithPassword:self.registerPassWord error:&error1];
    		
    		    if (error1 != nil) {
    		        NSLog(@"注冊出現問題");
    		    }
    		}
    		
    		/// 與服務器連接超時
    		- (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender {
    		    
    		    NSLog(@"連接服務器超時,請檢查網絡鏈接后再試!");
    		}
    		
    		/// 注冊成功
    		- (void)xmppStreamDidRegister:(XMPPStream *)sender {
    		    
    		    NSLog(@"注冊成功");
    		}
    		
    		/// 注冊失敗
    		- (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error {
    		    
    		    NSLog(@"注冊失敗");
    		}
    

4.2 用戶登錄、注銷

  • 初始化

    	/// 包含頭文件
    	#import <XMPPFramework/XMPPFramework.h>
    	
    	/// 遵守協議
    	<XMPPStreamDelegate>
    	
    	/// 定義 XMPP 服務器相關信息
    	#define HOST_DOMAIN     @"jhq0228-macbookair.local"
    	#define HOST_NAME       @"jhq0228-macbookair.local"
    	#define HOST_PORT       5222
    	
    	/// 登錄的賬號
    	@property (nonatomic, copy) NSString *loginUserName;
    	
    	/// 登錄的密碼
    	@property (nonatomic, copy) NSString *loginPassWord;
    
    	/// XMPP 流
    	@property (nonatomic, strong) XMPPStream *stream;
    	
    	/// 初始化
    	self.stream = [[XMPPStream alloc] init];
    	self.stream.hostName = HOST_NAME;
    	self.stream.hostPort = HOST_PORT;
    	[self.stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
    
  • 與服務器建立鏈接

    	/// 與服務器建立鏈接
    	[self connectToSercerWithUserName:self.loginUserName resource:nil];
    	
    	#pragma mark 與服務器連接通信
    	
    		/// 與服務器建立鏈接,自定義方法
    		- (void)connectToSercerWithUserName:(NSString *)userName resource:(NSString *)resource {
    		    
    		    if ([self.stream isConnected]) {
    		        [self disconnectWithServer];
    		    }
    		    
    		    // jid
    		    self.stream.myJID = [XMPPJID jidWithUser:userName domain:HOST_DOMAIN resource:resource];
    		    
    		    NSError *error = nil;
    		    
    		    // 進行連接
    		    [self.stream connectWithTimeout:30.0 error:&error];
    		    
    		    if (error != nil) {
    		        NSLog(@"連接出現問題");
    		    }
    		}
    
  • 進行登錄認證

    	#pragma mark XMPPStreamDelegate 協議方法
    		
    		/// 與服務器連接成功
    		- (void)xmppStreamDidConnect:(XMPPStream *)sender {
    		    
    		    NSError *error = nil;
    		    
    		    // 進行登錄認證
    		    [self.stream authenticateWithPassword:self.loginPassWord error:&error];
    		    
    		    if (error != nil) {
    		        NSLog(@"登錄認證出現問題");
    		    }
    		}
    		
    		/// 與服務器連接超時
    		- (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender {
    		    
    		    NSLog(@"連接服務器超時,請檢查網絡鏈接后再試!");
    		}
    		
    		/// 登錄成功
    		- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
    		    
    		    NSLog(@"登錄成功");
    		    
    		    // 設置用戶在線狀態,如果沒有添加,別人給你發的消息服務器默認為離線狀態,是不會給你發送的
    		    XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"];
    		    [self.stream sendElement:presence];
    		}
    		
    		/// 登錄失敗
    		- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error {
    		    
    		    NSLog(@"登錄失敗");
    		}
    
  • 與服務器斷開鏈接,用戶注銷

    	#pragma mark 與服務器連接通信
    	
    		/// 與服務器斷開鏈接,用戶注銷,自定義方法
    		- (void)disconnectWithServer {
    		    
    		    // 斷開鏈接
    		    [self.stream disconnect];
    		}
    		
    	#pragma mark XMPPStreamDelegate 協議方法
    		
    		/// 注銷成功
    		- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error {
    		    
    		    NSLog(@"注銷成功");
    		    
    		    // 設置用戶下線狀態
    		    XMPPPresence *presene = [XMPPPresence presenceWithType:@"unavailable"];
    		    [self.stream sendElement:presene];
    		}
    
  • 用戶登錄信息本地化存儲

    	/// 包含頭文件
    	#import <SAMKeychain/SAMKeychain.h>
    	
    	/// 用戶名和密碼
    	@property (nonatomic, copy) NSString *userName;
    	@property (nonatomic, copy) NSString *userPasswd;
    	
    	/// 是否記住密碼
    	@property (nonatomic, assign, getter=isSavePasswd) BOOL savePasswd;
    
    	/// 保存用戶登錄信息
    	- (void)saveUserLoginInfo {
    	    
    	    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    	    
    	    [userDefaults setObject:self.userName forKey:@"userNameKey"];
    	    [userDefaults setBool:self.isSavePasswd forKey:@"isSavePwdKey"];
    	    [userDefaults synchronize];
    	    
    	    if (self.isSavePasswd) {
    	        [SAMKeychain setPassword:self.userPasswd forService:[NSBundle mainBundle].bundleIdentifier account:self.userName];
    	        
    	        NSLog(@"保存用戶登錄信息");
    	        
    	    } else {
    	        self.userPasswd = nil;
    	        [SAMKeychain deletePasswordForService:[NSBundle mainBundle].bundleIdentifier account:self.userName];
    	        
    	        NSLog(@"不保存用戶登錄信息");
    	    }
    	}
    	
    	/// 讀取用戶登錄信息
    	- (void)loadUserLoginInfo {
    	    
    	    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    	    
    	    self.userName = [userDefaults objectForKey:@"userNameKey"];
    	    self.savePasswd = [userDefaults boolForKey:@"isSavePwdKey"];
    	    
    	    self.userPasswd = [SAMKeychain passwordForService:[NSBundle mainBundle].bundleIdentifier account:self.userName];
    	}
    

4.3 好友管理

  • 初始化

    	/// 遵守協議
    	<XMPPStreamDelegate, XMPPRosterDelegate, XMPPRosterMemoryStorageDelegate>
    	
    	/// 好友列表
    	@property (nonatomic, strong) XMPPRoster *roster;
    	
    	/// 本地好友存儲器
    	@property (nonatomic, strong) XMPPRosterMemoryStorage *rosterMemoryStorage;
    	
    	// 添加好友模塊
    	self.rosterMemoryStorage = [[XMPPRosterMemoryStorage alloc] init];
    	self.roster = [[XMPPRoster alloc] initWithRosterStorage:self.rosterMemoryStorage
    	                                          dispatchQueue:dispatch_get_global_queue(0, 0)];
    	[self.roster activate:self.stream];                                       // 激活
    	[self.roster addDelegate:self delegateQueue:dispatch_get_main_queue()];   // 設置代理
    	[self.roster setAutoFetchRoster:YES];                                     // 設置好友同步策略,XMPP 一旦連接成功,自動同步好友到本地
    	[self.roster setAutoAcceptKnownPresenceSubscriptionRequests:NO];          // 關掉自動接收好友請求,默認開啟自動同意
    
  • 獲取好友列表

    	// 手動同步好友列表到本地好友存儲器
    	[self.roster fetchRoster];
    	
    	// 獲取好友列表,從本地好友存儲器中讀取好友信息
    	NSArray *users = self.rosterMemoryStorage.unsortedUsers;
    	
    	// 獲取好友賬號名稱
    	NSString *userName = [user[0] jid].user;
    	
    	// 獲取好友昵稱
    	NSString *userName = [users[0] nickname];
    	
    	// 獲取好友在線狀態
    	BOOL userStatus = [user[0] isOnline];
    	
    	#pragma mark XMPPRosterDelegate 協議方法
    
    		/// 開始同步好友列表到本地
    		- (void)xmppRosterDidBeginPopulating:(XMPPRoster *)sender withVersion:(NSString *)version {
    		    
    		}
    		
    		/// 同步到一個好友節點到本地
    		- (void)xmppRoster:(XMPPRoster *)sender didReceiveRosterItem:(NSXMLElement *)item {
    		    
    		}
    		
    		/// 同步好友列表到本地完成
    		- (void)xmppRosterDidEndPopulating:(XMPPRoster *)sender {
    		    
    		}
    
  • 刷新好友列表

    	#pragma mark - XMPPRosterMemoryStorageDelegate 協議方法
    
    		/// 本地好友存儲器發生改變
    		- (void)xmppRosterDidChange:(XMPPRosterMemoryStorage *)sender {
    		    
    		    // 如果設置了自動同步,當服務器的好友列表發生改變時,會自動同步存入本地好友存儲器 
    		}
    
  • 刷新好友狀態

    	#pragma mark XMPPStreamDelegate 協議方法
    
    		/// 好友狀態改變
    		- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence {
    		    
    		    // 收到對方取消定閱我的消息,對方刪除我、對方狀態改變
    		    
    		    if ([presence.type isEqualToString:@"unsubscribe"]) {
    		        
    		        // 從我的本地好友存儲器中將對方移除
    		        [self.roster removeUser:presence.from];
    		    }
    		}
    
  • 添加好友

    	/// 添加好友,自定義方法
    	- (void)addFriendWithUserName:(NSString *)userName remarkName:(NSString *)remarkName {
    	    
    	    NSString *jidString = userName;
    	    
    	    // 判斷有沒有域名,如果沒有域名,自己添加形成完整的 jid
    	    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    	    if (![jidString containsString:domainString]) {
    	        jidString = [jidString stringByAppendingString:domainString];
    	    }
    	    
    	    XMPPJID *friendJID = [XMPPJID jidWithString:jidString];
    	    
    	    // 添加好友,remarkName 為備注名稱
    	    [self.roster addUser:friendJID withNickname:remarkName];
    	    
    	    // [self.roster subscribePresenceToUser:friendJID];
    	}
    
  • 收到添加好友申請

    	#pragma mark XMPPRosterDelegate 協議方法
    
    		/// 收到添加好友請求
    		- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence {
    		   
        		NSString *name = [NSString stringWithFormat:@"添加 %@ 為好友?", presence.from.user];
        		
    			// 同意並添加對方為好友,YES 存入本地好友存儲器
    			[self.roster acceptPresenceSubscriptionRequestFrom:presence.from andAddToRoster:YES];
    				
    			// 拒絕添加對方為好友
    			[self.roster rejectPresenceSubscriptionRequestFrom:presence.from];
    		}
    
  • 刪除好友

    	/// 刪除好友,自定義方法
    	- (void)removeFriendWithUserName:(NSString *)userName {
    	    
    	    NSString *jidString = userName;
    	    
    	    // 判斷有沒有域名,如果沒有域名,自己添加形成完整的 jid
    	    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    	    if (![jidString containsString:domainString]) {
    	        jidString = [jidString stringByAppendingString:domainString];
    	    }
    	    
    	    XMPPJID *friendJID = [XMPPJID jidWithString:jidString];
    	    
    	    // 刪除好友
    	    [self.roster removeUser:friendJID];
    	}
    

4.4 文本消息管理

  • 初始化

    	/// 遵守協議
    	<XMPPStreamDelegate>
    	
    	/// 定義 XMPP 服務器相關信息
    	#define HOST_DOMAIN     @"jhq0228-macbookair.local"
    
  • 發送文本消息

    	/// 發送文本消息,自定義方法
    	- (void)sendMessage:(NSString *)message toUser:(NSString *)userName {
    	    
    	    // 消息結構
    	    /*
    	        <message type="chat" to="xiaoming@example.com">
    	            <body>Hello World</body>
    	        </message>
    	    */
    	    
    	    NSString *jidString = userName;                                             // 設置消息接收者
    	    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    	    if (![jidString containsString:domainString]) {
    	        jidString = [jidString stringByAppendingString:domainString];
    	    }
    	    
    	    // 構建消息
    	    NSXMLElement *msg = [NSXMLElement elementWithName:@"message"];
    	    [msg addAttributeWithName:@"type" stringValue:@"chat"];
    	    [msg addAttributeWithName:@"to" stringValue:jidString];
    	    
    	    NSXMLElement *body = [NSXMLElement elementWithName:@"body"];
    	    [body setStringValue:message];                                              // 設置文本消息內容
    	    
    	    [msg addChild:body];
    	    
    	    // 發送
    	    [self.stream sendElement:msg];
    	}
    
  • 接收文本消息

    	#pragma mark XMPPStreamDelegate 協議方法
    
    		/// 接收到消息
    		- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
    		    
    		    NSString *msg = [[message elementForName:@"body"] stringValue];
    		}
    
  • 消息回執

    • 這個是 XEP-0184 協議的內容。

    • 發送消息時附加回執請求

      	// 消息結構
      	/*
      		<message
      			from="northumberland@shakespeare.lit/westminster"
      			id="richars2-4.1.247"
      			to="kingrichard@royalty.england.lit/throne">
      			<body>Hello World</body>
      			<request xmlns="urn:xmpp:receipts"/>
      		</message>
      	 */
      	
      	NSString *jidString = userName;                                             // 設置消息接收者
      	NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
      	if (![jidString containsString:domainString]) {
      	    jidString = [jidString stringByAppendingString:domainString];
      	}
      	    
      	// 構建消息
      	NSString *siID = [XMPPStream generateUUID];
      	XMPPJID *jid = [XMPPJID jidWithString:jidString];
      	    
      	XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:jid elementID:siID];
      	    
      	NSXMLElement *receipt = [NSXMLElement elementWithName:@"request" xmlns:@"urn:xmpp:receipts"];
      	[msg addChild:receipt];                                                     // 設置消息回執
      	    
      	[msg addBody:message];                                                      // 設置消息內容
      	    
      	// 發送
      	[self.stream sendElement:msg];
      
    • 收到回執請求的消息,發送回執

      	/// 接收到消息,XMPPStreamDelegate 協議方法
      	- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
      	    
      	    // 消息結構
      	    /*
      	        <message
      	            from="kingrichard@royalty.england.lit/throne"
      	            id="bi29sg183b4v"
      	            to="northumberland@shakespeare.lit/westminster">
      	            <received xmlns="urn:xmpp:receipts" id="richars2-4.1.247">
      	        </message>
      	     */
      	    
      	    // 回執判斷
      	    NSXMLElement *request = [message elementForName:@"request"];
      	    
      	    if (request) {
      	        
      	        // 消息回執
      	        if ([request.xmlns isEqualToString:@"urn:xmpp:receipts"]) {
      	            
      	            // 組裝消息回執
      	            XMPPMessage *msg = [XMPPMessage messageWithType:[message attributeStringValueForName:@"type"]
      	                                                         to:message.from
      	                                                  elementID:[message attributeStringValueForName:@"id"]];
      	            
      	            NSXMLElement *recieved = [NSXMLElement elementWithName:@"received" xmlns:@"urn:xmpp:receipts"];
      	            [msg addChild:recieved];
      	            
      	            // 發送回執
      	            [self.stream sendElement:msg];
      	        }
      	        
      	    } else {
      	        
      	        NSXMLElement *received = [message elementForName:@"received"];
      	        
      	        if (received) {
      	            
      	            // 消息回執
      	            if ([received.xmlns isEqualToString:@"urn:xmpp:receipts"]) {
      	                
      	                // 發送成功
      	                NSLog(@"message send success!");
      	            }
      	        }
      	    }
      	    
      	    // 消息處理
      	    // ...
      	}
      

4.5 圖片消息管理

  • 圖片和語音文件發送的基本思路:

    • 先將圖片/語音轉化成二進制文件,然后將二進制文件進行 base64 編碼,編碼成字符串。在即將發送的 message 內添加一個子節點,節點的 stringValue(節點的值)設置這個編碼后的字符串。
    • 然后消息發出后取出消息文件的時候,通過 messageType 先判斷是不是圖片/語音信息,如果是圖片/語音信息先通過自己之前設置的節點名稱,把這個子節點的 stringValue 取出來,應該是一個 base64 之后的字符串。
  • 選擇圖片

    	/// 遵守協議
    	<UIImagePickerControllerDelegate, UINavigationControllerDelegate>
    	
    	UIImagePickerController *picker = [[UIImagePickerController alloc]init];
    	picker.delegate = self;
    	[self presentViewController:picker animated:YES completion:nil];
    	
    	#pragma mark - UIImagePickerControllerDelegate 代理方法
    	
    		- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
    		    
    		    UIImage *image = info[UIImagePickerControllerOriginalImage];
    		    NSData *imageData = UIImagePNGRepresentation(image);
    		    
    		    // 發送圖片消息,自定義方法
    		    [[XMPPManager defaultManager] sendMessage:imageData msgType:@"image" toUser:self.userName];
    		
    		    [self dismissViewControllerAnimated:YES completion:nil];
    		}
    
  • 發送圖片消息

    • msgType 自定義消息類型,image:圖片消息,audio:音頻消息
    	// 發送圖片消息
    	[[XMPPManager defaultManager] sendMessage:imageData msgType:@"image" toUser:self.userName];
    	
    	/// 發送圖片/音頻消息,自定義方法
    	- (void)sendMessage:(NSData *)msgData msgType:(NSString *)type toUser:(NSString *)userName {
    	    
    	    NSString *jidString = userName;                                             // 設置消息接收者
    	    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    	    if (![jidString containsString:domainString]) {
    	        jidString = [jidString stringByAppendingString:domainString];
    	    }
    	    
    	    XMPPJID *jid = [XMPPJID jidWithString:jidString];
    	    XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:jid];
    	    
    	    [msg addBody:type];
    	    
    	    // 轉換成 base64 的編碼
    	    NSString *base64str = [msgData base64EncodedStringWithOptions:0];
    	    
    	    // 設置節點內容
    	    XMPPElement *attachment = [XMPPElement elementWithName:@"attachment" stringValue:base64str];
    	    
    	    // 包含子節點
    	    [msg addChild:attachment];
    	    
    	    // 發送消息
    	    [self.stream sendElement:msg];
    	}
    
  • 接收圖片消息

    	#pragma mark XMPPStreamDelegate 協議方法
    
    		/// 接收到消息
    		- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
    		    
    			if ([message.body isEqualToString:@"image"]) {
    			    
    			    for (XMPPElement *node in message.children) {
    			        
    			        // 取出消息的解碼
    			        NSString *base64str = node.stringValue;
    			        NSData *data = [[NSData alloc] initWithBase64EncodedString:base64str options:0];
    			        
    			        UIImage *image = [[UIImage alloc] initWithData:data];
    			    }
    			}
    		}
    

4.6 語音消息管理

  • 圖片和語音文件發送的基本思路:

    • 先將圖片/語音轉化成二進制文件,然后將二進制文件進行 base64 編碼,編碼成字符串。在即將發送的 message 內添加一個子節點,節點的 stringValue(節點的值)設置這個編碼后的字符串。
    • 然后消息發出后取出消息文件的時候,通過 messageType 先判斷是不是圖片/語音信息,如果是圖片/語音信息先通過自己之前設置的節點名稱,把這個子節點的 stringValue 取出來,應該是一個 base64 之后的字符串。
  • 錄制/播放語音

    	/// 包含頭文件
    	#import <AVFoundation/AVFoundation.h>
    	
    	/// 錄音器
    	@property(nonatomic, strong) AVAudioRecorder *recorder;
    	
    	/// 錄音時長
    	@property(nonatomic, assign) NSTimeInterval recordTime;
    	
    	/// 錄音地址
    	@property(nonatomic, strong) NSURL *recordURL;
    	
    	/// 播放器
    	@property(nonatomic, strong) AVAudioPlayer *player;
    	
    	/// 開始錄音
    		
    		// 自定義方法
    		- (IBAction)startRecord:(UIButton *)sender {
    		    
    		    // 創建錄音文件保存路徑
    		    NSString *urlStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    		    self.recordURL = [NSURL URLWithString:[urlStr stringByAppendingPathComponent:@"myRecord.caf"]];
    		    
    		    // 創建錄音格式設置
    		    NSMutableDictionary *dicM = [NSMutableDictionary dictionary];
    		    [dicM setObject:@(kAudioFormatLinearPCM) forKey:AVFormatIDKey];     // 設置錄音格式
    		    [dicM setObject:@(8000) forKey:AVSampleRateKey];                    // 設置錄音采樣率,8000 是電話采樣率,對於一般錄音已經夠了
    		    [dicM setObject:@(1) forKey:AVNumberOfChannelsKey];                 // 設置通道,這里采用單聲道
    		    [dicM setObject:@(8) forKey:AVLinearPCMBitDepthKey];                // 每個采樣點位數,分為 8、16、24、32
    		    [dicM setObject:@(YES) forKey:AVLinearPCMIsFloatKey];               // 是否使用浮點數采樣
    		    NSDictionary *setting = [dicM copy];
    		    
    		    // 創建錄音機
    		    self.recorder = [[AVAudioRecorder alloc] initWithURL:self.recordURL settings:setting error:NULL];
    		
    		    // 開始錄音
    		    [self.recorder record];
    		}
    	
    	/// 停止錄音
    		
    		// 自定義方法
    		- (IBAction)stopRecord:(UIButton *)sender {
    		    
    		    NSTimeInterval time = self.recorder.currentTime;
    		    [self.recorder stop];
    		    
    		    if (time < 1.5) {
    		        NSLog(@"時間太短");
    		    } else {
    		        NSLog(@"錄音完成");
    		    }
    		}
    	
    	/// 播放錄音
    		
    		// 自定義方法
    		- (void)playAudioData:(NSData *)data {
    		    
    		    self.player = [[AVAudioPlayer alloc] initWithData:data error:NULL];
    		    self.player.numberOfLoops = 0;
    		    [self.player prepareToPlay];
    		    [self.player play];
    		}
    
  • 發送語音消息

    • msgType 自定義消息類型,image:圖片消息,audio:音頻消息
    	// 發送語音消息
    	NSData *audioData = [NSData dataWithContentsOfURL:self.recordURL];
    	NSString *type = [NSString stringWithFormat:@"audio:%.1f秒", self.recordTime];
    	[[XMPPManager defaultManager] sendMessage:audioData msgType:type toUser:self.userName];
    	
    	/// 發送圖片/音頻消息,自定義方法
    	- (void)sendMessage:(NSData *)msgData msgType:(NSString *)type toUser:(NSString *)userName {
    	    
    	    NSString *jidString = userName;                                             // 設置消息接收者
    	    NSString *domainString = [NSString stringWithFormat:@"@%@", HOST_DOMAIN];
    	    if (![jidString containsString:domainString]) {
    	        jidString = [jidString stringByAppendingString:domainString];
    	    }
    	    
    	    XMPPJID *jid = [XMPPJID jidWithString:jidString];
    	    XMPPMessage *msg = [XMPPMessage messageWithType:@"chat" to:jid];
    	    
    	    [msg addBody:type];
    	    
    	    // 轉換成 base64 的編碼
    	    NSString *base64str = [msgData base64EncodedStringWithOptions:0];
    	    
    	    // 設置節點內容
    	    XMPPElement *attachment = [XMPPElement elementWithName:@"attachment" stringValue:base64str];
    	    
    	    // 包含子節點
    	    [msg addChild:attachment];
    	    
    	    // 發送消息
    	    [self.stream sendElement:msg];
    	}
    
  • 接收語音消息

    	#pragma mark XMPPStreamDelegate 協議方法
    
    		/// 接收到消息
    		- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message {
    		    
    			if ([message.body hasPrefix:@"audio"]) {
    				    
    				for (XMPPElement *node in message.children) {
    				    
    					// 取出消息的解碼
    					NSString *base64str = node.stringValue;
    					NSData *data = [[NSData alloc] initWithBase64EncodedString:base64str options:0];
    				}
    			}
    		}
    

4.7 心跳檢測

  • 為了監聽服務器是否有效,增加心跳監聽,用 XEP-0199 協議。

  • 在 XMPPFrameWork 框架下,封裝了 XMPPAutoPing 和 XMPPPing 兩個類都可以使用,因為 XMPPAutoPing 已經組合進了 XMPPPing 類,所以 XMPPAutoPing 使用起來更方便。

  • 初始化並啟動 ping

    	/// 包含頭文件
    	#import <XMPPFramework/XMPPFramework.h>
    	
    	/// 遵守協議
    	<XMPPAutoPingDelegate>>
    	
    	/// 心跳檢測
    	@property (nonatomic, strong) XMPPAutoPing *autoPing;
    	
    	// 添加心跳檢測模塊
    	self.autoPing = [[XMPPAutoPing alloc] init];    // 發送的是一個 stream:ping,對方如果想表示自己是活躍的,應該返回一個 pong
    	[self.autoPing activate:self.stream];           // 激活
    	[self.autoPing addDelegate:self delegateQueue:dispatch_get_main_queue()];
    	self.autoPing.pingInterval = 1000;              // 定時發送 ping 時間
    	self.autoPing.respondsToQueries = YES;          // 不僅僅是服務器來得響應,如果是普通的用戶,一樣會響應
    	self.autoPing.targetJID = [XMPPJID jidWithString:HOST_DOMAIN];      // 設置 ping 目標服務器
                                                                           // 如果為 nil,則監聽 stream 當前連接上的那個服務器
    	
    	#pragma mark - XMPPAutoPingDelegate 協議方法
    	
    		/// 已經發送 ping
    		- (void)xmppAutoPingDidSendPing:(XMPPAutoPing *)sender {
    		    
    		    NSLog(@"xmppAutoPingDidSendPing");
    		}
    		
    		/// 接收到 pong
    		- (void)xmppAutoPingDidReceivePong:(XMPPAutoPing *)sender {
    		    
    		    NSLog(@"xmppAutoPingDidReceivePong");
    		}
    		
    		/// ping 超時
    		- (void)xmppAutoPingDidTimeout:(XMPPAutoPing *)sender {
    		    
    		    NSLog(@"xmppAutoPingDidTimeout");
    		}
    
  • 停止 ping

    	// 停止 ping
    	[self.autoPing deactivate];
    	[self.autoPing removeDelegate:self];
    	self.autoPing = nil;
    

4.8 自動重連

  • 當意外與服務器斷開連接,自動重新連接上去,並且將上一次的信息自動加上去。

  • 初始化

    	/// 包含頭文件
    	#import <XMPPFramework/XMPPFramework.h>
    	
    	/// 遵守協議
    	<XMPPReconnectDelegate>>
    	
    	/// 自動重連
    	@property (nonatomic, strong) XMPPReconnect *reconnect;
    	
    	// 添加自動重連模塊
    	self.reconnect = [[XMPPReconnect alloc] init];
    	[self.reconnect activate:self.stream];          // 激活
    	[self.reconnect addDelegate:self delegateQueue:dispatch_get_main_queue()];
    	self.reconnect.autoReconnect = YES;             // 設置是否自動重新連接
    
    	#pragma mark - XMPPReconnectDelegate 協議方法
    		
    		/// 設置是否自動重新連接
    		- (BOOL)xmppReconnect:(XMPPReconnect *)sender shouldAttemptAutoReconnect:(SCNetworkConnectionFlags)connectionFlags {
    		    
    		    return YES;
    		}
    		
    		/// 意外斷開連接
    		- (void)xmppReconnect:(XMPPReconnect *)sender didDetectAccidentalDisconnect:(SCNetworkConnectionFlags)connectionFlags {
    		    
    		    NSLog(@"didDetectAccidentalDisconnect");
    		}
    

5、XMPPFramework 快速登錄

6、XMPPFramework 重連以及其他問題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM