XMPP iOS IM开发

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

xmpp初始化

  在开始使用xmpp进行IM聊天以前,咱们须要初始化xmpp流,接入咱们须要的模块:git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#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。github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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方法:bash

1
2
//获取服务器好友列表
     [[[JBXMPPManager sharedInstance] xmppRoster] fetchRoster];

  

消息

  • 消息发送
    只须要调用xmpp的sendElement:方法,因为xmpp只支持文本,因此假如你想发送二进制的文件,好比语音图片等,能够先压缩而后用base64编码,接收方收到再作解码工做,好比语音能够压缩成amr格式,amr格式安卓能够直接播放,iOS须要在解压成wav格式,能够参考demo
1
2
3
4
5
6
- ( 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收到消息的时候将消息存储起来。
1
2
3
4
5
6
7
8
// 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),这样对方每收到一条消息的时候都会返回一条确认的消息,若是没收到该条确认消息能够认为发送失败,确认消息的格式以下:
1
2
3
<message to= "user2@lujiangbin.local" >
   <received xmlns= "urn:xmpp:receipts"  id = "消息ID" />
</message>

  

不过这种方法也有些弊端,好比每次收到一条消息都必须回复,必定程度上会浪费流量以及影响服务器的性能,因此通常采用流管理来实现消息确认。服务器

流关闭

当退出程序的时候,最好能给服务器发送关闭流的通知,也就是发送</stream:stream>结束流,服务器收到以后开始将后续发给该对象的消息收集到离线仓库中,当客户端从新上线的时候,服务端会主动将离线消息推送过来,这样不会丢失消息。因为客户端的操做常常是切到后台而后直接关掉程序,所以能够监听UIApplicationWillTerminateNotification消息,而后手动关闭流。网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[[ 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超时或者服务器本身的心跳包),它会认为对方还在线,将后续的消息发送过去,这样到服务器知道对方掉线的这段时间,期间的消息就丢失了,因此须要流管理来处理。app

  • 节确认(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值作比较,假如小的话会从新发送剩下的节,来防止节丢失。dom

  • 流恢复
    因为移动网络可能随时down掉,因此在咱们重连上来的时候须要的是快速恢复上一次的流,而不是从新新建一个流,roster的检索以及状态的广播,流管理能够经过上一次的流id(当启用流管理的时候,服务端会生成一个id来表示一个流)以及双方的h值来完成流的快速恢复以及这期间的节确认,发送未被确认的节。socket

  • 开启流管理
    要想启用流管理,客户端发送<enable/>元素给服务端,服务端返回<enabled/>元素表示该流已经被管理了,同时有一个id值来标示这个流,xmppframework开启流管理只须要调用
    enableStreamManagementWithResumption: maxTimeout:接口:async

客户端: <enable xmlns='urn:xmpp:sm:3' resume='true'/> 服务端: <enabled xmlns='urn:xmpp:sm:3' id='流id' resume='true'/>
1
2
3
4
5
- ( 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,这是因为服务器证书并非正式的证书,因此须要手动去认证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//设置手动认证证书
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工程能够在这里找到。

相关文章
相关标签/搜索