群聊分析
- app发送群消息到msg_server
- msg_server收到后,以本地时间戳设置消息创建时间(客户端时间不可靠)
- 转发
- 向数据库查询群ID有效性,非法则直接忽略
- 该成员是否在群成员内,非法则直接忽略
- 用户和群的会话是否存在,不存在自动创建一条会话关系
- 生成群内唯一消息ID
- 写入数据库表group_msg中(按照gruopId%8分表)
- 返回ack
- 返回ack(如果有多端登录,广播msg到多个端,各个端再回复ack确认)
- 向db_proxy_server查询群成员列表
- 查数据库
- 接收群成员
- 遍历
- 在线的直接send
- 离线的则推送,先去查询群推送标志设置。
- 查数据库
- 遍历所有离线成员的群推送标志列表
- 未设置为免打扰的,直接推送。ios:apns,android:各种推送如华为、小米或者第三方如极光、信鸽等
- 离线用户上线
- 拉群消息(和微信不一样,这里的策略是:先查会话,点击群后拉最近20条消息展示,然后存储本地sqlite数据库。再往上滚,则继续拉。)
TeamTalkd的这个方案,比较适合入门。采用了扩散读的方案(群消息只存储一份,拉取多次),具体代码不贴了。
msg_server主要看以下2个文件:
- MsgConn.cpp:_HandleClientMsgData,处理客户端的消息。
- DBServConn.cpp:_HandleMsgData,处理来自于db_proxy_server的响应。
db_proxy_server主要从消息驱动表入手,找到相关逻辑的实现:
- HandlerMap.cpp:消息驱动表实现,根据CmdID去调用相关的处理函数,从这里入手看。
- 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个成员):
- 当A发送了一条消息,记录total=1,A的offset=1,此时total-A.offset=0,所以A的未读消息数为0条。B此时上线,redis中没有B的offset,则B的群消息未读计数为total数,即1。
- 当B点击会话后,清除未读计数其实就是更新B.offset的过程,把offset记为当前群消息的总数,下次再登录,total-B.offset就为0了,也就是清除了。
- 此时A同理,离线期间B发了4条消息,total=5了,total-A.offset=4,就算出来离线期间的群未读消息数量了。
公式:
成员某群未读消息总数 = 群消息总数(im_group_msg) - 群成员已读消息总数(im_user_group)
那为什么要这么设计,使用已读总数,而不是未读总数呢(比如判断了用户不在线,未读数量就+1)?
其实这样能降低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