一,从简单的单聊、群聊、收发图文消息开始
阅读准备
在阅读本章之前,如果你还不太了解即时通讯服务的总体架构,建议先阅读即时通讯服务总览。 另外,如果你还没有下载对应开发环境(语言)的 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 那边操作的通知。