ChatService类中,定义了一个成员变量,就是RoomMgr,用来管理服务端的聊天室。
主要作用
RoomMgr主要作用有两个:
1、管理聊天室:包括创建聊天室、删除聊天室、查询聊天室,定期检查各个聊天室是否过期。这些功能需要维护或遍历聊天室的列表。
2、聊天室消息路由。服务接收到聊天室内的消息,首先需要通过管理类查询到具体的聊天室,再将消息传递给指定的聊天室,具体的逻辑处理在聊天室内部执行。
管理类的头文件定义如下:
Copy #pragma once
#include <unordered_map>
#include "stdint.h"
#include <mutex>
#include "ChatRoom.h"
#include "RoomIDMgr.h"
#include "ClientUser.h"
typedef std::mutex Mutex;
typedef std::lock_guard<Mutex> AutoLock;
class CHYEvent;
class RoomMgr
{
public:
RoomMgr();
public:
int CreateRoom(int64_t sesID, int& roomID);
int JoinRoom(int64_t sesID, int& roomID, ClientUser* user);
int OnSendRoomMsg(int64_t sesID, int nRoomID, std::string& msg);
int OnClientBreak(int64_t sesID);
bool IsRoomAlive(int roomID);
void Check();
void OnDestroyRoomEvent(CHYEvent* pEvent);
public:
std::unordered_map<int, ChatRoom*> m_mapRoom; // <roomid, roomPtr>
Mutex m_lock;
private:
RoomIDMgr m_roomIDMgr;
};
RoomMgr对聊天室进行管理,考虑到聊天室没有排序需求,且需要快速查询,数据结构选择了unordered_map,并使用了互斥锁,对map进行多线程的保护。
RoomIDMgr用于分配房间ID,在创建房间的时候会使用到。
创建房间、删除房间,都会导致房间数量的变化,都是需要Mgr类进行管理。
加入房间和发送聊天消息,需要路由到具体房间进行处理,Mgr主要负责查询和转发。客户端主动离开房间,或者断开连接被动离开房间,需要改变房间内的用户列表信息,也需要Mgr通知到具体房间进行操作。
创建房间
Copy int RoomMgr::CreateRoom(int64_t sesID, int& roomID)
{
AutoLock lock(m_lock);
// 检查是否已经创建过,创建过直接返回已创建的房间
for (auto itr : m_mapRoom)
{
if (itr.second->m_nCreaterID == sesID)
{
roomID = itr.second->m_id;
return ROOM_AREADY_EXIST;
}
}
int nID = m_roomIDMgr.GetID();
ChatRoom*pRoom = new ChatRoom();
pRoom->Init(nID, sesID);
m_mapRoom[nID] = pRoom;
roomID = nID;
return ROOM_CREATE_SUCC;
}
创建房间前,先检查用户是否已经创建过房间,如果创建过,直接返回之前创建的房间ID,这里不允许一个用户同时创建多个房间。
返回的错误码统一定义在ErrCodeDef.h中,文件中也定义了加入房间的一些错误情况
Copy #pragma once
const int ROOM_CREATE_SUCC = 0;
const int ROOM_AREADY_EXIST = 1;
const int JOIN_ROOM_SUCC = 0;
const int JOIN_ROOM_AREADY_IN = 1;
const int JOIN_ROOM_NOT_FIND = 2;
const int JOIN_ROOM_MAX_USER = 3;
const int JOIN_ROOM_DESTORY = 4;
const int SEND_MSG_ROOM_NOT_FIND = 1;
房间的ID是6位数,资源有限,需要重复利用,因此使用RoomIDMgr类进行管理,创建时申请一个ID,此处后续还需增加申请ID失败的处理逻辑。
new一个新的ChatRoom,并使用房间id和创建者id进行初始化,将房间指针交给m_mapRoom进行管理。roomID最为一个引用参数,将房间id返回给调用方。
函数中对m_mapRoom进行了查询,并且新增房间需要修改,因此函数入口处加锁,保护map。
定期检查
Copy void RoomMgr::Check()
{
AutoLock lock(m_lock);
for (auto itr : m_mapRoom)
{
itr.second->Check();
}
}
第3节中提到服务的OnTimeOut函数每30秒执行一次,每次执行时,调用了RoomMgr的Check函数,因此,每30秒Mgr都会遍历所有的房间,进行常规检测,确定房间是否符合销毁的条件。
如果在Room的Check函数中,发现房间过期,且房间内没人了,则需要进入房间销毁流程。但是Room不能自己直接销毁自己,需要通知Mgr执行销毁逻辑。
事件机制和删除房间
这里我们使用了事件通知机制,避免在Room中直接调用RoomMgr的方法,即不能由下层调用上层的函数,规避多线程的死锁风险。
RoomMgr构造时便注册了事件监听,由OnDestroyRoomEvent函数,处理EVENT_TYPE_DESTORY_ROOM事件。事件id定义在ChatEventDef.h头文件中。
Copy RoomMgr::RoomMgr()
{
EventMgr::Instance()->RegisterEvent(EVENT_TYPE_DESTORY_ROOM, this, [&](CHYEvent* event){
OnDestroyRoomEvent(event);
});
}
void RoomMgr::OnDestroyRoomEvent(CHYEvent* pEvent)
{
DestroyRoomEvent* e = (DestroyRoomEvent*)pEvent;
int nRoomID = e->nRoomID;
AutoLock lock(m_lock);
if (m_mapRoom.count(nRoomID) == 0)
{
LogWarn("OnDestroyRoomEvent Failed! room %d is not exist!", nRoomID);
return;
}
LogInfo("OnDestroyRoomEvent room %d", nRoomID);
ChatRoom* pRoom = m_mapRoom[nRoomID];
pRoom->DestoryRoom();
m_mapRoom.erase(nRoomID);
m_roomIDMgr.Recycle(nRoomID);
delete pRoom;
}
这样,在CreateRoom里创建了房间,在OnDestroyRoomEvent中删除了房间,完成了房间管理的闭环。另外,最好在RoomMgr的析构函数中进行Room的清理,避免内存泄漏。本例中,RoomMgr对象是服务的成员变量,生命周期和整个服务相同,所以不在析构中处理也没有问题。
消息路由
需要路由给聊天室的消息有加入房间,离开房间,发送消息,实现如下
Copy int RoomMgr::JoinRoom(int64_t sesID, int& roomID, ClientUser* user)
{
AutoLock lock(m_lock);
// 检查是否已经在某个房间中,如果在某个房间,直接返回该房间号
for (auto itr : m_mapRoom)
{
if (itr.second->HasUser(sesID))
{
roomID = itr.second->m_id;
return JOIN_ROOM_AREADY_IN;
}
}
if (m_mapRoom.count(roomID) == 0)
{
return JOIN_ROOM_NOT_FIND;
}
return m_mapRoom[roomID]->JoinRoom(sesID,user);
}
用户只允许加入到一个房间,因此在加入前,需要先遍历聊天室,看它是否已经在其他聊天室了,如果在其他聊天室,直接返回所在聊天室的房间ID,不在聊天室内,可以进入下一步逻辑。
然后判断用户想要加入的聊天室是否存在,如果房间不存在,直接返回错误。
最后,根据房间号找到对应房间,调用房间的加入方法,由房间进一步判断是否能够加入。如果房间内判断已达人数上限,或者房间的状态不适合加入,则由房间返回对应的错误信息。
Copy int RoomMgr::OnClientBreak(int64_t sesID)
{
AutoLock lock(m_lock);
// 如果在某个房间,直接离开
for (auto itr : m_mapRoom)
{
itr.second->LeaveRoom(sesID);
}
return 0;
}
用户网络断开后,不能再接收聊天室消息,这种情况下,系统可以通知聊天室用户已经离开,聊天室内部清理这个用户的相关信息。
Copy int RoomMgr::OnSendRoomMsg(int64_t sesID, int nRoomID, std::string& msg)
{
LogInfo("OnSendRoomMsg %lld %d %s", sesID, nRoomID, msg.c_str());
AutoLock lock(m_lock);
if (m_mapRoom.count(nRoomID) == 0)
{
LogInfo("OnSendRoomMsg can not find room %lld %d %s", sesID, nRoomID, msg.c_str());
return SEND_MSG_ROOM_NOT_FIND;
}
return m_mapRoom[nRoomID]->SendRoomMsg(sesID, msg);
}
最后,最重要的发送聊天室消息的函数。先根据房间ID找到对应的房间,再调用房间的SendRoomMsg函数,由房间处理发送消息。
小结
本节我们主要介绍了房间管理类,它负责创建并管理房间,并将客户端消息路由给具体房间处理。
管理类定期遍历调用房间的检测函数,房间内部检测到需要删除,通过事件机制抛出给管理类执行删除逻辑。
下一节,我们看房间类的实现,如何处理客户端消息和抛出删除事件。