文章目錄
- 前言
- Ⅰ. 匹配隊列實現
- Ⅱ. 匹配隊列管理類實現
- 完整代碼

前言
五子棋對戰的玩家匹配是根據自己的天梯分數進行匹配的,而服務器中將玩家天梯分數分為三個檔次:
- 青銅:天梯分數小于 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重新開始出隊// 但是如果是第二個人出隊時候失敗了,就要先將已經出隊的第一個人的信息重新入隊再continueuint64_t uid1;bool ret = queue.pop(uid1);if(ret == false)continue;uint64_t uid2;ret = queue.pop(uid2);if(ret == false){queue.push(uid1); // 要先將出隊的那個人重新放到隊列中再continuecontinue;}// 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重新開始出隊// 但是如果是第二個人出隊時候失敗了,就要先將已經出隊的第一個人的信息重新入隊再continueuint64_t uid1;bool ret = queue.pop(uid1);if(ret == false)continue;uint64_t uid2;ret = queue.pop(uid2);if(ret == false){queue.push(uid1); // 要先將出隊的那個人重新放到隊列中再continuecontinue;}// 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