搭建完本地服务器以后,咱们即可以着手客户端的工做,这里咱们使用XMPPFramework这个开源库,安卓平台可使用Smack(最好使用4.1以及以后的版本,支持流管理),为了简单起见这里只实现登录、获取好友列表以及聊天等功能,页面以下所示:html
在开始使用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];
|
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];
}
|
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];
}
}
|
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
服务端: <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 xmlns='urn:xmpp:sm:3' h='客户端接收的h值' previd='流id'/> 服务端: <resumed xmlns='urn:xmpp:sm:3' h='服务端接收的h值' previd='流id'/>
xmppframework将这部分逻辑封装在内部,不过这些h跟流id的值是存储在内存中,当程序退出的时候这些值就没了,也就没法恢复流。因此实际应用的时候须要将这些值保存到本地,好比demo里的XMPPStreamManagementPersistentStorage。
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工程能够在这里找到。