MyBatis+SpringMVC+SpringBootSpringBoot精选IM即时通讯

Java web后端直播接入腾讯IM聊天

2020-03-27  本文已影响0人  itapechang

类似于斗鱼直播间的聊天

直播.png

接入第三方IM,大部分功能实现依赖于前端。后端侧重于创建群组的时机,以及考虑群组解散的时机(如果有合理的退群机制和定期清理群人数的机制,当我没说,不用考虑解散群组机制。因为对腾讯IM来说一个人只能同时加入200个群组限制)。如果后端需要对聊天的内容和群组变化记录入库,就需要用到腾讯云IM的回调机制。对于直播间人数获取,可以获取IM群组人数,不过还是要设计好严格的退群加群机制,才能保证人数正确。

本文主要介绍建群和解散群组的实现方式

web直播间群组的创建是和主播进行绑定,一个主播对应一个群组,同时主播也是该群组的管理员,拥有授权,禁言,踢人等操作。当一个普通用户升级为主播的那一刻,后端就创建了一个IM群组,同时授权管理员是该主播。

用户加入IM群组的前提是必须先登录腾讯的IM系统。就好比你要加QQ群聊天,就必须先登录QQ一样,因此对于腾讯IM群组的private/public/chatroom的群组模式是没有不登录这个概念的。这也就解释了,为什么直播发言必须要求用户登录账号(同时也登录了IM系统,同步web的账号信息到IM系统中的账号)。而不登录web的用户要想看到群组的内容,也必须登录IM系统,只是此刻以游客的身份进行登录,也就是随机的账号登录IM系统,只是不能进行发言,这需要前端进行限制操作。

登录IM系统需要账号密码:

对于登录web的用户来说,IM的账号就是web体系的用户ID或者其他唯一标识,而密码则需要调用后端接口getUserSig获取(参考腾讯userSig机制)
在IM文档中可以获取到
Base64URL和GenUserSig,该加密用来生成IM密码

public class Base64URL {

    public static byte[] base64EncodeUrl(byte[] input){
        byte[] base64 = new BASE64Encoder().encode(input).getBytes();
        for (int i = 0; i < base64.length; ++i)
            switch (base64[i]) {
                case '+':
                    base64[i] = '*';
                    break;
                case '/':
                    base64[i] = '-';
                    break;
                case '=':
                    base64[i] = '_';
                    break;
                default:
                    break;
            }
        return base64;
    }

    public static byte[] base64DecodeUrl(byte[] input) throws IOException {
        byte[] base64 = input.clone();
        for (int i = 0; i < base64.length; ++i)
            switch (base64[i]) {
                case '*':
                    base64[i] = '+';
                    break;
                case '-':
                    base64[i] = '/';
                    break;
                case '_':
                    base64[i] = '=';
                    break;
                default:
                    break;
            }
        return new BASE64Decoder().decodeBuffer(base64.toString());
    }
}
public class TXGenUserSig {

    private long sdkappid;
    private String key;

    public TXGenUserSig(long sdkappid, String key) {
        this.sdkappid = sdkappid;
        this.key = key;
    }

    private String hmacsha256(String identifier, long currTime, long expire, String base64Userbuf) {
        String contentToBeSigned = "TLS.identifier:" + identifier + "\n"
                + "TLS.sdkappid:" + sdkappid + "\n"
                + "TLS.time:" + currTime + "\n"
                + "TLS.expire:" + expire + "\n";
        if (null != base64Userbuf) {
            contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n";
        }
        try {
            byte[] byteKey = key.getBytes("UTF-8");
            Mac hmac = Mac.getInstance("HmacSHA256");
            SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256");
            hmac.init(keySpec);
            byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes("UTF-8"));
            return (new BASE64Encoder().encode(byteSig)).replaceAll("\\s*", "");
        } catch (UnsupportedEncodingException e) {
            return "";
        } catch (NoSuchAlgorithmException e) {
            return "";
        } catch (InvalidKeyException e) {
            return "";
        }
    }

    private String genSig(String identifier, long expire, byte[] userbuf) {

        long currTime = System.currentTimeMillis()/1000;

        JSONObject sigDoc = new JSONObject();
        sigDoc.put("TLS.ver", "2.0");
        sigDoc.put("TLS.identifier", identifier);
        sigDoc.put("TLS.sdkappid", sdkappid);
        sigDoc.put("TLS.expire", expire);
        sigDoc.put("TLS.time", currTime);

        String base64UserBuf = null;
        if (null != userbuf) {
            base64UserBuf = new BASE64Encoder().encode(userbuf);
            sigDoc.put("TLS.userbuf", base64UserBuf);
        }
        String sig = hmacsha256(identifier, currTime, expire, base64UserBuf);
        if (sig.length() == 0) {
            return "";
        }
        sigDoc.put("TLS.sig", sig);
        Deflater compressor = new Deflater();
        compressor.setInput(sigDoc.toString().getBytes(Charset.forName("UTF-8")));
        compressor.finish();
        byte [] compressedBytes = new byte[2048];
        int compressedBytesLength = compressor.deflate(compressedBytes);
        compressor.end();
        return (new String(Base64URL.base64EncodeUrl(Arrays.copyOfRange(compressedBytes,
                0, compressedBytesLength)))).replaceAll("\\s*", "");
    }

    public String genSig(String identifier, long expire) {
        return genSig(identifier, expire, null);
    }

    public String genSigWithUserBuf(String identifier, long expire, byte[] userbuf) {
        return genSig(identifier, expire, userbuf);
    }
}

腾讯IM侧重前端,对java web后端没有好的sdk支持

自定义一个IM的配置类

@Configuration
public class TXIMConfiguration {


    private static long sdkappid;


    private static String key;

    private static String identifier;

    //userSig 有效期7天
    private static final long EXPIRE_TIME=7*24*60*60;

    private static TXGenUserSig txGenUserSig=null;

    @Value("${txim.sdkappid}")
    public  void setSdkappid(long sdkappid) {
        TXIMConfiguration.sdkappid = sdkappid;
    }

    @Value("${txim.key}")
    public  void setKey(String key) {
        TXIMConfiguration.key = key;
    }

    @Value("${txim.identifier}")
    public  void setIdentifier(String identifier) {
        TXIMConfiguration.identifier = identifier;
    }


    @Bean
    public Object services(){
        txGenUserSig=new TXGenUserSig(sdkappid,key);
        return Boolean.TRUE;
    }

    public static String getUserSig(String identifier){
        return txGenUserSig.genSig(identifier, EXPIRE_TIME);
    }

    /**
     * 创建IM群组API URL
     * @return
     */
    public static String getCreateGroupURL(){
        return "https://console.tim.qq.com/v4/group_open_http_svc/create_group?"+"sdkappid="+sdkappid+"&identifier="+identifier+"&usersig="+getUserSig(identifier)+
                "&random="+ CodeUtil.getRandomNumber(32)+"&contenttype=json";
    }

    /**
     * 解散IM群组API URL
     * @return
     */
    public static String getDestoryGroupURL(){
        return "https://console.tim.qq.com/v4/group_open_http_svc/destroy_group?"+"sdkappid="+sdkappid+"&identifier="+identifier+"&usersig="+getUserSig(identifier)+
                "&random="+CodeUtil.getRandomNumber(32)+"&contenttype=json";
    }

    /**
     * 检测账号 API URL
     * @return
     */
    public static String getCheckAccountURL(){
        return "https://console.tim.qq.com/v4/im_open_login_svc/account_check?"+"sdkappid="+sdkappid+"&identifier="+identifier+"&usersig="+getUserSig(identifier)+
                "&random="+CodeUtil.getRandomNumber(32)+"&contenttype=json";
    }

    /**
     * 单个账号导入 API URL
     * @return
     */
    public static String getAccountImportURL(){
        return "https://console.tim.qq.com/v4/im_open_login_svc/account_import?"+"sdkappid="+sdkappid+"&identifier="+identifier+"&usersig="+getUserSig(identifier)+
                "&random="+CodeUtil.getRandomNumber(32)+"&contenttype=json";
    }

}

获取userSig的接口

 /**
     * 获取登录IM聊天室的账号密码
     * @param
     * @return
     */
    @RequestMapping("/getUserSig")
    public ResultBean getUserSig(String account){
        try {
            String userSig = TXIMConfiguration.getUserSig(account);
            return ResultBean.setOk(0, "认证成功",userSig);
        }catch (Exception e){
            logger.error("腾讯SDK认证失败,检查秘钥 "+e);
            return ResultBean.setError(1,"认证失败");
        }
    }

创建IM群组和解散IM群组 这里以Springboot线程池异步方式执行

准备好实体参数实体类
CheckItem

public class CheckItem {
    private String UserID;
    public CheckItem(String userID) {
        UserID = userID;
    }

    @JSONField(name = "UserID")
    public String getUserID() {
        return UserID;
    }

    public void setUserID(String userID) {
        UserID = userID;
    }
}

GroupInfo

public class GroupInfo {

    //群主UserId
    private String Owner_Account;

    //群组类型 Private/Public/ChatRoom/
    private String Type;

    //自定义群组ID
    private String GroupId;

    //群名称
    private String Name;

    //最大群成员数量
    private int MaxMemberCount;

    //申请加群方式
    private String ApplyJoinOption;


    public GroupInfo(String Owner_Account, String Type, String GroupId, String Name, int MaxMemberCount,String ApplyJoinOption) {
        this.Owner_Account = Owner_Account;
        this.Type = Type;
        this.GroupId = GroupId;
        this.Name = Name;
        this.MaxMemberCount = MaxMemberCount;
        this.ApplyJoinOption = ApplyJoinOption;
    }

    public GroupInfo(String Type, String GroupId, String Name, int MaxMemberCount,String ApplyJoinOption) {
        this.Type = Type;
        this.GroupId = GroupId;
        this.Name = Name;
        this.MaxMemberCount = MaxMemberCount;
        this.ApplyJoinOption = ApplyJoinOption;
    }

    @JSONField(name="Owner_Account")
    public String getOwner_Account() {
        return Owner_Account;
    }

    public void setOwner_Account(String owner_Account) {
        Owner_Account = owner_Account;
    }
    @JSONField(name="Type")
    public String getType() {
        return Type;
    }

    public void setType(String type) {
        Type = type;
    }
    @JSONField(name="GroupId")
    public String getGroupId() {
        return GroupId;
    }

    public void setGroupId(String groupId) {
        GroupId = groupId;
    }
    @JSONField(name="Name")
    public String getName() {
        return Name;
    }

    public void setName(String name) {
        Name = name;
    }
    @JSONField(name="MaxMemberCount")
    public int getMaxMemberCount() {
        return MaxMemberCount;
    }

    public void setMaxMemberCount(int maxMemberCount) {
        MaxMemberCount = maxMemberCount;
    }
    @JSONField(name="ApplyJoinOption")
    public String getApplyJoinOption() {
        return ApplyJoinOption;
    }

    public void setApplyJoinOption(String applyJoinOption) {
        ApplyJoinOption = applyJoinOption;
    }
}

业务实现,根据自己需要来

@Service
public class TXIMAsynServiceImpl implements TXIMAsynService {

    private Logger logger = LoggerFactory.getLogger(TXIMAsynServiceImpl.class);

    /**
     * 异步创建群组
     * @param lessonId
     * @param periodIds
     */
    @Override
    @Async("asynTxImServiceExecutor")
    public void createIMGroup(String account) {
        logger.info("异步线程执行创建群组操作开始...userId=" + account);
        //该账号未注册到IM系统中,注册后才可以将该账号指定为IM群主
        if(account!=null) {
            String accountStr = String.valueOf(account);
            String checkRes = checkSingleAccount(accountStr);
            if (checkRes == null) {
                account = null;
            } else if ("Not Imported".equals(checkRes)) {
                //注册账号到IM体系
                if ("OK".equals(importSingleAccount(accountStr))) {
                    logger.info("注册账号到IM成功,账号:{}", account);
                } else {
                    logger.error("注册账号到IM失败,账号:{}", account);
                }
            }
        }

            //开始创建群组,如果账号为空创建无群主群组,否则有群主群组,这里创建群组类型为Public
            GroupInfo groupInfo = account == null ?
                    new GroupInfo("Public", String.valueOf(periodId), "课时" + periodId, 200, "FreeAccess")
                    : new GroupInfo(String.valueOf(account), "Public", String.valueOf(periodId), "课时" + periodId, 200, "FreeAccess");
            String contentType = JSON.toJSONString(groupInfo);
            //获取第三方API地址
            String urlAddParam = TXIMConfiguration.getCreateGroupURL();
            String res = HttpUtil.doPostJson(urlAddParam, contentType);
            String actionStatus = JSONObject.parseObject(res).getString("ActionStatus");
            if ("OK".equals(actionStatus)) {
                logger.info("调用腾讯IM创建群组成功,参数:" + contentType + "结果:{}", res);
                //创建完群组后,执行自己业务逻辑
    
            } else {
                logger.error("调用腾讯IM创建群组失败,{}", res);
            }
 
    }

    /**
     * 解散群组。
     * @param periodIds
     */
    @Override
    @Async("asynTxImServiceExecutor")
    public void destoryIMGroup(List<Integer> periodIds) {
        for (Integer periodId : periodIds) {
            String destoryGroupURL = TXIMConfiguration.getDestoryGroupURL();
            String contentType = JSON.toJSONString(new HashMap<String, Object>(1) {
                {
                    put("GroupId", String.valueOf(periodId));
                }
            });
            String res = HttpUtil.doPostJson(destoryGroupURL, contentType);
            String actionStatus = JSONObject.parseObject(res).getString("ActionStatus");
            if ("OK".equals(actionStatus)) {
                logger.info("调用腾讯IM删除群组成功,参数:" + contentType + "结果:{}", res);
            } else {
                logger.error("调用腾讯IM删除群组失败,{}", res);
            }

        }
    }


    /**
     * 检查单个账号
     *  指定群组的群主时需要先检查群主是否注册到IM系统中,否则指定不成功
     * @param account
     * @return
     */
    private String checkSingleAccount(String account) {

        String checkContent = JSON.toJSONString(new HashMap<String, Object>(1) {
            {
                put("CheckItem", Arrays.asList(new CheckItem(account)));
            }
        });
        String accountCheckRes = HttpUtil.doPostJson(TXIMConfiguration.getCheckAccountURL(), checkContent);
        String accountCheckStatus = JSONObject.parseObject(accountCheckRes).getString("ActionStatus");
        if ("OK".equals(accountCheckStatus)) {
            logger.info("调用腾讯IM账号检查接口成功,账号:" + account + "结果:{}", accountCheckRes);
            String resultItem = JSONObject.parseObject(accountCheckRes).getJSONArray("ResultItem").getString(0);
            //NotImported  Imported
            return (String) JSON.parseObject(resultItem, Map.class).get("AccountStatus");
        } else {
            logger.error("调用腾讯IM账号检查接口失败,账号:" + account + "参数:" + checkContent + "结果:{}", accountCheckRes);
            return null;
        }
    }

    /**
     * 注册单个账号
     *
     * @param account
     * @return
     */
    private String importSingleAccount(String account) {
        //json参数
        String checkContent = JSON.toJSONString(new HashMap<String, Object>(1) {
            {
                put("Identifier", account);
            }
        });
        String importAccountRes = HttpUtil.doPostJson(TXIMConfiguration.getAccountImportURL(), checkContent);
        logger.info("调用腾讯IM单个账号导入接口," + "参数:" + checkContent + "结果:{}", importAccountRes);
        return JSON.parseObject(importAccountRes).getString("ActionStatus");
    }

}

注意,腾讯IM参数首字母是大写,小写就无法识别,转换成JSON时候,会将参数首字母变成小写,需要注意这个问题,我这里用@JSONField来解决

上一篇下一篇

猜你喜欢

热点阅读