IM開發-XMPP iOS開發(轉)


最近項目中需要集成IM功能,市面上有很多的第三方提供im服務,比如環信、融雲等,但都有使用限制的地方,如果不使用第三方可以自己去實現一套IM系統,不過一個IM系統涉及到的東西比較多,開發難度較高。另一種選擇是使用xmpp,xmpp的優點是有很多的開源實現,比如服務端的ejabberd、Openfire,iOS以及安卓端都很優秀的開源庫可以使用,而且傳輸安全以及擴展性強等(環信也是基於xmpp);同時也有一些缺點,比如不能傳輸二進制數據以及費流量等,有些地方需要去改進。
下面記錄如何使用XMPP來簡單實現IM功能,在這之前需要先搭建本地服務器用於測試。

ejabberd服務器搭建

為了實現IM聊天,需要先搭建一個XMPP服務器,這里我們采用ejabberd來作為服務器,ejabbered采用Erlang語言編寫,由於語言的特性天生適合高並發的情景。

  • 安裝Erlang
    在安裝ejabbered之前需要先安裝Erlang,在控制台輸入命令
    brew install erlang
    

     

    等待安裝完成即可。
  • 安裝ejabbered
    ejabbered服務器的安裝非常簡單,下載ejabberd安裝包直接安裝即可。

    ejabbered安裝包.png

    安裝完之后可以在Applications目錄先找到:

    目錄結構.png

    由於我們的服務器只是測試用的,比較重要的是bin目錄下的start和stop命令,這兩個分別用於開啟和關閉xmpp服務,設置這兩個命令為可執行:
    chmod 755 stop
    chmod 755 start
    

     

    現在輸入命令 ./start可以看到如下的頁面

啟動頁面.png
  • 添加用戶
    現在我們的服務器只有administrators,我們需要增加幾個用戶,也方便后面測試,可以輸入http://localhost:5280/admin/ 到ejabberd的后台管理頁面:

管理頁面.png


點擊虛擬主機,選擇用戶菜單可以看到添加用戶的頁面如下,我們添加了user1@lujiangbin.local和user2@lujiangbin.local兩個用戶:


添加用戶.png
  • 測試服務器
    現在可以來測試服務器是否能正常運行了。要測試的話我們需要兩個支持xmpp協議的客戶端,可以使用Mac OSX自帶的iMessage(登陸user1)和Adium(登陸user2) ,其中支持xmpp協議的客戶端有:
    http://xmpp.org/xmpp-software/clients/

打開iMessage添加用戶


添加用戶user1.png

iMessage可能會提示服務器的證書需要驗證,點擊繼續即可:


屏幕快照 2015-10-08 下午3.34.55.png


可以看到user1已經登陸成功了。


登陸成功

接着打開Adium添加user2@lujiangbin.local用戶,添加過程跟iMessage類似:


添加用戶1.png


設置連接服務器為localhost:


添加用戶2.png

由於現在user1和user2還不是好友關系,因此沒法進行聊天,點擊iMessage添加好友user2@lujiangbin.local:


添加好友


在Adium會收到驗證請求,點擊接受雙方互加好友,接着就可以進行im聊天了:


好友添加.png

聊天.png


這樣我們確認本地的xmpp服務器是可用的,接下來就可以着手客戶端的開發了

 

這里我們使用XMPPFramework這個開源庫,安卓平台可以使用Smack(最好使用4.1以及之后的版本,支持流管理),為了簡單起見這里只實現登陸、獲取好友列表以及聊天等功能,頁面如下所示:


user2的好友列表.png

聊天.png

xmpp初始化

  在開始使用xmpp進行IM聊天之前,我們需要初始化xmpp流,接入我們需要的模塊:

#define JBXMPP_HOST @"lujiangbin.local"
#define JBXMPP_PORT 5222
- (void)setupStream
{
    if (!_xmppStream) {
        _xmppStream = [[XMPPStream alloc] init];

        [self.xmppStream setHostName:JBXMPP_HOST]; //設置xmpp服務器地址
        [self.xmppStream setHostPort:JBXMPP_PORT]; //設置xmpp端口,默認5222
        [self.xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
        [self.xmppStream setKeepAliveInterval:30]; //心跳包時間

        //允許xmpp在后台運行
        self.xmppStream.enableBackgroundingOnSocket=YES;

        //接入斷線重連模塊
        _xmppReconnect = [[XMPPReconnect alloc] init];
        [_xmppReconnect setAutoReconnect:YES];
        [_xmppReconnect activate:self.xmppStream];

        //接入流管理模塊,用於流恢復跟消息確認,在移動端很重要
        _storage = [XMPPStreamManagementMemoryStorage new];
        _xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_storage];
        _xmppStreamManagement.autoResume = YES;
        [_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
        [_xmppStreamManagement activate:self.xmppStream];

        //接入好友模塊,可以獲取好友列表
        _xmppRosterMemoryStorage = [[XMPPRosterMemoryStorage alloc] init];
        _xmppRoster = [[XMPPRoster alloc] initWithRosterStorage:_xmppRosterMemoryStorage];
        [_xmppRoster activate:self.xmppStream];
        [_xmppRoster addDelegate:self delegateQueue:dispatch_get_main_queue()];

        //接入消息模塊,將消息存儲到本地
        _xmppMessageArchivingCoreDataStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];
        _xmppMessageArchiving = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:_xmppMessageArchivingCoreDataStorage dispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 9)];
        [_xmppMessageArchiving activate:self.xmppStream];
    }
}

 

登陸

xmpp的登陸過程比較繁瑣,登陸過程包括初始化流、TLS握手和SASL驗證等,想要了解各個階段服務端跟客戶端之間交互的內容可以查看這里,就不在詳細介紹。XMPPFramework將整個復雜的登陸過程都封裝起來了,客戶端調用connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr連接服務器,然后在xmppStreamDidConnect代理方法輸入密碼驗證登陸,這里我們使用在搭建服務器時創建的兩個用戶,user1和user2。

#define JBXMPP_DOMAIN @"lujiangbin.local"
-(void)loginWithName:(NSString *)userName andPassword:(NSString *)password
{
    _myJID = [XMPPJID jidWithUser:userName domain:JBXMPP_DOMAIN resource:@"iOS"];
    self.myPassword = password;
    [self.xmppStream setMyJID:_myJID];
    NSError *error = nil;
    [_xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&error];
}

#pragma mark -- connect delegate
//輸入密碼驗證登陸
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
    NSError *error = nil;
   [[self xmppStream] authenticateWithPassword:_myPassword error:&error];
}

//登陸成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
    NSLog(@"%s",__func__);
    //發送在線通知給服務器,服務器才會將離線消息推送過來
    XMPPPresence *presence = [XMPPPresence presence]; // 默認"available" 
    [[self xmppStream] sendElement:presence];
    //啟用流管理
    [_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
}
//登陸失敗
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
    NSLog(@"%s",__func__);
}

 

獲取好友列表

登陸成功之后,我們可以通過XMPPRoster去獲取好友列表,在示例中我們為了簡單起見使用
XMPPRosterMemoryStorage將好友存儲在內存中,在實際場景你可以將好友存儲在
XMPPRosterCoreDataStorage,xmppframework使用coredata將好友保存到本地,可以在初始化xmpp流的時候設置。為了獲取好友列表,只需調用fetchRoster方法:

//獲取服務器好友列表
    [[[JBXMPPManager sharedInstance] xmppRoster] fetchRoster];

 

消息

  • 消息發送
    只需要調用xmpp的sendElement:方法,由於xmpp只支持文本,所以假如你想發送二進制的文件,比如語音圖片等,可以先壓縮然后用base64編碼,接收方收到再做解碼工作,比如語音可以壓縮成amr格式,amr格式安卓可以直接播放,iOS需要在解壓成wav格式,可以參考demo
- (void)sendMessage:(NSString *)message to:(XMPPJID *)jid
{
    XMPPMessage* newMessage = [[XMPPMessage alloc] initWithType:@"chat" to:jid];
    [newMessage addBody:message]; //消息內容
    [_xmppStream sendElement:newMessage];
}

 

  • 消息接收
    當收到消息的時候,xmppframework會調用didReceiveMessage:代理方法,由於我們在初始化流的時候將消息設置存儲到本地,可以看到XMPPMessageArchiving在didReceiveMessage收到消息的時候將消息存儲起來。
// XMPPMessageArchiving.m
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
{
     if ([self shouldArchiveMessage:message outgoing:YES xmppStream:sender])
     {
         [xmppMessageArchivingStorage archiveMessage:message outgoing:YES     xmppStream:sender];
     }
}

 

  • 消息確認
    為了防止發出去的消息丟失了,可以接入消息回執模塊(XEP-184),這樣對方每收到一條消息的時候都會返回一條確認的消息,如果沒收到該條確認消息可以認為發送失敗,確認消息的格式如下:
  <message to="user2@lujiangbin.local"> <received xmlns="urn:xmpp:receipts" id="消息ID"/> </message>

不過這種方法也有些弊端,比如每次收到一條消息都必須回復,一定程度上會浪費流量以及影響服務器的性能,所以一般采用流管理來實現消息確認。

流關閉

當退出程序的時候,最好能給服務器發送關閉流的通知,也就是發送</stream:stream>結束流,服務器收到之后開始將后續發給該對象的消息收集到離線倉庫中,當客戶端重新上線的時候,服務端會主動將離線消息推送過來,這樣不會丟失消息。由於客戶端的操作經常是切到后台然后直接關掉程序,因此可以監聽UIApplicationWillTerminateNotification消息,然后手動關閉流。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil];

#pragma mark -- terminate
/**
 *  申請后台更多的時間來完成關閉流的任務
 */
-(void)applicationWillTerminate
{
    UIApplication *app=[UIApplication sharedApplication];
    UIBackgroundTaskIdentifier taskId;
    taskId=[app beginBackgroundTaskWithExpirationHandler:^(void){
        [app endBackgroundTask:taskId];
    }];
    if(taskId==UIBackgroundTaskInvalid){
        return;
    }
    [_xmppStream disconnectAfterSendingEndStream];
}

 

流管理

Stream Management是為了流恢復跟節確認而增加的。理想情況下,客戶端發送關閉流的通知給服務器,服務器將后續的消息存儲到離線倉庫,等客戶端再登陸上線的時候推送過來,但是在移動端網絡可能隨時斷掉,這時候服務器並不會馬上察覺(只能依靠TCP超時或者服務器自己的心跳包),它會認為對方還在線,將后續的消息發送過去,這樣到服務器知道對方掉線的這段時間,期間的消息就丟失了,所以需要流管理來處理。

  • 節確認(stanza acknowledgement)
    用來確認一段時間內節(包括<iq/>,<message/>,<presence/>,不是<iq/>
    ,<message/>,或<presence/>這樣的stanzas不會在流管理中被確認跟計數的)是否被對方接收,客戶端跟服務端都各自有有兩個h值用來維護這些信息。從客戶端來看,其中一個h值用於記錄收到的節,比如當收到服務推送的消息時,會將該h值加1;另一個h值用於記錄發出去的節,當發出一條消息時該h值也加1,所以為了確認消息是否被收到其實都是在比較雙方的兩個h值。
    為了查詢這些h值,xmpp定義了<a/>和<r/>兩個元素,<r/>用戶請求節的確認消息,<a/>用於回答節的確認消息,必須攜帶自己已處理的h值。
服務端: <r xmlns='urn:xmpp:sm:3'/> 客戶端: <a xmlns='urn:xmpp:sm:3' h='3'/>

比如服務端發送<r>請求,客戶端返回自己接受收到的h值(3),然后服務端會根據這個h值跟它自己記錄發出去的節的h值做比較,假如小的話會重新發送剩下的節,來防止節丟失。

  • 流恢復
    由於移動網絡可能隨時down掉,所以在我們重連上來的時候需要的是快速恢復上一次的流,而不是重新新建一個流,roster的檢索以及狀態的廣播,流管理可以通過上一次的流id(當啟用流管理的時候,服務端會生成一個id來表示一個流)以及雙方的h值來完成流的快速恢復以及這期間的節確認,發送未被確認的節。

  • 開啟流管理
    要想啟用流管理,客戶端發送<enable/>元素給服務端,服務端返回<enabled/>元素表示該流已經被管理了,同時有一個id值來標示這個流,xmppframework開啟流管理只需要調用
    enableStreamManagementWithResumption: maxTimeout:接口:

客戶端: <enable xmlns='urn:xmpp:sm:3' resume='true'/> 服務端: <enabled xmlns='urn:xmpp:sm:3' id='流id' resume='true'/>
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
    //登陸完成后,啟用流管理
    [_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
}

 

 
  • 請求流恢復
    當客戶端想要恢復一個流的時候,需要發送<resume/>元素以及一個previd值,也就是想要恢復的上一次的流id,當流可以恢復的時候,服務端會返回<resumed/>元素,雙方都會攜帶一個h值用於節確認。
客戶端: <resume xmlns='urn:xmpp:sm:3' h='客戶端接收的h值' previd='流id'/> 服務端: <resumed xmlns='urn:xmpp:sm:3' h='服務端接收的h值' previd='流id'/>

xmppframework將這部分邏輯封裝在內部,不過這些h跟流id的值是存儲在內存中,當程序退出的時候這些值就沒了,也就無法恢復流。所以實際應用的時候需要將這些值保存到本地,比如demo里的XMPPStreamManagementPersistentStorage。

xmpp注意點

  • 文件http上傳
    由於xmpp只支持文本,所以類似音頻這種二進制文件需要用base64轉成文本形式,但更好的方式是采用http上傳文件,消息體保存的是文件對應的URL。
  • 登陸改進
    xmpp的登陸涉及到始化流、TLS握手和SASL驗證等,步驟比較繁瑣,可以根據情況簡化流程。
  • TLS加密
    假如我們的im需要加密,可以開啟TLS,不過iOS的TLS不支持壓縮
    GCDAsyncSocket內部已經幫我們封裝協商的過程,不過我們可能會收到錯誤:kCFStreamErrorDomainSSL Code=-9807,這是由於服務器證書並不是正式的證書,所以需要手動去認證:
//設置手動認證證書
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings setObject:@YES forKey:GCDAsyncSocketManuallyEvaluateTrust];
[asyncSocket startTLS:settings];

- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
     // 開始接收數據
     [sock readDataWithTimeout:TIMEOUT_XMPP_READ_STREAM tag:TAG_XMPP_READ_STREAM];
}

//在delegate方法中,手動信任
-(void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL))completionHandler
{
    if (completionHandler)
        completionHandler(YES);
}

 

一個簡單的demo工程可以在這里找到。


免責聲明!

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



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