搭建完本地服务器以后,咱们即可以着手客户端的工做,这里咱们使用XMPPFramework这个开源库,安卓平台可使用Smack(最好使用4.1以及以后的版本,支持流管理),为了简单起见这里只实现登录、获取好友列表以及聊天等功能,页面以下所示:html
在开始使用xmpp进行IM聊天以前,咱们须要初始化xmpp流,接入咱们须要的模块:git
#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
#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
//获取服务器好友列表 [[[JBXMPPManager sharedInstance] xmppRoster] fetchRoster];
- (void)sendMessage:(NSString *)message to:(XMPPJID *)jid { XMPPMessage* newMessage = [[XMPPMessage alloc] initWithType:@"chat" to:jid]; [newMessage addBody:message]; //消息内容 [_xmppStream sendElement:newMessage]; }
// XMPPMessageArchiving.m - (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message { if ([self shouldArchiveMessage:message outgoing:YES xmppStream:sender]) { [xmppMessageArchivingStorage archiveMessage:message outgoing:YES xmppStream:sender]; } }
<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超时或者服务器本身的心跳包),它会认为对方还在线,将后续的消息发送过去,这样到服务器知道对方掉线的这段时间,期间的消息就丢失了,因此须要流管理来处理。app
服务端: <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'/>
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender { //登录完成后,启用流管理 [_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0]; }
客户端: <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。
//设置手动认证证书 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工程能够在这里找到。