美文网首页技术iOSiOS基础扩展
IM开发(2)-XMPP iOS开发

IM开发(2)-XMPP iOS开发

作者: 树下老男孩 | 来源:发表于2015-10-20 12:37 被阅读10047次

    搭建完本地服务器之后,我们便可以着手客户端的工作,这里我们使用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工程可以在这里找到。

    相关文章

      网友评论

      • New_55c7:XMPPStreamManagementPersistentStorage 这个类好像xmppframework 没有
      • New_55c7:本地存储 杀死APP重登恢复流 该怎么做呀
      • 大泽大瑞:想添加注册功能应该怎么办呢,,,求大大指点
        大泽大瑞:注册的部分已经搞定了,求大大指点下怎么添加好友啊
      • 惹人:Error Domain=NSPOSIXErrorDomain Code=61 "Connection refused" UserInfo={NSLocalizedDescription=Connection refused, NSLocalizedFailureReason=Error in connect() function} 这是为什么?
      • WKCaesar:我现在用cocopods 倒入XMPPFramework都报错,版本是1.0.1。倒入后编译报module.modulemap:1626:8: Redefinition of module 'dnssd',找不到解决方法
      • 路小丫:1、在文本框输入账号和密码登陆,若第一次输入错误的账号和密码,再次输入正确的账号和密码就登陆不了,一直提示登陆错误。
      • Alan_yo:大神,想請教下退出之後消息怎麼保存在本地?圖片是發不了的嗎?
      • 多喝烫水_KK:求解答!运行打印 [JBXMPPManager xmppStreamDidDisconnect:withError:],更改了JBXMPP_HOST和JBXMPP_DOMAIN,请问有人跟我一样吗?
        多喝烫水_KK:@树下的老男孩 现在都没什么问题了,但我还是想问一下[XMPPJID jidWithUser:userName domain:JBXMPP_DOMAIN resource:@"iOS"]里面的resource是什么?
        多喝烫水_KK:@树下的老男孩 一直只走 - (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error 代理,我把[XMPPJID jidWithUser:userName domain:JBXMPP_DOMAIN resource:@"iOS"]改成[XMPPJID jidWithString:userName]就登录成功了。
        树下老男孩:@G1rl 什么error
      • 多喝烫水_KK:求解答,登录时总是报出:[JBXMPPManager xmppStreamDidDisconnect:withError:] 是怎么回事。。???
        多喝烫水_KK:我有一个账号是 first@cbaas.local,密码是123456,在后台才建立的,demo里无法登录
      • 大_瓶_子:我在使用的时候可以发出信息,但是一接受消息就会崩溃,接受消息的coredate XMPPMessageArchiving_Message_CoreDataObject *recordMessage = fetchedObjects[i];
        recordMessage里面会奔溃
        大_瓶_子:@树下的老男孩 好吧,
        树下老男孩:@大_瓶_子 存储的问题吧,不过关注的应该是收发消息的流程,至于存储可以自己另外去做
      • 巴图鲁:膜拜
      • 风___________:楼主请教个问题,点击登陆之后,
        - (void)xmppStreamDidConnect:(XMPPStream *)sender 走了
        然后会运行
        - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
        返回error为:
        <failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized></not-authorized></failure>

        搜了一下说
        _myJID = [XMPPJID jidWithUser:userName domain:JBXMPP_DOMAIN resource:@"iOS"];有问题
        但是JBXMPP_DOMAIN 是正确的,终端ping JBXMPP_DOMAIN 可以
        用户名密码也是正确的
        请问这是为什么呢,找了好久也没找到愿意
      • 原野de呼唤:大神,请问SSL加密该怎么配置呢?这方面的资料很少啊
      • toplee:你用的是那个版本的xmpp啊???
      • Terrnce:楼主,你好,我搭建完本地服务器之后,demo在模拟器上可以登录,但是在真机上登录不了
        Terrnce:@树下的老男孩 解决了。忘记把127.0.0.1换成局域网访问地址了。谢谢楼主:smiley::smiley:
        树下老男孩:@陈伟鑫 是不是没在一个网络
        有腹肌的小胖子:@陈伟鑫 为安装ejabber装好后 一直登不上 你有没有遇到过
      • 有腹肌的小胖子:2016-08-11 21:57:07.149 IMChatDemo[28948:896329] -[JBXMPPManager xmppStreamDidDisconnect:withError:]
        2016-08-11 21:57:07.151 IMChatDemo[28948:896329] -[JBXMPPManager xmppStream:socketDidConnect:]
        2016-08-11 21:57:07.163 IMChatDemo[28948:896329] -[JBXMPPManager xmppStreamDidAuthenticate:]
        2016-08-11 21:57:07.171 IMChatDemo[28948:896329] -[JBXMPPManager xmppStreamDidDisconnect:withError:]
        2016-08-11 21:57:07.175 IMChatDemo[28948:896329] -[JBXMPPManager xmppStream:socketDidConnect:]


        大神为啥 我运行的时候 就一直掉线重连
      • 可惜我是双鱼座哦:大神话说你这个下拉刷新拿出来的数据是本地已经有的吗。就是用户没有发那些消息……。还有一个发送照片的时候 用户重复给你发图片的时候都在重复收到第一次发给你的照片
      • DrunkenMouse: :disappointed_relieved: 好不容易搭好了服务器添加了两个用户,然后调试楼主代码的时候发现登陆不上去,用户是自己创建的,域名也改了 但就是登陆错误 好无奈
        DrunkenMouse:@DrankMouse 楼主,你得Domain是在哪里查询的啊?连接错误信息是Error Domain=kCFStreamErrorDomainNetDB Code=8 "nodename nor servname provided, or not known" 实在不知道该怎么解决了
        DrunkenMouse:@DrankMouse 定位搜索之后发现是XMPP与主机断开连接,楼主有什么晓得的能指点一下吗?
      • 你好牛:大神 为什么 我运行demo 登录账号 进不去 一直提示连接失败
        树下老男孩:@qmdckq 看看有没有啥log
        你好牛:@树下的老男孩 就行 demo 的 登录界面
        树下老男孩:@qmdckq 什么错误?是socket连接不上么
      • Jayne_Kuo:终于找到你大神 ,我想问xmpp的推送怎么做?像QQ微信那样?或者 方不方便加个QQ什么的
        Jayne_Kuo:@树下的老男孩 嗯
        树下老男孩:@Jayne_Kuo 推送还是需要用苹果自己的推送呀
      • 从前有一个蕊蕊: 验证失败,<failure xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><not-authorized></not-authorized></failure>
        怎么解决啊啊啊啊
        e6dfc9262c26:http://www.tuicool.com/articles/Jza6nuu
      • JohnQ:楼主 为什么我在使用spark的时候和demo进行对话的时候 这个时候我关闭spark的对话框的时候demo程序会崩溃,还有就是我在spark发送离线消息给demo的时候 demo启动后却没有接受到,还有就是在demo处于列表页面的时候spark发送消息到demo中也无任何的提示
        树下老男孩:@9862d3eb1db7 崩溃这种异常加个断点应该找看看哪里的问题,离线消息没收到应该是流管理交互的h值错了吧,没收到消息?didreceivemessage回调没调到吗
      • 吊儿郎当的认真:怎么应用退到后台仍然可以接受消息? 加上VOIP 苹果会拒 :fearful:
        吊儿郎当的认真:@树下的老男孩 什么意思?:fearful:
        树下老男孩:@吊儿郎当的认真 后台要保持好像其它方式了吧
      • 65067d1326a2:前几天看了篇文章实际应用中的IM都不用XMPP的,因为费流量费电弱网络还不好
        树下老男孩:@醋溜草莓便当 弱网络就需要使用流管理了,你说的什么文章给个链接看看 :smiley:
        65067d1326a2:@树下的老男孩 我听说弱网络下回不好,然后看了一篇文章说有个协议叫Redis
        树下老男孩:@醋溜草莓便当 是会比较耗流量,但是库的支持比较多,可以自己改进,节省不必要的数据传递,类似环信等很多第三方的IM也都是基于此改进的,如果对流量比较敏感可以考虑类似Protocol Buffer等
      • ibabyblue:你好,为什么我运行demo的时候,填写账号密码登录毫无反应呢?服务已经启动了!
        ibabyblue:@树下的老男孩 谢谢 我找到原因了 是我自己的错!账号填写格式错了, 再次感谢你!!
        树下老男孩:@Cloud_90 查看下返回的error
      • 4c47c3b1054a:挺有用的
      • 991cb9475138:树下,我问一下 Xmpp 断网发送的消息,在连接网络后,消息自动发送怎么处理。我这边发送一条r 返回一条 a 但是,当我发送10条消息,其中有3条在断网情况下的发送,在 我连接上网络后,服务器发送给我的是7 而不是10.这个问题怎么解决?
        991cb9475138:@树下的老男孩 ok 多谢 我研究研究
        树下老男孩:@AbeiOS 不过一般断网好像是能够比较快察觉到,这种就不让他继续发消息,但好像iOS7你断网的时候,socket断掉的回调不会调用,这时候可以借助一些reachability辅助检测,https://github.com/robbiehanson/CocoaAsyncSocket/issues/131
        树下老男孩:@AbeiOS 嗯,这时候需要自己做重发,我看xmpp里面好像没做这块,https://github.com/FreeMind-LJ/XMPPFramework/commit/8cb9bc12a6befaf8211a26abf37cd26597c00d0d
      • 郑大爷:楼主的demo运行报错了
        树下老男孩:@郑大爷 什么错误,我是本地可以运行才上传上去的
      • c19c2bcd88ba:请问你这个demo登录的账号和密码是多少啊
        ibabyblue:@郑大爷 那个是.pch文件目录不正确的原因,你将.pch文件的路径修改成你自己的就好啦!
        郑大爷:@树下的老男孩 在Xcode6上运行<command line>报错了
        树下老男孩:@我是主角我不能死 用你在创建服务器的时候创建的账号,或者登陆服务器后台自己添加用户
      • pockyzhang:这个相当详细!!!牛!
      • fewerworld:来拜技术神……

      本文标题:IM开发(2)-XMPP iOS开发

      本文链接:https://www.haomeiwen.com/subject/pezgcttx.html