這一章節(jié)會(huì)從基礎(chǔ)模塊開(kāi)始逐步實(shí)現(xiàn)私聊功能
XMPP 基礎(chǔ)模塊
基礎(chǔ)模塊
1碑定、 XMPPJID
: 身份模塊,在 XMPP
中代表各個(gè)實(shí)體對(duì)象(例如:每一個(gè)用戶督惰、每一個(gè)房間)不傅,所有的消息都是根據(jù) JID
進(jìn)行分發(fā)傳遞的。
完整的格式:user@domain/resource
必要參數(shù)
-
user
: 使用XMPP
服務(wù)的實(shí)體 ID 赏胚,具有唯一性访娶。 -
domain
: 服務(wù)器網(wǎng)關(guān)地址,可以是 IP 也可以是 域名
非必要參數(shù)
-
resource
: 用來(lái)表示一個(gè)特定的會(huì)話(與某個(gè)設(shè)備)觉阅,連接(與某個(gè)地址)崖疤,或者一個(gè)附屬于某個(gè)節(jié)點(diǎn)ID實(shí)體相關(guān)實(shí)體的對(duì)象(比如多用戶聊天室中的一個(gè)參加者)
/* XMPPFramework 中的初始化方法 */
[XMPPJID jidWithUser:userName domain:domain resource:@"iOS"];
2、XMPPStream
: 流模塊(或者可以叫通信模塊)典勇,XMPP
最基本的一個(gè)模塊劫哼,可以用來(lái)加載其他核心(JID)或者拓展模塊(Mut Chat)。通常在客戶端的所有操作都是在此模塊上進(jìn)行的割笙。
/* XMPPFramework 中的初始化方法 */
// 創(chuàng)建對(duì)象
_xmppStream = [[XMPPStream alloc] init];
// 配置服務(wù)器地址
[_xmppStream setHostName:XMPP_HOST];
// 配置服務(wù)器端口號(hào)(可以不配置权烧,默認(rèn)值為 5222)
[_xmppStream setHostPort:XMPP_PORT];
// 設(shè)置代理及回調(diào)隊(duì)列
[_xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
// 設(shè)置心跳包發(fā)送間隔
[_xmppStream setKeepAliveInterval:30];
// 設(shè)置允許后臺(tái)連接
[_xmppStream setEnableBackgroundingOnSocket:YES];
在 App 的一個(gè)完整的生命周期內(nèi)應(yīng)該只初始化一次此對(duì)象眯亦。
3、XMPPReconnect
: 斷線重連模塊般码,沒(méi)有什么特殊的功能妻率,但最好加上“遄#可以避免因弱網(wǎng)情況下導(dǎo)致掉線而收不到消息宫静。
/* XMPPFramework 中的初始化方法 */
// 創(chuàng)建對(duì)象
_xmppReconnect = [[XMPPReconnect alloc] initWithDispatchQueue:dispatch_get_main_queue()];
// 啟用自動(dòng)重連
[_xmppReconnect setAutoReconnect:YES];
// 激活模塊,一定要加這句代碼否則不會(huì)執(zhí)行
[_xmppReconnect activate:_xmppStream];
4券时、XMPPStreamManagement
: 流管理模塊孤里,這個(gè)模塊其實(shí)應(yīng)該是和 XMPPReconnect
配套使用,作用是在客戶端和服務(wù)器通訊消息中加入一個(gè) </r>
標(biāo)識(shí)橘洞,用來(lái)同步捌袜、確認(rèn)消息是否被成功接收。
/* XMPPFramework 中的初始化方法 */
// 創(chuàng)建流狀態(tài)緩存對(duì)象
_streamStorage = [[XMPPStreamManagementMemoryStorage alloc] init];
// 創(chuàng)建流管理對(duì)象
_xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_streamStorage];
// 設(shè)置自動(dòng)恢復(fù)
[_xmppStreamManagement setAutoResume:YES];
// 設(shè)置代理和返回隊(duì)列
[_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
// 激活模塊
[_xmppStreamManagement activate:_xmppStream];
上面這幾個(gè)模塊是維持 XMPP
正常通訊所必須的震檩,也如大家看到的在 XMPPFramework
中所有模塊的加載都需要一個(gè)步驟:[xxx activate:_xmppStream]
所以當(dāng)大家以后自己添加某個(gè)模塊但沒(méi)有執(zhí)行的時(shí)候可以先檢查一下是否添加了這句代碼琢蛤。
常用代理方法
一個(gè)簡(jiǎn)單的通訊其實(shí)只需要實(shí)現(xiàn) XMPPStream
中以下幾個(gè)常用的代理方法:
// 與服務(wù)器成功建立連接
- (void)xmppStreamDidConnect:(XMPPStream *)sender;
// 注冊(cè) XMPP 賬號(hào)成功
- (void)xmppStreamDidRegister:(XMPPStream *)sender;
// 注冊(cè) XMPP 賬號(hào)失敗 ( error 失敗原因 )
- (void)xmppStream:(XMPPStream *)sender didNotRegister:(NSXMLElement *)error;
// 登錄成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender;
// 登錄失敗 ( error 失敗原因 )
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error;
// 收到 <message></message> 類型的消息
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message;
// 發(fā)送消息成功
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message;
// 發(fā)送消息失敗
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error;
簡(jiǎn)單通訊
實(shí)現(xiàn)步驟
1.初始化流
- (void)initalize
{
// 初始化連接
_xmppStream = [[XMPPStream alloc] init];
[_xmppStream setHostName:XMPP_HOST];
[_xmppStream setHostPort:XMPP_PORT];
[_xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStream setKeepAliveInterval:30];
[_xmppStream setEnableBackgroundingOnSocket:YES];
// 接入斷線重連模塊
_xmppReconnect = [[XMPPReconnect alloc] initWithDispatchQueue:dispatch_get_main_queue()];
[_xmppReconnect setAutoReconnect:YES];
[_xmppReconnect activate:_xmppStream];
// 接入流管理模塊
_streamStorage = [[XMPPStreamManagementMemoryStorage alloc] init];
_xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_streamStorage];
[_xmppStreamManagement setAutoResume:YES];
[_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStreamManagement activate:_xmppStream];
}
2.連接服務(wù)器
- (void)xmppConnect
{
NSError *connectError = nil;
if (![_xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&connectError]) {
NSLog(@"XMPP Connect With Error : %@", connectError);
}
}
3.登錄賬號(hào)
還記得上面提到的 XMPPStream
中的提到成功連接服務(wù)器的代理方法么?
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
// 一定要在成功連接服務(wù)器之后再登錄
NSError *error = nil;
if (![_xmppStream authenticateWithPassword:_myPassword error:&error])
{
NSLog(@"Error Authenticate : %@", error);
}
}
// 登錄成功
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
NSLog(@"Authenticate Success !");
// 發(fā)送上線消息 ( 如果僅僅登錄成功不發(fā)送上線消息的話是無(wú)法正常聊天的 )
XMPPPresence *presence = [XMPPPresence presence];
[_xmppStream sendElement:presence];
// 啟用流管理
[_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
}
// 登錄失敗
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
NSLog(@"Did Not Authenticate : %@", error);
}
4.開(kāi)始聊天
- (void)sendMessage:(NSString *)body to:(XMPPJID *)toJID
{
// 生成一個(gè)消息的唯一標(biāo)識(shí)
NSString *uuId = [_xmppStream generateUUID];
// 初始化消息對(duì)象 (type 類型:私聊-@"chat"抛虏,多人聊天-@"groupchat")
XMPPMessage *message = [XMPPMessage messageWithType:@"chat" to:toJID];
// 拼接消息體
[message addBody:body];
// 拼接消息 ID
[message addAttributeWithName:@"id" stringValue:uuId];
// 發(fā)送消息
[_xmppStream sendElement:message];
}
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
NSLog(@"Did Receive Message : %@", message);
}
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
{
NSLog(@"Send Message Success!");
}
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error
{
NSLog(@"Fail Send Message : %@", error);
}
是不是很簡(jiǎn)單博其,輕輕松松幾步讓你開(kāi)始聊天。
簡(jiǎn)單封裝
IM 作為一個(gè) App 的基本功能迂猴,肯定不會(huì)只在某一個(gè)界面去使用慕淡,那么做一下封裝就顯得很有必要了。下面直接上代碼來(lái)實(shí)現(xiàn)一個(gè)單例:
XMPPManager.h
//
// XMPPManager.h
// MusicSampleChat
//
// Created by LonlyCat on 16/9/21.
// Copyright ? 2016年 LonlyCat. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "XMPP.h"
#import "XMPPReconnect.h"
#import "XMPPStreamManagement.h"
#import "XMPPStreamManagementMemoryStorage.h"
#define XMPP_PORT 5222
#define XMPP_HOST 192.168.1.103 // 地址自己替換個(gè)可用的
typedef void(^MessageStatusHandle)(BOOL);
typedef void(^MessageReceiveHandle)(XMPPMessage *);
@interface XMPPManager : NSObject
/** 與服務(wù)器通訊鏈接 */
@property (nonatomic, strong) XMPPStream *xmppStream;
/** XMPP 用戶對(duì)象(自己) */
@property (nonatomic, strong) XMPPJID *myJID;
/** XMPP 密碼 */
@property (nonatomic, copy ) NSString *myPassword;
/** 斷線重連模塊 */
@property (nonatomic, strong) XMPPReconnect *xmppReconnect;
/** 流管理模塊 */
@property (nonatomic, strong) XMPPStreamManagement *xmppStreamManagement;
@property (nonatomic, strong) XMPPStreamManagementMemoryStorage *streamStorage;
/**
登錄
@param userName 用戶名
@param password 密碼
*/
- (void)loginWithName:(NSString *)userName password:(NSString *)password;
/**
登出
*/
- (void)logOut;
/**
上線
*/
- (void)goOnline;
/**
下線
*/
- (void)goOffline;
/**
發(fā)送消息
@param body 內(nèi)容
@param toJID 發(fā)送對(duì)象
@param type 聊天類型 @"chat" / @"groupchat"
@param statusHandle 消息發(fā)送狀態(tài)回調(diào) YES 發(fā)送成功 NO 發(fā)送失敗
@return XMPPMessage
*/
- (XMPPMessage *)sendMessage:(NSString *)body
to:(XMPPJID *)toJID
type:(NSString *)type
statusHandle:(MessageStatusHandle)statusHandle;
/**
添加一個(gè)收到消息的回調(diào)沸毁,key 不能為空
@param receiveHandle 回調(diào)
@param key 查詢峰髓、刪除關(guān)鍵字
@return 是否添加成功
*/
- (BOOL)addReceiveHandle:(MessageReceiveHandle)receiveHandle
forKey:(NSString *)key;
/**
根據(jù)關(guān)鍵字移除一個(gè)消息回調(diào)
*/
- (void)removeReceiveHandleByKey:(NSString *)key;
@end
XMPPManager* getXMPPManager();
XMPPManager.m
#import "XMPPManager.h"
@interface XMPPManager ()
@property (nonatomic, strong) NSMutableDictionary *statusHandleDic;
@property (nonatomic, strong) NSMutableDictionary *receiveHandlesDic;
@end
@implementation XMPPManager
+ (XMPPManager *)sharedInstance
{
static XMPPManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[XMPPManager alloc] init];
manager.statusHandleDic = [NSMutableDictionary dictionary];
manager.receiveHandlesDic = [NSMutableDictionary dictionary];
});
return manager;
}
XMPPManager* getXMPPManager()
{
XMPPManager *manager = [XMPPManager sharedInstance];
return manager;
}
#pragma mark - 初始化
- (instancetype)init
{
self = [super init];
if (self) {
[self initalize];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:UIApplicationWillTerminateNotification
object:nil];
return self;
}
- (void)initalize
{
// 初始化連接
_xmppStream = [[XMPPStream alloc] init];
[_xmppStream setHostName:XMPP_HOST];
[_xmppStream setHostPort:XMPP_PORT];
[_xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStream setKeepAliveInterval:30];
[_xmppStream setEnableBackgroundingOnSocket:YES];
// 接入斷線重連模塊
_xmppReconnect = [[XMPPReconnect alloc] initWithDispatchQueue:dispatch_get_main_queue()];
[_xmppReconnect setAutoReconnect:YES];
[_xmppReconnect activate:_xmppStream];
// 接入流管理模塊
_streamStorage = [[XMPPStreamManagementMemoryStorage alloc] init];
_xmppStreamManagement = [[XMPPStreamManagement alloc] initWithStorage:_streamStorage];
[_xmppStreamManagement setAutoResume:YES];
[_xmppStreamManagement addDelegate:self delegateQueue:dispatch_get_main_queue()];
[_xmppStreamManagement activate:_xmppStream];
}
#pragma mark - 登錄狀態(tài)
- (void)loginWithName:(NSString *)userName password:(NSString *)password
{
NSLog(@"userName : %@, password : %@",userName, password);
_myPassword = password;
NSString *domain = XMPP_HOST;
_myJID = [XMPPJID jidWithUser:userName domain:domain resource:@"iOS"];
[_xmppStream setMyJID:_myJID];
if ([_xmppStream isConnected]) {
[self xmppAuthen];
} else {
[self xmppConnect];
}
}
- (void)logOut
{
[self goOffline];
[_xmppStream disconnectAfterSending];
}
- (void)goOnline
{
XMPPPresence *presence = [XMPPPresence presence];
[_xmppStream sendElement:presence];
}
- (void)goOffline
{
XMPPPresence *presence = [XMPPPresence presenceWithType:@"unavailable"];
[_xmppStream sendElement:presence];
}
#pragma mark - 消息發(fā)送 / 接收
- (IMMessageModel *)sendMessage:(NSString *)body
to:(XMPPJID *)toJID
type:(NSString *)type
statusHandle:(MessageStatusHandle)statusHandle
{
return [self sendMessage:body to:toJID type:type extend:nil statusHandle:statusHandle];
}
- (XMPPMessage *)sendMessage:(NSString *)body
to:(XMPPJID *)toJID
type:(NSString *)type
extend:(DDXMLElement *)extend
statusHandle:(MessageStatusHandle)statusHandle
{
NSString *uuId = [_xmppStream generateUUID];
XMPPMessage *message = [XMPPMessage messageWithType:type to:toJID];
[message addBody:body];
[message addAttributeWithName:@"id" stringValue:uuId];
[_xmppStream sendElement:message];
[_statusHandleDic setObject:statusHandle forKey:uuId];
return message;
}
- (BOOL)addReceiveHandle:(MessageReceiveHandle)receiveHandle
forKey:(NSString *)key
{
if (receiveHandle && [key isKindOfClass:[NSString class]]) {
[_receiveHandlesDic setObject:receiveHandle forKey:key];
return YES;
}
return NO;
}
- (void)removeReceiveHandleByKey:(NSString *)key
{
[_receiveHandlesDic removeObjectForKey:key];
}
#pragma mark - XMPPStreamDelegate
#pragma mark connect
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
NSLog(@"%s", __func__);
[self xmppAuthen];
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
NSLog(@"%s", __func__);
}
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
NSLog(@"Authenticate Success !");
// 發(fā)送上線消息
[self goOnline];
// 啟用流管理
[_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
// 發(fā)送 xmpp 登錄成功通知
[[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginSuccess object:nil];
}
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
NSLog(@"Did Not Authenticate : %@", error);
[[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginFail object:error];
}
#pragma mark message
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
NSLog(@"Did Receive Message : %@", message);
if ([message isErrorMessage]) {
// 警告類消息
NSLog(@"error message : %@ ", [message errorMessage]);
return;
}
NSString *type = [[message attributeForName:@"type"] stringValue];
if ([message isChatMessageWithBody]) {
// 私聊消息
[self sendReceiveMessageNotification:message];
} else if ([message isGroupChatMessageWithBody]) {
// 群聊消息
[self sendReceiveMessageNotification:message];
} else if ([type isEqualToString:@"normal"]) {
// 常規(guī)類型(已知邀請(qǐng)群聊為常規(guī)類型,不用在此處理)
}
}
- (void)xmppStream:(XMPPStream *)sender didSendMessage:(XMPPMessage *)message
{
NSLog(@"Did Send Message !");
NSString *key = [message elementID];
[self handleMessageStatusByKey:key result:YES];
}
- (void)xmppStream:(XMPPStream *)sender didFailToSendMessage:(XMPPMessage *)message error:(NSError *)error
{
NSLog(@"Fail Send Message : %@", error);
NSString *key = [message elementID];
[self handleMessageStatusByKey:key result:NO];
}
#pragma mark - 私有方法
- (void)xmppConnect
{
NSError *connectError = nil;
if (![_xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&connectError]) {
NSLog(@"XMPP Connect With Error : %@", connectError);
}
}
- (void)xmppAuthen
{
NSError *error = nil;
if (![_xmppStream authenticateWithPassword:_myPassword error:&error]) {
NSLog(@"Error Authenticate : %@", error);
[[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginFail object:error];
}
}
- (void)handleMessageStatusByKey:(NSString *)string result:(BOOL)result
{
MessageStatusHandle statusHandle = [_statusHandleDic objectForKey:string];
if (statusHandle) {
statusHandle(result);
[_statusHandleDic removeObjectForKey:string];
statusHandle = nil;
}
}
- (void)sendReceiveMessageNotification:(XMPPMessage *)message
{
NSArray *receiveHandleArray = [_receiveHandlesDic allValues];
[receiveHandleArray enumerateObjectsUsingBlock:^(MessageReceiveHandle _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop)
{
obj(message);
}];
}
#pragma mark -- terminate
/**
* 申請(qǐng)后臺(tái)時(shí)間來(lái)清理下線的任務(wù)
*/
- (void)applicationWillTerminate:(NSNotification *)noti
{
[self goOffline];
[_xmppStream disconnectAfterSending];
}
@end
有了這個(gè)封裝好的單例我們就可以開(kāi)始聊天了( 當(dāng)然你首先得有一個(gè)搭好的 XMPP 服務(wù)器 )息尺。
結(jié)尾
如果你是有一定經(jīng)驗(yàn)的開(kāi)發(fā)者携兵,看完之后肯定能發(fā)現(xiàn)倆個(gè)問(wèn)題:
-
XMPPMessage
的傳值使用的是原始格式 - 消息對(duì)象沒(méi)有進(jìn)行存儲(chǔ)
這倆個(gè)問(wèn)題對(duì)應(yīng)的能力應(yīng)該是 iOS 開(kāi)發(fā)者的基本功,所以我希望你能自己修改下上面的代碼來(lái)解決搂誉。
PS : XMPPFramework
框架本身是實(shí)現(xiàn)了對(duì) XMPPMessage
的存儲(chǔ)徐紧,實(shí)現(xiàn)方式是使用的 CoreData
。雖然方便了開(kāi)發(fā)者進(jìn)行開(kāi)發(fā)炭懊,但對(duì)于某些需求的定制造成了局限( 比如你想保存用戶的聊天記錄到服務(wù)器 )并级。所以在這里我還是推薦大家使用 realm
或者 SQLite
來(lái)實(shí)現(xiàn)數(shù)據(jù)的存儲(chǔ)