首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【在线五子棋对战】八、在线用户管理模块 && 房间管理模块

【在线五子棋对战】八、在线用户管理模块 && 房间管理模块

作者头像
利刃大大
发布2025-07-22 08:32:34
发布2025-07-22 08:32:34
9500
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

前言

​ 在线用户管理,是对于当前 游戏大厅游戏房间 中的用户进行管理,主要是建立起用户与 WebSocket 长连接的映射关系,这个模块具有两个功能:

  1. 能够让程序中根据用户信息,进而找到能够与用户客户端进行通信的 WebSocket 连接,进而实现与客户端的通信
  2. 判断⼀个用户是否在线,或者判断用户是否已经掉线

在线用户管理类的设计与实现

​ 首先因为要用到 websocketpp 库,所以先包一下头文件,顺序用 using 将定义改短一点,这些操作我们都把它 放在 util.hpp 头文件下,这样子方便后面其它文件也要用到 websocketpp 库的时候直接用,不用重复去包头文件!

代码语言:javascript
代码运行次数:0
运行
复制
// util.hpp头文件
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
using wsserver_t = websocketpp::server<websocketpp::config::asio>;

​ 接着就是成员变量和接口的设计了,我们将其 放到 online.hpp 头文件下,具体参考下面:

代码语言:javascript
代码运行次数:0
运行
复制
#ifndef __MY_ONLINE_H__
#define __MY_ONLINE_H__
#include "util.hpp"
#include <mutex>
#include <unordered_map>

class online_manager
{
private:
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user; // 用于建立游戏大厅用户的ID与websocket通信连接的关系
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user; // 用于建立游戏房间用户的ID与websocket通信连接的关系
    std::mutex _mtx; // 映射等操作需要加锁保护
public:
    // websocket连接建立的时候才会加入游戏大厅&游戏房间在线用户管理
    void enterHall(uint64_t uid, wsserver_t::connection_ptr& conn);
    void enterRoom(uint64_t uid, wsserver_t::connection_ptr& conn);

    // websocket连接断开的时候,才会移除游戏大厅&游戏房间在线用户管理
    void exitHall(uint64_t uid);
    void exitRoom(uint64_t uid);

    // 判断当前指定用户是否在游戏大厅/游戏房间
    bool isInHall(uint64_t uid);
    bool isInRoom(uint64_t uid);

    // 通过用户ID在游戏大厅/游戏房间用户管理中获取对应的websocket通信连接
    wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid);
    wsserver_t::connection_ptr get_conn_from_room(uint64_t uid);
};

#endif

​ 可以看出,与 websocket 建立长连接之后,我们是通过 wsserver_t::connection_ptr 来进行通信的,这个类的功能只是为我们后面通信操作建立起一个环境,下面我们分别来实现他们的函数体,其实非常简单,就是搭建起映射关系,就是哈希表的操作而已!

最重要就是每个操作都得加锁,如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
#ifndef __MY_ONLINE_H__
#define __MY_ONLINE_H__
#include "util.hpp"
#include <mutex>
#include <unordered_map>

class online_manager
{
private:
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user; // 用于建立游戏大厅用户的ID与websocket通信连接的关系
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user; // 用于建立游戏房间用户的ID与websocket通信连接的关系
    std::mutex _mtx; // 映射等操作需要加锁保护
public:
    // websocket连接建立的时候才会加入游戏大厅&游戏房间在线用户管理
    void enterHall(uint64_t uid, wsserver_t::connection_ptr& conn)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        _hall_user[uid] = conn;
    }
    void enterRoom(uint64_t uid, wsserver_t::connection_ptr& conn)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        _room_user[uid] = conn;
    }

    // websocket连接断开的时候,才会移除游戏大厅&游戏房间在线用户管理
    void exitHall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        _hall_user.erase(uid);
    }
    void exitRoom(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        _room_user.erase(uid);
    }

    // 判断当前指定用户是否在游戏大厅/游戏房间
    bool isInHall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        auto ret = _hall_user.find(uid);
        if(ret == _hall_user.end())
            return false;
        return true;
    }
    bool isInRoom(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        auto ret = _room_user.find(uid);
        if(ret == _room_user.end())
            return false;
        return true;
    }

    // 通过用户ID在游戏大厅/游戏房间用户管理中获取对应的websocket通信连接
    wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        auto ret = _hall_user.find(uid);
        if(ret == _hall_user.end())
            return wsserver_t::connection_ptr();
        return ret->second;
    }
    wsserver_t::connection_ptr get_conn_from_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        auto ret = _room_user.find(uid);
        if(ret == _room_user.end())
            return wsserver_t::connection_ptr();
        return ret->second;
    }
};

#endif

前言

​ 对于房间的管理模块,主要是分为两种:房间类、房间管理类。

​ 它们其实是有差别的,房间类的对象是一个房间内的所有动作和状态;而房间管理类的对象是所有房间之间的管理,它们是不同的!

​ 下面我们分为两部分来讲解它们!

Ⅰ. 房间类实现

​ 首先,需要设计一个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建立一个小范围的关联关系,一个房间中任意一个用户发生的任何动作,都会被 广播 给房间中的其他用户。

​ 我们只需要做好两点:管理房间中的数据处理房间中的动作

  1. 管理的数据
    • 房间id
    • 房间的状态:决定了一个玩家退出房时需要做的动作
    • 房间中玩家的数量:决定了房间什么时候被销毁
    • 白棋玩家id
    • 黑棋玩家id
    • 数据库用户信息表的句柄:当玩家胜利/失败的时候更新需要修改数据
    • 棋盘信息:就是维护一个二维数组
    • 在线用户的管理句柄:因为我们需要对所有在线用户进行广播,必须要有在线用户的管理句柄
  2. 房间中产生的动作
    • 下棋
    • 聊天

​ 对于房间中产生的动作,我们要知道的是不管哪个动作,只要是合理的,都要广播给房间里的其它用户。

​ 下面我们根据这些内容先设计一下接口和成员变量,把它们放在头文件 room.hpp 中:

代码语言:javascript
代码运行次数:0
运行
复制
#define BOARD_ROW 15
#define BOARD_COL 15
#define WHITE_CHESS 1 // 白棋颜色
#define BLACK_CHESS 2 // 黑棋颜色
typedef enum { GAME_START, GAME_OVER }room_status;

class room
{
private:
    uint64_t _room_id;                    // 房间id
    room_status _status;                  // 房间状态
    int _player_num;                      // 房间玩家个数
    uint64_t _white_id;                   // 白棋id
    uint64_t _black_id;                   // 黑棋id
    user_table* _table_user;              // 数据库用户信息管理句柄
    online_manager* _online_user;         // 在线用户管理句柄
    std::vector<std::vector<int>> _board; // 棋盘
public:
    room(uint64_t room_id, user_table* table_user, online_manager* online_user)
        : _room_id(room_id), _table_user(table_user), _online_user(online_user),
          _player_num(0), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
    {
        DLOG("%lu 房间创建成功!", _room_id);
    }
    ~room()
    {
        DLOG("%lu 房间销毁成功!", _room_id);
    }

    //  获取成员变量的函数
    uint64_t getRoomID() { return _room_id; }
    room_status getStatus() { return _status; }
    int getPlayerNum() { return _player_num; }
    uint64_t getWhiteID() { return _white_id; }
    uint64_t getBlackID() { return _black_id; }

    // 添加玩家接口
    void add_white(uint64_t uid) { _white_id = uid; _player_num++; }
    void add_black(uint64_t uid) { _black_id = uid; _player_num++; }

    // 总的请求处理函数,在函数内部区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播
    void request_handle(Json::Value& req);

    // 处理下棋动作,返回下棋响应
    Json::Value chess_handle(Json::Value& req);

    // 处理聊天动作,返回下棋响应
    Json::Value chat_handle(Json::Value& req);

    // 处理玩家退出房间的动作
    void exit_handle(uint64_t uid);

    // 将特定的信息广播给房间中所有玩家
    void broadcast(Json::Value& req);
    
private:
	// 检查是否下棋完会有玩家胜利
    // 返回值:0表示没有玩家胜利,1表示白棋胜利,2表示黑棋胜利
    uint64_t check_win(int row, int col, int color);
    
    // 这个函数是来判断是否五星连珠的
    bool five(int row, int col, int color, int row_offset, int col_offset);
};

一杯茶的时间,搞懂 RESTful API

​ 在实现剩下的几个接口之前,我们先要知道整个项目的通信接口的设计,是采用 RESTful 风格的,如下图所示:

在这里插入图片描述
在这里插入图片描述

​ 不同的请求方法就是有不同的目的,而正文部分则是通过 xml/json 格式进行数据的格式组织的。后面到了服务器的整体封装的时候会讲各部分的数据格式,这里先给出下棋和聊天的数据格式:

对于下棋动作

请求的 json 格式:

代码语言:javascript
代码运行次数:0
运行
复制
{
    "optype": "put_chess", // put_chess表示当前请求是下棋操作
    "room_id": 222,   	   // room_id表示当前动作属于哪个房间
    "uid": 1, 			   // 当前的下棋操作是哪个用户发起的
    "row": 3, 			   // 当前下棋位置的行号
    "col": 2 			   // 当前下棋位置的列号
}

响应的 json 格式:

代码语言:javascript
代码运行次数:0
运行
复制
{
    "optype": "put_chess",
    "result": false         // 下棋操作不合理
    "reason": "走棋失败具体原因...."
}
{
     "optype": "put_chess",
     "result": true,        // 下棋操作合理
     "reason": "对方掉线,不战而胜!" / "对方/己方五星连珠,战无敌/虽败犹荣!",
     "room_id": 222,
     "uid": 1,
     "row": 3,
     "col": 2,
     "winner": 0   // 0表示未分胜负,非0表示已分胜负 (uid是谁,谁就赢了)
}
对于聊天动作

请求的 json 格式:

代码语言:javascript
代码运行次数:0
运行
复制
{
    "optype": "chat",
    "room_id": 222,
    "uid": 1,
    "message": "赶紧点"
}

响应的 json 格式:

代码语言:javascript
代码运行次数:0
运行
复制
{
    "optype": "chat",
    "result": false
    "reason": "聊天失败具体原因....比如有敏感词..."
}
{
    "optype": "chat",
    "result": true,
    "room_id": 222,
    "uid": 1,
    "message": "赶紧点"
}

​ 了解这些格式之后,我们下面来实现上面的核心函数!

request_handle()请求处理的总函数

​ 这个函数的任务是在函数内部区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播,并且需要的话还要更新数据库的用户表信息、设置一下房间的状态等属性。

代码语言:javascript
代码运行次数:0
运行
复制
void request_handle(Json::Value& req)
{
    DLOG("总的请求处理函数开始");
    Json::Value response;

    // 1. 首先需要判断当前请求的房间号是否与当前房间的房间号匹配
    uint64_t room_id = req["room_id"].asUInt64();
    if(_room_id != room_id)
    {
        DLOG("房间号不匹配");
        response["optype"] = req["optype"].asString();
        response["result"] = false;
        response["reason"] = "游戏房间不匹配";
        broadcast(response); // 广播信息
        return;
    }

    // 2. 根据不同的请求调用不同的处理函数
    //	  是根据json数据字段中的"optype"来确定是何种请求的
    if(req["optype"].asString() == "put_chess")
    {
        DLOG("收到是下棋请求");
        response = chess_handle(req);
        // 判断如果不是平局,那么就得更新数据库,因为有人获胜了
        if(response["winner"].asUInt64() != 0)
        {
            DLOG("有人胜利");
            uint64_t winner_id = response["winner"].asUInt64();
            uint64_t loser_id = (winner_id == _white_id ? _black_id : _white_id);
            _table_user->win(winner_id);
            _table_user->lose(loser_id);

            // 记得还得修改房间状态
            _status = GAME_OVER;
        }
    }
    else if(req["optype"].asString() == "chat")
    {
        DLOG("收到是聊天请求");
        response = chat_handle(req);
    }
    else
    {
        DLOG("未知请求");
        response["optype"] = req["optype"].asString();
        response["result"] = false;
        response["reason"] = "未知请求类型";
    }

    // 3. 广播信息。广播之前先日志输出一下
    std::string body;
    json_util::serialize(response, body);
    DLOG("房间-广播动作: %s", body.c_str());
    broadcast(response);
}

chess_handle()下棋动作处理函数

​ 这个函数的任务是 处理下棋动作,并且返回下棋信息的响应

​ 期间需要先判断一些玩家是否都在线,因为不在线的话就认为是掉线等情况,则需要将胜利者的 id 写到响应中返回,因为如果 响应中的胜利者 id 为 0 的话,代表的是平局,这关系到在 request_handle() 函数中是否进行更新数据库和修改房间状态的决定,具体的可以 request_handle() 函数中调用该函数之后做了什么处理!

​ 然后下棋的时候需要判断其合理性,因为可能该位置已经有棋子了,就要响应错误信息。

​ 最后判断是否有玩家胜利,就是根据下棋的位置的附近是否有构成五星连珠的情况,这放在了两个子函数上去实现,这里只需要调用 check_win() 函数即可!

代码语言:javascript
代码运行次数:0
运行
复制
Json::Value chess_handle(Json::Value& req)
{
    Json::Value response = req;

    // 1. 判断房间中两个玩家是否都在线,任意一个不在线的话就是对方获胜
    uint64_t uid = req["uid"].asUInt64();
    int chess_row = req["row"].asInt();
    int chess_col = req["col"].asInt();
    if(_online_user->isInRoom(_white_id) == false)
    {
        DLOG("对方掉线");
        response["result"] = true;
        response["reason"] = "运气真好!对方掉线,不战而胜!";
        response["winner"] = (Json::UInt64)_black_id;
        return response;
    }
    if(_online_user->isInRoom(_black_id) == false)
    {
        DLOG("对方掉线");
        response["result"] = true;
        response["reason"] = "运气真好!对方掉线,不战而胜!";
        response["winner"] = (Json::UInt64)_white_id;
        return response;
    }

    // 2. 根据走棋位置,判断当前走棋是否合理(比如位置是否已经被占用了)
    if(_board[chess_row][chess_col] != 0)
    {
        DLOG("该位置已有棋子");
        response["result"] = false;
        response["reason"] = "当前位置已经有了其他棋子!";
        return response;
    }
    int chess_color = (uid == _white_id ? WHITE_CHESS : BLACK_CHESS);
    _board[chess_row][chess_col] = chess_color;

    // 3. 判断是否有玩家胜利(从当前走棋位置开始判断是否存在五星连珠)
    DLOG("判断是否有玩家胜利:开始");
    uint64_t winner_id = check_win(chess_row, chess_col, chess_color);
    if(winner_id != 0)
        response["reason"] = "五星连珠,战无敌!";
    response["result"] = true;
    response["winner"] = (Json::UInt64)winner_id;
    DLOG("下棋操作结束");
    return response;
}
check_win()检查是否胜利函数

​ 因为如果直接在该函数中对四个方向的珠子进行判断的话,其实重复的代码会很多,我们将其提炼出来作为另一个子函数 five() 函数!

​ 其中规定向上和向右为 1,向下和向左为 -1,不变为 0

​ 返回值:0 表示没有玩家胜利,1 表示白棋胜利,2 表示黑棋胜利。

代码语言:javascript
代码运行次数:0
运行
复制
// 检查是否下棋完会有玩家胜利
// 返回值:0表示没有玩家胜利,1表示白棋胜利,2表示黑棋胜利
uint64_t check_win(int row, int col, int color)
{   
    // 从当前行、列、左斜、右斜线上判断是否有连续五个相同颜色的棋子
    // 规定向上和右为1,向下和左为-1,不变为0
    if(five(row, col, color, 0, 1)   ||
       five(row, col, color, 1, 0)   ||
       five(row, col, color, -1, -1) ||
       five(row, col, color, -1, 1))
    {
        return (color == WHITE_CHESS ? _white_id : _black_id);
    }
    return 0;
}
five()判断五星连珠函数

​ 其实思想就是从下棋位置出发,分别向其偏移方向进行检查,如果发现是同色棋子的话直接 count++,如果不是的话则停下来去检查反方向的棋子,最后返回 count >= 5 的结果,也就是如果大于等于 5 颗同色棋子则为 true,小于 5 颗就是 false

代码语言:javascript
代码运行次数:0
运行
复制
bool five(int row, int col, int color, int row_offset, int col_offset)
{
    int count = 0;
    int tmprow = row, tmpcol = col;
    // 先正向检查
    while(tmprow >= 0 && tmprow < BOARD_ROW &&
            tmpcol >= 0 && tmpcol < BOARD_COL &&
            _board[tmprow][tmpcol] == color)
    {
        // 同色棋子数量++
        count++;
        // 检索位置继续向后偏移
        tmprow += row_offset;
        tmpcol += col_offset;
    }

    // 再反向检查
    tmprow = row - row_offset, tmpcol = col - col_offset;
    while(tmprow >= 0 && tmprow < BOARD_ROW &&
            tmpcol >= 0 && tmpcol < BOARD_COL &&
            _board[tmprow][tmpcol] == color)
    {
        // 同色棋子数量++
        count++;
        // 检索位置继续向后偏移
        tmprow -= row_offset;
        tmpcol -= col_offset;
    }

    return (count >= 5);
}

chat_handle()聊天动作处理函数

​ 这个函数的任务是 处理聊天动作,并且返回下棋响应

​ 期间还要判断一下发送的消息是否含有一些敏感词,这里就只是举个例子,实际上是可以用数组存起来这些敏感词进行处理的!

​ 而广播操作是在 request_handle() 函数中处理的,这里不需要管!

代码语言:javascript
代码运行次数:0
运行
复制
Json::Value chat_handle(Json::Value& req)
{
    Json::Value response = req;

    // 1. 检测消息中是否包括敏感词
    std::string msg = req["message"].asString();
    size_t pos = msg.find("垃圾");
    if(pos != std::string::npos)
    {
        response["result"] = false;
        response["reason"] = "消息中包含敏感词,不能发送!";
        return response;
    }

    // 2. 没有异常问题的话,将"result"字段设为true然后返回即可
    response["result"] = true;
    return response;
}

exit_handle()玩家退出房间的处理函数

​ 这个函数其实 不属于 request_handle() 来管的,因为这个函数并不算是一种请求函数,而是一种玩家退出房间之后自然而然会触发的函数,所以在这个函数里面我们需要有独立的功能,比如涉及到当前玩家非正常退出的话,那么对手肯定是属于胜利了,那么就得 更新数据库的用户表信息、并且 设置房间状态广播信息给房间内所有玩家 等。

​ 所以这个函数 最重要的功能就是判断在退出房间的时候,房间是正在游戏还是游戏已经结束了,对于游戏已经结束来说,不需要做什么处理,等着房间被销毁即可;对于正在游戏来说,那么就会涉及到上面说的那些功能。

​ 并且 退出房间之后我们需要对房间人数进行一一操作

代码语言:javascript
代码运行次数:0
运行
复制
void exit_handle(uint64_t uid)
{
    Json::Value response;

    // 1. 如果是在下棋时候退出,则对方获胜;如果下棋结束了退出,则是正常退出,不用额外处理
    if(_status == GAME_START)
    {
        response["optype"] = "put_chess";
        response["result"] = true;
        response["reason"] = "对方掉线,不战而胜!";
        response["room_id"] = (Json::UInt64)_room_id;
        response["uid"] = (Json::UInt64)uid;
        response["row"] = -1;
        response["col"] = -1;

        uint64_t winner_id = (Json::UInt64)(uid == _white_id ? _black_id : _white_id);
        response["winner"] = (Json::UInt64)winner_id;

        // 更新数据库
        uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
        _table_user->win(winner_id);
        _table_user->lose(loser_id);

        // 设置房间状态
        _status = GAME_OVER;

        // 广播信息给房间内玩家
        broadcast(response);
    }

    // 2. 只要退出了,都得对玩家人数进行减少
    _player_num--;
}

broadcast()广播函数

主要分为两大步骤:

  1. 对要响应的信息进行序列化,将 Json::Value 中的数据序列化成为 json 格式字符串
  2. 获取房间中的用户的通信连接,并且响应信息
    • 我们写过获取房间中的用户的通信连接这个接口,就是在线用户管理模块中的 get_conn_from_room() 函数,并且我们有其成员变量,直接使用即可!
    • 而发送信息就是 connection 类中的 send() 函数
代码语言:javascript
代码运行次数:0
运行
复制
void broadcast(Json::Value& req)
{
    // 1. 对要响应的信息进行序列化,将Json::Value中的数据序列化成为json格式字符串
    std::string body;
    json_util::serialize(req, body);

    // 2. 获取房间中的用户的通信连接,并且响应信息
    wsserver_t::connection_ptr white_conn = _online_user->get_conn_from_room(_white_id);
    if(white_conn.get() != nullptr)
        white_conn->send(body);
    else
        DLOG("房间-白棋玩家连接获取失败");

    wsserver_t::connection_ptr black_conn = _online_user->get_conn_from_room(_black_id);
    if(black_conn.get() != nullptr)
        black_conn->send(body);
    else
        DLOG("房间-黑棋玩家连接获取失败");
}

Ⅱ. 房间管理类实现

​ 房间管理类的任务就是来管理我们上面的房间类,而管理房间的 接口 无非就是增删查嘛,不过查询和删除的时候,既可以通过用户 ID 来查询/删除,也可以通过房间 ID 来查询/删除。除此之外就是 成员变量 的设计,大概如下所示:

  • 数据库用户信息表管理句柄:这个就是我们封装的 user_table 类的指针
  • 在线用户管理句柄:这个就是我们封装的 online_manager 类的指针
  • 房间 ID 分配计数器:其实就是一个 uint64_t 类型的变量,用来累加,并且分配给房间,作为房间 ID
  • 互斥锁:因为接口操作涉及到增删,那么肯定需要互斥锁来保护,防止数据不安全
  • 房间id与房间信息的管理哈希表:说白了就是 房间ID房间信息 的映射,存放在哈希表中
  • 用户id与房间id的管理哈希表:说白了就是 用户ID房间ID 的映射,存放在哈希表中

​ 下面给出房间管理类的大体框架,和房间类一样,是放在头文件 room.hpp 中的:

代码语言:javascript
代码运行次数:0
运行
复制
using room_ptr = std::shared_ptr<room>; // 声明一个房间类的智能指针类型

class room_manager
{
private:
    user_table* _user_tb;         // 数据库用户信息表管理句柄                          
    online_manager* _online_user; // 在线用户管理句柄
    uint64_t count;               // 房间 ID 分配计数器
    std::mutex _mtx;              // 互斥锁
    std::unordered_map<uint64_t, room_ptr> rid_rinfo_hash; // 房间id与房间信息的管理哈希表
    std::unordered_map<uint64_t, uint64_t> uid_rid_hash;   // 用户id与房间id的管理哈希表
public:
    // 构造函数和析构函数
    room_manager(user_table* user_tb, online_manager* online_user)
        : _user_tb(user_tb), _online_user(online_user)
    { DLOG("房间管理模块初始化完毕!"); }
    ~room_manager() { DLOG("房间管理模块即将销毁!"); }

    // 增加房间函数,并返回该房间的智能指针管理对象
    room_ptr addRoom(uint64_t uid1, uint64_t uid2);

    // 通过房间ID获取房间信息
    room_ptr getRoom_ByRoomID(uint64_t roomID);

    // 通过用户ID获取房间信息
    room_ptr getRoom_ByUserID(uint64_t userID);

    // 通过房间ID销毁房间
    void removeRoom(uint64_t roomID);

    // 删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
    void removeUser(uint64_t userID);
};

​ 下面我们来实现一下这些函数的函数体!

addRoom()添加房间函数

​ 这个函数的任务就是要创建一个房间,将两个在线用户放到房间中去,所以得 先判断两个用户是否是在线状态。

​ 并且 添加用户到房间、管理房间信息等操作,都是需要加锁保护的!其中管理房间信息就是将用户和房间、房间和房间之间的关系映射起来,就是对哈希表的操作!最后返回该房间的智能指针管理对象即可。

​ 注意最后 别忘了给计数器加一,不然会导致多个房间的 ID 重复了!

代码语言:javascript
代码运行次数:0
运行
复制
// 增加房间函数,并返回该房间的智能指针管理对象
room_ptr addRoom(uint64_t uid1, uint64_t uid2)
{
    // 背景:两个用户在游戏大厅中进行对战匹配,匹配成功后创建房间
    // 1. 校验两个用户是否都还在游戏大厅中,只有都在才需要创建房间
    if(_online_user->isInHall(uid1) == false || _online_user->isInHall(uid2) == false)
    {
        DLOG("有用户不在大厅中,创建房间失败!");
        return room_ptr();
    }

    // 2. 创建房间,将用户信息添加到房间中
    std::unique_lock<std::mutex> lock(_mtx); // 从这里开始的操作都要加锁保护
    room_ptr rp(new room(count, _user_tb, _online_user));
    rp->add_white(uid1);
    rp->add_black(uid2);

    // 3. 将房间信息管理起来,记得最后要对计数器++
    uid_rid_hash[uid1] = count;
    uid_rid_hash[uid2] = count;
    rid_rinfo_hash[count] = rp;
    count++; // 这步别忘了

    // 4. 返回房间信息
    return rp;
}

获取房间信息函数

​ 两个函数的操作,都需要进行加锁保护,防止线程问题!其它问题并不大,就是哈希表的一个操作。

​ 最重要的是在 getRoom_ByUserID() 中获取 房间ID 之后,我们不能直接调用 getRoom_ByRoomID() 去获取房间信息,因为两个函数都有加锁,重复加锁的话会导致死锁,所以只能再写一遍!

代码语言:javascript
代码运行次数:0
运行
复制
// 通过房间ID获取房间信息
room_ptr getRoom_ByRoomID(uint64_t roomID)
{
    std::unique_lock<std::mutex> lock(_mtx); // 需要加锁保护
    auto ret = rid_rinfo_hash.find(roomID);
    if(ret == rid_rinfo_hash.end())
    {
        return room_ptr();
    }
    return ret->second;
}

// 通过用户ID获取房间信息
room_ptr getRoom_ByUserID(uint64_t userID)
{
    std::unique_lock<std::mutex> lock(_mtx); // 需要加锁保护

    // 1. 先通过用户ID查找房间ID
    auto ret = uid_rid_hash.find(userID);
    if(ret == uid_rid_hash.end())
    {
        return room_ptr();
    }

    // 2. 再通过房间ID获取房间信息
    // 注意:不能直接调用getRoom_ByRoomID来获取房间信息,因为会重复加锁导致死锁
    auto it = rid_rinfo_hash.find(ret->second);
    if(it == rid_rinfo_hash.end())
    {
        return room_ptr();
    }
    return it->second;
}

removeRoom()销毁房间函数

代码语言:javascript
代码运行次数:0
运行
复制
// 通过房间ID销毁房间
void removeRoom(uint64_t roomID)
{
    // 因为房间信息是通过shared_ptr在哈希表中进行管理,因此只要将shared_ptr从哈希表中移除
    // 则当shared_ptr计数器==0,外界没有对房间信息进行操作保存的情况下就会释放
    // 但是因为房间中的用户信息等也要移除,不然会造成内存泄漏问题
    // 所以我们要先移除必须的用户信息再移除房间管理信息

    // 1. 通过房间ID,获取房间信息
    room_ptr rp = getRoom_ByRoomID(roomID);
    if(rp.get() == nullptr)
        return;

    // 2. 通过房间信息,获取房间中所有用户的ID
    uint64_t uid1 = rp->getBlackID();
    uint64_t uid2 = rp->getWhiteID();

    // 3. 移除房间管理中的用户信息
    std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
    uid_rid_hash.erase(uid1);
    uid_rid_hash.erase(uid2);

    // 4. 移除房间管理信息
    rid_rinfo_hash.erase(roomID);
}

removeUser()移除房间内用户函数

代码语言:javascript
代码运行次数:0
运行
复制
// 删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
void removeUser(uint64_t userID)
{
    // 1. 首先获取房间信息
    room_ptr rp = getRoom_ByUserID(userID);
    if(rp.get() == nullptr) 
        return;

    // 2. 处理玩家退出动作
    rp->exit_handle(userID);

    // 3. 判断一下房间是否没有用户了,没有的话销毁房间
    if(rp->getPlayerNum() == 0)
        removeRoom(rp->getRoomID());
}

完整代码

代码语言:javascript
代码运行次数:0
运行
复制
#ifndef __MY_ROOM_H__
#define __MY_ROOM_H__
#include "util.hpp"
#include "online.hpp"
#include "logger.hpp"
#include "db.hpp"
#include <vector>

#define BOARD_ROW 15
#define BOARD_COL 15
#define WHITE_CHESS 1
#define BLACK_CHESS 2
typedef enum { GAME_START, GAME_OVER }room_status;

class room
{
private:
    uint64_t _room_id;                   // 房间id
    room_status _status;                 // 房间状态
    int _player_num;                     // 房间玩家个数
    uint64_t _white_id;                  // 白棋id
    uint64_t _black_id;                  // 黑棋id
    user_table* _table_user;             // 数据库用户信息管理句柄
    online_manager* _online_user;        // 在线用户管理句柄
    std::vector<std::vector<int>> _board; // 棋盘
public:
    room(uint64_t room_id, user_table* table_user, online_manager* online_user)
        : _room_id(room_id), _table_user(table_user), _online_user(online_user),
          _player_num(0), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
    {
        DLOG("%lu 房间创建成功!", _room_id);
    }
    ~room()
    {
        DLOG("%lu 房间销毁成功!", _room_id);
    }

    //  获取成员变量的函数
    uint64_t getRoomID() { return _room_id; }
    room_status getStatus() { return _status; }
    int getPlayerNum() { return _player_num; }
    uint64_t getWhiteID() { return _white_id; }
    uint64_t getBlackID() { return _black_id; }

    // 添加玩家接口
    void add_white(uint64_t uid) { _white_id = uid; _player_num++; }
    void add_black(uint64_t uid) { _black_id = uid; _player_num++; }

    // 总的请求处理函数,在函数内部区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播
    void request_handle(Json::Value& req)
    {
        DLOG("总的请求处理函数开始");
        Json::Value response;

        // 1. 首先需要判断当前请求的房间号是否与当前房间的房间号匹配
        uint64_t room_id = req["room_id"].asUInt64();
        if(_room_id != room_id)
        {
            DLOG("房间号不匹配");
            response["optype"] = req["optype"].asString();
            response["result"] = false;
            response["reason"] = "游戏房间不匹配";
            broadcast(response); // 广播信息
            return;
        }

        // 2. 根据不同的请求调用不同的处理函数
        //	  是根据json数据字段中的"optype"来确定是何种请求的
        if(req["optype"].asString() == "put_chess")
        {
            DLOG("收到是下棋请求");
            response = chess_handle(req);
            // 判断如果不是平局,那么就得更新数据库,因为有人获胜了
            if(response["winner"].asUInt64() != 0)
            {
                DLOG("有人胜利");
                uint64_t winner_id = response["winner"].asUInt64();
                uint64_t loser_id = (winner_id == _white_id ? _black_id : _white_id);
                _table_user->win(winner_id);
                _table_user->lose(loser_id);

                // 记得还得修改房间状态
                _status = GAME_OVER;
            }
        }
        else if(req["optype"].asString() == "chat")
        {
            DLOG("收到是聊天请求");
            response = chat_handle(req);
        }
        else
        {
            DLOG("未知请求");
            response["optype"] = req["optype"].asString();
            response["result"] = false;
            response["reason"] = "未知请求类型";
        }

        // 3. 广播信息。广播之前先日志输出一下
        std::string body;
        json_util::serialize(response, body);
        DLOG("房间-广播动作: %s", body.c_str());
        broadcast(response);
    }

    // 处理下棋动作,返回下棋响应
    Json::Value chess_handle(Json::Value& req)
    {
        Json::Value response = req;

        // 1. 判断房间中两个玩家是否都在线,任意一个不在线的话就是对方获胜
        uint64_t uid = req["uid"].asUInt64();
        int chess_row = req["row"].asInt();
        int chess_col = req["col"].asInt();
        if(_online_user->isInRoom(_white_id) == false)
        {
            DLOG("对方掉线");
            response["result"] = true;
            response["reason"] = "运气真好!对方掉线,不战而胜!";
            response["winner"] = (Json::UInt64)_black_id;
            return response;
        }
        if(_online_user->isInRoom(_black_id) == false)
        {
            DLOG("对方掉线");
            response["result"] = true;
            response["reason"] = "运气真好!对方掉线,不战而胜!";
            response["winner"] = (Json::UInt64)_white_id;
            return response;
        }

        // 2. 根据走棋位置,判断当前走棋是否合理(比如位置是否已经被占用了)
        if(_board[chess_row][chess_col] != 0)
        {
            DLOG("该位置已有棋子");
            response["result"] = false;
            response["reason"] = "当前位置已经有了其他棋子!";
            return response;
        }
        int chess_color = (uid == _white_id ? WHITE_CHESS : BLACK_CHESS);
        _board[chess_row][chess_col] = chess_color;

        // 3. 判断是否有玩家胜利(从当前走棋位置开始判断是否存在五星连珠)
        DLOG("判断是否有玩家胜利:开始");
        uint64_t winner_id = check_win(chess_row, chess_col, chess_color);
        if(winner_id != 0)
            response["reason"] = "五星连珠,战无敌!";
        response["result"] = true;
        response["winner"] = (Json::UInt64)winner_id;
        DLOG("下棋操作结束");
        return response;
    }

    // 处理聊天动作,返回下棋响应
    Json::Value chat_handle(Json::Value& req)
    {
        Json::Value response = req;

        // 1. 检测消息中是否包括敏感词
        std::string msg = req["message"].asString();
        size_t pos = msg.find("垃圾");
        if(pos != std::string::npos)
        {
            response["result"] = false;
            response["reason"] = "消息中包含敏感词,不能发送!";
            return response;
        }

        // 2. 没有异常问题的话,将"result"字段设为true然后返回即可
        response["result"] = true;
        return response;
    }

    // 处理玩家退出房间的动作 -- 不属于request_handle来管的,因为退出动作在这里并不属于请求类型
    void exit_handle(uint64_t uid)
    {
        Json::Value response;

        // 1. 如果是在下棋时候退出,则对方获胜;如果下棋结束了退出,则是正常退出,不用额外处理
        if(_status == GAME_START)
        {
            response["optype"] = "put_chess";
            response["result"] = true;
            response["reason"] = "对方掉线,不战而胜!";
            response["room_id"] = (Json::UInt64)_room_id;
            response["uid"] = (Json::UInt64)uid;
            response["row"] = -1;
            response["col"] = -1;

            uint64_t winner_id = (Json::UInt64)(uid == _white_id ? _black_id : _white_id);
            response["winner"] = (Json::UInt64)winner_id;

            // 更新数据库
            uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
            _table_user->win(winner_id);
            _table_user->lose(loser_id);

            // 设置房间状态
            _status = GAME_OVER;

            // 广播信息给房间内玩家
            broadcast(response);
        }

        // 2. 只要退出了,都得对玩家人数进行减少
        _player_num--;
    }

    // 将特定的信息广播给房间中所有玩家
    void broadcast(Json::Value& req)
    {
        // 1. 对要响应的信息进行序列化,将Json::Value中的数据序列化成为json格式字符串
        std::string body;
        json_util::serialize(req, body);

        // 2. 获取房间中的用户的通信连接,并且响应信息
        wsserver_t::connection_ptr white_conn = _online_user->get_conn_from_room(_white_id);
        if(white_conn.get() != nullptr)
            white_conn->send(body);
        else
            DLOG("房间-白棋玩家连接获取失败");
        
        wsserver_t::connection_ptr black_conn = _online_user->get_conn_from_room(_black_id);
        if(black_conn.get() != nullptr)
            black_conn->send(body);
        else
            DLOG("房间-黑棋玩家连接获取失败");
    }

private:
    // 检查是否下棋完会有玩家胜利
    // 返回值:0表示没有玩家胜利,1表示白棋胜利,2表示黑棋胜利
    uint64_t check_win(int row, int col, int color) 
    {
        // 从下棋位置的四个不同方向上检测是否出现了5个及以上相同颜色的棋子(横行,纵列,正斜,反斜)
        if (five(row, col, 0, 1, color) || 
            five(row, col, 1, 0, color) ||
            five(row, col, -1, 1, color)||
            five(row, col, -1, -1, color)) {
            //任意一个方向上出现了true也就是五星连珠,则设置返回值
            return color == WHITE_CHESS ? _white_id : _black_id;
        }
        return 0;
    }

    bool five(int row, int col, int row_offset, int col_offset, int color)
    {
        int count = 1;
        int tmprow = row + row_offset;
        int tmpcol = col + col_offset;
        // 先正向检查
        while(tmprow >= 0 && tmprow < BOARD_ROW &&
                tmpcol >= 0 && tmpcol < BOARD_COL &&
                _board[tmprow][tmpcol] == color)
        {
            // 同色棋子数量++
            count++;
            // 检索位置继续向后偏移
            tmprow += row_offset;
            tmpcol += col_offset;
        }

        // 再反向检查
        tmprow = row - row_offset;
        tmpcol = col - col_offset;
        while(tmprow >= 0 && tmprow < BOARD_ROW &&
                tmpcol >= 0 && tmpcol < BOARD_COL &&
                _board[tmprow][tmpcol] == color)
        {
            // 同色棋子数量++
            count++;
            // 检索位置继续向后偏移
            tmprow -= row_offset;
            tmpcol -= col_offset;
        }

        return (count >= 5);
    }
};


using room_ptr = std::shared_ptr<room>; // 声明一个房间类的智能指针类型

class room_manager
{
private:
    user_table* _user_tb;         // 数据库用户信息表管理句柄                          
    online_manager* _online_user; // 在线用户管理句柄
    uint64_t count;               // 房间 ID 分配计数器
    std::mutex _mtx;              // 互斥锁
    std::unordered_map<uint64_t, room_ptr> rid_rinfo_hash; // 房间id与房间信息的管理哈希表
    std::unordered_map<uint64_t, uint64_t> uid_rid_hash;   // 用户id与房间id的管理哈希表
public:
    // 构造函数和析构函数
    room_manager(user_table* user_tb, online_manager* online_user)
        : _user_tb(user_tb), _online_user(online_user)
    { DLOG("房间管理模块初始化完毕!"); }
    ~room_manager() { DLOG("房间管理模块即将销毁!"); }

    // 增加房间函数,并返回该房间的智能指针管理对象
    room_ptr addRoom(uint64_t uid1, uint64_t uid2)
    {
        // 背景:两个用户在游戏大厅中进行对战匹配,匹配成功后创建房间
        // 1. 校验两个用户是否都还在游戏大厅中,只有都在才需要创建房间
        if(_online_user->isInHall(uid1) == false || _online_user->isInHall(uid2) == false)
        {
            DLOG("有用户不在大厅中,创建房间失败!");
            return room_ptr();
        }

        // 2. 创建房间,将用户信息添加到房间中
        std::unique_lock<std::mutex> lock(_mtx); // 从这里开始的操作都要加锁保护
        room_ptr rp(new room(count, _user_tb, _online_user));
        rp->add_white(uid1);
        rp->add_black(uid2);

        // 3. 将房间信息管理起来,记得最后要对计数器++
        uid_rid_hash[uid1] = count;
        uid_rid_hash[uid2] = count;
        rid_rinfo_hash[count] = rp;
        count++; // 这步别忘了

        // 4. 返回房间信息
        return rp;
    }

    // 通过房间ID获取房间信息
    room_ptr getRoom_ByRoomID(uint64_t roomID)
    {
        std::unique_lock<std::mutex> lock(_mtx); // 需要加锁保护
        auto ret = rid_rinfo_hash.find(roomID);
        if(ret == rid_rinfo_hash.end())
        {
            return room_ptr();
        }
        return ret->second;
    }

    // 通过用户ID获取房间信息
    room_ptr getRoom_ByUserID(uint64_t userID)
    {
        std::unique_lock<std::mutex> lock(_mtx); // 需要加锁保护

        // 1. 先通过用户ID查找房间ID
        auto ret = uid_rid_hash.find(userID);
        if(ret == uid_rid_hash.end())
        {
            return room_ptr();
        }

        // 2. 再通过房间ID获取房间信息
        // 注意:不能直接调用getRoom_ByRoomID来获取房间信息,因为会重复加锁导致死锁
        auto it = rid_rinfo_hash.find(ret->second);
        if(it == rid_rinfo_hash.end())
        {
            return room_ptr();
        }
        return it->second;
    }

    // 通过房间ID销毁房间
    void removeRoom(uint64_t roomID)
    {
        // 因为房间信息是通过shared_ptr在哈希表中进行管理,因此只要将shared_ptr从哈希表中移除
        // 则当shared_ptr计数器==0,外界没有对房间信息进行操作保存的情况下就会释放
        // 但是因为房间中的用户信息等也要移除,不然会造成内存泄漏问题
        // 所以我们要先移除必须的用户信息再移除房间管理信息

        // 1. 通过房间ID,获取房间信息
        room_ptr rp = getRoom_ByRoomID(roomID);
        if(rp.get() == nullptr)
            return;

        // 2. 通过房间信息,获取房间中所有用户的ID
        uint64_t uid1 = rp->getBlackID();
        uint64_t uid2 = rp->getWhiteID();

        // 3. 移除房间管理中的用户信息
        std::unique_lock<std::mutex> lock(_mtx); // 加锁保护
        uid_rid_hash.erase(uid1);
        uid_rid_hash.erase(uid2);

        // 4. 移除房间管理信息
        rid_rinfo_hash.erase(roomID);
    }

    // 删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用
    void removeUser(uint64_t userID)
    {
        // 1. 首先获取房间信息
        room_ptr rp = getRoom_ByUserID(userID);
        if(rp.get() == nullptr) 
            return;

        // 2. 处理玩家退出动作
        rp->exit_handle(userID);

        // 3. 判断一下房间是否没有用户了,没有的话销毁房间
        if(rp->getPlayerNum() == 0)
            removeRoom(rp->getRoomID());
    }
};

#endif
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-07-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 在线用户管理类的设计与实现
  • 前言
  • Ⅰ. 房间类实现
    • 对于下棋动作
    • 对于聊天动作
    • request_handle()请求处理的总函数
    • chess_handle()下棋动作处理函数
      • check_win()检查是否胜利函数
      • five()判断五星连珠函数
    • chat_handle()聊天动作处理函数
    • exit_handle()玩家退出房间的处理函数
    • broadcast()广播函数
  • Ⅱ. 房间管理类实现
    • addRoom()添加房间函数
    • 获取房间信息函数
    • removeRoom()销毁房间函数
    • removeUser()移除房间内用户函数
  • 完整代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档