XMPP - 多人聊天

MUC 簡述

其實想了解一個框架最好的方法就是去閱讀官方文檔怔接,下面會分別給出中英文檔的連接骑篙。(當然如果你沒興趣看文檔的話靶端,也可以直接略過看我提煉出來比較重要的幾個概念)

XEP-0045: 多用戶聊天
XEP-0045: Multi-User Chat

角色(Roles) 和 崗位(Affiliations)

這兩個概念是基于一個用戶在一個聊天室內擁有的權限所提出來的杨名,但倆者對應的權限和時效性是有區(qū)別的须喂。

角色(Roles)

Roles are temporary in that they do not necessarily persist across a user's visits to the room and MAY change during the course of an occupant's visit to the room. An implementation MAY persist roles across visits and SHOULD do so for moderated rooms (since the distinction between visitor and participant is critical to the functioning of a moderated room).

簡單來說意思就是 角色 是動態(tài)變化的坞生。隨著 加入 或者 離開 聊天室這些行為,用戶的 角色 也會發(fā)生變化卒废。

下面是所有已經定義的角色:

  • 主持人(Moderator): 聊天室中權限最高的角色
  • 與會者(Participant): 聊天室的主要參與人
  • 游客(Visitor): 聊天室的瀏覽者
  • 無角色(None): 未加入聊天室的用戶

以下是四種角色對應的具體權限:

權限 游客 與會者 主持人
在聊天室中出席
接受消息
接收/廣播 出席信息
改變可用性狀態(tài)
改變聊天室昵稱
發(fā)送私人消息
邀請用戶
發(fā)送公開消息
修改標題
踢出用戶
授予發(fā)言權
撤銷發(fā)言權

注: 所有對權限的操作均無法對平級用執(zhí)行

崗位(Affiliations)

雖然國內大部分都將 Affiliations 翻譯成 崗位 ,但我個人覺得如果符合中文的理解电谣,主持人 豈不是更符合 崗位 的字面意思辰企。當然這里只是抱怨一下镐捧,我還是會沿用這一翻譯臭增,避免與其它文章中叫法不一給讀者造成閱讀上的障礙列牺。

大家可以把 聊天室 看成是一個俱樂部瞎领,由以下幾種 崗位 組成:

  • 所有者(Owner): 一般為聊天室創(chuàng)建者
  • 管理員(Admin): 所有者可授予其他成員
  • 會員(Member): 聊天室 的正式成員
  • 排斥者(Outcast): 簡單來說就是黑名單九默,排斥者無法進入 聊天室

崗位 相比起 角色 來說是靜態(tài)的身份宾毒,不會隨著用戶上下線诈铛,進入退出聊天室而變動,只有 所有者管理員 主動修改用戶的 崗位

崗位 相關的權限如下:

權限 Outcast(被排斥者) None(無) Member(成員) Admin(管理員) Owner(所有者)
進入聊天室
注冊一個開放的聊天室 N/A N/A N/A
接收成員列表
加入一個僅限會員的聊天室
禁止成員并把用戶的崗位刪除
編輯成員列表
編輯主持人列表
編輯管理員列表
編輯所有者列表
變更聊天室定義
銷毀聊天室

聊天室類型

用戶創(chuàng)建完聊天室后可以選擇性配置聊天室或者使用默認配置耳峦,不同的配置參數(shù)則會使聊天室類型發(fā)生改變。

下面關于不同類型聊天室的區(qū)別摘自 XEP-0045: 多用戶聊天

Hidden Room(隱藏聊天室) : 一個無法被任何用戶以普通方法如搜索和服務查詢來發(fā)現(xiàn)的聊天室; 反義詞: 公開(public)聊天室.

Members-Only Room(僅限會員的聊天室) : 如果一個用戶不在成員列表中則無法加入的一個聊天室; 反義詞: 開放(open)聊天室.

Moderated Room(被主持的聊天室) : 只有有"發(fā)言權"的用戶才可以發(fā)送消息給所有房客的聊天室; 反義詞: 非主持的(Unmoderated)聊天室.

Non-Anonymous Room(非匿名聊天室) : 一個房客的全JID會暴露給所有其他房客的聊天室, 盡管房客可以選擇任何期望的聊天室昵稱; 相對的是半匿名(Semi-Anonymous)聊天室.

Open Room(開放聊天室) : 任何人可以加入而不需要在成員列表中的聊天室; 反義詞: 僅限會員的聊天室.

Password-Protected Room(密碼保護聊天室) : 一個用戶必須提供正確密碼才能加入的聊天室; 反義詞: 非保密聊天室.

Persistent Room(持久聊天室) : 如果最后一個房客退出也不會被銷毀的聊天室; 反義詞: 臨時聊天室.

Public Room(公開聊天室) : 用戶可以通過普通方法如搜索和服務查詢來發(fā)現(xiàn)的聊天室; 反義詞: 隱藏聊天室.

Semi-Anonymous Room(半匿名聊天室) : 一個房客的全JID只能被聊天室管理員發(fā)現(xiàn)的聊天室; 相對的是非匿名(Non-Anonymous)聊天室.

Temporary Room(臨時聊天室) : 如果最后一個房客退出就會被銷毀的聊天室; 反義詞: 持久聊天室.

Unmoderated Room(非主持的聊天室) : 任何房客都被允許發(fā)送消息給所有房客的聊天室; 反義詞: 被主持的聊天室.

Unsecured Room(非保密聊天室) : 任何人不需要提供密碼就可以進入的聊天室; 反義詞: 密碼保護聊天室.

不同的 聊天室類型 針對不同 崗位 的用戶進入聊天室時會設置一個默認的角色妨退,具體如下表:

聊天室類型 \ 崗位 會員 管理員 所有者
被主持的聊天室 游客 與會者 主持人 主持人
非主持的聊天室 與會者 與會者 主持人 主持人
僅會員加入的聊天室 (無法加入聊天室) 與會者 主持人 主持人
開放聊天室 與會者 與會者 主持人 主持人

MUC 實操

在把基本的概念介紹一遍后妇萄,我們終于要開始 Code 環(huán)節(jié)啦蜕企!

MUC 初始化

1、首先還是得在我們的 XMPPStream 初始化時加入 MUC 模塊

- (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];
    
    .....
    
    // 接入群聊模塊
    _xmppMuc = [[XMPPMUC alloc] init];
    [_xmppMuc addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [_xmppMuc activate:_xmppStream];
}

2冠句、在登錄成功后轻掩,調用 discoverServices 來發(fā)現(xiàn)群聊服務

- (void)xmppStreamDidAuthenticate:(XMPPStream *)sender
{
    NSLog(@"Authenticate Success !");
    
    // 發(fā)送上線消息
    [self goOnline];
    
    // 啟用流管理
    [_xmppStreamManagement enableStreamManagementWithResumption:YES maxTimeout:0];
    
    // 獲取群列表
    [self.xmppMuc discoverServices];
    
    // 發(fā)送 xmpp 登錄成功通知
    [[NSNotificationCenter defaultCenter] postNotificationName:XMPPLoginSuccess object:nil];
}

3、實現(xiàn) MUC 模塊的代理方法

/** 成功發(fā)現(xiàn) xmpp 服務器上的 服務地址 */
- (void)xmppMUC:(XMPPMUC *)sender didDiscoverServices:(NSArray *)services
{
    NSLog(@"Discover Services Success : %@", services);
    
    // 取出多人聊天服務地址
    DDXMLElement *item = [services firstObject];
    
    // 查找聊天室
    _roomService = [[item attributeForName:@"jid"] stringValue];
    [_xmppMuc discoverRoomsForServiceNamed:_roomService];
}

/** 發(fā)現(xiàn) xmpp 服務器上的 服務地址失敗 */
- (void)xmppMUCFailedToDiscoverServices:(XMPPMUC *)sender withError:(NSError *)error
{
    NSLog(@"Discover Services Failed : %@", error);
}

/** 成功獲取到 serviceName 服務器上的聊天室集 */
- (void)xmppMUC:(XMPPMUC *)sender didDiscoverRooms:(NSArray *)rooms forServiceNamed:(NSString *)serviceName
{
    
}

/** 獲取 serviceName 服務器上的聊天室集失敗 */
- (void)xmppMUC:(XMPPMUC *)sender failedToDiscoverRoomsForServiceNamed:(NSString *)serviceName withError:(NSError *)error
{
    NSLog(@"Discover Rooms Failed : %@", error);
}

這里對于 [self.xmppMuc discoverServices]; 方法還是有一點說明的。在實際開發(fā)中可以省略此步驟讓后臺直接告知 多人聊天服務地址 然后直接調用 [_xmppMuc discoverRoomsForServiceNamed:_roomService] 去查詢聊天室集即可。

MUC 創(chuàng)建聊天室

1、發(fā)送創(chuàng)建聊天室請求

/**
 請求創(chuàng)建一個聊天室
 
 @param roomName      聊天室名稱
 @param createHandle  創(chuàng)建回調
 @return 請求是否發(fā)送
 */
- (BOOL)createRoomWithName:(NSString *)roomName
                    handle:(MucCreateHandle)createHandle
{
    if (_createHandle) {
        
        return NO;
    }
    
    _roomName     = roomName;
    _createHandle = createHandle;
    
    NSString  *roomId = [_xmppStream generateUUID];
    
    XMPPJID         *jid  = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@", roomId, _roomService]];
    XMPPRoomMemoryStorage  *roomStorage = [[XMPPRoomMemoryStorage alloc] init];
    
    XMPPCustomRoom  *room = [[XMPPCustomRoom alloc] initWithRoomStorage:roomStorage
                                                                    jid:jid
                                                          dispatchQueue:dispatch_get_main_queue()];
    [room addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [room activate:_xmppStream];
    [room createRoomUsingName:_myJID.user password:nil];
    
    return YES;
}

2课蔬、實現(xiàn) XMPPRoom 代理、并發(fā)送配置信息


- (void)sendDefaultRoomConfig:(XMPPRoom *)room
{
    DDXMLElement  *x = [DDXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"];
    
    // 配置聊天室名稱
    DDXMLElement  *nameField = [DDXMLElement elementWithName:@"field"];
    [nameField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_roomname"];
    DDXMLElement  *nameValue = [DDXMLElement elementWithName:@"value" stringValue:_roomName];
    [nameField addChild:nameValue];
    [x addChild:nameField];
    
    // 配置聊天室永久存在
    DDXMLElement  *exitField = [DDXMLElement elementWithName:@"field"];
    [exitField addAttributeWithName:@"var" stringValue:@"muc#roomconfig_persistentroom"];
    DDXMLElement  *exitValue = [DDXMLElement elementWithName:@"value" stringValue:@"1"];
    [exitField addChild:exitValue];
    [x addChild:exitField];
    
    [room configureRoomUsingOptions:x];
    
    _roomName = @"";
}

#pragma mark - room delegate
/** 成功創(chuàng)建聊天室 */
- (void)xmppRoomDidCreate:(XMPPRoom *)sender
{
    // 1. 發(fā)送默認配置
    [self sendDefaultRoomConfig:sender];
    
    // 2. 成功回調 (為了確保 UI 刷新時已經獲取到了聊天室信息襟锐,回調放在 xmppRoom:didFetchInfoList: 中執(zhí)行
}

/** 創(chuàng)建聊天室失敗 */
- (void)xmppRoom:(XMPPRoom *)sender didFailToCreate:(NSError *)error
{
    if (_createHandle) {
        
        _createHandle(NO, nil);
        _createHandle = nil;
    }
}

/** 成功配置聊天室信息 */
- (void)xmppRoom:(XMPPRoom *)sender didFetchInfoList:(DDXMLElement *)identity
{   
    if (_createHandle) {
        
        _createHandle(YES, sender);
        _createHandle = nil;
    }
    
    if (_joinHandle) {
        
        _joinHandle(YES);
        _joinHandle = nil;
    }
}

在這里有一點需要說明下:

配置聊天室屬性時如果不設置為永久聊天室的話,聊天室會在所有人離開聊天室( 用戶下線也被視為離開聊天室 )后自動銷毀。下次調用 [_xmppMuc discoverRoomsForServiceNamed:_roomService] 方法獲得的聊天室集中將不會有此聊天室

MUC 接受邀請加入议双、邀請用戶加入 聊天室

接受聊天室邀請并加入聊天室


- (void)joinRoomWithJid:(NSString *)roomJid
               nickName:(NSString *)nickName
                 handle:(MucJoinHandle)joinHandle
{
    DLog(@"XMPPJID = %@",roomJid);
    _joinHandle = joinHandle;
    
    XMPPJID *roomJID = [XMPPJID jidWithString:roomJid];
    XMPPRoomMemoryStorage  *roomStorage = [[XMPPRoomMemoryStorage alloc] init];
    
    XMPPRoom *xmppRoom = [[XMPPRoom alloc]
                                initWithRoomStorage:roomStorage
                                jid:roomJID
                                dispatchQueue:dispatch_get_main_queue()];
    [xmppRoom activate:[self xmppStream]];
    [xmppRoom addDelegate:self delegateQueue:dispatch_get_main_queue()];
    [xmppRoom joinRoomUsingNickname:nickName history:nil];
}

/** MUC 代理 */

/** 此方法在收到邀請時會調用 */
- (void)xmppMUC:(XMPPMUC *)sender roomJID:(XMPPJID *)roomJID didReceiveInvitation:(XMPPMessage *)message
{
    // 解析消息類型
    DDXMLElement * x = [message elementForName:@"x" xmlns:XMPPMUCUserNamespace];
    DDXMLElement * invite  = [x elementForName:@"invite"];
    if (invite != nil)
    {
        // 確定為邀請信息后逾礁,解析聊天室 JID
        NSString *nickName = _myJID.user;
        NSString *roomJid = [[message attributeForName:@"from"] stringValue];
        // 加入聊天室
        [self joinRoomWithJid:roomJid nickName:nickName handle:nil];
    }
}

邀請用戶加入聊天室

- (void)inviteFriends:(XMPPJID *)jid
{
    // _room 為要邀請的房間 [_room class] 為 XMPPRoom
    [_room inviteUser:jid withMessage:@"happy together !"];
}

MUC 發(fā)送/接受聊天室會話

說到發(fā)送聊天室會話债热,不知道大家是否還記得私聊會話的發(fā)送

/** 當 toJID 為 XMPPRoom 的 JID 和 type 為 @"groupchat" 時配并,發(fā)送的此條會話即為聊天室會話 */
- (void)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];
    
    // 生成時間戳
    NSDate      *now = [NSDate date];
    NSInteger   since1970 = [now timeIntervalSince1970];
    NSString    *dateString = [NSString stringWithFormat:@"%@", @(since1970)];
    
    // 添加擴展
    DDXMLElement  *external  = [DDXMLElement elementWithName:@"demoExternal"];
    
    DDXMLElement  *version   = [DDXMLElement elementWithName:@"demoVersion" stringValue:IM_VERSION];
    DDXMLElement  *avatar    = [DDXMLElement elementWithName:@"avatar" stringValue:getValueByKey(LOGOURL)];
    DDXMLElement  *nickName  = [DDXMLElement elementWithName:@"nickName" stringValue:getValueByKey(NICKNAME)];
    DDXMLElement  *date      = [DDXMLElement elementWithName:@"date" stringValue:dateString];
    
    [external addChild:avatar];
    [external addChild:version];
    [external addChild:nickName];
    [external addChild:date];
    if (extend) {
        
        [external addChild:extend];
    }
    
    [message addChild:external];
    
    [_xmppStream sendElement:message];
    [_statusHandleDic setObject:statusHandle forKey:uuId];
}

我們有兩種方式獲取到聊天室會話

// xmppstream delegate
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
    if ([message isGroupChatMessageWithBody]) 
    {
        // 此處為聊天室會話消息
    }
}

// xmpproom delegate
- (void)xmppRoom:(XMPPRoom *)sender didReceiveMessage:(XMPPMessage *)message fromOccupant:(XMPPJID *)occupantJID
{
    // 此處為 sender 聊天室內的會話消息
}

好了,雖然有些細節(jié)沒有說但一個完整的多人聊天步驟基本上就是個樣子迄委。一些真實開發(fā)中我遇到的坑和細節(jié)會在下一篇中講解信轿。

最后祝大家 have fun !

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末即彪,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子绰疤,更是在濱河造成了極大的恐慌龙屉,老刑警劉巖五芝,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枢步,死亡現(xiàn)場離奇詭異,居然都是意外死亡货葬,警方通過查閱死者的電腦和手機尊残,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門赁濒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拒炎,你說我怎么就攤上這事挪拟。” “怎么了击你?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵玉组,是天一觀的道長。 經常有香客問我丁侄,道長惯雳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任绒障,我火速辦了婚禮吨凑,結果婚禮上,老公的妹妹穿的比我還像新娘户辱。我一直安慰自己鸵钝,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布庐镐。 她就那樣靜靜地躺著恩商,像睡著了一般。 火紅的嫁衣襯著肌膚如雪必逆。 梳的紋絲不亂的頭發(fā)上怠堪,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音名眉,去河邊找鬼粟矿。 笑死,一個胖子當著我的面吹牛损拢,可吹牛的內容都是我干的陌粹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼福压,長吁一口氣:“原來是場噩夢啊……” “哼掏秩!你這毒婦竟也來了?” 一聲冷哼從身側響起荆姆,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蒙幻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后胆筒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邮破,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了决乎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片队询。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖构诚,靈堂內的尸體忽然破棺而出蚌斩,到底是詐尸還是另有隱情,我是刑警寧澤范嘱,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布送膳,位于F島的核電站,受9級特大地震影響丑蛤,放射性物質發(fā)生泄漏叠聋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一受裹、第九天 我趴在偏房一處隱蔽的房頂上張望碌补。 院中可真熱鬧,春花似錦棉饶、人聲如沸厦章。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袜啃。三九已至,卻和暖如春幸缕,著一層夾襖步出監(jiān)牢的瞬間群发,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工发乔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熟妓,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓栏尚,卻偏偏與公主長得像起愈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容

  • 點擊查看原文 Web SDK 開發(fā)手冊 SDK 概述 網易云信 SDK 為 Web 應用提供一個完善的 IM 系統(tǒng)...
    layjoy閱讀 13,758評論 0 15
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,078評論 25 707
  • 一、Xmpp資源綁定 XMPP協(xié)議設計中引入了一個抽象的資源綁定過程或南,何為資源揪胃,如何綁定?首先這得從JID的格式設...
    AndryYu閱讀 2,836評論 0 3
  • 文/遇見 2017.11.15 你還記得嗎缰猴? 曾經我們海誓山盟产艾, 要永遠,永遠在一起。 我們一起成長闷堡, 感受春的氣...
    遇見最美的煙火閱讀 168評論 0 1
  • 城市無序的表象下存在著復雜的社會和經濟方面的有序隘膘。 城市規(guī)劃理論受到霍華德影響甚大,后者認為杠览,要通過在大城市周圍建...
    夏城女史閱讀 525評論 0 0