IM开发(2)-XMPP iOS开发

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

user2的好友列表.png

聊天.png

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工程可以在这里找到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容