目录
一、前言
二、工程配置
三、注册功能的实现
四、登录功能的实现
五、登录注册阶段需要考虑的问题
问题1、断线重连
问题2、重新连接
一、前言
-
XMPP概述:
- XMPP协议是一个开源的即时通讯协议,它位于应用层。
- XMPP是一个典型的C/S架构,即服务端/客户端架构,并不是直接的客户端到客户端的通信,而是客户端1发送消息到服务端,服务端再把消息派发给客户端2,XMPP是基于socket实现的。
- XMPP是以XML为基础的,这就表明其可扩展性非常高,所以XMPP的消息就不仅仅只能是简单的文本,而且可以携带复杂的数据或者是各种格式的文件。
正在为App新增一个客服功能,可选择三方,但是成熟的三方都要花钱,于是选择了XMPP自己开发,使用的是XMPPFramework这个开源库。会一步一步的,详细的记录开发过程,为的是理解每一个开发节点,从而保证项目的稳定性。
服务端使用openfire服务器,因为我们服务端已经搭好了,我就没自己搭。如果你没有,可以自己搭建一个本地的openfire服务器。
文章中只是部分代码,所以如果有看不通顺的地方,可以下载本文Demo,对比着看,会更清晰一些。
二、工程配置
-
使用CocoaPods把XMPPFramework整到项目里去
-
导入两个库:libxml2.tbd,libresolv.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体系中用户的唯一标识符,由登录账号、服务器域名和资源名生成的,格式为“登录账号@服务器域名/资源名”,如“[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]]];
}
-
效果:
Demo下载地址:https://github.com/yiyi0202/XMPP--IM
下一篇:XMPP实现IM--拉取好友列表、添加删除好友 |
---|