XMPP - 多人聊天

MUC 简述

其实想了解一个框架最好的方法就是去阅读官方文档,下面会分别给出中英文档的连接。(当然如果你没兴趣看文档的话,也可以直接略过看我提炼出来比较重要的几个概念)

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

角色(Roles) 和 岗位(Affiliations)

这两个概念是基于一个用户在一个聊天室内拥有的权限所提出来的,但俩者对应的权限和时效性是有区别的。

角色(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).

简单来说意思就是 角色 是动态变化的。随着 加入 或者 离开 聊天室这些行为,用户的 角色 也会发生变化。

下面是所有已经定义的角色:

  • 主持人(Moderator): 聊天室中权限最高的角色
  • 与会者(Participant): 聊天室的主要参与人
  • 游客(Visitor): 聊天室的浏览者
  • 无角色(None): 未加入聊天室的用户

以下是四种角色对应的具体权限:

权限 游客 与会者 主持人
在聊天室中出席
接受消息
接收/广播 出席信息
改变可用性状态
改变聊天室昵称
发送私人消息
邀请用户
发送公开消息
修改标题
踢出用户
授予发言权
撤销发言权

注: 所有对权限的操作均无法对平级用执行

岗位(Affiliations)

虽然国内大部分都将 Affiliations 翻译成 岗位 ,但我个人觉得如果符合中文的理解,主持人 岂不是更符合 岗位 的字面意思。当然这里只是抱怨一下,我还是会沿用这一翻译,避免与其它文章中叫法不一给读者造成阅读上的障碍。

大家可以把 聊天室 看成是一个俱乐部,由以下几种 岗位 组成:

  • 所有者(Owner): 一般为聊天室创建者
  • 管理员(Admin): 所有者可授予其他成员
  • 会员(Member): 聊天室 的正式成员
  • 排斥者(Outcast): 简单来说就是黑名单,排斥者无法进入 聊天室

岗位 相比起 角色 来说是静态的身份,不会随着用户上下线,进入退出聊天室而变动,只有 所有者管理员 主动修改用户的 岗位

岗位 相关的权限如下:

权限 Outcast(被排斥者) None(无) Member(成员) Admin(管理员) Owner(所有者)
进入聊天室
注册一个开放的聊天室 N/A N/A N/A
接收成员列表
加入一个仅限会员的聊天室
禁止成员并把用户的岗位删除
编辑成员列表
编辑主持人列表
编辑管理员列表
编辑所有者列表
变更聊天室定义
销毁聊天室

聊天室类型

用户创建完聊天室后可以选择性配置聊天室或者使用默认配置,不同的配置参数则会使聊天室类型发生改变。

下面关于不同类型聊天室的区别摘自 XEP-0045: 多用户聊天

Hidden Room(隐藏聊天室) : 一个无法被任何用户以普通方法如搜索和服务查询来发现的聊天室; 反义词: 公开(public)聊天室.

Members-Only Room(仅限会员的聊天室) : 如果一个用户不在成员列表中则无法加入的一个聊天室; 反义词: 开放(open)聊天室.

Moderated Room(被主持的聊天室) : 只有有"发言权"的用户才可以发送消息给所有房客的聊天室; 反义词: 非主持的(Unmoderated)聊天室.

Non-Anonymous Room(非匿名聊天室) : 一个房客的全JID会暴露给所有其他房客的聊天室, 尽管房客可以选择任何期望的聊天室昵称; 相对的是半匿名(Semi-Anonymous)聊天室.

Open Room(开放聊天室) : 任何人可以加入而不需要在成员列表中的聊天室; 反义词: 仅限会员的聊天室.

Password-Protected Room(密码保护聊天室) : 一个用户必须提供正确密码才能加入的聊天室; 反义词: 非保密聊天室.

Persistent Room(持久聊天室) : 如果最后一个房客退出也不会被销毁的聊天室; 反义词: 临时聊天室.

Public Room(公开聊天室) : 用户可以通过普通方法如搜索和服务查询来发现的聊天室; 反义词: 隐藏聊天室.

Semi-Anonymous Room(半匿名聊天室) : 一个房客的全JID只能被聊天室管理员发现的聊天室; 相对的是非匿名(Non-Anonymous)聊天室.

Temporary Room(临时聊天室) : 如果最后一个房客退出就会被销毁的聊天室; 反义词: 持久聊天室.

Unmoderated Room(非主持的聊天室) : 任何房客都被允许发送消息给所有房客的聊天室; 反义词: 被主持的聊天室.

Unsecured Room(非保密聊天室) : 任何人不需要提供密码就可以进入的聊天室; 反义词: 密码保护聊天室.

不同的 聊天室类型 针对不同 岗位 的用户进入聊天室时会设置一个默认的角色,具体如下表:

聊天室类型 \ 岗位 会员 管理员 所有者
被主持的聊天室 游客 与会者 主持人 主持人
非主持的聊天室 与会者 与会者 主持人 主持人
仅会员加入的聊天室 (无法加入聊天室) 与会者 主持人 主持人
开放聊天室 与会者 与会者 主持人 主持人

MUC 实操

在把基本的概念介绍一遍后,我们终于要开始 Code 环节啦!

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 来发现群聊服务

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

3、实现 MUC 模块的代理方法

/** 成功发现 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];
}

/** 发现 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]; 方法还是有一点说明的。在实际开发中可以省略此步骤让后台直接告知 多人聊天服务地址 然后直接调用 [_xmppMuc discoverRoomsForServiceNamed:_roomService] 去查询聊天室集即可。

MUC 创建聊天室

1、发送创建聊天室请求

/**
 请求创建一个聊天室
 
 @param roomName      聊天室名称
 @param createHandle  创建回调
 @return 请求是否发送
 */
- (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、实现 XMPPRoom 代理、并发送配置信息


- (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
/** 成功创建聊天室 */
- (void)xmppRoomDidCreate:(XMPPRoom *)sender
{
    // 1. 发送默认配置
    [self sendDefaultRoomConfig:sender];
    
    // 2. 成功回调 (为了确保 UI 刷新时已经获取到了聊天室信息,回调放在 xmppRoom:didFetchInfoList: 中执行
}

/** 创建聊天室失败 */
- (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 发送/接受聊天室会话

说到发送聊天室会话,不知道大家是否还记得私聊会话的发送

/** 当 toJID 为 XMPPRoom 的 JID 和 type 为 @"groupchat" 时,发送的此条会话即为聊天室会话 */
- (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 聊天室内的会话消息
}

好了,虽然有些细节没有说但一个完整的多人聊天步骤基本上就是个样子。一些真实开发中我遇到的坑和细节会在下一篇中讲解。

最后祝大家 have fun !

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,652评论 0 15
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 一、Xmpp资源绑定 XMPP协议设计中引入了一个抽象的资源绑定过程,何为资源,如何绑定?首先这得从JID的格式设...
    AndryYu阅读 2,806评论 0 3
  • 文/遇见 2017.11.15 你还记得吗? 曾经我们海誓山盟, 要永远,永远在一起。 我们一起成长, 感受春的气...
    遇见最美的烟火阅读 165评论 0 1
  • 城市无序的表象下存在着复杂的社会和经济方面的有序。 城市规划理论受到霍华德影响甚大,后者认为,要通过在大城市周围建...
    夏城女史阅读 512评论 0 0