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

目录

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


一、前言


  • XMPP概述:

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

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

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

二、工程配置


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

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

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

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

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

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

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

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


三、注册功能的实现


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

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

XMPPStream:XMPP的基础服务类,客户端和服务端的通信管道。

XMPPJID:XMPP体系中用户的唯一标识符,由登录账号、服务器域名和资源名生成的,格式为“登录账号@服务器域名/资源名”,如“[email protected]/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--登录注册及断线重连、重新连接_第5张图片
  • Demo下载地址:https://github.com/yiyi0202/XMPP--IM

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

你可能感兴趣的:(第三篇:XMPP实现IM--登录注册及断线重连、重新连接)