在学习iOS以来一直想要研究即时聊天方面的技术,无奈工作或时间原因一直搁浅此计划,近日偷得时闲开始着手与XMPP的学习。在学习之前我一直认为XMPP对我来说是一个很有技术的挑战,在了解了协议的具体形式后,才发觉其实技术的难度只在跟你底层代码原理的掌握程度的熟练度有关,说通俗一点,很多东西其实我们都会,只是在各个框架或技术中我们没有考虑到的东西别人都考虑周全!比如你若对socket有一定的了解并懂得xml数据解析那你就可以看懂大部分的xmpp文档!所以只要掌握了相对来说底层的一些技术那么对于学习于此技术相关的东西都会变得轻松起来。
在开始学习XMPP之前希望各位朋友首先对 socket和tcp 有一定的了解,会使用coreData和xml解析和搭建XMPP相关服务器,这样学习起来就更为简单!因为xmpp的数据库使用的是coreData,数据传输协议用的是xml格式。如对socket和tcp不太了解,可以参考我之前写的博客。
1.XMPP核心类(XMPPStream)
XMPPStream * _xmppStream , 其作用与socket相同,用于创建客户端与服务器端的链接,并能后对流事件进行监听获得其流事件,在类对象可以设置异步队列。这样就可将一些耗时操作放到后台进行。另外xmpp使用模块功能, 所有的模块的激活使用都需要关联_xmppStream对象
1.1登录
在初始化好_xmppStream后,设置其代理,并进行链接,在socket进行服务器链接时会要求输入ip地址和端口号,而_xmppStream也是大体相同,让我们来看看代码
//1.配置xmppStream信息 //创建xmpp流 _xmppStream = [[XMPPStream alloc] init]; //添加代理及队列 [_xmppStream addDelegate:self delegateQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)]; //2.设置用户登录信息 //其中第一参数为用户id,domain为服务器域名,resource为手机类型 XMPPJID * userJid = [XMPPJID jidWithUser:@"用户id" domain:@"服务器域名" resource:@"iphone"]; //3.配置xmppstream的用户信息,ip及端口号(xmpp默认端口号都为5222) _xmppStream.myJID = userJid; //此处使用自己主机作为服务器 _xmppStream.hostName = @"127.0.0.1"; _xmppStream.hostPort = 5222; //4.与服务器进行链接 //发起链接,此处超过20秒后会回调链接失败方法,若成功则会调用链接成功方法 NSError *error; [_xmppStream connectWithTimeout:20 error:&error]; if (error != nil) { BQLog(@"发起链接失败:%@",error.localizedDescription); }
当链接到主机成功后,需要验证密码,此时用户需要将密码发送到服务器进行验证,当验证成功后用户则登录成功,但在登录成功时,用户还是处于离线状态,需要想服务器发送在线信息,更新个人状态
/** 链接到主机 */ - (void)xmppStreamDidConnect:(XMPPStream *)sender { BQLog(@"链接成功"); //链接到主机后需要发送密码才能进行登录 [self sendPwdToHost]; } /** 链接到主机超时 */ - (void)xmppStreamConnectDidTimeout:(XMPPStream *)sender { BQLog(@"链接主机超时"); } /** 验证密码*/ - (void)sendPwdToHost { NSError *error; //验证密码 [_xmppStream authenticateWithPassword:[[NSUserDefaults standardUserDefaults] objectForKey:PASS_WORD] error:&error]; if (error != nil) { BQLog(@"信息发送失败%@",error.localizedDescription); } } /** 密码验证成功 */ - (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { BQLog(@"登录成功"); //登录成功后需要向服务器发送在线状态 [self sendOnlineToHost]; } /** 密码验证失败 */ - (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(DDXMLElement *)error { BQLog(@"登录失败:%@",error); } /** 向服务器发送在线状态 */ - (void)sendOnlineToHost { //在线类,(XMPPPresence为DDXMLElement子类) XMPPPresence *presence = [XMPPPresence presenceWithType:@"available"]; //发送在线信息到服务器 [_xmppStream sendElement:presence]; }
1.2注册
之所以将注册放到登录后面来讲是因为注册所用代码与登录相差无几, 唯一的区别就是将验证密码改为注册密码 ,当信息注册好后再执行登录步骤即可
- (void)sendPwdToHost { NSError *error; //进行密码注册,密码发送成功后会回调下面2个方法 [_xmppStream registerWithPassword:[[NSUserDefaults standardUserDefaults] objectForKey:REGISTER_PWD] error:&error]; if (error != nil) { BQLog(@"密码发送失败%@",error.localizedDescription); } } /** 注册成功调用*/ - (void)xmppStreamDidRegister:(XMPPStream *)sender{ BQLog(@"用户名注册成功"); } /** 注册失败调用*/ - (void)xmppStream:(XMPPStream *)sender didNotRegister:(DDXMLElement *)error { BQLog(@"用户名注册失败"); }
2.电子名片模块(包含头像模块)
2.1电子名片模块配置与激活
电子名片模块在xmpp框架下拓展文件XEP-0054当中,需要导入所需头文件,再 配置好模块后利用_xmppStream将其激活 才可使用。其中_vCardStorage是xmpp所做的本地缓存处理。对于这个缓存处理就不多说了,各位朋友应该知道其具体作用的。头像模块的作用会在后续的文章中用到。此处不会涉及
//创建电子名片对象存取器 _vCardStorage = [XMPPvCardCoreDataStorage sharedInstance]; //创建电子名片对象 _vCard = [[XMPPvCardTempModule alloc] initWithvCardStorage:_vCardStorage]; //电子名片需要被激活(电子名片一般配合头像模块一起使用) [_vCard activate:_xmppStream]; //头像模块 _avatar = [[XMPPvCardAvatarModule alloc] initWithvCardTempModule:_vCard]; //激活头像模块 [_avatar activate:_xmppStream];
2.2电子名片的读取更新
在激活这些模块后就可以异步读取到个人电子名片信息了。接下来便是个人电子名片信息的更新,此处较为简单,没有什么思路便直接上代码。读取的话当然是直接获取myvCaed值即可。此处需要注意的是由于xmpp协议传输格式都为xml数据格式,部分节点可能没有解析到,那就需要自己去解析获取。
//电子名片信息缓存(个人理解为数据库的内容) ,这里的[[BQXMPPTool sharedXMPPTool].vCardvCard就是上方所罗列的_vCard XMPPvCardTemp *myvCard = [BQXMPPTool sharedXMPPTool].vCard.myvCardTemp; //以下为设置其具体信息 if (self.headImageView != nil) { //此处图片数据较大只是简单处理以下 myvCard.photo = UIImageJPEGRepresentation(self.headImageView.image, 0.5); } myvCard.nickname = self.nicknameLabel.text; myvCard.orgName = self.departmentLabel.text; myvCard.title = self.positionLabel.text; myvCard.note = self.phoneLabel.text; myvCard.mailer = self.emailLabel.text; //更新电子名片信息到服务器 [[BQXMPPTool sharedXMPPTool].vCard updateMyvCardTemp:myvCard];
3.花名册(好友)模块
3.1花名册模块配置与激活
//创建花名册存储器 _rosterStorage = [[XMPPRosterCoreDataStorage alloc] init]; //创建花名册模块 _roster = [[XMPPRoster alloc] initWithRosterStorage:_rosterStorage]; //激活花名册模块 [_roster activate:_xmppStream];
3.2获取好友列表和信息
所有添加过的好友信息都存储在花名册存储器当中,此时我们需要根据花名册存储器的上下文找到对应数据库并查找其中的好友资料,所有的好友都有订阅状态('none','from','to','both'),我们需要的是已添加和订阅的好友,所以需要进行好友过滤,最后可以利用NSFetchedResultsController控制器来实时监听数据库内容。当数据库内容有任何改变时都会调用其回调方法,在回调方法里我们便可重新刷新界面显示最新的好友列表,所有的好友信息都在控制器结果中以XMPPUserCoreDataStorageObject类对象来表示。
//1.配置花名册数据库上下文(2种方式) _rosterContext = [BQXMPPTool sharedXMPPTool].rosterStorage.mainThreadManagedObjectContext; //2.从上下文中取出对应的模型 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"XMPPUserCoreDataStorageObject"]; //3.设置结果排序规则 NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"displayName" ascending:YES]; request.sortDescriptors = @[sort]; //3.1利用条件筛选,过滤 NSPredicate *predic = [NSPredicate predicateWithFormat:@"subscription != %@",@"none"]; request.predicate = predic; //4.根据规则配置对应数据库中的数据存取器 _resultsCtl = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:_rosterContext sectionNameKeyPath:nil cacheName:nil]; //5.利用获取数据 NSError *error; [_resultsCtl performFetch:&error]; if (error) { BQLog(@"%@",error); return; } //6.设置代理(配置数据存取器后,每当数据有改变时都会回调方法) _resultsCtl.delegate = self; //取得对应好友的用户信息,此处展示第一个好友信息 XMPPUserCoreDataStorageObject *user = _resultsCtl.fetchedObjects[0]; //此处就可利用头像模块取出好友头像 NSData *imageData = [[BQXMPPTool sharedXMPPTool].avatar photoDataForJID:user.jid];
3.3好友的添加,验证与删除
好友的添加(订阅)主要分三步: 1. 输入 jid(XMPPJid) 2. 判断是否存在此好友 3. 没有的话 添加 ( 订阅 ) 好友,当接受到好友的添加后代理方法里会有接受到好友订阅信息的回调,我们只需要在里面来验证好友即可,关于好友的删除和订阅是成对的,相当于将订阅改为删除即可。下面实现代码
//先来订阅(添加)好友 //拼接实际的用户id(实际id由用户名加域名组成) NSString *strJid = [NSString stringWithFormat:@"%@@%@",self.textField.text,[BQXMPPTool sharedXMPPTool].hostName]; XMPPJID *jid = [XMPPJID jidWithString:strJid]; //判断是否存在此好友 BOOL hasFirend = [[BQXMPPTool sharedXMPPTool].rosterStorage userExistsWithJID:jid xmppStream:[BQXMPPTool sharedXMPPTool].xmppStream]; if (hasFirend == YES || [self.textField.text isEqualToString:[[NSUserDefaults standardUserDefaults] objectForKey:USER_NAME]]) { //列表中若存在此好友给予提示 NSString *message = hasFirend ? @"好友以存在" : @"不能添加自己"; UIAlertController *alertVc = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert]; [alertVc addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alertVc animated:YES completion:nil]; }else { //列表中不存在此好友则订阅该好友 [[BQXMPPTool sharedXMPPTool].roster subscribePresenceToUser:jid]; } //接下来是好友的验证 #pragma mark 处理加好友回调,加好友 - (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence { //请求的用户 NSString *presenceFromUser =[NSString stringWithFormat:@"%@", [[presence from] user]]; XMPPJID *jid = [XMPPJID jidWithString:presenceFromUser]; //接受该好友订阅并订阅该好友 [_roster acceptPresenceSubscriptionRequestFrom:jid andAddToRoster:YES]; } //最后是好友的删除,同样根据好友的jid(XMPPJid)只需一个方法即可完成 [_roster removeUser:jid];
4.聊天消息模块
4.1聊天模块的配置与激活
聊天模块存放在xmpp框架中拓展文件夹XEP-0136下,默认未导入头文件,需要导入其头文件后才能使用
//创建聊天模块存储器 _messageStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance]; //创建聊天模块 _message = [[XMPPMessageArchiving alloc] initWithMessageArchivingStorage:_messageStorage]; //激活聊天模块 [_message activate:_xmppStream];
4.2消息信息的获取
消息的获取也和上面花名册的获取大体相同,同样是利用消息信息数据库来获取,下面是实现代码
//获取聊天消息的上下文 _msgContext = [BQXMPPTool sharedXMPPTool].messageStorage.mainThreadManagedObjectContext; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"XMPPMessageArchiving_Message_CoreDataObject"]; //排序 NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"timestamp" ascending:YES]; request.sortDescriptors = @[sort]; //将不满足条件的过滤 NSPredicate *predic = [NSPredicate predicateWithFormat:@"bareJidStr = %@",_firendInfo.jidStr]; request.predicate = predic; //获取数据库结果 _msgResultsCtl = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:_msgContext sectionNameKeyPath:nil cacheName:nil]; _msgResultsCtl.delegate = self; NSError *error; [_msgResultsCtl performFetch:&error]; if (error) { BQLog(@"数据库读取出错:%@",error.localizedDescription); }
4.3消息的发送和读取
在现有的xmpp消息体系中针对于iOS还不支持语音,图片等资料传输,在此我们需要在其传输协议中添加节点来帮助我们判断所传输内容格式。先让我们来看看消息的传输格式,在其中我手动添加了MessageType来帮助我进行判断此消息的类型(100:纯文本,101:语音消息,102:图片消息)。在后面即可根据消息类型来判断如何加载消息(消息里可直接添加二进制数据文件)。此处本人做了资料(语音,图片等)上传处理,以减轻服务器压力,这样只传输url地址。当用户读取相对应消息后只需做好缓存即可。
下面直接上消息发送和获取的代码
//先来看看消息的发送,此处做了一个简单的封装 /** 根据传入的消息类型和消息体来进行消息发送 */ - (void)sendMessageWithMsgType:(MessageType)msgType withBody:(NSString *)body { //消息的发送需要用到核心类 XMPPStream *stream = [BQXMPPTool sharedXMPPTool].xmppStream; //配置消息体,'chat'代表聊天消息 XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:_firendInfo.jid]; //增加一个消息类型节点 [message addAttributeWithName:@"MessageType" intValue:msgType]; //添加消息体 [message addBody:body]; //发送消息 [stream sendElement:message]; } //消息的读取 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { XMPPMessageArchiving_Message_CoreDataObject *msg = _msgResultsCtl.fetchedObjects[indexPath.row]; XMPPMessage *message = msg.message; MessageType type = [message attributeIntValueForName:@"MessageType"]; UITableViewCell *cell; //此时根据消息的类型再分别加载cell switch (type) { case MessageType_Text: cell = [self tableView:tableView cellWithIdentifier:kIdentifiText]; break; case MessageType_Sound: cell = [self tableView:tableView cellWithIdentifier:kIdentifiSound]; break; case MessageType_Image: cell = [self tableView:tableView cellWithIdentifier:kIdentifiImage]; break; default: break; } return cell; }
5.自动(断线重练)链接模块
在iOS当中若程序进入后台那很可能就会导致客户端与服务器的链接失效,此时便可导入自动连接模块以方便重新链接,代码较为简单,变化不多说直接上代码
//创建自动链接模块 _reconnect = [[XMPPReconnect alloc] init]; //激活自动链接模块 [_reconnect activate:_xmppStream];
6.总结
看过上述代码的同学可以发现,在xmpp当中不论什么模块的使用无非就是3个步骤:
1.创建模块
2.激活模块
3.使用模块
本篇文章中所罗列的都为关键代码,更多的代码并没有展示出来,但通过上述代码的学习,我相信各位都会对xmpp框架的使用方式和学习方式有一个大致方向。关于本篇所对应的完整代码的链接如下 https://github.com/PurpleSweetPotatoes/XMPP_Learn_Demo ,本demo旨在学习xmpp框架的用法和思路,不完善处请见谅!如本文或代码中有任何错或不规范处请指出,谢谢!