美文网首页程序员
第三篇:XMPP实现IM--登录注册及断线重连、重新连接

第三篇:XMPP实现IM--登录注册及断线重连、重新连接

作者: 意一ineyee | 来源:发表于2018-06-26 17:09 被阅读88次

    目录

    一、前言
    二、工程配置
    三、注册功能的实现
    四、登录功能的实现
    五、登录注册阶段需要考虑的问题
     问题1、断线重连
     问题2、重新连接

    一、前言


    • XMPP概述:

      • XMPP协议是一个开源的即时通讯协议,它位于应用层
      • XMPP是一个典型的C/S架构,即服务端/客户端架构,并不是直接的客户端到客户端的通信,而是客户端1发送消息到服务端,服务端再把消息派发给客户端2,XMPP是基于socket实现的
      • XMPP是以XML为基础的,这就表明其可扩展性非常高,所以XMPP的消息就不仅仅只能是简单的文本,而且可以携带复杂的数据或者是各种格式的文件
    • 正在为App新增一个客服功能,可选择三方,但是成熟的三方都要花钱,于是选择了XMPP自己开发,使用的是XMPPFramework这个开源库。会一步一步的,详细的记录开发过程,为的是理解每一个开发节点,从而保证项目的稳定性。

    • 服务端使用openfire服务器,因为我们服务端已经搭好了,我就没自己搭。如果你没有,可以自己搭建一个本地的openfire服务器。

    • 文章中只是部分代码,所以如果有看不通顺的地方,可以下载本文Demo,对比着看,会更清晰一些。

    二、工程配置


    • 使用CocoaPods把XMPPFramework整到项目里去

    • 导入两个库:libxml2.tbdlibresolv.tbd

    • 配置XMPPFramework的XMPPConfig.h文件,里面需要填写openfire服务器的域名、IP地址和端口号,还有一个resource,前三者和服务端要上就可以,resource是自定义的。

    • 若遇见'libxml/tree.h' file not found这个错误,在Header Search Paths里添加${SDK_ROOT}/usr/include/libxml2即可。



    三、注册功能的实现


    因为XMPPFramework的API比较多,所以我们会创建一个ProjectXMPP的单例来统一处理这些API,避免XMPPFramework的API过分散落在项目的各个地方,而给后期的维护带来麻烦。

    那么在开始之前,我们先把注册功能会用到的一些比较重要的类和方法列在这里,方便下面对比查看。

    XMPPStream:XMPP的基础服务类,客户端和服务端的通信管道。
    
    XMPPJID:XMPP体系中用户的唯一标识符,由登录账号、服务器域名和资源名生成的,格式为“登录账号@服务器域名/资源名”,如“11@grhao.com/iOS”。
    
    // 连接服务端
    - (BOOL)connectWithTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr;
    
    // 验证注册密码
    - (BOOL)registerWithPassword:(NSString *)password error:(NSError **)errPtr;
    
    
    #pragma mark - XMPPStreamDelegate
    
    // 连接服务端超时
    - (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender;
    
    // 连接服务端成功
    - (void)xmppStreamDidConnect:(XMPPStream *)sender;
    
    // 注册成功
    - (void)xmppStreamDidRegister:(XMPPStream *)sender;
    
    // 注册失败
    - (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error;
    

    我们为ProjectXMPP定义了一个XMPPStream类的对象stream,初始化单例的时候,就需要完成stream的初始化。

    -----------ProjectXMPP.h----------
    
    /// XMPP的基础服务类,客户端和服务端的通信管道
    @property (strong, nonatomic) XMPPStream *stream;
    
    -----------ProjectXMPP.m----------
    
    // 创建stream
    self.stream = [[XMPPStream alloc] init];
    // 设置服务器IP地址
    self.stream.hostName = kHostName;
    // 设置服务器端口号
    self.stream.hostPort = kHostPort;
    // 设置stream的代理
    [self.stream addDelegate:self delegateQueue:dispatch_get_global_queue(0, 0)];
    

    注册功能主要分两步:连接服务器验证注册密码

    -----------ProjectXMPP.m----------
    
    #pragma mark - 注册
    
    - (void)registerWithAccount:(NSString *)account password:(NSString *)password {
        
        // 记录注册密码
        self.registerPassword = password;
        
        // 记录连接服务端的目的
        self.connectToServerPurpose = ConnectToServerPurposeRegister;
        
        // 连接服务端
        [self connectToServerWithAccount:account];
    }
    

    连接服务器。

    -----------ProjectXMPP.m----------
    
    #pragma mark - private methods
    
    // 连接服务端
    - (void)connectToServerWithAccount:(NSString *)account {
        
        // 如果已连接到服务端,就先断开连接
        if ([self.stream isConnected]) {
            
            // 断开连接
            [self.stream disconnect];
        }
        
        
        // 生成用户的jid:XMPP体系中用户的唯一标识符,由登录账号、服务器域名和资源名生成的
        XMPPJID *jid = [XMPPJID jidWithUser:account domain:kDomainName resource:kResource];
        // 把jid配置到stream中
        self.stream.myJID = jid;
        
        
        // 连接服务端
        [self.stream connectWithTimeout:30 error:nil];
    }
    

    连接服务器成功和失败的回调,并且在连接服务器成功后,开始验证注册密码。

    -----------ProjectXMPP.m----------
    
    #pragma mark - XMPPStreamDelegate
    
    // 连接服务端超时
    - (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender {
        
        [ProjectHUD showMBProgressHUDToView:kWindow withText:@"连接服务端超时,请重试!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:nil];
    }
    
    // 连接服务端成功
    - (void)xmppStreamDidConnect:(XMPPStream *)sender {
        
        NSLog(@"===========>连接服务端成功");
        
        // 连接服务端成功后,验证密码
        if (self.connectToServerPurpose == ConnectToServerPurposeLogin) {
            
            // 验证登录密码
            [self.stream authenticateWithPassword:self.loginPassword error:nil];
        }else {
            
            // 验证注册密码
            [self.stream registerWithPassword:self.registerPassword error:nil];
        }
    }
    

    接下来,验证注册密码成功和失败的回调,我们是把它们放在注册界面RegisterViewController里面的,在这里可以做一些自定义的业务。

    -----------RegisterViewController.m----------
    
    #pragma mark - XMPPStreamDelegate
    
    // 注册成功
    - (void)xmppStreamDidRegister:(XMPPStream *)sender {
        
        NSLog(@"===========>注册成功");
        
        [ProjectHUD showMBProgressHUDToView:kWindow withText:@"恭喜你,注册成功!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:^{
            
            [self.navigationController popViewControllerAnimated:YES];
        }];
    }
    
    // 注册失败
    - (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error {
        
        [ProjectHUD showMBProgressHUDToView:kWindow withText:@"注册失败,请重试!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:nil];
    }
    

    这样我们就完成了注册功能。

    四、登录功能的实现


    实现之前,我们同样看下登录功能相对于注册功能一些额外的类和方法。

    XMPPPresence:这是一个用来表明用户在线状态的一个类。presence.from--消息的发送方,为XMPPJID类。presence.to--消息的接收方,为XMPPJID类。presence.type==available--可用、在线状态。presence.type==unavailable,不可用,离线状态。
    
    // 验证登录密码
    - (BOOL)authenticateWithPassword:(NSString *)inPassword error:(NSError **)errPtr;
    
    
    #pragma mark - XMPPStreamDelegate
    
    // 登录成功
    - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender;
    
    // 登录失败
    - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error;
    

    登录和注册的逻辑流程其实差不多,也是两大步:连接服务器验证登录密码,这两步的代码和上面实现注册功能时是共用的,此处不再重复,这里我们也提供了退出登录的方法。

    -----------ProjectXMPP.m----------
    
    #pragma mark - 登录
    
    - (void)loginWithAccount:(NSString *)account password:(NSString *)password {
        
        // 记录登录密码
        self.loginPassword = password;
        
        // 记录连接服务端的目的
        self.connectToServerPurpose = ConnectToServerPurposeLogin;
        
        // 连接服务器
        [self connectToServerWithAccount:account];
    }
    
    - (void)logout {
        
        // 下线
        [self becomeUnavailable];
        
        // 断开连接
        [self.stream disconnect];
    }
    

    和注册一样,我们在验证了登录密码之后,登录密码验证成功和失败的回调,写在了LoginViewController里,在这里可以做一些自定义的业务。

    -----------LoginViewController.m----------
    
    #pragma mark - XMPPStreamDelegate
    
    // 登录成功
    - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
        
        NSLog(@"===========>登录成功");
        
        // 上线
        [[ProjectXMPP sharedXMPP] becomeAvailable];
        
        // 存储用户的一些信息
        UserModel *currentUser = [[UserModel alloc] init];
        currentUser.jid = [XMPPJID jidWithUser:self.accountTextField.text domain:kDomainName resource:kResource];
        currentUser.password = self.passwordTextField.text;
        [kNSUserDefaults yy_setComplexObject:currentUser forKey:@"currentUser"];
        
        // 切换登录状态
        [kNSUserDefaults setBool:YES forKey:@"isLogin"];
        
        // 进入App
        [ProjectHUD showMBProgressHUDToView:kWindow withText:@"恭喜你,注册成功!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:^{
            
            [[UIApplication sharedApplication].keyWindow setRootViewController:[[UINavigationController alloc] initWithRootViewController:[[FriendsListViewController alloc] init]]];
        }];
    }
    
    // 登录失败
    - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error {
        
        [ProjectHUD showMBProgressHUDToView:kWindow withText:@"注册失败,请重试!" atPosition:(MBProgressHUDTextPositionMiddle) autohideAfter:2 completionHandlerAfterAutohide:nil];
    }
    

    上面的代码中,我们创建了一个UserModel,是为了将来记录用户的电子名片信息,这里也可以先忽略;同时为下一篇做准备,我们创建了好友列表界面FriendsListViewController,在登录成功之后,跳转过去,点退出按钮可以切换到登录界面。

    -----------FriendsListViewController.m----------
    
    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"退出登录" style:(UIBarButtonItemStylePlain) target:self action:@selector(logoutAction)];
    
    
    - (void)logoutAction {
    
        // 退出登录
        [[ProjectXMPP sharedXMPP] logout];
    
        // 切换登录状态
        [kNSUserDefaults setBool:NO forKey:@"isLogin"];
    
        // 退出App
        [[UIApplication sharedApplication].keyWindow setRootViewController:[[UINavigationController alloc] initWithRootViewController:[[LoginViewController alloc] init]]];
    }
    

    这样我们就实现了登录和退出登录的功能。

    五、登录注册阶段需要考虑的问题:断线重连和重新连接


    因为在这个阶段,我们的代码量相对较少,而且登录注册也直接的关系着用户的上下线状态,因此我们会在此阶段就着重关心一下用户的在线状态,其中最重要的就是要做好断线重连重新连接

    好,现在我们在openfire服务器的后台来跟踪一下用户的在线状态。

    • 【1号状态】用户登录之前:我们先注册一个账号和密码都为“11”的用户,注册成功之后,换句话说就是用户登录之前,该用户的状态为离线,没问题,就应该是这样。

    • 【2号状态】用户登录:现在我们登录“11”这个账号,如果登录失败的话,用户的状态依旧为离线,而登录成功后,用户的状态会切换为在线,也没问题。

      登录失败
      登录成功
    • 【3号状态】用户退出登录之后:现在我们把“11”这个账号退出登录,用户的状态会切换为离线,也没问题。

    上面这三种仅仅是最普通的状况,不易出现问题。接下来,我们需要看一些特殊的情况,XMPP对用户的在线状态是怎样控制的,我们从而做出合适的处理。

    在此之前,我们首先要知道XMPP就是基于socket实现的,在socket编程这篇文章里,我们也提到过由于国内运营商的NAT超时,socket连接是有可能在5分钟之后断开的,因为NAT超时时间为5分钟。同时XMPP是一个C/S架构,而不是P2P架构,所以一个客户端的上下线状态仅仅取决于客户端socket与服务端socket连接的正常与否。

    • 【4号状态】用户登录成功后,App在前台运行,但是不聊天传输数据,过了NAT超时时间看看会不会有问题:按着这种要求,把App放那经过10分钟之后(即超过了NAT超时时长的5分钟),在openfire服务器后台看到用户依旧处于在线状态,即这个socket没有断开,为什么呢?因为XMPPFramework已经帮我们做好了心跳保活这样的机制,因此,只要我们建立了连接,并且App处于运行状态,那么即便我们不在这条连接上传输数据,连接也是不会断开的。所以得出结论,这种情况下,socket连接是不会断开的,用户会处于正常的在线状态,我们不需要处理。

    • 【5号状态】用户登录成功后,App在前台运行,此时断网了断网后,连接不会立马断开,用户依旧处于在线状态,5分钟后,连接断开,用户会处于离线状态。

    • 【6号状态】用户登录成功后,App进入后台App进入后台,连接不会立马断开,用户依旧处于在线状态,5分钟后,连接断开,用户会处于离线状态。

    • 【7号状态】用户登录成功后,杀掉App杀掉App,用户的在线状态会立马切换为离线状态

    • 【8号状态】用户登录成功后,锁屏了(包括手机自动锁屏和我们主动锁屏)手机锁屏后,用户的在线状态会立马切换为离线状态

    好,现在针对【4号】~【8号】这五种状态:

    • 【4号】我们是不用管的。
    • 【5号】要管,要做断线重连,管的就是来网之后自动重新连接服务端socket,恢复用户在线,这才是正常的。
    • 【6号】要管,要做断线重连,因为进入后台,5分钟后断开连接,用户下了线,但是这个时候App还没被杀掉,要做的是断线重连;但是如果App放在后台很长时间,就会被杀掉,我们可以把这总情况归类为【7号】,去做重新连接。
    • 【7号】要管,要做重新连接,因为我们已经记录了用户的登录状态,当用户登录成功后,下次打开App是自动登录的,所以我们在杀掉App,再次打开App后要把断开的连接重新连接上,让用户处于在线状态。
    • 【8号】要管,需要做断线重连,管的就是再次亮屏之后自动重新连接服务端socket,恢复用户在线,这才是正常的。
    1、断线重连的实现

    好,针对【5号】、【6号】、【8号】的断线重连,其实很简单,因为XMPPFramework为我们提供了一个断线重连的类XMPPReconnect,其中有一个代理方法如下,可以帮助我们很轻易的就实现断线重连。

    #pragma mark - XMPPReconnectDelegate
    
    // 检测到任何异常断开连接的时候,都会触发该代理方法
    - (void)xmppReconnect:(XMPPReconnect *)sender didDetectAccidentalDisconnect:(SCNetworkConnectionFlags)connectionFlags;
    

    现在我们在ProjectXMPP里定义一个断线重连的对象。

    -----------ProjectXMPP.h----------
    
    /// 断线重连
    @property (strong, nonatomic) XMPPReconnect *reconnect;
    

    并在初始化单例的时候,初始化该对象。

    -----------ProjectXMPP.m----------
    
    /// 断线重连
    self.reconnect = [[XMPPReconnect alloc] init];
    [self.reconnect activate:self.stream];
    

    然后在AppDelegate里遵循XMPPReconnectDelegate协议,并设置AppDelegate为reconnect的代理。

    -----------AppDelegate.m----------
    
    [[ProjectXMPP sharedXMPP].reconnect addDelegate:self delegateQueue:dispatch_get_main_queue()];
    

    然后在断线重连的代理方法里,调用登录的方法。

    -----------AppDelegate.m----------
    
    #pragma mark - XMPPReconnectDelegate
    
    // 断线重连
    - (void)xmppReconnect:(XMPPReconnect *)sender didDetectAccidentalDisconnect:(SCNetworkConnectionFlags)connectionFlags {
        
        [[ProjectXMPP sharedXMPP] loginWithAccount:[UserModel currentUser].jid.user password:[UserModel currentUser].password];
    }
    

    当然,我们也需要在AppDelegate里遵循XMPPStreamDelegate协议,并设置AppDelegate为stream的代理,为的是在登录成功的回调里,把用户的状态切换为在线。

    -----------AppDelegate.m----------
    
    [[ProjectXMPP sharedXMPP].stream addDelegate:self delegateQueue:dispatch_get_main_queue()];
    
    -----------AppDelegate.m----------
    
    #pragma mark - XMPPStreamDelegate
    
    // 登录成功,这里的登录成功应该只针对自动登录这种情况,避免和登录界面的回调出现重复调用
    - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender {
        
        if ([kNSUserDefaults boolForKey:@"isLogin"]) {
            
            NSLog(@"===========>自动登录成功");
            
            // 上线
            [[ProjectXMPP sharedXMPP] becomeAvailable];
        }
        
    }
    

    这样我们就完成了断线重连操作,很简单吧。

    2、重新连接的实现

    针对【7号】的重新连接,其实更简单,我们只需要在AppDelegate的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;方法里调用一下登录的方法就可以了,同样登录成功后会触发登录成功的回调,会把用户的状态切换为在线。

    -----------AppDelegate.m----------
    
    if ([kNSUserDefaults boolForKey:@"isLogin"]) {
        
        // 重新连接
        [[ProjectXMPP sharedXMPP] loginWithAccount:[UserModel currentUser].jid.user password:[UserModel currentUser].password];
        
        [self.window setRootViewController:[[UINavigationController alloc] initWithRootViewController:[[FriendsListViewController alloc] init]]];
    }else {
        
        [self.window setRootViewController:[[UINavigationController alloc] initWithRootViewController:[[LoginViewController alloc] init]]];
    }
    

    下一篇:XMPP实现IM--拉取好友列表、添加删除好友

    相关文章

      网友评论

        本文标题:第三篇:XMPP实现IM--登录注册及断线重连、重新连接

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