在线用户管理,是对于当前 游戏大厅 和 游戏房间 中的用户进行管理,主要是建立起用户与 WebSocket
长连接的映射关系,这个模块具有两个功能:
WebSocket
连接,进而实现与客户端的通信 首先因为要用到 websocketpp
库,所以先包一下头文件,顺序用 using
将定义改短一点,这些操作我们都把它 放在 util.hpp
头文件下,这样子方便后面其它文件也要用到 websocketpp
库的时候直接用,不用重复去包头文件!
// util.hpp头文件
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
using wsserver_t = websocketpp::server<websocketpp::config::asio>;
接着就是成员变量和接口的设计了,我们将其 放到 online.hpp
头文件下,具体参考下面:
#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
来进行通信的,这个类的功能只是为我们后面通信操作建立起一个环境,下面我们分别来实现他们的函数体,其实非常简单,就是搭建起映射关系,就是哈希表的操作而已!
最重要就是每个操作都得加锁,如下所示:
#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
对于房间的管理模块,主要是分为两种:房间类、房间管理类。
它们其实是有差别的,房间类的对象是一个房间内的所有动作和状态;而房间管理类的对象是所有房间之间的管理,它们是不同的!
下面我们分为两部分来讲解它们!
首先,需要设计一个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建立一个小范围的关联关系,一个房间中任意一个用户发生的任何动作,都会被 广播 给房间中的其他用户。
我们只需要做好两点:管理房间中的数据、处理房间中的动作。
对于房间中产生的动作,我们要知道的是不管哪个动作,只要是合理的,都要广播给房间里的其它用户。
下面我们根据这些内容先设计一下接口和成员变量,把它们放在头文件 room.hpp
中:
#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
风格的,如下图所示:
不同的请求方法就是有不同的目的,而正文部分则是通过 xml
/json
格式进行数据的格式组织的。后面到了服务器的整体封装的时候会讲各部分的数据格式,这里先给出下棋和聊天的数据格式:
请求的 json 格式:
{
"optype": "put_chess", // put_chess表示当前请求是下棋操作
"room_id": 222, // room_id表示当前动作属于哪个房间
"uid": 1, // 当前的下棋操作是哪个用户发起的
"row": 3, // 当前下棋位置的行号
"col": 2 // 当前下棋位置的列号
}
响应的 json 格式:
{
"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 格式:
{
"optype": "chat",
"room_id": 222,
"uid": 1,
"message": "赶紧点"
}
响应的 json 格式:
{
"optype": "chat",
"result": false
"reason": "聊天失败具体原因....比如有敏感词..."
}
{
"optype": "chat",
"result": true,
"room_id": 222,
"uid": 1,
"message": "赶紧点"
}
了解这些格式之后,我们下面来实现上面的核心函数!
这个函数的任务是在函数内部区分请求类型,根据不同的请求调用不同的处理函数,得到响应进行广播,并且需要的话还要更新数据库的用户表信息、设置一下房间的状态等属性。
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);
}
这个函数的任务是 处理下棋动作,并且返回下棋信息的响应。
期间需要先判断一些玩家是否都在线,因为不在线的话就认为是掉线等情况,则需要将胜利者的 id 写到响应中返回,因为如果 响应中的胜利者 id 为 0 的话,代表的是平局,这关系到在 request_handle()
函数中是否进行更新数据库和修改房间状态的决定,具体的可以 request_handle()
函数中调用该函数之后做了什么处理!
然后下棋的时候需要判断其合理性,因为可能该位置已经有棋子了,就要响应错误信息。
最后判断是否有玩家胜利,就是根据下棋的位置的附近是否有构成五星连珠的情况,这放在了两个子函数上去实现,这里只需要调用 check_win()
函数即可!
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;
}
因为如果直接在该函数中对四个方向的珠子进行判断的话,其实重复的代码会很多,我们将其提炼出来作为另一个子函数 five()
函数!
其中规定向上和向右为 1,向下和向左为 -1,不变为 0
返回值:0 表示没有玩家胜利,1 表示白棋胜利,2 表示黑棋胜利。
// 检查是否下棋完会有玩家胜利
// 返回值: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;
}
其实思想就是从下棋位置出发,分别向其偏移方向进行检查,如果发现是同色棋子的话直接 count++
,如果不是的话则停下来去检查反方向的棋子,最后返回 count >= 5
的结果,也就是如果大于等于 5 颗同色棋子则为 true
,小于 5 颗就是 false
。
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);
}
这个函数的任务是 处理聊天动作,并且返回下棋响应。
期间还要判断一下发送的消息是否含有一些敏感词,这里就只是举个例子,实际上是可以用数组存起来这些敏感词进行处理的!
而广播操作是在 request_handle()
函数中处理的,这里不需要管!
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--;
}
主要分为两大步骤:
Json::Value
中的数据序列化成为 json 格式字符串get_conn_from_room()
函数,并且我们有其成员变量,直接使用即可!connection
类中的 send()
函数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 来查询/删除。除此之外就是 成员变量 的设计,大概如下所示:
下面给出房间管理类的大体框架,和房间类一样,是放在头文件 room.hpp
中的:
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);
};
下面我们来实现一下这些函数的函数体!
这个函数的任务就是要创建一个房间,将两个在线用户放到房间中去,所以得 先判断两个用户是否是在线状态。
并且 添加用户到房间、管理房间信息等操作,都是需要加锁保护的!其中管理房间信息就是将用户和房间、房间和房间之间的关系映射起来,就是对哈希表的操作!最后返回该房间的智能指针管理对象即可。
注意最后 别忘了给计数器加一,不然会导致多个房间的 ID 重复了!
// 增加房间函数,并返回该房间的智能指针管理对象
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()
去获取房间信息,因为两个函数都有加锁,重复加锁的话会导致死锁,所以只能再写一遍!
// 通过房间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());
}
#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