IM開發(fā)(2)-XMPP iOS開發(fā)

搭建完本地服務(wù)器之后算途,我們便可以著手客戶端的工作溪窒,這里我們使用XMPPFramework這個開源庫,安卓平臺可以使用Smack(最好使用4.1以及之后的版本焕济,支持流管理),為了簡單起見這里只實現(xiàn)登陸盔几、獲取好友列表以及聊天等功能吼蚁,頁面如下所示:

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]; //設(shè)置xmpp服務(wù)器地址
        [self.xmppStream setHostPort:JBXMPP_PORT]; //設(shè)置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];
        
        //接入流管理模塊,用于流恢復(fù)跟消息確認顺献,在移動端很重要
        _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驗證等度硝,想要了解各個階段服務(wù)端跟客戶端之間交互的內(nèi)容可以查看這里,就不在詳細介紹寿冕。XMPPFramework將整個復(fù)雜的登陸過程都封裝起來了蕊程,客戶端調(diào)用connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr連接服務(wù)器,然后在xmppStreamDidConnect代理方法輸入密碼驗證登陸驼唱,這里我們使用在搭建服務(wù)器時創(chuàng)建的兩個用戶藻茂,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__);
   //發(fā)送在線通知給服務(wù)器,服務(wù)器才會將離線消息推送過來
   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去獲取好友列表掀序,在示例中我們?yōu)榱撕唵纹鹨娛褂?br> XMPPRosterMemoryStorage將好友存儲在內(nèi)存中,在實際場景你可以將好友存儲在
XMPPRosterCoreDataStorage惭婿,xmppframework使用coredata將好友保存到本地不恭,可以在初始化xmpp流的時候設(shè)置。為了獲取好友列表财饥,只需調(diào)用fetchRoster方法:

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

消息

  • 消息發(fā)送
    只需要調(diào)用xmpp的sendElement:方法换吧,由于xmpp只支持文本,所以假如你想發(fā)送二進制的文件佑力,比如語音圖片等,可以先壓縮然后用base64編碼筋遭,接收方收到再做解碼工作打颤,比如語音可以壓縮成amr格式,amr格式安卓可以直接播放漓滔,iOS需要在解壓成wav格式编饺,可以參考demo
- (void)sendMessage:(NSString *)message to:(XMPPJID *)jid
{
    XMPPMessage* newMessage = [[XMPPMessage alloc] initWithType:@"chat" to:jid];
    [newMessage addBody:message]; //消息內(nèi)容
    [_xmppStream sendElement:newMessage];
}
  • 消息接收
    當收到消息的時候,xmppframework會調(diào)用didReceiveMessage:代理方法响驴,由于我們在初始化流的時候?qū)⑾⒃O(shè)置存儲到本地透且,可以看到XMPPMessageArchiving在didReceiveMessage收到消息的時候?qū)⑾⒋鎯ζ饋怼?/li>
// XMPPMessageArchiving.m
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
{
     if ([self shouldArchiveMessage:message outgoing:YES xmppStream:sender])
     {
         [xmppMessageArchivingStorage archiveMessage:message outgoing:YES     xmppStream:sender];
     }
}
  • 消息確認
    為了防止發(fā)出去的消息丟失了,可以接入消息回執(zhí)模塊(XEP-184)豁鲤,這樣對方每收到一條消息的時候都會返回一條確認的消息秽誊,如果沒收到該條確認消息可以認為發(fā)送失敗,確認消息的格式如下:
  
  <message to="user2@lujiangbin.local">
    <received xmlns="urn:xmpp:receipts" id="消息ID"/>
  </message>
 

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

流關(guān)閉

當退出程序的時候最易,最好能給服務(wù)器發(fā)送關(guān)閉流的通知怒坯,也就是發(fā)送</stream:stream>結(jié)束流,服務(wù)器收到之后開始將后續(xù)發(fā)給該對象的消息收集到離線倉庫中藻懒,當客戶端重新上線的時候剔猿,服務(wù)端會主動將離線消息推送過來,這樣不會丟失消息嬉荆。由于客戶端的操作經(jīng)常是切到后臺然后直接關(guān)掉程序归敬,因此可以監(jiān)聽UIApplicationWillTerminateNotification消息,然后手動關(guān)閉流员寇。

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

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

流管理

Stream Management是為了流恢復(fù)跟節(jié)確認而增加的弄慰。理想情況下,客戶端發(fā)送關(guān)閉流的通知給服務(wù)器蝶锋,服務(wù)器將后續(xù)的消息存儲到離線倉庫陆爽,等客戶端再登陸上線的時候推送過來,但是在移動端網(wǎng)絡(luò)可能隨時斷掉扳缕,這時候服務(wù)器并不會馬上察覺(只能依靠TCP超時或者服務(wù)器自己的心跳包)慌闭,它會認為對方還在線,將后續(xù)的消息發(fā)送過去躯舔,這樣到服務(wù)器知道對方掉線的這段時間驴剔,期間的消息就丟失了,所以需要流管理來處理粥庄。

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

比如服務(wù)端發(fā)送<r>請求画恰,客戶端返回自己接受收到的h值(3)卖怜,然后服務(wù)端會根據(jù)這個h值跟它自己記錄發(fā)出去的節(jié)的h值做比較,假如小的話會重新發(fā)送剩下的節(jié)阐枣,來防止節(jié)丟失马靠。

  • 流恢復(fù)
    由于移動網(wǎng)絡(luò)可能隨時down掉奄抽,所以在我們重連上來的時候需要的是快速恢復(fù)上一次的流,而不是重新新建一個流甩鳄,roster的檢索以及狀態(tài)的廣播逞度,流管理可以通過上一次的流id(當啟用流管理的時候,服務(wù)端會生成一個id來表示一個流)以及雙方的h值來完成流的快速恢復(fù)以及這期間的節(jié)確認妙啃,發(fā)送未被確認的節(jié)档泽。

  • 開啟流管理
    要想啟用流管理,客戶端發(fā)送<enable/>元素給服務(wù)端揖赴,服務(wù)端返回<enabled/>元素表示該流已經(jīng)被管理了馆匿,同時有一個id值來標示這個流,xmppframework開啟流管理只需要調(diào)用
    enableStreamManagementWithResumption: maxTimeout:接口:

客戶端: <enable xmlns='urn:xmpp:sm:3' resume='true'/>
服務(wù)端: <enabled xmlns='urn:xmpp:sm:3' id='流id' resume='true'/>

- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
    //登陸完成后燥滑,啟用流管理
    [_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
}

  • 請求流恢復(fù)
    當客戶端想要恢復(fù)一個流的時候渐北,需要發(fā)送<resume/>元素以及一個previd值,也就是想要恢復(fù)的上一次的流id铭拧,當流可以恢復(fù)的時候赃蛛,服務(wù)端會返回<resumed/>元素,雙方都會攜帶一個h值用于節(jié)確認搀菩。
客戶端: <resume xmlns='urn:xmpp:sm:3' h='客戶端接收的h值' previd='流id'/>
服務(wù)端: <resumed xmlns='urn:xmpp:sm:3' h='服務(wù)端接收的h值' previd='流id'/>

xmppframework將這部分邏輯封裝在內(nèi)部呕臂,不過這些h跟流id的值是存儲在內(nèi)存中,當程序退出的時候這些值就沒了肪跋,也就無法恢復(fù)流歧蒋。所以實際應(yīng)用的時候需要將這些值保存到本地,比如demo里的XMPPStreamManagementPersistentStorage州既。

xmpp注意點

  • 文件http上傳
    由于xmpp只支持文本谜洽,所以類似音頻這種二進制文件需要用base64轉(zhuǎn)成文本形式,但更好的方式是采用http上傳文件易桃,消息體保存的是文件對應(yīng)的URL褥琐。
  • 登陸改進
    xmpp的登陸涉及到始化流锌俱、TLS握手和SASL驗證等晤郑,步驟比較繁瑣,可以根據(jù)情況簡化流程贸宏。
  • TLS加密
    假如我們的im需要加密造寝,可以開啟TLS,不過iOS的TLS不支持壓縮吭练。
    GCDAsyncSocket內(nèi)部已經(jīng)幫我們封裝協(xié)商的過程诫龙,不過我們可能會收到錯誤:kCFStreamErrorDomainSSL Code=-9807,這是由于服務(wù)器證書并不是正式的證書鲫咽,所以需要手動去認證:
//設(shè)置手動認證證書
NSMutableDictionary *settings = [NSMutableDictionary dictionary];
[settings setObject:@YES forKey:GCDAsyncSocketManuallyEvaluateTrust];
[asyncSocket startTLS:settings];

- (void)socketDidSecure:(GCDAsyncSocket *)sock
{
     // 開始接收數(shù)據(jù)
     [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工程可以在這里找到谷异。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锦聊,隨后出現(xiàn)的幾起案子歹嘹,更是在濱河造成了極大的恐慌,老刑警劉巖孔庭,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尺上,死亡現(xiàn)場離奇詭異,居然都是意外死亡圆到,警方通過查閱死者的電腦和手機怎抛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芽淡,“玉大人马绝,你說我怎么就攤上這事⊥旅啵” “怎么了迹淌?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長己单。 經(jīng)常有香客問我唉窃,道長,這世上最難降的妖魔是什么纹笼? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任纹份,我火速辦了婚禮,結(jié)果婚禮上廷痘,老公的妹妹穿的比我還像新娘蔓涧。我一直安慰自己,他們只是感情好笋额,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布元暴。 她就那樣靜靜地躺著,像睡著了一般兄猩。 火紅的嫁衣襯著肌膚如雪茉盏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天枢冤,我揣著相機與錄音鸠姨,去河邊找鬼。 笑死淹真,一個胖子當著我的面吹牛讶迁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播核蘸,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼巍糯,長吁一口氣:“原來是場噩夢啊……” “哼啸驯!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祟峦,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坯汤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后搀愧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惰聂,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年咱筛,在試婚紗的時候發(fā)現(xiàn)自己被綠了搓幌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡迅箩,死狀恐怖溉愁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饲趋,我是刑警寧澤拐揭,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站奕塑,受9級特大地震影響堂污,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜龄砰,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一盟猖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧换棚,春花似錦式镐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至夕玩,卻和暖如春你弦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背风秤。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工鳖目, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扮叨,地道東北人缤弦。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像彻磁,于是被迫代替她去往敵國和親碍沐。 傳聞我的和親對象是個殘疾皇子狸捅,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容