最近在寫一支與即時訊息有關的社交APP,需要使用 XMPP 來完成即時訊息聊天的功能,因此在這裡我做個簡單筆記記錄一下最近的XMPP開發心得,不過我也是自己慢慢摸索,算不上什麼厲害的人就是了,所以不介意的話就參考看看吧。
<前言>
此篇主要是做一些最最最基本前置的設定,與程式碼都還無關,但是這些設定沒做好,後面可就頭大拉!
<正文開始>
首先,若是要開發與XMPP的APP,需要先架一台 XMPPServer,可以使用ejabber或是openfire伺服器,有關XMPP SERVER的細節可以參考: 這裡。因此有關伺服器的部分我就不多贅述了。在完成架設伺服器之後,接著就要來完成XMPP iOS Client端的部分嘍。
一開始,我們要先到 這裡 下載XMPP Framework,可以直接點選Download ZIP。下載完成後,開啓Xcode,建立一個新的專案(我將新專案取名為:XMPPTry),把剛剛在下載的XMPPFramework-master資料夾中的Authentication, Categories, Core, Extensions, Utilities, Vendor 匯入到新專案XMPPTry內(新專案的資料夾內應該要有這些檔案)。
其中,特別注意資料夾內有個Sample_XMPPFramework.h ,把它複製一份到你的APP資料夾中,並把名稱改為:XMPPFramework.h,這樣一來之後你就可以只要include這個header就可以了。至於裡面內容的設定細節後面會再討論,完成後在我的新專案資料夾XMPPTry內應如下圖。
然後打開Xcode把這些東西都匯入進去。
接下來,在Xcode中將一些你所需要的Framework加到專案內,如下圖匯入這些Framework。
然後將Build Setting裡的Header Search Path加上 /libxml2
現在,如果都沒有問題,就即將進入設定APP Delegate的部分拉!!
我們把一些XMPP的主要方法寫在APPDelegate中,基本上我這裡是參考剛剛所下載下來的XMPP Framework裡面的範例程式加以修改而成。
在 AppDelegate.h 中寫入以下程式碼。
到此為止,我們算是將XMPP一開始最基本的匯入工作完成了(當然,還沒正式開工寫程式呢)。
我們接著打開剛才曾經說過的XMPPFramework.h檔案,將你即將會需要用到的檔案import進來(基本上在Sample_XMPPFramework.h中除了XMPP.h外所有的module都被Comment掉了,你可以自己選擇接下來要使用哪些標頭檔),下面的是我所使用的,我把它們uncomment之後,將XMPPFramework.h加入我們的專案中。
其中,
XMPPReconnect.h
負責重新連線
XMPPMessageArchivingCoreDataStorage.h
負責處理訊息的資料儲存 (透過Core Data)
XMPPRoster.h
XMPPRosterCoreDataStorage.h
負責處理好友清單與資料儲存
XMPPvCardTemp.h
XMPPvCardTempModule.h
XMPPvCardAvatarModule.h
XMPPvCardCoreDataStorage.h
負責處理個人資料的內容,包含姓名、暱稱、大頭照、地址等等
XMPPCapabilitiesCoreDataStorage.h
XMPPCapabilities.h
負責支持Capabilities
XMPPMUC.h
XMPPRoomCoreDataStorage.h
負責處理聊天室與相關的資料儲存,多人聊天等等
我們把一些XMPP的主要方法寫在APPDelegate中,基本上我這裡是參考剛剛所下載下來的XMPP Framework裡面的範例程式加以修改而成。
在 AppDelegate.h 中寫入以下程式碼。
//
// JTAppDelegate.h
// XMPPTry
//
// Created by MacBook Pro on 2014/8/12.
// Copyright (c) 2014年 Jacky Tsai. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "XMPPFramework.h"
#import <CoreData/CoreData.h>
@interface JTAppDelegate : UIResponder <UIApplicationDelegate, XMPPRosterDelegate>
{
XMPPStream *xmppStream;
XMPPReconnect *xmppReconnect;
XMPPRoster *xmppRoster;
XMPPRosterCoreDataStorage *xmppRosterStorage;
XMPPvCardCoreDataStorage *xmppvCardStorage;
XMPPvCardTempModule *xmppvCardTempModule;
XMPPvCardAvatarModule *xmppvCardAvatarModule;
XMPPCapabilities *xmppCapabilities;
XMPPCapabilitiesCoreDataStorage *xmppCapabilitiesStorage;
XMPPMessageArchivingCoreDataStorage *xmppMessageArchivingCoreDataStorage;
XMPPMessageArchiving *xmppMessageArchivingModule;
UINavigationController *navigationController;
LoginViewController *loginViewController;
NSString *password;
BOOL customCertEvaluation;
BOOL isXmppConnected;
}
@property (nonatomic, strong, readonly) XMPPStream *xmppStream;
@property (nonatomic, strong, readonly) XMPPReconnect *xmppReconnect;
@property (nonatomic, strong, readonly) XMPPRoster *xmppRoster;
@property (nonatomic, strong, readonly) XMPPRosterCoreDataStorage *xmppRosterStorage;
@property (nonatomic, strong, readonly) XMPPvCardTempModule *xmppvCardTempModule;
@property (nonatomic, strong, readonly) XMPPvCardAvatarModule *xmppvCardAvatarModule;
@property (nonatomic, strong, readonly) XMPPCapabilities *xmppCapabilities;
@property (nonatomic, strong, readonly) XMPPCapabilitiesCoreDataStorage *xmppCapabilitiesStorage;
@property (nonatomic, strong) XMPPMessageArchivingCoreDataStorage *xmppMessageArchivingCoreDataStorage;
@property (nonatomic, strong) XMPPMessageArchiving *xmppMessageArchivingModule;
- (NSManagedObjectContext *)managedObjectContext_roster;
- (NSManagedObjectContext *)managedObjectContext_capabilities;
- (BOOL)connect;
- (void)disconnect;
- (void)setupStream;
- (void)teardownStream;
- (void)goOnline;
- (void)goOffline;
@end
在AppDelegate.h 中的程式碼,我們宣告了一些會使用到關於XMPP的變數,接下來,在AppDelegate.m 中 import 這些檔案。
結束之後,synthesize在.h檔中的property,完成後如下圖。
設定Core Data
#import "GCDAsyncSocket.h"
#import "XMPP.h"
#import "XMPPLogging.h"
#import "XMPPReconnect.h"
#import "XMPPCapabilitiesCoreDataStorage.h"
#import "XMPPRosterCoreDataStorage.h"
#import "XMPPvCardAvatarModule.h"
#import "XMPPvCardCoreDataStorage.h"
#import "DDLog.h"
#import "DDTTYLogger.h"
#import <AudioToolbox/AudioServices.h>
#import <CFNetwork/CFNetwork.h>
接著在.m檔中開始撰寫以下的函數
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Configure logging framework
[DDLog addLogger:[DDTTYLogger sharedInstance]];
//設定接收推播 (Push notification)
[[UIApplication sharedApplication] registerForRemoteNotificationTypes: UIRemoteNotificationTypeAlert|UIRemoteNotificationTypeSound|UIRemoteNotificationTypeBadge];
// 設定 XMPP stream
[self setupStream];
// 設定XMPP連線,未連線成功則跳到logIn頁面 (loginViewController )
if (![self connect])
{
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 0.0 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[navigationController presentViewController:loginViewController animated:YES completion:NULL];
});
}
return YES;
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
#if TARGET_IPHONE_SIMULATOR
DDLogError(@"The iPhone simulator does not process background network traffic. "
@"Inbound traffic is queued until the keepAliveTimeout:handler: fires.");
#endif
if ([application respondsToSelector:@selector(setKeepAliveTimeout:handler:)])
{
[application setKeepAliveTimeout:600 handler:^{
DDLogVerbose(@"KeepAliveHandler");
// Do other keep alive stuff here.
}];
}
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
[self connect ];
}
- (void)dealloc
{
[self teardownStream];
}
設定Core Data
#pragma mark Core Data
- (NSManagedObjectContext *)managedObjectContext_roster
{
return [xmppRosterStorage mainThreadManagedObjectContext];
}
- (NSManagedObjectContext *)managedObjectContext_capabilities
{
return [xmppCapabilitiesStorage mainThreadManagedObjectContext];
}
- (void)setupStream {
NSAssert(xmppStream == nil, @"Method setupStream invoked multiple times");
xmppStream = [[XMPPStream alloc] init];
//確定裝置並非模擬器
#if !TARGET_IPHONE_SIMULATOR
{
//let xmpp run in the background
xmppStream.enableBackgroundingOnSocket = YES;
}
#endif
//init Reconnect 斷線後可以重新連線
xmppReconnect = [[XMPPReconnect alloc] init];
// 此處可以自行更動成你想要使用的儲存方式
// 這裡是使用core data進行資料儲存
xmppRosterStorage = [[XMPPRosterCoreDataStorage alloc] init];
xmppRoster = [[XMPPRoster alloc] initWithRosterStorage:xmppRosterStorage];
// 設定是否自動從伺服器抓取你的好友名單
xmppRoster.autoFetchRoster = YES;
// 設定是否自動接受送來的好友邀請
xmppRoster.autoAcceptKnownPresenceSubscriptionRequests = YES;
//XMPPRoster 會自動整合 XMPPvCardAvatarModule 以取得roster中使用者的大頭照.
//vCard Avatar module 會和標準的 vCard Temp module 一起下載使用者大頭照.
xmppvCardStorage = [XMPPvCardCoreDataStorage sharedInstance];
xmppvCardTempModule = [[XMPPvCardTempModule alloc] initWithvCardStorage:xmppvCardStorage];
xmppvCardAvatarModule = [[XMPPvCardAvatarModule alloc] initWithvCardTempModule:xmppvCardTempModule];
// 設定 capabilities: For hashing
xmppCapabilitiesStorage = [XMPPCapabilitiesCoreDataStorage sharedInstance];
xmppCapabilities = [[XMPPCapabilities alloc] initWithCapabilitiesStorage:xmppCapabilitiesStorage];
xmppCapabilities.autoFetchHashedCapabilities = YES;
xmppCapabilities.autoFetchNonHashedCapabilities = NO;
// 設定Core Data
xmppMessageArchivingCoreDataStorage = [XMPPMessageArchivingCoreDataStorage sharedInstance];
xmppMessageArchivingModule = [[XMPPMessageArchiving alloc]initWithMessageArchivingStorage:xmppMessageArchivingCoreDataStorage];
[xmppMessageArchivingModule setClientSideMessageArchivingOnly:YES];
// Activate xmpp modules
[xmppReconnect activate:xmppStream];
[xmppRoster activate:xmppStream];
[xmppvCardTempModule activate:xmppStream];
[xmppvCardAvatarModule activate:xmppStream];
[xmppCapabilities activate:xmppStream];
[xmppMessageArchivingModule activate:xmppStream];
// 加自己為 delegate
[xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
[xmppRoster addDelegate:self delegateQueue:dispatch_get_main_queue()];
[xmppvCardTempModule addDelegate:self delegateQueue:dispatch_get_main_queue()];
[xmppvCardAvatarModule addDelegate:self delegateQueue:dispatch_get_main_queue()];
[xmppMessageArchivingModule addDelegate:self delegateQueue:dispatch_get_main_queue()];
//設定伺服器的Host Name & Port (這裡請設定你自己的伺服器IP和Port)
[xmppStream setHostName:@"140.112.XXX.XX"];
[xmppStream setHostPort:5222];
// You may need to alter these settings depending on the server you're connecting to
customCertEvaluation = YES;
}
- (void)teardownStream {
[xmppStream removeDelegate:self];
[xmppRoster removeDelegate:self];
[xmppReconnect deactivate];
[xmppRoster deactivate];
[xmppvCardTempModule deactivate];
[xmppvCardAvatarModule deactivate];
[xmppCapabilities deactivate];
[xmppStream disconnect];
xmppStream = nil;
xmppReconnect = nil;
xmppRoster = nil;
xmppRosterStorage = nil;
xmppvCardStorage = nil;
xmppvCardTempModule = nil;
xmppvCardAvatarModule = nil;
xmppCapabilities = nil;
xmppCapabilitiesStorage = nil;
}
設定使用者上下線
- (void)goOnline
{
XMPPPresence *presence = [XMPPPresence presence];
[[self xmppStream] sendElement:presence];
}
- (void)goOffline
{
XMPPPresence *presence = [XMPPPresence presenceWithType:@"unavailable"];
[[self xmppStream] sendElement:presence];
}
設定XMPP的連線
#pragma mark Connect/disconnect
- (BOOL)connect
{
if (![xmppStream isDisconnected]) {
NSLog(@"已經連好線拉");
return YES;
}
NSString *myJID = [[NSUserDefaults standardUserDefaults] stringForKey: @"JID"];
NSString *myPassword = [[NSUserDefaults standardUserDefaults] stringForKey: @"JPassword"];
if (myJID == nil || myPassword == nil) {
return NO;
}
[xmppStream setMyJID:[XMPPJID jidWithString:myJID]];
password = myPassword;
NSError *error = nil;
if (![xmppStream connectWithTimeout:XMPPStreamTimeoutNone error:&error])
{
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error connecting"
message:@"See console for error details."
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alertView show];
DDLogError(@"Error connecting: %@", error);
return NO;
}
return YES;
}
- (void)disconnect
{
[self goOffline];
[xmppStream disconnect];
}
設定XMPPStream Delegate (這裡的Function 較多,要仔細看一下)
#pragma mark XMPPStream Delegate
- (void)xmppStream:(XMPPStream *)sender socketDidConnect:(GCDAsyncSocket *)socket
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}
- (void)xmppStream:(XMPPStream *)sender willSecureWithSettings:(NSMutableDictionary *)settings
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
NSString *expectedCertName = [xmppStream.myJID domain];
if (expectedCertName)
{
[settings setObject:expectedCertName forKey:(NSString *)kCFStreamSSLPeerName];
}
if (customCertEvaluation)
{
[settings setObject:@(YES) forKey:GCDAsyncSocketManuallyEvaluateTrust];
}
}
- (void)xmppStream:(XMPPStream *)sender didReceiveTrust:(SecTrustRef)trust
completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
dispatch_queue_t bgQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(bgQueue, ^{
SecTrustResultType result = kSecTrustResultDeny;
OSStatus status = SecTrustEvaluate(trust, &result);
if (status == noErr && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {
completionHandler(YES);
}
else {
completionHandler(NO);
}
});
}
- (void)xmppStreamDidSecure:(XMPPStream *)sender
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
isXmppConnected = YES;
NSError *error = nil;
if (![[self xmppStream] authenticateWithPassword:password error:&error])
{
DDLogError(@"Error authenticating: %@", error);
}
}
//這個函數特別重要,他是認證使用者是否為user的函數,當使用者欲建立連線時,會call這個函數
//很多人在做註冊新會員時,就需要透過此函數新增使用者
//這裡寫的是如果通過認證就上傳vcard(個人資料)
- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
NSLog(@"認證通過~~");
// 上傳使用者的資料 (vCard) , 先拿看看vCard,若不存在創一個新的上傳,若已存在,直接更新
XMPPvCardTemp *temp = [self.xmppvCardTempModule myvCardTemp];
if (!temp)
{
NSXMLElement *vCardXML = [NSXMLElement elementWithName:@"vCard" xmlns:@"vcard-temp"];
XMPPvCardTemp *newvCardTemp = [XMPPvCardTemp vCardTempFromElement:vCardXML];
[newvCardTemp setNickname:@"UserABC"];
[newvCardTemp setPhoto:nil];
[xmppvCardTempModule updateMyvCardTemp:newvCardTemp];
}
else{
temp.nickname = @"UserABC";
temp.photo = nil ;
[self.xmppvCardTempModule updateMyvCardTemp:temp];
}
[self goOnline];
}
- (void)xmppStream:(XMPPStream *)sender didNotAuthenticate:(NSXMLElement *)error
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}
- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
return NO;
}
//收到他人送來的訊息時會呼叫此函數
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
if ([message isChatMessageWithBody])
{
XMPPUserCoreDataStorageObject *user = [xmppRosterStorage userForJID:[message from]
xmppStream:xmppStream
managedObjectContext:[self managedObjectContext_roster]];
NSString *nickName = [user nickname];
if (nickName == NULL) {
nickName = @"Someone";
}
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive)
{
//處理使用者正在使用APP時收到新訊息的狀況,本處先讓手機震動一下
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}
else
{
//若收到訊息時並未開啟APP,則推播新訊息給User
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertAction = @"OK";
localNotification.alertBody = [NSString stringWithFormat:@"%@ send you a message",nickName];
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
}
}
}
//收到他人上下線時會呼叫此函數
- (void)xmppStream:(XMPPStream *)sender didReceivePresence:(XMPPPresence *)presence
{
DDLogVerbose(@"%@: %@ - %@", THIS_FILE, THIS_METHOD, [presence fromStr]);
if (presence.status) {
//處理接到他人上下線狀態更動
}
}
- (void)xmppStream:(XMPPStream *)sender didReceiveError:(id)error
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
}
- (void)xmppStreamDidDisconnect:(XMPPStream *)sender withError:(NSError *)error
{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
if (!isXmppConnected)
{
DDLogError(@"Unable to connect to server. Check xmppStream.hostName");
}
}
#pragma mark xmppRoster Deledgate
//當接收到他人對你的訂閱請求
- (void)xmppRoster:(XMPPRoster *)sender didReceivePresenceSubscriptionRequest:(XMPPPresence *)presence{
DDLogVerbose(@"%@: %@", THIS_FILE, THIS_METHOD);
XMPPUserCoreDataStorageObject *user = [xmppRosterStorage userForJID:[presence from]
xmppStream:xmppStreammanagedObjectContext:[self managedObjectContext_roster]];
DDLogVerbose(@"didReceivePresenceSubscriptionRequest from user %@ ", user.jidStr);
//直接接受他人對你的訂閱請求
[xmppRoster acceptPresenceSubscriptionRequestFrom:[presence from]
andAddToRoster:YES];
#pragma mark Receive Notification
//處理推播相關的函數
NSLog(@"Received description: %@", [userInfo description]);
#if 0
NSDictionary *aps = [userInfo objectForKey:@"aps"];if(aps != nil) {
NSDictionary *alert = [aps objectForKey:@"alert"];
if(alert != nil) {
NSString *loc_key = [alert objectForKey:@"loc-key"];
NSString *body = [alert objectForKey:@"body"];
if(loc_key != nil) {
if([loc_key isEqualToString:@"IM_MSG"]) {
// ???
}
else if([loc_key isEqualToString:@"IC_MSG"]) {
// ???
}
}
if (body != nil) {
PNMessage *pnMessage = [NSEntityDescription insertNewObjectForEntityForName:@"PNMessage"
inManagedObjectContext:[self managedObjectContext]];
[dateFormatter setDateFormat:@"HH:mm:ss"];
NSString *TimeStr = [dateFormatter stringFromDate:[NSDate date]];
[dateFormatter setDateFormat:@"yyyy/MM/dd"];
NSString *DateStr = [dateFormatter stringFromDate:[NSDate date]];
[dateFormatter release];
pnMessage.messageText = body;
NSError *error = nil;
if (![[self managedObjectContext]save:&error]) {
NSLog(@"新增物件時遇到錯誤");
}
else {
NSLog(@"儲存成功:fromUser=%@ messageText=%@",
pnMessage.fromUser, pnMessage.messageText);}
}
}
}
#endif
}
//Receive the push notification
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification{
// process background incoming call
NSLog(@"didReceiveLocalNotification!");
application.applicationIconBadgeNumber = 0;
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Alarm"
message:text delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alertView show];