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 !