最近項目中須要集成IM功能,市面上有不少的第三方提供im服務,好比環信、融雲等,但都有使用限制的地方,若是不使用第三方能夠本身去實現一套IM系統,不過一個IM系統涉及到的東西比較多,開發難度較高。另外一種選擇是使用xmpp,xmpp的優勢是有不少的開源實現,好比服務端的ejabberd、Openfire,iOS以及安卓端都很優秀的開源庫可使用,並且傳輸安全以及擴展性強等(環信也是基於xmpp);同時也有一些缺點,好比不能傳輸二進制數據以及費流量等,有些地方須要去改進。
下面記錄如何使用XMPP來簡單實現IM功能,在這以前須要先搭建本地服務器用於測試。html
爲了實現IM聊天,須要先搭建一個XMPP服務器,這裏咱們採用ejabberd來做爲服務器,ejabbered採用Erlang語言編寫,因爲語言的特性天生適合高併發的情景。git
brew install erlang
等待安裝完成便可。
chmod 755 stop chmod 755 start
如今輸入命令 ./start能夠看到以下的頁面
點擊虛擬主機,選擇用戶菜單能夠看到添加用戶的頁面以下,咱們添加了user1@lujiangbin.local和user2@lujiangbin.local兩個用戶:github
打開iMessage添加用戶安全
iMessage可能會提示服務器的證書須要驗證,點擊繼續便可:服務器
能夠看到user1已經登錄成功了。網絡
接着打開Adium添加user2@lujiangbin.local用戶,添加過程跟iMessage相似:併發
設置鏈接服務器爲localhost:app
因爲如今user1和user2還不是好友關係,所以無法進行聊天,點擊iMessage添加好友user2@lujiangbin.local:dom
在Adium會收到驗證請求,點擊接受雙方互加好友,接着就能夠進行im聊天了:socket
這樣咱們確認本地的xmpp服務器是可用的,接下來就能夠着手客戶端的開發了
這裏咱們使用XMPPFramework這個開源庫,安卓平臺可使用Smack(最好使用4.1以及以後的版本,支持流管理),爲了簡單起見這裏只實現登錄、獲取好友列表以及聊天等功能,頁面以下所示:
在開始使用xmpp進行IM聊天以前,咱們須要初始化xmpp流,接入咱們須要的模塊:
#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。
#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方法:
//獲取服務器好友列表 [[[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超時或者服務器本身的心跳包),它會認爲對方還在線,將後續的消息發送過去,這樣到服務器知道對方掉線的這段時間,期間的消息就丟失了,因此須要流管理來處理。
服務端: <r xmlns='urn:xmpp:sm:3'/> 客戶端: <a xmlns='urn:xmpp:sm:3' h='3'/>
好比服務端發送<r>請求,客戶端返回本身接受收到的h值(3),而後服務端會根據這個h值跟它本身記錄發出去的節的h值作比較,假如小的話會從新發送剩下的節,來防止節丟失。
流恢復
因爲移動網絡可能隨時down掉,因此在咱們重連上來的時候須要的是快速恢復上一次的流,而不是從新新建一個流,roster的檢索以及狀態的廣播,流管理能夠經過上一次的流id(當啓用流管理的時候,服務端會生成一個id來表示一個流)以及雙方的h值來完成流的快速恢復以及這期間的節確認,發送未被確認的節。
開啓流管理
要想啓用流管理,客戶端發送<enable/>元素給服務端,服務端返回<enabled/>元素表示該流已經被管理了,同時有一個id值來標示這個流,xmppframework開啓流管理只須要調用
enableStreamManagementWithResumption: maxTimeout:接口:
客戶端: <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工程能夠在這裏找到。