搭建完本地服務器以後,咱們即可以着手客戶端的工做,這裏咱們使用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工程能夠在這裏找到。