C++服务开发入门指南
  • 序言
  • 前言
  • 一、一个简单的服务
    • 1 什么是服务
    • 2 服务可以用来做什么
    • 3 简单服务框架
  • 二、网络通信服务框架
    • 1 网络服务的基本概念
    • 2 增加监听端口
    • 3 处理客户端会话
    • 小结
  • 三、添加基础模块
    • 1 日志模块
    • 2 定时器
    • 3 事件机制
    • 4 线程池
    • 5 线程安全
    • 小结
  • 四、一个聊天服务
    • 1 需求描述及分析
    • 2 概要设计
    • 3 创建服务项目
    • 4 ClientUser实现
    • 5 RoomMgr实现
    • 6 ChatRoom实现
    • 7 RoomIDMgr实现
    • 小结
  • 五、测试、迭代及重构
    • 1 测试
    • 2 迭代
    • 3 重构
    • 4 版本号
  • 六、架构设计
    • 1 单点服务
    • 2 分布式服务
  • 七、部署及发布
    • 1 部署环境
    • 2 编译环境
    • 3 部署服务
    • 4 发布服务
  • 八、线上问题处理
    • 1 线上问题
    • 2 问题处理
  • 九、程序员的职业规划
    • 职业规划
Powered by GitBook
On this page
  • 主要作用
  • 创建房间
  • 定期检查
  • 事件机制和删除房间
  • 消息路由
  • 小结
  1. 四、一个聊天服务

5 RoomMgr实现

Previous4 ClientUser实现Next6 ChatRoom实现

Last updated 2 years ago

ChatService类中,定义了一个成员变量,就是RoomMgr,用来管理服务端的聊天室。

主要作用

RoomMgr主要作用有两个:

1、管理聊天室:包括创建聊天室、删除聊天室、查询聊天室,定期检查各个聊天室是否过期。这些功能需要维护或遍历聊天室的列表。

2、聊天室消息路由。服务接收到聊天室内的消息,首先需要通过管理类查询到具体的聊天室,再将消息传递给指定的聊天室,具体的逻辑处理在聊天室内部执行。

管理类的头文件定义如下:

#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通知到具体房间进行操作。

创建房间

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中,文件中也定义了加入房间的一些错误情况

#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。

定期检查

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头文件中。

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对象是服务的成员变量,生命周期和整个服务相同,所以不在析构中处理也没有问题。

消息路由

需要路由给聊天室的消息有加入房间,离开房间,发送消息,实现如下

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,不在聊天室内,可以进入下一步逻辑。

然后判断用户想要加入的聊天室是否存在,如果房间不存在,直接返回错误。

最后,根据房间号找到对应房间,调用房间的加入方法,由房间进一步判断是否能够加入。如果房间内判断已达人数上限,或者房间的状态不适合加入,则由房间返回对应的错误信息。

int RoomMgr::OnClientBreak(int64_t sesID)
{
    AutoLock lock(m_lock);

    // 如果在某个房间,直接离开
    for (auto itr : m_mapRoom)
    {
        itr.second->LeaveRoom(sesID);
    }
    return 0;
}

用户网络断开后,不能再接收聊天室消息,这种情况下,系统可以通知聊天室用户已经离开,聊天室内部清理这个用户的相关信息。

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函数,由房间处理发送消息。

小结

本节我们主要介绍了房间管理类,它负责创建并管理房间,并将客户端消息路由给具体房间处理。

管理类定期遍历调用房间的检测函数,房间内部检测到需要删除,通过事件机制抛出给管理类执行删除逻辑。

下一节,我们看房间类的实现,如何处理客户端消息和抛出删除事件。