XMPP iOS开发(IM开发,转)
搭建完本地服务器之后,我们便可以着手客户端的工作,这里我们使用XMPPFramework这个开源库,安卓平台可以使用Smack(最好使用4.1以及之后的版本,支持流管理),为了简单起见这里只实现登陆、获取好友列表以及聊天等功能,页面如下所示:


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工程可以在这里找到。
原文链接:http://www.jianshu.com/p/eb273cb8ac94
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
XMPP iOS开发(IM开发,转)的更多相关文章
- 在MAC上搭建cordova3.4.0的IOS和android开发环境
Hello,大家好,今天给大家说说在mac上搭建cordova3.4.0的iOS和Android开发环境,首先下载cordova,地址:https://cordova.apache.org/#down ...
- XE7 & IOS开发之开发账号(3):证书、AppID、设备、授权profile的申请使用,附Debug真机调试、Ad hoc下iPA文件生成演示(XCode5或以上版本推荐,有图有真相)
网上能找到的关于Delphi XE系列的移动开发的相关文章甚少,本文尽量以详细的图文内容.傻瓜式的表达来告诉你想要的答案. 原创作品,请尊重作者劳动成果,转载请注明出处!!! 注意,以下讨论都是以&q ...
- XE7 & IOS开发之开发账号(2):发布证书、发布授权profile的申请使用,附Ad hoc真机调试、生成ipa文件演示(XCode所有版本通用,有图有真相)
网上能找到的关于Delphi XE系列的移动开发的相关文章甚少,本文尽量以详细的图文内容.傻瓜式的表达来告诉你想要的答案. 原创作品,请尊重作者劳动成果,转载请注明出处!!! 注意,以下讨论都是以&q ...
- XE7 & IOS开发之开发账号(1):开发证书、AppID、设备、开发授权profile的申请使用,附Debug真机调试演示(XCode所有版本通用,有图有真相)
网上能找到的关于Delphi XE系列的移动开发的相关文章甚少,本文尽量以详细的图文内容.傻瓜式的表达来告诉你想要的答案. 原创作品,请尊重作者劳动成果,转载请注明出处!!! 注意,以下讨论都是以&q ...
- iOS原生地图开发指南续——大头针与自定义标注
iOS原生地图开发指南续——大头针与自定义标注 出自:http://www.sxt.cn/info-6042-u-7372.html 在上一篇博客中http://my.oschina.net/u/23 ...
- 从C#到Objective-C,循序渐进学习苹果开发(5)--利用XCode来进行IOS的程序开发
本随笔系列主要介绍从一个Windows平台从事C#开发到Mac平台苹果开发的一系列感想和体验历程,本系列文章是在起步阶段逐步积累的,希望带给大家更好,更真实的转换历程体验.前面几篇随笔主要介绍C#和O ...
- iOS系统提供开发环境下命令行编译工具:xcodebuild
iOS系统提供开发环境下命令行编译工具:xcodebuild[3] xcodebuild 在介绍xcodebuild之前,需要先弄清楚一些在XCode环境下的一些概念[4]: Workspace:简单 ...
- iOS原生地图开发详解
在上一篇博客中:http://my.oschina.net/u/2340880/blog/414760.对iOS中的定位服务进行了详细的介绍与参数说明,在开发中,地位服务往往与地图框架结合使用,这篇博 ...
- iOS原生地图开发进阶——使用导航和附近兴趣点检索
iOS原生地图开发进阶——使用导航和附近兴趣点检索 iOS中的mapKit框架对国际化的支持非常出色.在前些篇博客中,对这个地图框架的基础用法和标注与覆盖物的添加进行了详细的介绍,这篇博客将介绍两个更 ...
- iOS越狱程序开发
iOS越狱程序开发http://www.docin.com/p-760246852.html
随机推荐
- Linux下静态编译Qt程序
一般情况下,我们用Qt编译出来的程序是要依赖于系统Qt库的,也就是这个程序移到别的没有安装Qt库的系统上是不能使用的.会提示缺少……库文件之类的错误.这就是动态编译的结果. 但是如果我们想编译一个程序 ...
- .NET设计模式系列文章
原文地址:http://terrylee.cnblogs.com/archive/2006/06/01/334911.html 最初写探索设计模式系列的时候,我只是想把它作为自己学习设计模式的读书笔记 ...
- 【最大流之EdmondsKarp算法】【HDU1532】模板题
题意:裸的最大流,什么是最大流,参考别的博客 运用复杂度最高的EK算法 O(M*N),模板来自紫书 #include <cstdio> #include <cstdlib> # ...
- InfluxDB 开源分布式时序、事件和指标数据库
InfluxDB 是一个开源分布式时序.事件和指标数据库.使用 Go 语言编写,无需外部依赖.其设计目标是实现分布式和水平伸缩扩展. 特点 schemaless(无结构),可以是任意数量的列 Scal ...
- js 创建类和继承的几种方法
在面向对象编程中,类(class)是对象(object)的模板,定义了同一组对象(又称"实例")共有的属性和方法.JavaScript语言里是没有类的概念的,但是我们通过以下方法也 ...
- CSS文字超出div或者span时显示省略号
我们常常需要在文本过长时显示,将超出显示成省略号: 思想为: 首先设置宽度,然后让超出的部分隐藏如果有超出则在最后显示省略号让文本不换行 具体css代码为: .title{ width:200px;o ...
- css selection改变文字反选的背景颜色
<style type="text/css"><!--.ai::-moz-selection { background:#cc0000; color:#fff; ...
- C++ vector 实现二维数组时, 在类的头文件中定义时遇到"应输入类型符"的问题?
见下,当我在类的声明文件中定义二维vector时,提示我应输入类型说明符; 但是相同的格式定义,在类中将二维vector修改为在源文件中定义就可以顺利通过,并顺利执行打印 打印结果如下: 望大神来解惑 ...
- C语言静态函数静态变量
C语言程序可以看成由一系列外部对象构成,这些外部对象可能是变量或函数.而内部对象是指定义在函数内部的函数参数及变量. 外部变量定义在函数之外,因此可以在许多函数中使用.由于C语言不允许在一个函数中定义 ...
- eclipse 使用maven 创建web3.1项目
最近刚刚开始学java, 在框架搭建的过程中, 遭遇了诸多的坑, 和各种各样的不解与困惑, 由于没有什么java的相关基础, 看到各种xml的配置文件, 当真是一个头两个大. 并且并不知道那个配置到底 ...