TeamTalk源码分析——群聊技术方案和群未读计数的实现

群聊分析

在这里插入图片描述

  1. app发送群消息到msg_server
  2. msg_server收到后,以本地时间戳设置消息创建时间(客户端时间不可靠)
  3. 转发
  4. 向数据库查询群ID有效性,非法则直接忽略
  5. 该成员是否在群成员内,非法则直接忽略
  6. 用户和群的会话是否存在,不存在自动创建一条会话关系
  7. 生成群内唯一消息ID
  8. 写入数据库表group_msg中(按照gruopId%8分表)
  9. 返回ack
  10. 返回ack(如果有多端登录,广播msg到多个端,各个端再回复ack确认)
  11. 向db_proxy_server查询群成员列表
  12. 查数据库
  13. 接收群成员
  14. 遍历
  15. 在线的直接send
  16. 离线的则推送,先去查询群推送标志设置。
  17. 查数据库
  18. 遍历所有离线成员的群推送标志列表
  19. 未设置为免打扰的,直接推送。ios:apns,android:各种推送如华为、小米或者第三方如极光、信鸽等
  20. 离线用户上线
  21. 拉群消息(和微信不一样,这里的策略是:先查会话,点击群后拉最近20条消息展示,然后存储本地sqlite数据库。再往上滚,则继续拉。)

TeamTalkd的这个方案,比较适合入门。采用了扩散读的方案(群消息只存储一份,拉取多次),具体代码不贴了。

msg_server主要看以下2个文件:

  1. MsgConn.cpp:_HandleClientMsgData,处理客户端的消息。
  2. DBServConn.cpp:_HandleMsgData,处理来自于db_proxy_server的响应。

db_proxy_server主要从消息驱动表入手,找到相关逻辑的实现:

  1. HandlerMap.cpp:消息驱动表实现,根据CmdID去调用相关的处理函数,从这里入手看。
  2. business/GroupMessageModel.cpp:群聊消息处理,包括存储,未读计数更新等。

群未读计数分析

TeamTalk的实现主要分成2块:

  • 群的总未读计数im_group_msg(1个群1个key)
  • 成员的未读计数im_user_group(1个群N个Key)

key的规则如下:

  • 群的总未读计数(其实就是群消息数):groupID + _im_group_msg ,示例:2_im_group_msg
  • 群成员未读计数(更准确的说是成员已读时,群消息数):userID + _ + groupID + _im_user_group,示例:7_2_im_user_group

算法原理如下(假设一个群有A和B共2个成员):
TeamTalk的实现主要分成2块:
群的总未读计数im_group_msg(1个群1个key)
成员的未读计数im_user_group(1个群N个Key)

key的规则如下:

  • 群的总未读计数:groupID+_im_group_msg ,示例:2_im_group_msg
  • 群成员未读计数:userID+_+groupID+_im_user_group,示例:7_2_im_user_group

算法原理(假设一个群有A和B共2个成员):
在这里插入图片描述

  1. 当A发送了一条消息,记录total=1,A的offset=1,此时total-A.offset=0,所以A的未读消息数为0条。B此时上线,redis中没有B的offset,则B的群消息未读计数为total数,即1。
  2. 当B点击会话后,清除未读计数其实就是更新B.offset的过程,把offset记为当前群消息的总数,下次再登录,total-B.offset就为0了,也就是清除了。
  3. 此时A同理,离线期间B发了4条消息,total=5了,total-A.offset=4,就算出来离线期间的群未读消息数量了。

公式:

成员某群未读消息总数 = 群消息总数(im_group_msg) - 群成员已读消息总数(im_user_group)

那为什么要这么设计,使用已读总数,而不是未读总数呢(比如判断了用户不在线,未读数量就+1)?

扫描二维码关注公众号,回复: 12520723 查看本文章

其实这样能降低redis的更新频率,否则200人的群,一个人发了一条消息,redis就要更新199次。上面的设计,只需要更新2次。

附redis的结构的示例:
在这里插入图片描述
发消息代码:

bool CGroupMessageModel::incMessageCount(uint64_t nUserId, uint32_t nGroupId) {
    
    
    bool bRet = false;
    CacheManager *pCacheManager = CacheManager::getInstance();
    CacheConn *pCacheConn = pCacheManager->GetCacheConn("unread");
    if (pCacheConn) {
    
    
        // 2002_im_group_msg
        // |——count: +1
        string strGroupKey = int2string(nGroupId) + GROUP_TOTAL_MSG_COUNTER_REDIS_KEY_SUFFIX;
        pCacheConn->hincrBy(strGroupKey, GROUP_COUNTER_SUBKEY_COUNTER_FIELD, 1);

        // 2002_im_group_msg下所有的key取出来
        map<string, string> mapGroupCount;
        bool bRet = pCacheConn->hgetAll(strGroupKey, mapGroupCount);
        if (bRet) {
    
    
            // 1_2002_im_user_group
            // |——count: 1
            string strUserKey =
                    int2string(nUserId) + "_" + int2string(nGroupId) + GROUP_USER_MSG_COUNTER_REDIS_KEY_SUFFIX;
            string strReply = pCacheConn->hmset(strUserKey, mapGroupCount);
            if (!strReply.empty()) {
    
    
                bRet = true;
            } else {
    
    
                ERROR("hmset %s failed !", strUserKey.c_str());
            }
        } else {
    
    
            ERROR("hgetAll %s failed!", strGroupKey.c_str());
        }
        pCacheManager->RelCacheConn(pCacheConn);
    } else {
    
    
        ERROR("no cache connection for unread");
    }
    return bRet;
}

清除未读计数:

bool CGroupMessageModel::clearMessageCount(uint64_t nUserId, uint32_t nGroupId) {
    
    
    bool bRet = false;
    CacheManager *pCacheManager = CacheManager::getInstance();
    CacheConn *pCacheConn = pCacheManager->GetCacheConn("unread");
    if (pCacheConn) {
    
    
        // 用总数覆盖offset,即total-offset=0
        string strGroupKey = int2string(nGroupId) + GROUP_TOTAL_MSG_COUNTER_REDIS_KEY_SUFFIX;
        map<string, string> mapGroupCount;
        bool bRet = pCacheConn->hgetAll(strGroupKey, mapGroupCount);
        pCacheManager->RelCacheConn(pCacheConn);
        if (bRet) {
    
    
            string strUserKey =
                    int2string(nUserId) + "_" + int2string(nGroupId) + GROUP_USER_MSG_COUNTER_REDIS_KEY_SUFFIX;
            string strReply = pCacheConn->hmset(strUserKey, mapGroupCount);
            if (strReply.empty()) {
    
    
                ERROR("hmset %s failed !", strUserKey.c_str());
            } else {
    
    
                bRet = true;
            }
        } else {
    
    
            ERROR("hgetAll %s failed !", strGroupKey.c_str());
        }
    } else {
    
    
        ERROR("no cache connection for unread");
    }
    return bRet;
}

获取用户的未读消息总数:

void CGroupMessageModel::getUnReadCntAll(uint64_t nUserId, uint32_t &nTotalCnt) {
    
    
    list<uint32_t> lsGroupId;
    CGroupModel::getInstance()->getUserGroupIds(nUserId, lsGroupId, 0);
    uint32_t nCount = 0;

    CacheManager *pCacheManager = CacheManager::getInstance();
    CacheConn *pCacheConn = pCacheManager->GetCacheConn("unread");
    if (pCacheConn) {
    
    
        for (auto it = lsGroupId.begin(); it != lsGroupId.end(); ++it) {
    
    
            uint32_t nGroupId = *it;
            string strGroupKey = int2string(nGroupId) + GROUP_TOTAL_MSG_COUNTER_REDIS_KEY_SUFFIX;
            string strGroupCnt = pCacheConn->hget(strGroupKey, GROUP_COUNTER_SUBKEY_COUNTER_FIELD);
            if (strGroupCnt.empty()) {
    
    
//                log("hget %s : count failed !", strGroupKey.c_str());
                continue;
            }
            uint32_t nGroupCnt = (uint32_t) (atoi(strGroupCnt.c_str()));

            string strUserKey =
                    int2string(nUserId) + "_" + int2string(nGroupId) + GROUP_USER_MSG_COUNTER_REDIS_KEY_SUFFIX;
            string strUserCnt = pCacheConn->hget(strUserKey, GROUP_COUNTER_SUBKEY_COUNTER_FIELD);

            uint32_t nUserCnt = (strUserCnt.empty() ? 0 : ((uint32_t) atoi(strUserCnt.c_str())));
            // 这里就是上面说的:total - offset = 未读数量
            if (nGroupCnt >= nUserCnt) {
    
    
                nCount = nGroupCnt - nUserCnt;
            }
            if (nCount > 0) {
    
    
                nTotalCnt += nCount;
            }
        }
        pCacheManager->RelCacheConn(pCacheConn);
    } else {
    
    
        ERROR("no cache connection for unread");
    }
}

关于作者

推荐下自己的开源IM,纯Golang编写:

CoffeeChat:

https://github.com/xmcy0011/CoffeeChat
opensource im with server(go) and client(flutter+swift)

参考了TeamTalk、瓜子IM等知名项目,包含服务端(go)和客户端(flutter+swift),单聊和机器人(小微、图灵、思知)聊天功能已完成,目前正在研发群聊功能,欢迎对golang和跨平台开发flutter技术感兴趣的小伙伴Star加关注。

————————————————
版权声明:本文为CSDN博主「许非」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xmcy001122/article/details/109316394

猜你喜欢

转载自blog.csdn.net/xmcy001122/article/details/109316394
今日推荐