5. 互动消息

最近更新时间:2018-05-28 16:44:35

5.1 功能简介

本节要介绍的内容是在主播和观众之间的IM消息解决方案,它主要用于实现如下几个功能点:
(1)普通文本消息
(2)弹幕消息
(3)点赞消息
(4)系统通知,比如“XXX已加入房间”或者“主播已离开”等等。

腾讯云通讯 服务提供了一套完整的成体系的即时通讯解决方案,本节我们将介绍如何使用腾讯云IM通讯服务实现上述的几个功能点。

5.2 开通服务

在使用腾讯云通讯服务之前,首先要将这个服务开通,可以按照如下步骤操作:

进入云通讯管理控制台,如果还没有服务,直接单击直接开通云通讯按钮即可。新认证的腾讯云账号,云通讯的应用列表是空的,如下图:

单击创建应用接入按钮创建一个新的应用接入,即您要接入腾讯云IM通讯服务的App的名字,我们的测试应用名称叫做“小直播演示”,如下图所示:

单击确定按钮,之后就可以在应用列表中看到刚刚添加的项目了,如下图所示:

上图的列表中,SDKAPPID 这一列显示的是腾讯云通讯业务ID,这个ID在对接各平台版本的IM SDK时都会用到,右侧有一个应用配置按钮,单击这里进入下一步的配置工作,如下图所示。

此处的配置项比较多,但大部分都不重要,您可以不配置或者之后有需要再随时调整,比如:

  • 账号名称 :您可以随便填写,注意不要用特殊奇葩火星文等字符。

  • 验证方式 :目前有且仅有“系统生成公钥”,所以这一项配置您无需关心。

  • 账号管理员名称 :是一个可以方便调试用的UserId,如果您是工程师,直接填写自己后面常用的账号id就可以了,使用云通讯服务的高级特性是才有可能用到。

  • 验证短信签名 :就是短信文本的前缀,填写App的中文名称就可以了。

  • 集成模式 :推荐选择托管模式。

关于集成模式的更详细介绍,我们在 5.3 里展开介绍,不过现在您要做的就是单击保存按钮就可以了。

5.3 集成模式

“集成模式”这四个字所关注的是IM消息收发方身份(账号)的问题,考虑到各种各样客户的需求,腾讯云的提供了两种解决方案,访客(托管)模式独立(鉴权)模式

集成模式 适用场景 对接难度 设计目标
访客(托管)模式 适合对身份确认要求不高的场景,比如允许App用户都可以发言的直播间聊天室场景。 采用guestLogin模式登录 IM SDK即可,对接成本极低。 设计目标是降低对接成本,让您能够不对接账号系统的情况下就能使用IM服务,腾讯云会为每个App用户生成一个“访客账号“,该账号只会用来收发消息。
独立(鉴权)模式 身份确认要求非常高的客户,比如只能允许自己App的注册用户才可以收发消息。 需要使用UserSig安全签名,对接成本比较高。 设计目标是将消息收发权限交给客户掌控,它要求在您的登录服务器确认App用户合法身份后需派发签名,腾讯云通过检查签名真实性后才允许收发消息。这样可以让消息的收发者都100%是您的账号体系里的账号。

5.3.1 访客(托管)模式

访客模式的设计目标是降低对接成本,让您能够不对接账号系统的情况下就能使用IM服务,腾讯云会为每个App用户生成一个“访客账号“,该账号只会用来收发消息。您可以将这些访客账号理解为一个个“傀儡账号”。消息发送者的真实用户信息(昵称、头像等等)不跟访客账号绑定,而是跟着消息体一起发出去。

该模式的接入流程非常简单:点点鼠标 + 写几行对接代码就能搞定。

(1)首先,在 5.2 中集成模式选项里选择托管模式,这样能让腾讯云为访客模式的访客账号提供后台支持。
(2)在客户端代码中对接 IM SDK 的访客登录模式即可,这里包括 TLS(TLS 的全称是 “Tencent Login Service” ,是腾讯云 IM SDK 的核心组件)和 IM SDK 两处的函数调用。

如果您已经有了小直播源码,调用一个函数(iOS平台:位于TCIMPlatform.h 中的 guestLogin 函数;Android平台:位于TCLoginMgr.java 中的 guestLogin 函数)就可以达成目标,函数内部已经帮您封装了相关逻辑,如下是源码解释:

  • iOS 平台
    • 查询SDKAPPID ,ID的获取参考文章上半部分的5.2
    • 调用 TLSHelper 头文件中的 TLSGuestLogin 函数,它会到腾讯云后台请求一个访客账号,如果第一步骤中的 SDKAPPID 没有设置错误,并且手机可以接入互联网,那么您会成功拿到一个 TLSUserInfo 对象,TLSUserInfo.identifier 即为访客账号的ID。
    • 使用 TLSHelper 的 getTLSUserSig 函数可以为该 identifier 生成一个合法的 UserSig 签名。
    • 最后,使用刚刚拿到的 identifier 和 UserSig 可以构造一个 TIMLoginParam 对象,之后使用 TIMManager 的 login 函数即可完成 IM SDK 的登录流程了。
// 示例代码 1: 调用TLS(Tencent Login Server)的访客模式登录
- (void)guestLogin:(UIButton *)button {
    TLSUserInfo *info = [[TLSHelper getInstance] getGuestIdentifier];
    int ret = [[TLSHelper getInstance] TLSGuestLogin:self];
    if (ret == 0) {
           // 登录中,转个登录中的菊花吧
    } else {
           // TLSHelper 出错了, 请检查 IMSDK_APPID 是不是搞错了
    }
}

// 示例代码 2: 在TLS(Tencent Login Server)登录成功后调用IM SDK 登录
- (void)OnGuestLoginSuccess:(TLSUserInfo *)userInfo {
    TIMLoginParam *loginParam = [[TIMLoginParam alloc] init];
    loginParam.appidAt3rd = @"88888888"; // 填写您的SDKAPPID
    loginParam.sdkAppId = 88888888       // 填写您的SDKAPPID
    loginParam.accountType = @"0";
    loginParam.identifier = userInfo.identifier;
    loginParam.userSig = [[TLSHelper getInstance] getTLSUserSig:userInfo.identifier];

    [[TIMManager sharedInstance] login:loginParam succ:^{
             // OK, 登录成功,后面就可以发消息了!
    } fail:^(int code, NSString *msg) {
        // im sdk 登录失败,请检查 IMSDK_APPID 是不是搞错了
    }];
}
  • Android 平台
    • 查询SDKAPPID ,ID的获取参考文章上半部分的5.2
    • 调用TLS(Tencent Login Service)的 TLSGuestLogin 函数完成访客登录,此时它会到腾讯云后台请求一个访客账号,如果第一步骤中的SDKAPPID 没有设置错误,并且手机可以接入互联网,那么您会成功拿到一个 TLSUserInfo 对象,TLSUserInfo.identifier 即为访客账号的ID。
    • 使用TLS的 getUserSig 函数可以为该 identifier 生成一个合法的 UserSig 签名。
    • 最后,使用刚刚拿到的 identifier 和 UserSig 调用 TIMManager 的 login 函数即可完成 IM SDK 的登录流程了。
// 示例代码: Android 平台下实现IM SDK 的访客登录
public void guestLogin() {
   //调用TLS(Tencent Login Server)的访客登录模式
   mTLSLoginHelper.TLSGuestLogin(new TLSGuestLoginListener() {
       @Override
       public void OnGuestLoginSuccess(TLSUserInfo tlsUserInfo) {
           //设置 SDKAPPID
           TIMUser user = new TIMUser();
           user.setAccountType(String.valueOf(TCConstants.IMSDK_ACCOUNT_TYPE));
           user.setAppIdAt3rd(String.valueOf(TCConstants.IMSDK_APPID));
           user.setIdentifier(tlsUserInfo.identifier);
           String userSig = mTLSLoginHelper.getUserSig(identifier);

            //发起 IM SDK 登录操作,登录成功后就可以发消息了
            TIMManager.getInstance().login(TCConstants.IMSDK_APPID, user, userSig, new TIMCallBack() {
                @Override
                public void onSuccess() {
                    // OK, 登录成功,后面就可以发消息了!
                }
                              @Override
                public void onError(int i, String s) {
                   // im sdk 登录失败,请检查 IMSDK_APPID 是不是搞错了
                }
            });
        }
    });
}

5.3.2 独立(鉴权)模式

独立模式的设计目标是将消息收发权限交给客户掌控,它要求在您的登录服务器确认App用户合法身份后需派发签名,腾讯云通过检查签名真实性后才允许收发消息,这样可以让消息的收发者都100%是您的账号体系里的账号。

该模式适合对身份确认要求非常高的客户,比如只能允许自己App的注册用户才可以收发消息,如果您没有非常迫切的相关需求,不推荐使用该模式。

该模式对接步骤如下:

  1. 在腾讯云通讯管理控制台,将集成模式选为独立模式,并下载签名用的公私钥。

  2. 手机App的登录逻辑依旧按您原来的流程,即ID + PASSWORD到您的登录服务器进行验证。如果登录成功了,您的登录服务器需要额外签发一个UserSig签名给App,该签名使用步骤1中下载到的私钥对相应的ID进行非对称加密

  3. 手机App在拿到ID和UserSig以后,就可以调用IM SDK的imLogin(ID, UserSig)接口进行登录了,腾讯云后台会使用步骤一中相应的公钥进行解密,从而验证ID是否得到了您的登录服务器的认可。

以上步骤的示意图如下:

这里貌似有点复杂,实际上它背后的原理是关于两家公司的后台服务器怎么相互信任的故事,我们打个更形象的比方:

  1. 某个女性用户在您的App上登录了,她使用了之前在您这里注册的ID和登录口令,ID叫做 “花仙子”。

  2. 您这边在拿到ID和口令后立刻进行了验证,并且确认她可以登录App。于此同时,您还开给她一张放行条(UserSig),上面写着“ '花仙子' 是良民的干活,到您腾讯的地盘,您可得伺候好了,不要亏待她...”,为了确保这个放行条不被伪造,您还在上面用正楷签了名。

  3. 腾讯云在看了您的放行条以后,确认签字为您本人亲笔所签,自然会提供相应的服务。

上述就是消息鉴权背后的对接方案,它的核心目标就是限制哪些没有得到您的后台服务器认可的用户收发消息,但让您的服务器检查每一条消息又是不合适的,所以才演化出了这样一套方案。

5.4 消息收发

腾讯云通讯服务支持多种消息类型的格式,比如文本、图片、表情、语音甚至小文件,这部分可以在云通讯文档专区 进行了解。

相对而言,直播中的消息形态比较简单,主要是如下几种类型:

  • 普通文本消息:包括发送者的昵称以及消息本身。
  • 弹幕消息:本质也是文本消息,只是消息的展示方式会更显花哨。
  • 点赞消息:当一位观众为主播点赞时,要保证其他观众也能看到
  • 系统通知,比如“XXX已加入房间”或者“主播已离开”等等。

针对这种比较简单的场景,我们在小直播中相应地采用了一种非常简单的办法:统一采用文本消息通道收发消息,因为有多字段(如消息类型、用户头像、昵称等)拼装需求的情况,我们采用了json格式对数据进行组装。

比如:“花仙子” 给主播发了一条消息 “帅哥笑一下” ,按照上述方案,真正发送的文本消息为:

{
    "userAction": 1,
    "userId": 27149, 
    "nickName": "花仙子", 
    "headPic": "http: //www.test.com/headpic/27149.png",
    "msg": "帅哥笑一下"
}

其中,userAction 是我们在小直播中定义的消息类型,一共有 5 种,它们分别是:

userAction 数字 含义
AVIMCMD_Custom_Text 1 文本消息
AVIMCMD_Custom_EnterLive 2 用户加入直播
AVIMCMD_Custom_ExitLive 3 用户退出直播
AVIMCMD_Custom_Like 4 点赞消息
AVIMCMD_Custom_Danmaku 5 弹幕消息

接下来是小直播里的源码摘抄,源码的作用是发送一条普通的文本消息,谨供您参考:

5.4.1 iOS平台

//源码位于 TCMsgHandler.m 文件中
- (void)sendMessage:(AVIMCommand)cmd userId:(NSString *)userId
             nickName:(NSString *)nickName 
                         headPic:(NSString *)headPic 
                         msg:(NSString *)msgContent
{
    if ((AVIMCMD_Custom_Text == cmd || AVIMCMD_Custom_Danmaku == cmd) && msgContent.length == 0)
    {
        DebugLog(@"sendMessage failed, msg length is 0");
        return;
    }

    NSDictionary* dict = @{@"userAction" : @(cmd),
        @"userId" : TC_PROTECT_STR(userId), 
        @"nickName" : TC_PROTECT_STR(nickName), 
        @"headPic" : TC_PROTECT_STR(headPic), 
        @"msg" : TC_PROTECT_STR(msgContent)};

    NSData* data = [TCUtil dictionary2JsonData:dict];
    NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

    TIMTextElem *textElem = [[TIMTextElem alloc] init];
    [textElem setText:content];

    TIMMessage *timMsg = [[TIMMessage alloc] init];
    [timMsg addElem:textElem];

    [_chatRoomConversation sendMessage:timMsg succ:^{
        DebugLog(@"sendMessage success, cmd:%d", cmd);
    } fail:^(int code, NSString *msg) {
        DebugLog(@"sendMessage failed, cmd:%d, code:%d, errmsg:%@", cmd, code, msg);
    }];
}

5.4.2 Android 平台

//源码位于 TCChatRoomMgr.java 文件中
private void sendMessage(int cmd, String param, TIMValueCallBack<TIMMessage> timValueCallBack) 
{
    JSONObject sendJson = new JSONObject();
    try {
        sendJson.put("userAction", cmd);
        sendJson.put("userId", TCUserInfoMgr.getInstance().getUserId());
        sendJson.put("nickName", TCUserInfoMgr.getInstance().getNickname());
        sendJson.put("headPic", TCUserInfoMgr.getInstance().getHeadPic());
        sendJson.put("msg", param);
    } catch (JSONException e) {
        e.printStackTrace();
    }

    String cmds = sendJson.toString();
    TIMMessage msg = new TIMMessage();
    TIMTextElem elem = new TIMTextElem();
    elem.setText(cmds);

    if (msg.addElement(elem) != 0) {
        Log.d(TAG, "addElement failed");
        return;
    }
    sendTIMMessage(msg, timValueCallBack);
}