
前言
五子棋对战的玩家匹配是根据自己的天梯分数进行匹配的,而服务器中将玩家天梯分数分为三个档次:
- 青铜:天梯分数小于 2000 分
- 白银:天梯分数介于 2000~3000 分之间
- 黄金:天梯分数大于 3000 分
而实现玩家匹配的思想非常简单,为不同的档次设计各自的匹配队列,当一个队列中的玩家数量大于等于 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);
}
};
Ⅱ. 匹配队列管理类实现
因为我们将段位分为了三个段位,为了便于管理,我们 用三个匹配队列来管理三个段位,并且每个匹配队列中还要有各自的线程入口函数,因为如果都放在一个线程中跑的话,此时阻塞力度有点大!下面是管理类的一些成员变量设计:
- 三个匹配队列对象
- 三个线程:每个线程分别对应每个匹配队列对象
- 房间管理类句柄:因为我们要将对应的用户放到对应的房间,那么就得有创建房间等操作
- 数据库用户信息表句柄:因为我们需要获取用户的天梯分数来判断要将用户放到哪个匹配队列中,所以要有该句柄
- 在线用户管理句柄:我们需要在用户匹配成功之后,判断一下用户还是否在线,如果不在线了那么就得做一下特殊处理
而管理类的接口无非就是下面三个:
- 添加用户到匹配队列接口
- 从匹配队列中移除用户接口
- 线程入口函数:
- 因为涉及到线程,那么就得有线程入口函数,并且要有三个线程入口函数,由于它们的实现其实是类似的,代码中可以用一个接口来封装一下这三个线程入口函数,减少代码量,具体参考代码
- 而这三个线程的主要工作无非就是判断各自的匹配队列是否人数大于 2,是的话就要出队两个用户,为他们创建房间,并且向它们发送对战匹配成功的信息,也就是响应,当然匹配失败也要响应!
下面先来看一下匹配对战的 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