五子棋对战的玩家匹配是根据自己的天梯分数进行匹配的,而服务器中将玩家天梯分数分为三个档次:
而实现玩家匹配的思想非常简单,为不同的档次设计各自的匹配队列,当一个队列中的玩家数量大于等于 2 的时候,则意味着同一档次中,有两个及以上的人要进行实战匹配,则出队队列中的前两个用户,相当于队首两个个玩家匹配成功,这时候为其创建房间,并将两个用户信息加入房间中。
和之前几个模块设计理念一样,我们要有一个局部模块和全局模块,对于对战玩家匹配模块来说,其实就是将用户放到匹配队列中,等匹配到足够人数的时候,就将匹配人数放到游戏房间里面,所以这里分为两个类:
匹配队列虽然看起来用队列来实现挺不错的,有先进先出的思想,但是有一个问题,就是玩家可能在匹配的时候,有想取消匹配的操作,那么我们就得提供退出匹配的接口,也就是将用户从匹配队列中删除,但是如果用队列来实现的话,并不是很好办,所以我们采用 双向链表来实现匹配队列!
除此之外,因为当队列没有两名成员的时候,是不能进行加入房间操作的,所以我们用 条件变量 + 互斥锁 来实现阻塞队列的功能!所以我们大概要实现的接口如下所示:
数据出队表示要进入游戏房间了
,而 移除指定数据表示取消匹配
! 因为这些接口实现比较简单,这里直接给出实现,将它们放到 头文件 matcher.hpp
中:
template <class T>
class match_queue
{
private:
std::list<T> _block_queue; // 阻塞队列 -- 用双向链表实现
std::mutex _mtx; // 互斥锁 -- 实现线程安全
std::condition_variable _cond; // 条件变量 -- 主要用于阻塞消费者,当队列元素个数小于2的时候阻塞
public:
// 获取队列元素个数
int size()
{
std::unique_lock<std::mutex> lock(_mtx);
return _block_queue.size();
}
// 判断队列是否为空
bool isEmpty()
{
std::unique_lock<std::mutex> lock(_mtx);
return _block_queue.empty();
}
// 阻塞线程
void wait()
{
std::unique_lock<std::mutex> lock(_mtx);
_cond.wait(lock);
}
// 数据入队,并唤醒线程
void push(T& data)
{
std::unique_lock<std::mutex> lock(_mtx);
_block_queue.push_back(data);
_cond.notify_all();
}
// 数据出队 -- 相当于匹配成功要进入房间,data是输出型参数
bool pop(T& data)
{
std::unique_lock<std::mutex> lock(_mtx);
if(_block_queue.empty())
return false;
data = _block_queue.front();
_block_queue.pop_front();
return true;
}
// 移除指定的数据 -- 相当于取消匹配
void remove(T& data)
{
std::unique_lock<std::mutex> lock(_mtx);
_block_queue.remove(data);
}
};
因为我们将段位分为了三个段位,为了便于管理,我们 用三个匹配队列来管理三个段位,并且每个匹配队列中还要有各自的线程入口函数,因为如果都放在一个线程中跑的话,此时阻塞力度有点大!下面是管理类的一些成员变量设计:
而管理类的接口无非就是下面三个:
下面先来看一下匹配对战的 json 数据格式:
开始对战匹配:
{
"optype": "match_start"
}
/* 后台正确处理后回复 */
{
"optype": "match_start", //表⽰成功加⼊匹配队列
"result": true
}
/* 后台处理出错回复 */
{
"optype": "match_start"
"result": false,
"reason": "具体原因...."
}
/* 匹配成功了给客户端的回复 */
{
"optype": "match_success", //表⽰成匹配成功
"result": true
}
停止匹配:
{
"optype": "match_stop"
}
/* 后台正确处理后回复 */
{
"optype": "match_stop"
"result": true
}
/* 后台处理出错回复 */
{
"optype": "match_stop"
"result": false,
"reason": "具体原因...."
}
所以大体的实现框架如下所示:
class match_manager
{
private:
match_queue<uint64_t> _bronze; // 青铜段位队列
match_queue<uint64_t> _silver; // 白银段位队列
match_queue<uint64_t> _gold; // 黄金段位队列
std::thread _bronze_thread; // 青铜段位线程
std::thread _silver_thread; // 白银段位线程
std::thread _gold_thread; // 黄金段位线程
online_manager* _onlineptr; // 在线用户管理句柄
user_table* _utableptr; // 数据库用户表信息管理句柄
room_manager* _roomptr; // 房间管理句柄
public:
match_manager(online_manager* onlineptr, user_table* utableptr, room_manager* roomptr)
: _onlineptr(onlineptr), _utableptr(utableptr), _roomptr(roomptr),
_bronze_thread(std::thread(&match_manager::_bronze_entry, this)),
_silver_thread(std::thread(&match_manager::_silver_entry, this)),
_gold_thread(std::thread(&match_manager::_gold_entry, this))
{ DLOG("匹配队列管理类初始化完毕...."); }
// 添加用户到匹配队列
bool addUser(uint64_t uid)
{}
// 将用户从匹配队列中删除,也就是取消匹配
bool delUser(uint64_t uid)
{}
private:
// 三个段位各自的线程入口函数
void _bronze_entry() { return thread_handle(_bronze); }
void _silver_entry() { return thread_handle(_silver); }
void _gold_entry() { return thread_handle(_gold); }
// 总的处理线程入口函数细节的函数
// 在这个函数中实现将用户到匹配队列、房间的分配、响应等操作
void thread_handle(match_queue<uint64_t>& queue)
{}
};
💥其中要注意在构造函数中,对于 c++11 方式的线程初始化的时候,指定入口函数前要先指明在哪个类中,并且要取地址,然后将其参数也附上,对于 成员函数来说,默认要传一个 this 指针,不要忘记!
也可以看到,因为三个入口函数其实操作都是一致的,为了避免写大量重复的代码,我们提炼出一个 thread_handle()
函数出来,我们只需要接收一个对应的匹配队列的参数来进行操作即可!
下面我们先来实现添加和删除用户的操作,相对比较简单:
// 根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列
bool addUser(uint64_t uid)
{
// 1. 根据用户ID,获取玩家信息
Json::Value root;
bool ret = _utableptr->select_by_id(uid, root);
if(ret == false)
{
DLOG("获取玩家:%d 信息失败!!", uid);
return false;
}
uint64_t score = root["score"].asUInt64();
// 2. 添加到指定的队列中
if(score < 2000)
_bronze.push(uid);
else if(score >= 2000 && score < 3000)
_silver.push(uid);
else
_gold.push(uid);
return true;
}
// 将用户从匹配队列中删除,也就是取消匹配
bool delUser(uint64_t uid)
{
// 1. 根据用户ID,获取玩家信息
Json::Value root;
bool ret = _utableptr->select_by_id(uid, root);
if(ret == false)
{
DLOG("获取玩家:%d 信息失败!!", uid);
return false;
}
uint64_t score = root["score"].asUInt64();
// 2. 将用户从匹配队列中删除
if(score < 2000)
_bronze.remove(uid);
else if(score >= 2000 && score < 3000)
_silver.remove(uid);
else
_gold.remove(uid);
return true;
}
可以看到两个函数的操作基本是一致的,其实可以封装一个子接口出来,但是这里就不封装了,它们的区别主要就是添加和删除,其它没有什么问题。
接下来就是最重要的线程入口函数的实现:
// 总的处理线程入口函数细节的函数
// 在这个函数中实现将用户到匹配队列、房间的分配、响应等操作
void thread_handle(match_queue<uint64_t>& queue)
{
// 放到死循环中
while(1)
{
// 1. 判断队列人数是否大于2,如果小于2则阻塞等待
if(queue.size() < 2)
queue.wait();
// 2. 走到这代表人数够了,出队两个玩家
// 这里有细节,如果第一个人出队的时候失败了,那么只需要continue重新开始出队
// 但是如果是第二个人出队时候失败了,就要先将已经出队的第一个人的信息重新入队再continue
uint64_t uid1;
bool ret = queue.pop(uid1);
if(ret == false)
continue;
uint64_t uid2;
ret = queue.pop(uid2);
if(ret == false)
{
queue.push(uid1); // 要先将出队的那个人重新放到队列中再continue
continue;
}
// 3. 校验两个玩家是否在线,如果有人掉线,也就是通信句柄是无效的
// 则要把另一个人重新添加入队列,因为当前玩家掉线,而另一个人则需要重新匹配
wsserver_t::connection_ptr conn1 = _onlineptr->get_conn_from_hall(uid1);
if(conn1.get() == nullptr)
{
this->addUser(uid2);
continue;
}
wsserver_t::connection_ptr conn2 = _onlineptr->get_conn_from_hall(uid2);
if(conn1.get() == nullptr)
{
this->addUser(uid1);
continue;
}
// 4. 为两个玩家创建房间,并将玩家加入房间中 -- 创建失败的话要重新将用户放到匹配队列
room_ptr rp = _roomptr->addRoom(uid1, uid2);
if(rp.get() == nullptr)
{
this->addUser(uid1);
this->addUser(uid2);
continue;
}
// 5. 对两个玩家进行json数据响应
Json::Value response;
response["optype"] = "match_success";
response["result"] = true;
std::string body;
json_util::serialize(response, body);
conn1->send(body);
conn2->send(body);
}
}
#ifndef __MY_MATCH_H__
#define __MY_MATCH_H__
#include "util.hpp"
#include "online.hpp"
#include "room.hpp"
#include "db.hpp"
#include <mutex>
#include <thread>
#include <condition_variable>
#include <list>
template <class T>
class match_queue
{
private:
std::list<T> _block_queue; // 阻塞队列 -- 用双向链表实现
std::mutex _mtx; // 互斥锁 -- 实现线程安全
std::condition_variable _cond; // 条件变量 -- 主要用于阻塞消费者,当队列元素个数小于2的时候阻塞
public:
// 获取队列元素个数
int size()
{
std::unique_lock<std::mutex> lock(_mtx);
return _block_queue.size();
}
// 判断队列是否为空
bool isEmpty()
{
std::unique_lock<std::mutex> lock(_mtx);
return _block_queue.empty();
}
// 阻塞线程
void wait()
{
std::unique_lock<std::mutex> lock(_mtx);
_cond.wait(lock);
}
// 数据入队,并唤醒线程
void push(T& data)
{
std::unique_lock<std::mutex> lock(_mtx);
_block_queue.push_back(data);
_cond.notify_all();
}
// 数据出队 -- 相当于匹配成功要进入房间,data是输出型参数
bool pop(T& data)
{
std::unique_lock<std::mutex> lock(_mtx);
if(_block_queue.empty())
return false;
data = _block_queue.front();
_block_queue.pop_front();
return true;
}
// 移除指定的数据 -- 相当于取消匹配
void remove(T& data)
{
std::unique_lock<std::mutex> lock(_mtx);
_block_queue.remove(data);
}
};
class match_manager
{
private:
match_queue<uint64_t> _bronze; // 青铜段位队列
match_queue<uint64_t> _silver; // 白银段位队列
match_queue<uint64_t> _gold; // 黄金段位队列
std::thread _bronze_thread; // 青铜段位线程
std::thread _silver_thread; // 白银段位线程
std::thread _gold_thread; // 黄金段位线程
online_manager* _onlineptr; // 在线用户管理句柄
user_table* _utableptr; // 数据库用户表信息管理句柄
room_manager* _roomptr; // 房间管理句柄
public:
match_manager(online_manager* onlineptr, user_table* utableptr, room_manager* roomptr)
: _onlineptr(onlineptr), _utableptr(utableptr), _roomptr(roomptr),
_bronze_thread(std::thread(&match_manager::_bronze_entry, this)),
_silver_thread(std::thread(&match_manager::_silver_entry, this)),
_gold_thread(std::thread(&match_manager::_gold_entry, this))
{ DLOG("匹配队列管理类初始化完毕...."); }
// 根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列
bool addUser(uint64_t uid)
{
// 1. 根据用户ID,获取玩家信息
Json::Value root;
bool ret = _utableptr->select_by_id(uid, root);
if(ret == false)
{
DLOG("获取玩家:%d 信息失败!!", uid);
return false;
}
uint64_t score = root["score"].asUInt64();
// 2. 添加到指定的队列中
if(score < 2000)
_bronze.push(uid);
else if(score >= 2000 && score < 3000)
_silver.push(uid);
else
_gold.push(uid);
return true;
}
// 将用户从匹配队列中删除,也就是取消匹配
bool delUser(uint64_t uid)
{
// 1. 根据用户ID,获取玩家信息
Json::Value root;
bool ret = _utableptr->select_by_id(uid, root);
if(ret == false)
{
DLOG("获取玩家:%d 信息失败!!", uid);
return false;
}
uint64_t score = root["score"].asUInt64();
// 2. 将用户从匹配队列中删除
if(score < 2000)
_bronze.remove(uid);
else if(score >= 2000 && score < 3000)
_silver.remove(uid);
else
_gold.remove(uid);
return true;
}
private:
// 三个段位各自的线程入口函数
void _bronze_entry() { return thread_handle(_bronze); }
void _silver_entry() { return thread_handle(_silver); }
void _gold_entry() { return thread_handle(_gold); }
// 总的处理线程入口函数细节的函数
// 在这个函数中实现将用户到匹配队列、房间的分配、响应等操作
void thread_handle(match_queue<uint64_t>& queue)
{
// 放到死循环中
while(1)
{
// 1. 判断队列人数是否大于2,如果小于2则阻塞等待
if(queue.size() < 2)
queue.wait();
// 2. 走到这代表人数够了,出队两个玩家
// 这里有细节,如果第一个人出队的时候失败了,那么只需要continue重新开始出队
// 但是如果是第二个人出队时候失败了,就要先将已经出队的第一个人的信息重新入队再continue
uint64_t uid1;
bool ret = queue.pop(uid1);
if(ret == false)
continue;
uint64_t uid2;
ret = queue.pop(uid2);
if(ret == false)
{
queue.push(uid1); // 要先将出队的那个人重新放到队列中再continue
continue;
}
// 3. 校验两个玩家是否在线,如果有人掉线,也就是通信句柄是无效的
// 则要把另一个人重新添加入队列,因为当前玩家掉线,而另一个人则需要重新匹配
wsserver_t::connection_ptr conn1 = _onlineptr->get_conn_from_hall(uid1);
if(conn1.get() == nullptr)
{
this->addUser(uid2);
continue;
}
wsserver_t::connection_ptr conn2 = _onlineptr->get_conn_from_hall(uid2);
if(conn1.get() == nullptr)
{
this->addUser(uid1);
continue;
}
// 4. 为两个玩家创建房间,并将玩家加入房间中 -- 创建失败的话要重新将用户放到匹配队列
room_ptr rp = _roomptr->addRoom(uid1, uid2);
if(rp.get() == nullptr)
{
this->addUser(uid1);
this->addUser(uid2);
continue;
}
// 5. 对两个玩家进行json数据响应
Json::Value response;
response["optype"] = "match_success";
response["result"] = true;
std::string body;
json_util::serialize(response, body);
conn1->send(body);
conn2->send(body);
}
}
};
#endif