Java web后端直播接入騰訊IM聊天

類似于斗魚直播間的聊天

直播.png

接入第三方IM举反,大部分功能實(shí)現(xiàn)依賴于前端挂签。后端側(cè)重于創(chuàng)建群組的時(shí)機(jī)尖滚,以及考慮群組解散的時(shí)機(jī)(如果有合理的退群機(jī)制和定期清理群人數(shù)的機(jī)制拍皮,當(dāng)我沒說,不用考慮解散群組機(jī)制听诸。因?yàn)閷︱v訊IM來說一個(gè)人只能同時(shí)加入200個(gè)群組限制)瑟由。如果后端需要對聊天的內(nèi)容和群組變化記錄入庫奸笤,就需要用到騰訊云IM的回調(diào)機(jī)制伟恶。對于直播間人數(shù)獲取碴开,可以獲取IM群組人數(shù),不過還是要設(shè)計(jì)好嚴(yán)格的退群加群機(jī)制博秫,才能保證人數(shù)正確潦牛。

對于正確的退群機(jī)制,后端要注意IM文檔中回調(diào)機(jī)制:在線狀態(tài)相關(guān)回調(diào)

文檔中該回調(diào)介紹為:
客戶端 kill 后臺進(jìn)程挡育,云服務(wù)器檢測到客戶端網(wǎng)絡(luò)斷開后觸發(fā)下線回調(diào)巴碗。
客戶端心跳超時(shí),包括客戶端 Crash即寒、關(guān)閉網(wǎng)絡(luò) 400 秒后橡淆,云服務(wù)器檢測到客戶端的心跳超時(shí)觸發(fā)下線回調(diào)

通俗講:也就是客戶端用戶無法正確執(zhí)行quitGroup操作時(shí),通過IM回調(diào)服務(wù)端接口來實(shí)現(xiàn)用戶退群操作蒿叠。

本文主要介紹建群和解散群組的實(shí)現(xiàn)方式

web直播間群組的創(chuàng)建是和主播進(jìn)行綁定明垢,一個(gè)主播對應(yīng)一個(gè)群組蚣常,同時(shí)主播也是該群組的管理員市咽,擁有授權(quán),禁言抵蚊,踢人等操作施绎。當(dāng)一個(gè)普通用戶升級為主播的那一刻溯革,后端就創(chuàng)建了一個(gè)IM群組,同時(shí)授權(quán)管理員是該主播谷醉。

用戶加入IM群組的前提是必須先登錄騰訊的IM系統(tǒng)致稀。就好比你要加QQ群聊天,就必須先登錄QQ一樣俱尼,因此對于騰訊IM群組的private/public/chatroom的群組模式是沒有不登錄這個(gè)概念的抖单。這也就解釋了,為什么直播發(fā)言必須要求用戶登錄賬號(同時(shí)也登錄了IM系統(tǒng)遇八,同步web的賬號信息到IM系統(tǒng)中的賬號)矛绘。而不登錄web的用戶要想看到群組的內(nèi)容,也必須登錄IM系統(tǒng)刃永,只是此刻以游客的身份進(jìn)行登錄货矮,也就是隨機(jī)的賬號登錄IM系統(tǒng),只是不能進(jìn)行發(fā)言斯够,這需要前端進(jìn)行限制操作囚玫。

登錄IM系統(tǒng)需要賬號密碼:

對于登錄web的用戶來說,IM的賬號就是web體系的用戶ID或者其他唯一標(biāo)識读规,而密碼則需要調(diào)用后端接口getUserSig獲取(參考騰訊userSig機(jī)制)
在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側(cè)重前端,對java web后端沒有好的sdk支持

自定義一個(gè)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);
    }

    /**
     * 創(chuàng)建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";
    }

    /**
     * 單個(gè)賬號導(dǎo)入 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, "認(rèn)證成功",userSig);
        }catch (Exception e){
            logger.error("騰訊SDK認(rèn)證失敗掖桦,檢查秘鑰 "+e);
            return ResultBean.setError(1,"認(rèn)證失敗");
        }
    }

創(chuàng)建IM群組和解散IM群組 這里以Springboot線程池異步方式執(zhí)行

準(zhǔn)備好實(shí)體參數(shù)實(shí)體類
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;

    //最大群成員數(shù)量
    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;
    }
}

業(yè)務(wù)實(shí)現(xiàn)本昏,根據(jù)自己需要來

@Service
public class TXIMAsynServiceImpl implements TXIMAsynService {

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

    /**
     * 異步創(chuàng)建群組
     * @param lessonId
     * @param periodIds
     */
    @Override
    @Async("asynTxImServiceExecutor")
    public void createIMGroup(String account) {
        logger.info("異步線程執(zhí)行創(chuàng)建群組操作開始...userId=" + account);
        //該賬號未注冊到IM系統(tǒng)中,注冊后才可以將該賬號指定為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);
                }
            }
        }

            //開始創(chuàng)建群組,如果賬號為空創(chuàng)建無群主群組祝沸,否則有群主群組,這里創(chuàng)建群組類型為Public
            GroupInfo groupInfo = account == null ?
                    new GroupInfo("Public", account, "用戶" + account, 200, "FreeAccess")
                    : new GroupInfo(account, "Public", account, "用戶" + account, 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("調(diào)用騰訊IM創(chuàng)建群組成功,參數(shù):" + contentType + "結(jié)果:{}", res);
                //創(chuàng)建完群組后涩惑,執(zhí)行自己業(yè)務(wù)邏輯
    
            } else {
                logger.error("調(diào)用騰訊IM創(chuàng)建群組失敗,{}", res);
            }
 
    }

    /**
     * 解散群組桑驱。
     * @param periodIds
     */
    @Override
    @Async("asynTxImServiceExecutor")
    public void destoryIMGroup(String account) {
        for (Integer periodId : periodIds) {
            String destoryGroupURL = TXIMConfiguration.getDestoryGroupURL();
            String contentType = JSON.toJSONString(new HashMap<String, Object>(1) {
                {
                    put("GroupId", account);
                }
            });
            String res = HttpUtil.doPostJson(destoryGroupURL, contentType);
            String actionStatus = JSONObject.parseObject(res).getString("ActionStatus");
            if ("OK".equals(actionStatus)) {
                logger.info("調(diào)用騰訊IM刪除群組成功,參數(shù):" + contentType + "結(jié)果:{}", res);
            } else {
                logger.error("調(diào)用騰訊IM刪除群組失敗,{}", res);
            }

        }
    }


    /**
     * 檢查單個(gè)賬號
     *  指定群組的群主時(shí)需要先檢查群主是否注冊到IM系統(tǒng)中赊级,否則指定不成功
     * @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("調(diào)用騰訊IM賬號檢查接口成功,賬號:" + account + "結(jié)果:{}", accountCheckRes);
            String resultItem = JSONObject.parseObject(accountCheckRes).getJSONArray("ResultItem").getString(0);
            //NotImported  Imported
            return (String) JSON.parseObject(resultItem, Map.class).get("AccountStatus");
        } else {
            logger.error("調(diào)用騰訊IM賬號檢查接口失敗,賬號:" + account + "參數(shù):" + checkContent + "結(jié)果:{}", accountCheckRes);
            return null;
        }
    }

    /**
     * 注冊單個(gè)賬號
     *
     * @param account
     * @return
     */
    private String importSingleAccount(String account) {
        //json參數(shù)
        String checkContent = JSON.toJSONString(new HashMap<String, Object>(1) {
            {
                put("Identifier", account);
            }
        });
        String importAccountRes = HttpUtil.doPostJson(TXIMConfiguration.getAccountImportURL(), checkContent);
        logger.info("調(diào)用騰訊IM單個(gè)賬號導(dǎo)入接口," + "參數(shù):" + checkContent + "結(jié)果:{}", importAccountRes);
        return JSON.parseObject(importAccountRes).getString("ActionStatus");
    }

}

注意预烙,騰訊IM參數(shù)首字母是大寫扁掸,小寫就無法識別翘县,轉(zhuǎn)換成JSON時(shí)候,會(huì)將參數(shù)首字母變成小寫忘伞,需要注意這個(gè)問題舀奶,我這里用@JSONField來解決

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末斋射,一起剝皮案震驚了整個(gè)濱河市涧至,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖蒜焊,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件优妙,死亡現(xiàn)場離奇詭異邪意,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)宴树,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門策菜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酒贬,你說我怎么就攤上這事又憨。” “怎么了锭吨?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵竟块,是天一觀的道長。 經(jīng)常有香客問我耐齐,道長浪秘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任埠况,我火速辦了婚禮耸携,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辕翰。我一直安慰自己夺衍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布喜命。 她就那樣靜靜地躺著沟沙,像睡著了一般河劝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上矛紫,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天赎瞎,我揣著相機(jī)與錄音,去河邊找鬼颊咬。 笑死务甥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喳篇。 我是一名探鬼主播敞临,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼麸澜!你這毒婦竟也來了挺尿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤炊邦,失蹤者是張志新(化名)和其女友劉穎票髓,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铣耘,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洽沟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜗细。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裆操。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖炉媒,靈堂內(nèi)的尸體忽然破棺而出踪区,到底是詐尸還是另有隱情,我是刑警寧澤吊骤,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布缎岗,位于F島的核電站,受9級特大地震影響白粉,放射性物質(zhì)發(fā)生泄漏传泊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一鸭巴、第九天 我趴在偏房一處隱蔽的房頂上張望眷细。 院中可真熱鬧,春花似錦鹃祖、人聲如沸溪椎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽校读。三九已至沼侣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間歉秫,已是汗流浹背蛾洛。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留端考,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓揭厚,卻偏偏與公主長得像却特,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子筛圆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 點(diǎn)擊查看原文 Web SDK 開發(fā)手冊 SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,700評論 0 15
  • 騰訊具有高并發(fā)裂明、高可靠的即時(shí)通信能力;利用 騰訊云通信 提供的 SDK 可以將即時(shí)通信功能快速集成到自己的 APP...
    michael_jia閱讀 3,108評論 2 4
  • 我一直很自卑 毫無自信 對生活失去了光彩 但是我不應(yīng)該著急 我只是暫時(shí)的被塵埃蒙蔽了雙眼 總會(huì)有被風(fēng)吹開的一天
    綠子世界閱讀 257評論 0 1
  • 一份工作,兩份收入提岔,你一直在占便宜仙蛉。 老板付你薪水,一天上班8個(gè)小時(shí)碱蒙,你用4個(gè)小時(shí)完成了工作荠瘪。剩下的4個(gè)小時(shí),上網(wǎng)...
    趙陽說閱讀 763評論 0 1