一,从简单的单聊、群聊、收发图文消息开始
阅读准备
在阅读本章之前,如果你还不太了解即时通讯服务的总体架构,建议先阅读即时通讯服务总览。 另外,如果你还没有下载对应开发环境(语言)的 SDK,请参考相应语言的 SDK 配置指南完成 SDK 安装与初始化:
本章导读
在很多产品里面,都存在让用户实时沟通的需求,例如:
- 员工与客户之间的实时交流,如房地产行业经纪人与客户的沟通,商业产品客服与客户的沟通,等等。
- 企业内部沟通协作,如内部的工作流系统、文档/知识库系统,增加实时互动的方式可能就会让工作效率得到极大提升。
- 直播互动, 不论是文体行业的大型电视节目中的观众互动、重大赛事直播,娱乐行业的游戏现场直播、网红直播,还是教育行业的在线课程直播、KOL 知识分享,在支持超大规模用户积极参与的同时,也需要做好内容审核管理。
- 应用内社交,游戏公会嗨聊,等等。社交产品要能长时间吸引住用户,除了实时性之外,还需要更多的创新玩法,对于标准化通讯服务会存在更多的功能扩展需求。
根据功能需求的层次性和技术实现的难易程度不同,我们分为多篇文档来一步步地讲解如何利用即时通讯服务实现不同业务场景需求:
- 本篇文档,我们会从实现简单的单聊/群聊开始,演示创建和加入「对话」、发送和接收富媒体「消息」的流程,同时让大家了解历史消息云端保存与拉取的机制,希望可以满足在成熟产品中快速集成一个简单的聊天页面的需求。
- 离线消息文档会介绍一些特殊消息的处理,例如 @ 成员提醒、撤回和修改、消息送达和被阅读的回执通知等,离线状态下的推送通知和消息同步机制,多设备登录的支持方案,以及如何扩展自定义消息类型,希望可以满足一个社交类产品的多方面需求。
- 权限与聊天室文档会介绍一下系统的安全机制,包括第三方的操作签名,同时也会介绍直播聊天室和临时对话的用法,希望可以帮助开发者提升产品的安全性和易用性,并满足特殊场景的需求。
- Hook 与系统对话文档会介绍即时通讯服务端 Hook 机制,系统对话的用法,以及给出一个基于这两个功能打造一个属于自己的聊天机器人的方案,希望可以满足业务层多种多样的扩展需求。
希望开 发者最终顺利完成产品开发的同时,也对即时通讯服务的体系结构有一个清晰的了解,以便于产品的长期维护和定制化扩展。
一对一单聊
在开始讨论聊天之前,我们需要介绍一下在即时通讯 SDK 中的 IMClient
对象:
IMClient
对应实体的是一个用户,它代表着一个用户以客户端的身份登录到了即时通讯的系统。
具体可以参考即时通讯服务总览中《clientId、用户和登录》一节的说明。
创建 IMClient
假设我们产品中有一个叫「Tom」的用户,首先我们在 SDK 中创建出一个与之对应的 IMClient
实例(创建实例前请确保已经成功初始化了 SDK):
- Unity
- Android
- iOS
- JavaScript
LCIMClient tom = new LCIMClient("Tom");
// clientId 为 Tom
LCIMClient tom = LCIMClient.getInstance("Tom");
// 定义一个常驻内存的属性变量
@property (nonatomic) LCIMClient *tom;
// 初始化
NSError *error;
tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error];
if (error) {
NSLog(@"init failed with error: %@", error);
} else {
NSLog(@"init succeeded");
}
// Tom 用自己的名字作为 clientId 来登录即时通讯服务
realtime
.createIMClient("Tom")
.then(function (tom) {
// 成功登录
})
.catch(console.error);
注意这里一个 IMClient
实例就代表一个终端用户,我们需要把它全局保存起来,因为后续该用户在即时通讯上的所有操作都需要 直接或者间接使用这个实例。
登录即时通讯服务器
创建好了「Tom」这个用户对应的 IMClient
实例之后,我们接下来需要让该实例「登录」即时通讯服务器。
只有登录成功之后客户端才能开始与其他用户聊天,也才能接收到云端下发的各种事件通知。
这里需要说明一点,有些 SDK(比如 C# SDK)在创建 IMClient
实例的同时会自动进行登录,另一些 SDK(比如 iOS 和 Android SDK)则需要调用开发者手动执行 open
方法进行登录:
- Unity
- Android
- iOS
- JavaScript
await tom.Open();
// Tom 创建了一个 client,用自己的名字作为 clientId 登录
LCIMClient tom = LCIMClient.getInstance("Tom");
// Tom 登录
tom.open(new LCIMClientCallback() {
@Override
public void done(LCIMClient client, LCIMException e) {
if (e == null) {
// 成功打开连接
}
}
});
// 定义一个常驻内存的属性变量
@property (nonatomic) LCIMClient *tom;
// 初始化,然后登录
NSError *error;
tom = [[LCIMClient alloc] initWithClientId:@"Tom" error:&error];
if (error) {
NSLog(@"init failed with error: %@", error);
} else {
[tom openWithCallback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// open succeeded
}
}];
}
// Tom 用自己的名字作为 clientId 登录,并且获取 IMClient 对象实例
realtime
.createIMClient("Tom")
.then(function (tom) {
// 成功登录
})
.catch(console.error);
使用 _User
登录
除了应用层指定 clientId
登录之外,我们也支持直接使用 _User
对象来创建 IMClient
并登录。这种方式能直接利用云端内置的用户鉴权系统而省掉登录签名操作,更方便地将存储和即时通讯这两个模块结合起来使用。示例代码如下:
- Unity
- Android
- iOS
- JavaScript
var user = await LCUser.Login("USER_NAME", "PASSWORD");
var client = new LCIMClient(user);
// 以 LCUser 的用户名和密码登录到存储服务
LCUser.logIn("Tom", "cat!@#123").subscribe(new Observer<LCUser>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(LCUser user) {
// 登录成功,与服务器连接
LCIMClient client = LCIMClient.getInstance(user);
client.open(new LCIMClientCallback() {
@Override
public void done(final LCIMClient avimClient, LCIMException e) {
// 执行其他逻辑
}
});
}
public void onError(Throwable throwable) {
// 登录失败(可能是密码错误)
}
public void onComplete() {}
});
// 定义一个常驻内存的属性变量
@property (nonatomic) LCIMClient *client;
// 登录 User,然后使用登录成功的 User 初始化 Client 并登录
[LCUser logInWithUsernameInBackground:USER_NAME password:PASSWORD block:^(LCUser * _Nullable user, NSError * _Nullable error) {
if (user) {
NSError *err;
client = [[LCIMClient alloc] initWithUser:user error:&err];
if (err) {
NSLog(@"init failed with error: %@", err);
} else {
[client openWithCallback:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
// open succeeded
}
}];
}
}
}];
var AV = require("leancloud-storage");
// 以 AVUser 的用户名和密码登录即时通讯服务
AV.User.logIn("username", "password")
.then(function (user) {
return realtime.createIMClient(user);
})
.catch(console.error.bind(console));
创建对话 Conversation
用户登录之后,要开始与其他人聊天,需要先创建一个「对话」。
对话(Conversation
)是消息的载体,所有消息都是发送给对话,即时通讯服务端会把消息下发给所有在对话中的成员。
Tom 完成了登录之后,就可以选择用户聊天了。现在他要给 Jerry 发送消息,所以需要先创建一个只有他们两个成员的 Conversation
:
- Unity
- Android
- iOS
- JavaScript
var conversation = await tom.CreateConversation(new string[] { "Jerry" }, name: "Tom & Jerry", unique: true);
tom.createConversation(Arrays.asList("Jerry"), "Tom & Jerry", null, false, true,
new LCIMConversationCreatedCallback() {
@Override
public void done(LCIMConversation conversation, LCIMException e) {
if(e == null) {
// 创建成功
}
}
});
// 创建与 Jerry 之间的对话
[self createConversationWithClientIds:@[@"Jerry"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) {
// handle callback
}];
// 创建与 Jerry 之间的对话
tom
.createConversation({
// tom 是一个 IMClient 实例
// 指定对话的成员除了当前用户 Tom(SDK 会默认把当前用户当做对话成员)之外,还有 Jerry
members: ["Jerry"],
// 对话名称
name: "Tom & Jerry",
unique: true,
})
.then(/* 略 */);
createConversation
这个接口会直接创建一个对话,并且该对话会被存储在 _Conversation
表内,可以打开 开发者中心 > 你的游戏 > 游戏服务 > 云服务 > 数据存储 > 结构化数据 查看数据。不同 SDK 提供的创建对话接口如下:
- Unity
- Android
- iOS
- JavaScript
/// <summary>
/// Creates a conversation
/// </summary>
/// <param name="members">The list of clientIds of participants in this conversation (except the creator)</param>
/// <param name="name">The name of this conversation</param>
/// <param name="unique">Whether this conversation is unique;
/// if it is true and an existing conversation contains the same composition of members,
/// the existing conversation will be reused, otherwise a new conversation will be created.</param>
/// <param name="properties">Custom attributes of this conversation</param>
/// <returns></returns>
public async Task<LCIMConversation> CreateConversation(
IEnumerable<string> members,
string name = null,
bool unique = true,
Dictionary<string, object> properties = null) {
return await ConversationController.CreateConv(members: members,
name: name,
unique: unique,
properties: properties);
}
/**
* 创建或查询一个已有 conversation
*
* @param members 对话的成员
* @param name 对话的名字
* @param attributes 对话的额外属性
* @param isTransient 是否是聊天室
* @param isUnique 如果已经存在符合条件的会话,是否返回已有回话
* 为 false 时,则一直为创建新的回话
* 为 true 时,则先查询,如果已有符合条件的回话,则返回已有的,否则,创建新的并返回
* 为 true 时,仅 members 为有效查询条件
* @param callback 结果回调函数
*/
public void createConversation(final List<String> members, final String name,
final Map<String, Object> attributes, final boolean isTransient, final boolean isUnique,
final LCIMConversationCreatedCallback callback);
/**
* 创建一个聊天对话
*
* @param members 对话参与者
* @param attributes 对话的额外属性
* @param isTransient 是否为聊天室
* @param callback 结果回调函数
*/
public void createConversation(final List<String> members, final String name,
final Map<String, Object> attributes, final boolean isTransient,
final LCIMConversationCreatedCallback callback);
/**
* 创建一个聊天对话
*
* @param conversationMembers 对话参与者
* @param name 对话名称
* @param attributes 对话属性
* @param callback 结果回调函数
* @since 3.0
*/
public void createConversation(final List<String> conversationMembers, String name,
final Map<String, Object> attributes, final LCIMConversationCreatedCallback callback);
/**
* 创建一个聊天对话
*
* @param conversationMembers 对话参与者
* @param attributes 对话属性
* @param callback 结果回调函数
* @since 3.0
*/
public void createConversation(final List<String> conversationMembers,
final Map<String, Object> attributes, final LCIMConversationCreatedCallback callback);
/// The option of conversation creation.
@interface LCIMConversationCreationOption : NSObject
/// The name of the conversation.
@property (nonatomic, nullable) NSString *name;
/// The attributes of the conversation.
@property (nonatomic, nullable) NSDictionary *attributes;
/// Create or get an unique conversation, default is `true`.
@property (nonatomic) BOOL isUnique;
/// The time interval for the life of the temporary conversation.
@property (nonatomic) NSUInteger timeToLive;
@end
/// Create a Normal Conversation. Default is a Normal Unique Conversation.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned.
/// @param callback Result callback.
- (void)createConversationWithClientIds:(NSArray<NSString *> *)clientIds
callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback;
/// Create a Normal Conversation. Default is a Normal Unique Conversation.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains the current client's ID. if the created conversation is unique, and the server has one unique conversation with the same members, that unique conversation will be returned.
/// @param option See `LCIMConversationCreationOption`.
/// @param callback Result callback.
- (void)createConversationWithClientIds:(NSArray<NSString *> *)clientIds
option:(LCIMConversationCreationOption * _Nullable)option
callback:(void (^)(LCIMConversation * _Nullable conversation, NSError * _Nullable error))callback;
/// Create a Chat Room.
/// @param callback Result callback.
- (void)createChatRoomWithCallback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback;
/// Create a Chat Room.
/// @param option See `LCIMConversationCreationOption`.
/// @param callback Result callback.
- (void)createChatRoomWithOption:(LCIMConversationCreationOption * _Nullable)option
callback:(void (^)(LCIMChatRoom * _Nullable chatRoom, NSError * _Nullable error))callback;
/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID.
/// @param callback Result callback.
- (void)createTemporaryConversationWithClientIds:(NSArray<NSString *> *)clientIds
callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback;
/// Create a Temporary Conversation. Temporary Conversation is unique in its Life Cycle.
/// @param clientIds The set of client ID. it's the members of the conversation which will be created. the initialized members always contains this client's ID.
/// @param option See `LCIMConversationCreationOption`.
/// @param callback Result callback.
- (void)createTemporaryConversationWithClientIds:(NSArray<NSString *> *)clientIds
option:(LCIMConversationCreationOption * _Nullable)option
callback:(void (^)(LCIMTemporaryConversation * _Nullable temporaryConversation, NSError * _Nullable error))callback;
/**
* 创建一个对话
* @param {Object} options 除了下列字段外的其他字段将被视为对话的自定义属性
* @param {String[]} options.members 对话的初始成员列表,必要参数,默认包含当前 client
* @param {String} [options.name] 对话的名字,可选参数,如果不传默认值为 null
* @param {Boolean} [options.transient=false] 是否为聊天室,可选参数
* @param {Boolean} [options.unique=false] 是 否唯一对话,当其为 true 时,如果当前已经有相同成员的对话存在则返回该对话,否则会创建新的对话
* @param {Boolean} [options.tempConv=false] 是否为临时对话,可选参数
* @param {Integer} [options.tempConvTTL=0] 可选参数,如果 tempConv 为 true,这里可以指定临时对话的生命周期。
* @return {Promise.<Conversation>}
*/
async createConversation({
members: m,
name,
transient,
unique,
tempConv,
tempConvTTL,
// 可添加更多属性
});
虽然不同语言/平台接口声明有所不同,但是支持的参数是基本一致的。在创建一个对话的时候,我们主要可以指定:
-
members
:必要参数,包含对话的初始成员列表,请注意当前用户作为对话的创建者,是默认包含在成员里面的,所以members
数组中可以不包含当前用户的clientId
。 -
name
:对话名字,可选参数,上面代码指定为了「Tom & Jerry」。 -
attributes
:对话的自定义属性,可选。上面示例代码没有指定额外属性,开发者如果指定了额外属性的话,以后其他成员可以通过LCIMConversation
的接口获取到这些属性值。附加属性在_Conversation
表中被保存在attr
列中。 -
unique
/isUnique
或者是LCIMConversationOptionUnique
:唯一对话标志位,可选。- 如果设置为唯一对话,云端会根据完整的成员列表先进行一次查询,如果已经有正好包含这些成员的对话存在,那么就返回已经存在的对话,否则才创建一个新的对话。
- 如果指定
unique
标志为假,那么每次调用createConversation
接口都会创建一个新的对话。 - 未指定
unique
时,SDK 默认值为真。 - 从通用的聊天场景来看,不管是 Tom 发出「创建和 Jerry 单聊对话」的请求,还是 Jerry 发出「创建和 Tom 单聊对话」的请求,或者 Tom 以后再次发出创建和 Jerry 单聊对话的请求,都应该是同一个对话才是合理的,否则可能因为聊天记录的不同导致用户混乱。
-
对话类型的其他标志,可选参数,例如
transient
/isTransient
表示「聊天室」,tempConv
/tempConvTTL
和LCIMConversationOptionTemporary
用来创建「临时对话」等等。什么都不指定就表示创建普通对话,对于这些标志位的含义我们先不管,以后会有说明。
创建对话之后,可以获取对话的内置属性,云端会为每一个对话生成一个全局唯一的 ID 属性:Conversation.id
,它是其他用户查询对话时常用的匹配字段。
发送消息
对话已经创建成功了,接下来 Tom 可以在这个对话中发出第一条文本消息了:
- Unity
- Android
- iOS
- JavaScript
var textMessage = new LCIMTextMessage("Jerry,起床了!");
await conversation.Send(textMessage);
LCIMTextMessage msg = new LCIMTextMessage();
msg.setText("Jerry,起床了!");
// 发送消息
conversation.sendMessage(msg, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
Log.d("Tom & Jerry", "发送成功!");
}
}
});
LCIMTextMessage *message = [LCIMTextMessage messageWithText:@"耗子,起床!" attributes:nil];
[conversation sendMessage:message callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
var { TextMessage } = require("leancloud-realtime");
conversation
.send(new TextMessage("Jerry,起床了!"))
.then(function (message) {
console.log("Tom & Jerry", "发送成功!");
})
.catch(console.error);
上面接口实现的功能就是向对话中发送一条消息,同一对话中其他在线成员会立刻收到此消息。
现在 Tom 发出了消息,那么接收者 Jerry 他要在界面上展示出来这一条新消息,该怎么来处理呢?
接收消息
在另一个设备上,我们用 Jerry
作为 clientId
来创建一个 IMClient
并登录即时通讯服务(与前两节 Tom 的处理流程一样):
- Unity
- Android
- iOS
- JavaScript
var jerry = new LCIMClient("Jerry");
// Jerry 登录
LCIMClient jerry = LCIMClient.getInstance("Jerry");
jerry.open(new LCIMClientCallback(){
@Override
public void done(LCIMClient client,LCIMException e){
if(e==null){
// 登录成功后的逻辑
}
}
});
NSError *error;
jerry = [[LCIMClient alloc] initWithClientId:@"Jerry" error:&error];
if (!error) {
[jerry openWithCallback:^(BOOL succeeded, NSError *error) {
// handle callback
}];
}
var { Event } = require("leancloud-realtime");
// Jerry 登录
realtime
.createIMClient("Jerry")
.then(function (jerry) {})
.catch(console.error);
Jerry 作为消息的被动接收方,他不需要主动创建与 Tom 的对话,可能也无法知道 Tom 创建好的对话信息,Jerry 端需要通过设置即时通讯客户端事件的回调函数,才能获取到 Tom 那边操作的通知。
即时通讯客户端事件回调能处理多种服务端通知,这里我们先关注这里会出现的两个事件:
- 用户被邀请进入某个对话的通知事件。Tom 在创建和 Jerry 的单聊对话的时候,Jerry 这边就能立刻收到一条通知,获知到类似于「Tom 邀请你加入了一个对话」的信息。
- 已加入对话中新消息到达的通知。在 Tom 发出「Jerry,起床了!」这条消息之后,Jerry 这边也能立刻收到一条新消息到达的通知,通知中带有消息具体数据以及对话、发送者等上下文信息。
现在,我们看看具体应该如何响应服务端发过来的通知。Jerry 端会分别处理「加入对话」的事件通知和「新消息到达」的事件通知:
- Unity
- Android
- iOS
- JavaScript
jerry.OnInvited = (conv, initBy) => {
WriteLine($"{initBy} 邀请 Jerry 加入 {conv.Id} 对话");
};
jerry.OnMessage = (conv, msg) => {
if (msg is LCIMTextMessage textMessage) {
// textMessage.ConversationId 是该条消息所属于的对话 ID
// textMessage.Text 是该文本消息的文本内容
// textMessage.FromClientId 是消息发送者的 clientId
}
};
// Java/Android SDK 通过定制自己的对话事件 Handler 处理服务端下发的对话事件通知
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本方法来处理当前用户被邀请到某个聊天对话事件
*
* @param client
* @param conversation 被邀请的聊天对话
* @param operator 邀请你的人
* @since 3.0
*/
@Override
public void onInvited(LCIMClient client, LCIMConversation conversation, String invitedBy) {
// 当前 clientId(Jerry)被邀请到对话,执行此处逻辑
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
// Java/Android SDK 通过定制自己的消息事件 Handler 处理服务端下发的消息通知
public static class CustomMessageHandler extends LCIMMessageHandler{
/**
* 重载此方 法来处理接收消息
*
* @param message
* @param conversation
* @param client
*/
@Override
public void onMessage(LCIMMessage message,LCIMConversation conversation,LCIMClient client){
if(message instanceof LCIMTextMessage){
Log.d(((LCIMTextMessage)message).getText()); // Jerry,起床了
}
}
}
// 设置全局的消息处理 handler
LCIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler());
// Objective-C SDK 通过实现 LCIMClientDelegate 代理来处理服务端通知
// 不了解 Objective-C 代理(delegate)概念的读者可以参考:
// https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/DelegatesandDataSources/DelegatesandDataSources.html
jerry.delegate = delegator;
/*!
当前用户被邀请加入对话的通知。
@param conversation - 所属对话
@param clientId - 邀请者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation invitedByClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"当前 clientId(Jerry)被 %@ 邀请,加入了对话",clientId]);
}
/*!
接收到新消息(使用内置消息格式)。
@param conversation - 所属对话
@param message - 具体的消息
*/
- (void)conversation:(LCIMConversation *)conversation didReceiveTypedMessage:(LCIMTypedMessage *)message {
NSLog(@"%@", message.text); // Jerry,起床了!
}
// JS SDK 通过在 IMClient 实例上监听事件回调来响应服务端通知
// 当前用户被添加至某个对话
jerry.on(Event.INVITED, function invitedEventHandler(payload, conversation) {
console.log(payload.invitedBy, conversation.id);
});
// 当前用户收到了某一条消息,可以通过响应 Event.MESSAGE 这一事件来处理。
jerry.on(Event.MESSAGE, function (message, conversation) {
console.log("收到新消息:" + message.text);
});
Jerry 端实现了上面两个事件通知函数之后,就顺利收到 Tom 发送的消息了。之后 Jerry 也可以回复消息给 Tom,而 Tom 端实现类似的接收流程,那么他们俩就可以开始愉快的聊天了。
我们现在可以回顾一下 Tom 和 Jerry 发送第一条消息的过程中,两方完整的处理时序:
在聊天过程中,接收方除了响应新消息到达通知之外,还需要响应多种对话成员变动通知,例如「新用户 XX 被 XX 邀请加入了对话」、「用户 XX 主动退出了对话」、「用户 XX 被管理员剔除出对话」,等等。 云端会实时下发这些事件通知给客户端,具体细节可以参考后续章节:成员变更的事件通知总结。
多人群聊
上面我们讨论了一对一单聊 的实现流程,假设我们还需要实现一个「朋友群」的多人聊天,接下来我们就看看怎么完成这一功能。
从即时通讯云端来看,多人群聊与单聊的流程十分接近,主要差别在于对话内成员数量的多少。群聊对话支持在创建对话的时候一次性指定全部成员,也允许在创建之后通过邀请的方式来增加新的成员。
创建多人群聊对话
在 Tom 和 Jerry 的对话中(假设对话 ID 为 CONVERSATION_ID
,这只是一个示例,并不代表实际数据),后来 Tom 又希望把 Mary 也拉进来,他可以使用如下的办法:
- Unity
- Android
- iOS
- JavaScript
// 首先根据 ID 获取 Conversation 实例
var conversation = await tom.GetConversation("CONVERSATION_ID");
// 邀请 Mary 加入对话
await conversation.AddMembers(new string[] { "Mary" });
// 首先根据 ID 获取 Conversation 实例
final LCIMConversation conv = client.getConversation("CONVERSATION_ID");
// 邀请 Mary 加入对话
conv.addMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() {
@Override
public void done(LCIMException e, List<String> successfulClientIds, List<LCIMOperationFailure> failures) {
// 添加成功
}
});
// 首先根据 ID 获取 Conversation 实例
LCIMConversationQuery *query = [self.client conversationQuery];
[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) {
// 邀请 Mary 加入对话
[conversation addMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"邀请成功!");
}
}];
}];
// 首先根据 ID 获取 Conversation 实例
tom
.getConversation("CONVERSATION_ID")
.then(function (conversation) {
// 邀请 Mary 加入对话
return conversation.add(["Mary"]);
})
.then(function (conversation) {
console.log("添加成功", conversation.members);
// 此时对话成员为:['Mary', 'Tom', 'Jerry']
})
.catch(console.error.bind(console));
而 Jerry 端增加「新成员加入」的事件通知处理函数,就可以及时获知 Mary 被 Tom 邀请加入当前对话了:
- Unity
- Android
- iOS
- JavaScript
jerry.OnMembersJoined = (conv, memberList, initBy) => {
WriteLine($"{initBy} 邀请了 {memberList} 加入了 {conv.Id} 对话");
}
其中 AVIMOnInvitedEventArgs
参数包含如下内容:
InvitedBy
:该操作的发起者JoinedMembers
:此次加入对话的包含的成员列表ConversationId
:被操作的对话
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本方法以处理聊天对话中的参与者加入事件
*
* @param client
* @param conversation
* @param members 加入的参与者
* @param invitedBy 加入事件的邀请人,有可能是加入的参与者本身
* @since 3.0
*/
@Override
public void onMemberJoined(LCIMClient client, LCIMConversation conversation,
List<String> members, String invitedBy) {
// 手机屏幕上会显示一小段文字:Mary 加入到 551260efe4b01608686c3e0f;操作者为:Tom
Toast.makeText(LeanCloud.applicationContext,
members + " 加入到 " + conversation.getConversationId() + ";操作者为:"
+ invitedBy, Toast.LENGTH_SHORT).show();
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
jerry.delegate = delegator;
#pragma mark - LCIMClientDelegate
/*!
对话中有新成员加入时所有成员都会收到这一通知。
@param conversation - 所属对话
@param clientIds - 加入的新成员列表
@param clientId - 邀请者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]);
}
// 有用户被添加至某个对话
jerry.on(
Event.MEMBERS_JOINED,
function membersjoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.invitedBy, conversation.id);
}
);
其中 payload
参数包含如下内容:
members
:字符串数组,被添加的用户clientId
列表invitedBy
:字符串,邀请者clientId
这一流程的时序图如下:
而 Mary 端如果要能加入到 Tom 和 Jerry 的对话中来,Ta 可以参照 一对一单聊 中 Jerry 侧的做法监听 INVITED
事件,就可以自己被邀请到了一个对话当中。
而 重新创建一个对话,并在创建的时候指定全部成员 的方式如下:
- Unity
- Android
- iOS
- JavaScript
var conversation = await tom.CreateConversation(new string[] { "Jerry","Mary" }, name: "Tom & Jerry & friends", unique: true);
tom.createConversation(Arrays.asList("Jerry","Mary"), "Tom & Jerry & friends", null,
new LCIMConversationCreatedCallback() {
@Override
public void done(LCIMConversation conversation, LCIMException e) {
if (e == null) {
// 创建成功
}
}
});
// Tom 建立了与朋友们的会话
[tom createConversationWithClientIds:@[@"Jerry", @"Mary"] callback:^(LCIMConversation * _Nullable conversation, NSError * _Nullable error) {
if (!error) {
NSLog(@"创建成功!");
}
}];
tom
.createConversation({
// 创建的时候直接指定 Jerry 和 Mary 一起加入多人群聊,当然根据需求可以添加更多成员
members: ["Jerry", "Mary"],
// 对话名称
name: "Tom & Jerry & friends",
unique: true,
})
.catch(console.error);
群发消息
多人群聊中一个成员发送的消息,会实时同步到所有其他在线成员,其处理流程与单聊中 Jerry 接收消息的过程是一样的。
例如,Tom 向好友群发送了一条欢迎消息:
- Unity
- Android
- iOS
- JavaScript
var textMessage = new LCIMTextMessage("大家好,欢迎来到我们的群聊对话!");
await conversation.Send(textMessage);
LCIMTextMessage msg = new LCIMTextMessage();
msg.setText("大家好,欢迎来到我们的群聊对话!");
// 发送消息
conversation.sendMessage(msg, new LCIMConversationCallback() {
@Override
public void done(LCIMException e) {
if (e == null) {
Log.d("群聊", "发送成功!");
}
}
});
[conversation sendMessage:[LCIMTextMessage messageWithText:@"大家好,欢迎来到我们的群聊对话!" attributes:nil] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"发送成功!");
}
}];
conversation.send(new TextMessage("大家好,欢迎来到我们的群聊对话"));
而 Jerry 和 Mary 端都会有 Event.MESSAGE
事件触发,利用它来接收群聊消息,并更新产品 UI。
将他人踢出对话
三个好友的群其乐融融不久,后来 Mary 出言不逊,惹恼了群主 Tom,Tom 直接把 Mary 踢出了对话群。Tom 端想要踢人,该怎么实现呢?
- Unity
- Android
- iOS
- JavaScript
await conversation.RemoveMembers(new string[] { "Mary" });
conv.kickMembers(Arrays.asList("Mary"), new LCIMOperationPartiallySucceededCallback() {
@Override
public void done(LCIMException e, List<String> successfulClientIds, List<LCIMOperationFailure> failures) {
}
});
[conversation removeMembersWithClientIds:@[@"Mary"] callback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"踢人成功!");
}
}];
conversation
.remove(["Mary"])
.then(function (conversation) {
console.log("移除成功", conversation.members);
})
.catch(console.error.bind(console));
Tom 端执行了这段代码之后会触发如下流程:
这里出现了两个新的事件:当前用户被踢出对话 KICKED
(Mary 收到的),成员 XX 被踢出对话 MEMBERS_LEFT
(Jerry 和 Tom 收到的)。其处理方式与邀请人的流程类似:
- Unity
- Android
- iOS
- JavaScript
jerry.OnMembersLeft = (conv, leftIds, kickedBy) => {
WriteLine($"{leftIds} 离开对话 {conv.Id};操作者为:{kickedBy}");
}
jerry.OnKicked = (conv, initBy) => {
WriteLine($"你已经离开对话 {conv.Id};操作者为:{initBy}");
};
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
/**
* 实现本方法以处理聊天对话中的参与者离开事件
*
* @param client
* @param conversation
* @param members 离开的参与者
* @param kickedBy 离开事件的发动者,有可能是离开的参与者本身
* @since 3.0
*/
@Override
public abstract void onMemberLeft(LCIMClient client,
LCIMConversation conversation, List<String> members, String kickedBy) {
Toast.makeText(LeanCloud.applicationContext,
members + " 离开对话 " + conversation.getConversationId() + ";操作者为:"
+ kickedBy, Toast.LENGTH_SHORT).show();
}
/**
* 实现本方法来处理当前用户被踢出某个聊天对话事件
*
* @param client
* @param conversation
* @param kickedBy 踢出你的人
* @since 3.0
*/
@Override
public abstract void onKicked(LCIMClient client, LCIMConversation conversation,
String kickedBy) {
Toast.makeText(LeanCloud.applicationContext,
"你已离开对话 " + conversation.getConversationId() + ";操作者为:"
+ kickedBy, Toast.LENGTH_SHORT).show();
}
}
// 设置全局的对话事件处理 handler
LCIMMessageManager.setConversationEventHandler(new CustomConversationEventHandler());
jerry.delegate = delegator;
#pragma mark - LCIMClientDelegate
/*!
对话中有成员离开时所有剩余成员都会收到这一通知。
@param conversation - 所属对话
@param clientIds - 离开的成员列表
@param clientId - 操作者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray<NSString *> * _Nullable)clientIds byClientId:(NSString * _Nullable)clientId {
;
}
/*!
当前用户被踢出对话的通知。
@param conversation - 所属对话
@param clientId - 操作者的 ID
*/
- (void)conversation:(LCIMConversation *)conversation kickedByClientId:(NSString * _Nullable)clientId {
;
}
// 有成员被从某个对话中移除
jerry.on(
Event.MEMBERS_LEFT,
function membersjoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.kickedBy, conversation.id);
}
);
// 有用户被踢出某个对话
jerry.on(
Event.KICKED,
function membersjoinedEventHandler(payload, conversation) {
console.log(payload.kickedBy, conversation.id);
}
);
用户主动加入对话
把 Mary 踢走之后,Tom 嫌人少不好玩,所以他找到了 William,说他和 Jerry 有一个很好玩的聊天群,并且把群的 ID(或名称)告知给了 William。William 也很想进入这个群看看他们究竟在聊什么,他自己主动加入了对话:
- Unity
- Android
- iOS
- JavaScript
var conv = await william.GetConversation("CONVERSATION_ID");
await conv.Join();
LCIMConversation conv = william.getConversation("CONVERSATION_ID");
conv.join(new LCIMConversationCallback(){
@Override
public void done(LCIMException e){
if(e==null){
// 加入成功
}
}
});
LCIMConversationQuery *query = [william conversationQuery];
[query getConversationById:@"CONVERSATION_ID" callback:^(LCIMConversation *conversation, NSError *error) {
[conversation joinWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"加入成功!");
}
}];
}];
william
.getConversation("CONVERSATION_ID")
.then(function (conversation) {
return conversation.join();
})
.then(function (conversation) {
console.log("加入成功", conversation.members);
// 此时对话成员为:['William', 'Tom', 'Jerry']
})
.catch(console.error.bind(console));
执行了这段代码之后会触发如下流程:
其他人则通过订阅 MEMBERS_JOINED
来接收 William 加入对话的通知 :
- Unity
- Android
- iOS
- JavaScript
jerry.OnMembersJoined = (conv, memberList, initBy) => {
WriteLine($"{memberList} 加入了 {conv.Id} 对话;操作者为:{initBy}");
}
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
@Override
public void onMemberJoined(LCIMClient client, LCIMConversation conversation,
List<String> members, String invitedBy) {
// 手机屏幕上会显示一小段文字:William 加入到 551260efe4b01608686c3e0f;操作者为:William
Toast.makeText(LeanCloud.applicationContext,
members + " 加入到 " + conversation.getConversationId() + ";操作者为:"
+ invitedBy, Toast.LENGTH_SHORT).show();
}
}
- (void)conversation:(LCIMConversation *)conversation membersAdded:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ 加入到对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]);
}
jerry.on(
Event.MEMBERS_JOINED,
function membersJoinedEventHandler(payload, conversation) {
console.log(payload.members, payload.invitedBy, conversation.id);
}
);
用户主动退出对话
随着 Tom 邀请进来的人越来越多,Jerry 觉得跟这些人都说不到一块去,他不想继续呆在这个对话里面了,所以选择自己主动退出对话,这时候可以调用下面的方法完成退群的操作:
- Unity
- Android
- iOS
- JavaScript
await conversation.Quit();
conversation.quit(new LCIMConversationCallback(){
@Override
public void done(LCIMException e){
if(e==null){
// 退出成功
}
}
});
[conversation quitWithCallback:^(BOOL succeeded, NSError *error) {
if (succeeded) {
NSLog(@"退出成功!");
}
}];
conversation
.quit()
.then(function (conversation) {
console.log("退出成功", conversation.members);
})
.catch(console.error.bind(console));
执行了这段代码 Jerry 就离开了这个聊天群,此后群里所有的事件 Jerry 都不会再知晓。各个成员接收到的事件通知流程如下:
而其他人需要通过订阅 MEMBERS_LEFT
来接收 Jerry 离开对话的事件通知:
- Unity
- Android
- iOS
- JavaScript
mary.OnMembersLeft = (conv, members, initBy) => {
WriteLine($"{members} 离开了 {conv.Id} 对话;操作者为:{initBy}");
}
public class CustomConversationEventHandler extends LCIMConversationEventHandler {
@Override
public void onMemberLeft(LCIMClient client, LCIMConversation conversation, List<String> members,
String kickedBy) {
// 有其他成员离开时,执行此处逻辑
}
}
// Mary 登录之后,Jerry 退出了对话,在 Mary 所在的客户端就会激发以下回调
- (void)conversation:(LCIMConversation *)conversation membersRemoved:(NSArray *)clientIds byClientId:(NSString *)clientId {
NSLog(@"%@", [NSString stringWithFormat:@"%@ 离开了对话,操作者为:%@",[clientIds objectAtIndex:0],clientId]);
}
mary.on(
Event.MEMBERS_LEFT,
function membersLeftEventHandler(payload, conversation) {
console.log(payload.members, payload.kickedBy, conversation.id);
}
);
成员变更的事件通知总结
前面的时序图和代码针对成员变更的操作做了逐步的分析和阐述,为了确保开发者能够准确的使用事件通知,如下表格做了一个统一的归类和划分:
假设 Tom 和 Jerry 已经在对话内了:
- Unity
- Android
- iOS
- JavaScript
操作 | Tom | Jerry | Mary | William |
---|---|---|---|---|
Tom 添加 Mary | OnMembersJoined | OnMembersJoined | OnInvited | / |
Tom 剔除 Mary | OnMembersLeft | OnMembersLeft | OnKicked | / |
William 加入 | OnMembersJoined | OnMembersJoined | / | OnMembersJoined |
Jerry 主动退出 | OnMembersLeft | OnMembersLeft | / | OnMembersLeft |
操作 | Tom | Jerry | Mary | William |
---|---|---|---|---|
Tom 添加 Mary | onMemberJoined | onMemberJoined | onInvited | / |
Tom 剔除 Mary | onMemberLeft | onMemberLeft | onKicked | / |
William 加入 | onMemberJoined | onMemberJoined | / | onMemberJoined |
Jerry 主动退出 | onMemberLeft | onMemberLeft | / | onMemberLeft |
操作 | Tom | Jerry | Mary | William |
---|---|---|---|---|
Tom 添加 Mary | membersAdded | membersAdded | invitedByClientId | / |
Tom 剔除 Mary | membersRemoved | membersRemoved | kickedByClientId | / |
William 加入 | membersAdded | membersAdded | / | membersAdded |
Jerry 主动退出 | membersRemoved | kickedByClientId | / | membersRemoved |
文本之外的聊天消息
上面的示例都是发送文本消息,但是实际上可能图片、视频、位置等消息也是非常常见的消息格式,接下来我们就看看如何发送这些富媒体类型的消息。
即时通讯服务默认支持文本、文件、图像、音频、视频、位置、二进制等不同格式的消息,除了二进制消息之外,普通消息的收发接口都是字符串,但是文本消息和文件、图像、音视频消息有一点区别:
- 文本消息发送的就是本身的内容
- 而其他的多媒体消息,例如一张图片,实际上即时通讯 SDK 会首先调用存储服务的
AVFile
接口,将图像的二进制文件上传到存储服务云端,再把图像下载的 URL 放入即时通讯消息结构体中,所以 图像消息不过是包含了图像下载链接的固定格式文本消息。
图像等二进制数据不随即时通讯消息直接下发的主要原因在于,文件存储服务默认都是开通了 CDN 加速选项的,通过文件下载对于终端用户来说可以有更快的展现速度,同时对于开发者来说也能获得更低的存储成本。
默认消息类型
即时通讯服务内置了多种结构化消息用来满足常见的需求:
TextMessage
文本消息ImageMessage
图像消息AudioMessage
音频消息VideoMessage
视频消息FileMessage
普通文件消息(.txt/.doc/.md 等各种)LocationMessage
地理位置消息
所有消息均派生自 LCIMMessage
,每种消息实例都具备如下属性:
- Unity
- Android
- iOS
- JavaScript
属性 | 类型 | 描述 |
---|---|---|
content | String | 消息内容。 |
clientId | String | 消息发送者的 clientId 。 |
conversationId | String | 消息所属对话 ID。 |
messageId | String | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
timestamp | long | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 |
receiptTimestamp | long | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 |
status | AVIMMessageStatus 枚举 | 消息状态,有五种取值:AVIMMessageStatusNone (未知)AVIMMessageStatusSending (发送中)AVIMMessageStatusSent (发送成功)AVIMMessageStatusReceipt (被接收)AVIMMessageStatusFailed (失败) |
ioType | AVIMMessageIOType 枚举 | 消息传输方向,有两种取值:AVIMMessageIOTypeIn (发给当前用户)AVIMMessageIOTypeOut (由当前用户发出) |
属性 | 类型 | 描述 |
---|---|---|
content | String | 消息内容。 |
clientId | String | 消息发送者的 clientId 。 |
conversationId | String | 消息所属对话 ID。 |
messageId | String | 消息发送成功之后,由云端给每条消息赋予的唯一 ID。 |
timestamp | long | 消息发送的时间。消息发送成功之后,由云端赋予的全局的时间戳。 |
receiptTimestamp | long | 消息被对方接收到的时间。消息被接收之后,由云端赋予的全局的时间戳。 |
status | MessageStatus 枚举 | 消息状态,有五种取值:StatusNone (未知)StatusSending (发送中)StatusSent (发送成功)StatusReceipt (被接收)StatusFailed (失败) |
ioType | MessageIOType 枚举 | 消息传输方向,有两种取值:TypeIn (发给当前用户)TypeOut (由当前用户发出) |
属性 | 类型 | 描述 |
---|---|---|
content | NSString | 消息内容。 |
clientId | NSString | 消息发送者的 clientId |