該博客對于學完C++和linux操作系統,但不知道如何用C++開發項目,已經不知道C++如何使用第三方庫的人來說一定很有幫助,請耐心看完!
先看一下游戲會顯示的前端界面,對理解這個游戲的前后端交互過程會有幫助
1. 開發環境
1.1 使用的操作系統:Ubuntu-22.04
我們可以先在虛擬機或者服務器上選擇或者安裝這個Ubuntu-22.04操作系統。最后我們這個網頁對戰五子棋的服務器是要部署在服務器上的。博主選用的是騰訊云的服務器。
1.2 安裝gcc/g++編譯器
我們程序使用g++進行編譯。我們在命令行輸入下面指令即可安裝
sudo apt-get install gcc g++
1.3 安裝gdb調試器
我們程序使用gdb進行調試。我們在命令行輸入下面指令即可安裝
sudo apt-get install gdb
1.4?安裝git?具。
我的代碼已經上傳到碼云之后:https://gitee.com/xwyg/Cpp_project.git
sudo apt-get install git
1.5?安裝cmake項?構建?具
我們項目的自動化構建和編譯只用到了make,而cmake用來執行websocket庫的構建和編譯。
sudo apt-get install cmake
1.6 安裝第三方庫:
安裝jsoncpp庫和?安裝jsoncpp庫
sudo apt-get install libjsoncpp-dev
sudo apt-get install libboost-all-dev
Jsoncpp庫用來處理JSON格式數據,我們在進行客戶端和服務器通信過程中正文部分傳遞的是字符串,單純字符串的提取處理要復雜一些,我們將其轉換成Json格式的數據,便于我們在請求和響應中提取對于信息。
我們采取的是Restful風格的網絡通信接口,即為使用GET/POST/PUT/DELETE代表不同請求類型,并且正文格式都是使用JSON格式序列化后的字符串
安裝websocketpp庫
Websocketpp沒有官方維護的預編譯包直接供apt-get使用,因此它的安裝比較復雜,需要我們自己下載源代碼進行編譯,大家可以在deepseek或者chatgpt中之間搜安裝教程。用這兩個生成的安裝方法還是很準確的。
Websocketpp庫是我們服務器使用的主要庫,它依賴于boost庫,處理WebSocket通信.
Websocket介紹:?
WebSocket協議是從HTML5開始?持的?種??端和服務端保持?連接的消息推送機制。
? 傳統的web程序都是屬于"?問?答"的形式,即客?端給服務器發送了?個HTTP請求,服務器給客?端返回?個HTTP響應。這種情況下服務器是屬于被動的??,如果客?端不主動發起請求服務器就無法主動給客戶端響應
? 網頁即時聊天 或者 我們做的五子棋游戲這樣的程序都是?常依賴"消息推送"的,即需要服務器主動推動消息到客戶端。如果只是使?原?的HTTP協議,要想實現消息推送?般需要通過客戶端"輪詢"的?式實現,而輪詢的成本?較?并且也不能及時的獲取到消息的響應。
基于上述兩個問題,就產?了WebSocket協議。WebSocket更接近于TCP這種級別的通信?式,?旦連接建?完成客戶端或者服務器都可以主動的向對?發送數據。
WebSocket原理解析
WebSocket協議本質上是?個基于TCP的協議。為了建??個WebSocket連接,客?端瀏覽器?先要向服務器發起?個HTTP請求,這個請求和通常的HTTP請求不同,包含了?些附加頭信息,通過這個附加頭信息完成握?過程并升級協議的過程。
WebSocket也有自己的報文格式,但是其實與本項目關系沒有那么大,我們之間WebSocketpp調用對應的接口即可,在后面項目代碼中會有具體的介紹。
WebSocketpp同時?持HTTP和Websocket兩種?絡協議,?較適?于我們本次的項?,所以我們選?該庫作為項?的依賴庫?來搭建HTTP和WebSocket服務器。
1.7 mysql安裝
博主安裝的是mysql 5.7版本的數據庫,各位安裝8.0版本的數據庫也是一樣的,各位從deepseek或者chatgpt搜mysql的安裝教程比較靠譜。
2.項目流程
這個流程圖非常重要,關系到我們整個游戲的運行流程,已經前后端交互流程,還有服務器的各個模塊之間的交互和聯系。
2.1 客戶端流程
客戶端流程: 進入用戶注冊頁面--->完成用戶注冊--->跳轉到用戶登錄頁面-->完成用戶登錄
---->跳轉到游戲大廳頁面--->點擊按鈕進入游戲匹配-->匹配成功跳轉到游戲房價頁面
---->游戲房間可以進行下棋或者聊天等操作--->游戲結束,加分或者扣分
--->該房間內無法下棋,彈出"回到大廳"按鈕--->用戶又可以點擊按鈕進入游戲匹配
2. 2 服務器流程
首先玩家想要訪問我們的服務器得通過訪問101.35.46.142:7080/register.html 注冊頁面或者101.35.46.142:7080/login.html登錄頁面,訪問這兩個頁面會向我們的服務器發送http請求。
對于注冊頁面發送的http請求,服務器從請求正文中獲取此時輸入的用戶名和密碼,然后進行數據庫中數據的插入;?對于登錄頁面發送的htpp請求,服務器從請求正文中獲取此時輸入的用戶名和密碼,然后服務器會建立用戶的session并保存( 此后每次用戶發送http或者websocket請求過來都會進行session的驗證), 此時客戶端跳轉到游戲大廳頁面,發送 大廳websocket長連接建立請求,服務器此時建立同客戶端的長連接,當客戶端點擊游戲大廳頁面的 "進入匹配" 按鈕之后,會向客戶端發送Websocket消息,服務器會按照該用戶的等級分數 把他加入對應的匹配隊列之中,等匹配成功之后會向兩個進行匹配的客戶端發送匹配成功的消息,客戶端收到該請求進入游戲房間,此時參與匹配的兩個客戶端(玩家)都會向服務器發送 房間webhasocket長連接建立請求,服務器同客戶端建立兩個房間長連接,此時用戶進行下棋或者聊天動作,都會向服務器發送對應請求,服務器也會給出對應的響應。
3.websocketpp庫的介紹
3.1? websocketpp常用接口
namespace websocketpp {
typedef lib::weak_ptr<void> connection_hdl;
template <typename config>
class endpoint : public config::socket_type {typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;typedef typename connection_type::ptr connection_ptr;typedef typename connection_type::message_ptr message_ptr;typedef lib::function<void(connection_hdl)> open_handler;typedef lib::function<void(connection_hdl)> close_handler;typedef lib::function<void(connection_hdl)> http_handler;typedef lib::function<void(connection_hdl,message_ptr)> message_handler;* websocketpp::log::alevel::none 禁?打印所有?志*/void set_access_channels(log::level channels);/*設置?志打印等級*/void clear_access_channels(log::level channels);/*清除指定等級的?志*//*設置指定事件的回調函數*/void set_open_handler(open_handler h);/*websocket握?成功回調處理函數*/void set_close_handler(close_handler h);/*websocket連接關閉回調處理函數*/void set_message_handler(message_handler h);/*websocket消息回調處理函數*/void set_http_handler(http_handler h);/*http請求回調處理函數*//*關閉連接接?*/void close(connection_hdl hdl, close::status::value code, std::string& reason);/*獲取connection_hdl 對應連接的connection_ptr*/connection_ptr get_con_from_hdl(connection_hdl hdl);/*websocketpp基于asio框架實現,init_asio?于初始化asio框架中的io_service調度器*/void init_asio();/*設置是否啟?地址重?*/void set_reuse_addr(bool value);/*設置endpoint的綁定監聽端?*/void listen(uint16_t port);/*對io_service對象的run接?封裝,?于啟動服務器*/std::size_t run();/*websocketpp提供的定時器,以毫秒為單位*/timer_ptr set_timer(long duration, timer_handler callback);
};template <typename config>
class server : public endpoint<connection<config>,config> {/*初始化并啟動服務端監聽連接的accept事件處理*/void start_accept();
}
看到這些代碼可能會一臉懵,我們只需要知道websocketpp命名空間下定義了 server類(繼承自endpoint),它就是我們要啟動的服務器,調用它的方法,我們就可以對服務器進行各種設置,同時它有一些回調函數:
set_open_handler(open_handler h);/*websocket握?成功回調處理函數*/
void set_close_handler(close_handler h);/*websocket連接關閉回調處理函數*/
void set_message_handler(message_handler h);/*websocket消息回調處理函數*/
void set_http_handler(http_handler h);/*http請求回調處理函數*/
這些函數需要我們傳入一個函數對象,這個函數對象是一個 void (connection_hdl hdl)類型,請看使用實例:
#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
using namespace std;
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;// websocket連接成功的回調函數
void OnOpen(websocketsvr *server,websocketpp::connection_hdl hdl){cout<<"連接成功"<<endl;
}// websocket連接成功的回調函數
void OnClose(websocketsvr *server,websocketpp::connection_hdl hdl){cout<<"連接關閉"<<endl;
}// websocket連接收到消息的回調函數
void OnMessage(websocketsvr *server,websocketpp::connection_hdl hdl,message_ptr msg){cout << "收到消息" << msg->get_payload() << endl;// 收到消息將相同的消息發回給websocket客?端server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}// websocket連接異常的回調函數
void OnFail(websocketsvr *server,websocketpp::connection_hdl hdl){
cout<<"連接異常"<<endl;
}
// 處理http請求的回調函數 返回?個html歡迎??
void OnHttp(websocketsvr *server,websocketpp::connection_hdl hdl){cout<<"處理http請求"<<endl;websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);std::stringstream ss;ss << "<!doctype html><html><head>"<< "<title>hello websocket</title><body>"<< "<h1>hello websocketpp</h1>"<< "</body></head></html>";con->set_body(ss.str()); //設置http響應正文con->set_status(websocketpp::http::status_code::ok); //設置http響應狀態碼
}
這是main(),告訴我們server如何初始化,格式都是一樣的,同時還需要綁定(注冊)對應請求來時的處理動作。
int main()
{// 使?websocketpp庫創建服務器websocketsvr server;// 設置websocketpp庫的?志級別 all表?打印全部級別?志 none表?什么?志都不打印server.set_access_channels(websocketpp::log::alevel::none);/*初始化asio*/server.init_asio();// 注冊http請求的處理函數server.set_http_handler(bind(&OnHttp, &server, ::_1));// 注冊websocket請求的處理函數server.set_open_handler(bind(&OnOpen, &server, ::_1));server.set_close_handler(bind(&OnClose, &server, _1));server.set_message_handler(bind(&OnMessage, &server, _1, _2));// 監聽8888端?server.listen(8888);// 開始接收tcp連接server.start_accept();// 開始運?服務器server.run();return 0;
}
Http客?端,使?瀏覽器作為http客?端即可,訪問服務器的8888端?。
任意瀏覽器輸入即可請求我們服務器:
前端如何發送websocket請求,這個不是我們的重點,但是也大致了解一下前端代碼:
<html>
<body><input type="text" id="message"><button id="submit">提交</button><script>// 創建 websocket 實例// ws://192.168.51.100:8888// 類?http// ws表?websocket協議// 192.168.51.100 表?服務器地址// 8888表?服務器綁定的端?let websocket = new WebSocket("ws://192.168.51.100:8888");// 處理連接打開的回調函數websocket.onopen = function () {console.log("連接建?");}// 處理收到消息的回調函數// 控制臺打印消息websocket.onmessage = function (e) {console.log("收到消息: " + e.data);}// 處理連接異常的回調函數websocket.onerror = function () {console.log("連接異常");}// 處理連接關閉的回調函數websocket.onclose = function () {console.log("連接關閉");}// 實現點擊按鈕后, 通過 websocket實例 向服務器發送請求let input = document.querySelector('#message');let button = document.querySelector('#submit');button.onclick = function () {console.log("發送消息: " + input.value);websocket.send(input.value);}</script>
</body></html>
服務器啟動,我們將上面的代碼復制到 abc.html文件中,打開并在輸入框輸入hello,即可得到以下內容:
?new WebSocket("ws://192.168.51.100:8888");然后將他綁定在一個按鈕中,點擊按鈕即可向后端發送websocket請求,此時服務器收到請求,會同客戶端建立websocket長連接
4.項目實現
4.1 日志宏的實現
我們不采用websocketpp庫中的日志類,而是自己編寫一個日志類,定義一個日志宏,傳入日志等級和可變參數,然后將線程ID,文件名,行號,時間與傳入的可變字符串拼接起來,一起向終端(或文件)打印。
1.?
strftime函數
其中?strftime函數
:將時間格式化為字符串。我們可以用snprintf(time_buffer, sizeof(time_buffer), "%d",format_time->tm_year + 1900)代替。
2.宏參數允許 進行 字符串拼接
?fprintf中第二個參數,要求傳入一個const char*字符串,我們可以直接拼接我們需要的格式化字符串和傳入的格式化字符串,這在函數無法實現???"[%p %s %s:%d] " format " \n"。
3.宏中可變參數
C++98允許宏傳入可變參數,由##__VA_ARGS__代替
代碼如下:
#pragma once#include <stdio.h>
#include <time.h>
#include<pthread.h>
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG //超過這個日志等級才會被輸出
// 宏里面有多條語句,使用do while(0),
#define LOG(level, format, ...) \do \{ \if (level < LOG_LEVEL) \break; \time_t t = time(NULL); \struct tm *ltm = localtime(&t); \char tmp[32] = {0}; \strftime(tmp, 31, "%H:%M:%S", ltm); \fprintf(stdout, "[%p %s %s:%d] " format " \n", (void *)pthread_self(), \tmp, __FILE__, __LINE__, ##__VA_ARGS__); \}while (0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
4.2?工具類的實現
我們實現四個工具類,工具類中封裝有靜態方法,全局都可以使用該方法
首先是? json_util,提供序列化和反序列化的方法。
序列化通過Json::StreamWriter對象指針將Json::Value對象寫入 str字符串中,反序列化通過Json::CharReader將str字符串寫入Json::Value對象中
class json_util
{
public:// 將jsonvalue對象寫入 str字符串中static bool serialize(const Json::Value &value, std::string &str){Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;int ret = sw->write(value, &ss);if (ret != 0){std::cout << "json serialize failed!" << std::endl;return false;}str = ss.str();return true;}// 將json字符串寫回到value對象中,由用戶自己做[""].asInt等處理static bool unserialize(const std::string &str, Json::Value &value){Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());bool ret = cr->parse(str.c_str(), str.c_str() + str.size(),&value, nullptr);if (!ret){ERR_LOG("json unserialize failed!");return false;}return true;}
};
第二個是mysql_util,提供數據庫的創建,銷毀,執行方法
class mysql_util
{
public:static MYSQL *mysql_create(const std::string &host,const std::string &user,const std::string &pass,const std::string &db,int port){MYSQL *mysql = mysql_init(NULL);if (mysql == NULL){ERR_LOG("mysql init failed!");return NULL;}if (mysql_real_connect(mysql, host.c_str(), user.c_str(),pass.c_str(), db.c_str(), port, NULL, 0) == NULL){ERR_LOG("mysql connect server failed! %s", mysql_error(mysql));mysql_close(mysql);return NULL;}if (mysql_set_character_set(mysql, "utf8") != 0){ERR_LOG("mysql set character failed!");mysql_close(mysql);return NULL;}DBG_LOG("mysql connect success!");return mysql;}static void mysql_destroy(MYSQL *mysql){if (mysql == NULL){return;}mysql_close(mysql);}static bool mysql_exec(MYSQL *mysql, const std::string &sql){if (mysql_query(mysql, sql.c_str()) != 0){ERR_LOG("SQL: %s", sql.c_str());ERR_LOG("ERR: %s", mysql_error(mysql));return false;}return true;}
};
第三個是string_util,提供字符串分割方法,因為我們涉及http 某一個請求頭的提取已經某一個cookie的提取,提取不會改變原有的字符串和分割字符串,提取到一個vector<string>中
class string_util
{
public:// 字符串 子串分割功能, 將分割的子串保存到一個字符串數組之中static int split(const std::string &in, const std::string &sep,std::vector<std::string> &arry){arry.clear();size_t pos, idx = 0; // pos保存為查找結果,如果pos和idx相等,該位置就是sep,則不保存while (idx < in.size()){pos = in.find(sep, idx);if (pos == std::string::npos){arry.push_back(in.substr(idx));break;}if (pos != idx){arry.push_back(in.substr(idx, pos - idx)); // 當前位置,長度}idx = pos + sep.size();}return arry.size();}
};
第四個是file_util,對靜態請求處理時將html文件返回給客戶端
class file_util
{
public:static bool read(const std::string &filename, std::string &body){std::ifstream file;// 打開?件file.open(filename.c_str(), std::ios::in | std::ios::binary);if (!file){std::cout << filename << " Open failed!" << std::endl;return false;}// 計算?件??file.seekg(0, std::ios::end);body.resize(file.tellg());file.seekg(0, std::ios::beg);file.read(&body[0], body.size());if (file.good() == false){std::cout << filename << " Read failed!" << std::endl;file.close();return false;}file.close();return true;}
};
4.3 數據庫操作模塊實現
我們先定義數據庫中的表,我們這個項目比較簡單,只有一張user表,提供用戶id,username,password,score,對戰總場次和獲勝場次這些字段
我們可以將每個表都封裝到一個類之中,提供一個對外的MYSQL句柄,執行對應的數據庫操作。
由于字符串中拼接比較麻煩,我們選擇#define 定義格式化字符串,再有sprintf()函數去進行寫入。
我們所需要進行數據庫操作的地方有 用戶插入,用戶登錄,通過用戶名查詢用戶,通過用戶id查詢用戶,用戶獲勝和用戶失敗這些情況,分別實現這些函數
#pragma once
#include "util.hpp"
#include <mutex>
#include <assert.h>
// user_table類,將所要執行數據庫操作的地方全部封裝到該類之中,包含一個MYSQL指針和mutex互斥量
// 調用了 mysql_util類中的 創建銷毀和執行方法
// 提供了 insert(user),login(user),win,lose,select_by_uid等等函數// 每個函數所要執行的sql由宏定義給出,sql的字符串都要以;結尾,同時varchar類型都要在''里面
// mysql_query是線程安全的,但是它和mysql_store_result(_mysql)保存一起就不是線程安全的了
class user_table
{
private:MYSQL *_mysql;std::mutex _mutex;public:user_table(const std::string &host,const std::string &user,const std::string &pass,const std::string &db,int port = 3306){_mysql = mysql_util::mysql_create(host, user, pass, db, port);assert(_mysql != NULL);}// 網絡中傳輸的是字符串,需要講它們序列化到一個個的request對象中,再調用_cal計算并將結果,反序列化成字符串返回bool insert(Json::Value &user){
#define INSERT_USER "insert user values(null, '%s', password('%s'), 1000, 0,0);"if (user["password"].isNull() || user["username"].isNull()){DBG_LOG("INPUT PASSWORD OR USERNAME");return false;}char sql[4096] = {0};sprintf(sql, INSERT_USER, user["username"].asCString(),user["password"].asCString());bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("insert user info failed!!\n");return false;}return true;}bool login(Json::Value & user) // 用戶登錄,并返回完整的用戶信息{if (user["password"].isNull() || user["username"].isNull()){DBG_LOG("INPUT PASSWORD OR USERNAME");return false;}// 以用戶名和密碼共同查詢,查詢到數據則表???名密碼?致,沒有信息則用戶名密碼錯誤
#define LOGIN_USER "select id, score, total_count,win_count from user where username='%s' and password=password('%s');"char sql[4096] = {0};sprintf(sql, LOGIN_USER, user["username"].asCString(),user["password"].asCString());MYSQL_RES *res = NULL;{// std::lock_guard<std::mutex> lock(_mutex);std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("user login failed!!\n");return false;}// 將查詢結果保存到本地res = mysql_store_result(_mysql);if (res == NULL){DBG_LOG("mysql_store_result exec error!!");return false;}}std::cout << res << std::endl;// 根據結果集獲取條目輸了int num_row = mysql_num_rows(res);if (num_row == 0){DBG_LOG("have no login user info!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);// 查詢結果集的四行數據設置進 原有user中user["id"] = std::stoi(row[0]); // 如果數據范圍小,默認int夠用則無需轉換user["score"] = std::stoi(row[1]); // (Json::UInt64)user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);std::cout << "jjjjj" << std::endl;mysql_free_result(res);return true;}bool select_by_name(const std::string &name, Json::Value &user) // 通過用戶名查詢用戶{
#define USER_BY_NAME "select id, score, total_count, win_count from user where username = '%s';"char sql[4096] = {0};sprintf(sql, USER_BY_NAME, name.c_str());MYSQL_RES *res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("get user by name failed!!\n");return false;}// 按理說要么有數據,要么沒有數據,就算有數據也只能有?條數據res = mysql_store_result(_mysql);if (res == NULL){DBG_LOG("hmysql_store_result!!");return false;}}int row_num = mysql_num_rows(res);if (row_num == 0){DBG_LOG("have no login user info!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stoi(row[0]);user["username"] = name;user["score"] = (Json::UInt64)std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}bool select_by_id(int id, Json::Value &user) // 通過id查詢用戶{
#define USER_BY_ID "select username,score,total_count,win_count from user where id = %d;"MYSQL_RES *res = NULL;char sql[4096] = {0};sprintf(sql, USER_BY_ID, id);{std::lock_guard<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("select_by_id mysql_exec error");return false;}res = mysql_store_result(_mysql);if (res == NULL){DBG_LOG("mysql_store_result error!!");return false;}}int row_num = mysql_num_rows(res);if (row_num == 0){DBG_LOG("have no login user info!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["username"] = row[0];user["score"] = std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}bool win(int id) // 用戶勝利時,總場次和勝利場次都加1{
#define USER_WIN "update user set score=score+30,total_count=total_count+1, \win_count=win_count+1 where id=%d;"char sql[1024] = {0};sprintf(sql, USER_WIN, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("update win user info failed!!\n");return false;}return true;}bool lose(int id) // 用戶失敗時,總場次加1,分數不變{
#define USER_LOSE "update user set score=score-30,total_count=total_count+1 where id=%d;"char sql[1024] = {0};sprintf(sql, USER_LOSE, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DBG_LOG("update win user info failed!!\n");return false;}return true;}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql = NULL;}};
4.4 用戶在線管理模塊實現
用戶在線管理模塊記錄了用戶進入我們服務器之后所處的存在狀態,是否離線,是在大廳還是房間
用戶首先發送http請求注冊頁面,輸入用戶名密碼完成數據庫插入,然后進入登錄頁面,登錄成功之后我們就需要將這個用戶管理起來,因為存在好多的客戶端,我們需要根據用戶id找到這些客戶端,因此選用?std::unordered_map<int, websocket_server::connection_ptr> _game_hall建立游戲大廳中用戶的管理和std::unordered_map<int, websocket_server::connection_ptr> _game_room;游戲房間中用戶的管理。
#pragma once
#include "util.hpp"
#include <mutex>
#include <unordered_map>// 在線用戶的管理類,在線用戶要么在游戲大廳,要么在游戲房間
// 維護用戶id到服務器連接的 游戲大廳map和用戶id到服務器連接的 游戲房間map,以及一個互斥量mutex
// 提供進入(退出)大廳,進入(退出)房間,獲取這個用戶的連接 等操作class online_manager
{// 使用map維護 從id到connection的關系
private:/*游戲?廳的客?端連接管理*/std::unordered_map<int, websocket_server::connection_ptr> _game_hall;/*游戲房間的客?端連接管理*/std::unordered_map<int, websocket_server::connection_ptr> _game_room;std::mutex _mutex;public:/*進?游戲?廳--游戲?廳連接建?成功后調?*/void enter_game_hall(int uid, const websocket_server::connection_ptr &conn){std::unique_lock<std::mutex> lock(_mutex);_game_hall.insert(std::make_pair(uid, conn));}/*退出游戲?廳--游戲?廳連接斷開后調?*/void exit_game_hall(int uid){std::unique_lock<std::mutex> lock(_mutex);_game_hall.erase(uid);}/*進?游戲房間--游戲房間連接建?成功后調?*/void enter_game_room(int uid, const websocket_server::connection_ptr &conn){std::unique_lock<std::mutex> lock(_mutex);_game_room.insert(std::make_pair(uid, conn));}/*退出游戲房間--游戲房間連接斷開后調?*/void exit_game_room(int uid){std::unique_lock<std::mutex> lock(_mutex);_game_room.erase(uid);}/*判斷用戶是否在游戲?廳*/bool in_game_hall(uint64_t uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_hall.find(uid);if (it == _game_hall.end()){return false;}return true;}/*判斷用戶是否在游戲房間*/bool in_game_room(uint64_t uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_room.find(uid);if (it == _game_room.end()){return false;}return true;}/*從游戲?廳中獲取指定??關聯的Socket連接*/websocket_server::connection_ptr get_conn_from_game_hall(uint64_t uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_hall.find(uid);if (it == _game_hall.end()){return nullptr;}return it->second;}/*從游戲房間中獲取指定??關聯的Socket連接*/websocket_server::connection_ptr get_conn_from_game_room(int uid){std::unique_lock<std::mutex> lock(_mutex);auto it = _game_room.find(uid);if (it == _game_room.end()){return nullptr;}return it->second;}online_manager(){}~online_manager(){}
};
4.5 會話管理模塊實現
現在基本所有網絡通信都要實現一個會話管理模塊,當用戶登錄成功之后,服務器使用一個SeeionId需要標記這個用戶,這樣后續用戶每次操作都會發送sessionid給服務器,服務器也可以做用戶驗證,同時識別這是哪個客戶端。
我們在類的設計上需要實現兩個類,一個會話類,一個會話管理類,
會話類中包含會話id,用戶id,會話狀態,定時器
這個定時器主要是看這個session下是否設置了定時器過期任務,如果websocket_server::timer_ptr為空,則為永久存在。
#pragma once
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include "util.hpp"typedef enum
{LOGIN,UNLOGIN
} ss_statu;// 一個會話類和一個會話管理類。
// 會話類中包含 會話id,用戶id,會話狀態,定時器(不過期和何時過期)
// 會話管理類中: 互斥鎖,websocket服務器(給它設置定時任務) 和
// 分配的下一個sessionid和sessionid到整個會話的map。 提供創建session 和設置過期時間的函數class session
{
private:uint64_t _ssid; // 標識符int _uid;ss_statu _statu;websocket_server::timer_ptr _tp; // 該session相關定時器
public:session(uint64_t ssid) : _ssid(ssid){DBG_LOG("SESSION %p 被創建!!", this);}~session() { DBG_LOG("SESSION %p 被釋放!!", this); }uint64_t ssid() { return _ssid; };void set_statu(ss_statu statu) { _statu = statu; }void set_user(int uid) { _uid = uid; }uint64_t get_user() { return _uid; }bool is_login() { return (_statu == LOGIN); }void set_timer(const websocket_server::timer_ptr &tp) { _tp = tp; }websocket_server::timer_ptr &get_timer() { return _tp; }
};
會話管理類中包含,需要分配的下一個會話id,websocket服務器(用于設置定時任務),一個會話id到整個會話的映射map.?
注意,websocket定時器取消時,它取消綁定函數會執行一次(不一定馬上執行),所以需要重新添加。
#define SESSION_TIMEOUT 3000
#define SESSION_FOREVER -1
using session_ptr = std::shared_ptr<session>;
class session_manager
{
private:uint64_t _next_ssid;std::mutex _mutex;std::unordered_map<uint64_t, session_ptr> _session;websocket_server *_server;public:session_manager(websocket_server *srv) : _next_ssid(1), _server(srv){DBG_LOG("session管理器初始化完畢!");}~session_manager() { DBG_LOG("session管理器即將銷毀!"); }session_ptr create_session(uint64_t uid, ss_statu statu){std::unique_lock<std::mutex> lock(_mutex);session_ptr ssp(new session(_next_ssid));ssp->set_statu(statu);ssp->set_user(uid); //創建會話時需要將用戶id 和用戶狀態都設置進去_session.insert(std::make_pair(_next_ssid, ssp));_next_ssid++;return ssp;}void append_session(const session_ptr &ssp){std::unique_lock<std::mutex> lock(_mutex);_session.insert(std::make_pair(ssp->ssid(), ssp));}session_ptr get_session_by_ssid(uint64_t ssid){std::unique_lock<std::mutex> lock(_mutex);auto it = _session.find(ssid);// 不存在這個ssid就返回空指針if (it == _session.end()){return session_ptr();}return it->second;}void remove_session(uint64_t ssid){std::unique_lock<std::mutex> lock(_mutex);_session.erase(ssid);}// 定時器tp->cancel 不是立即執行的,所以在_server->set_timer執行插入// session的定時任務重置需要 先取消再重新添加。 void set_session_expire_time(uint64_t ssid, int ms){session_ptr ssp = get_session_by_ssid(ssid);if (ssp.get() == nullptr){return;}websocket_server::timer_ptr tp = ssp->get_timer();if (tp.get() == nullptr && ms == SESSION_FOREVER){// 1. 在session永久存在的情況下,設置永久存在return;}else if (tp.get() == nullptr && ms != SESSION_FOREVER){// 2. 在session永久存在的情況下,設置指定時間之后被刪除的定時任務websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tmp_tp);}else if (tp.get() != nullptr && ms == SESSION_FOREVER){// 3. 在session設置了定時刪除的情況下,將session設置為永久存在// 取消定時任務tp->cancel();ssp->set_timer(websocket_server::timer_ptr());_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));}else{// 4. 在session設置了定時刪除的情況下,將session重置刪除時間。// 先取消定時任務,再把該session對象添加到管理隊列中tp->cancel();ssp->set_timer(websocket_server::timer_ptr()); _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));// 重新綁定新的定時任務websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tmp_tp);}}
};
4.6 房間管理模塊
用戶進入游戲大廳后,存在于在線管理模塊的map之中,然后用戶選擇進入匹配,此時為用戶創建匹配隊列,匹配成功創建房間,按照邏輯下來是先有匹配隊列再有房間,但是匹配隊列中必須調用創建房間的接口,去幫用戶進入房間之中。因此先介紹房間管理模塊。
第一個房間類,里面有成員房間id,房間狀態,棋盤,黑棋白棋用戶id,玩家數量,以及在線用戶和數據庫的管理句柄。它需要提供處理用戶請求(聊天或者下棋)的函數,以及判斷輸贏,將響應返回給所有房間用戶。
第二個房間管理類,分配房間的roomid,維護兩個map,即為房間id到整個房間的映射map和用戶id到房間id的映射map。
代碼如下:
#pragma once#include <memory>
#include "db.hpp"
#include "online.hpp"// 房間類和房間管理類
// 房間類中有房間id,房間狀態,棋盤,黑棋白棋用戶id,玩家數量,以及在線用戶和數據庫 管理句柄
// 提供handle_request識別請求,進行下棋或者聊天,同時有 用戶退出,廣播等等動作
// 房間管理類 分配的roomid,兩個map,提供房間的 create remove selectbyroomid等接口typedef enum
{GAME_START,GAME_OVER
} room_status;#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2class room
{
private:uint64_t _room_id;room_status _status;int _player_count;int _white_id;int _black_id;user_table *_tb_user;std::vector<std::vector<int>> _board;online_manager *_online_user;public:room(){}room(uint64_t room_id, user_table *tb, online_manager *online_user): _room_id(room_id), _status(GAME_START), _player_count(0),_tb_user(tb), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0)){DBG_LOG("%lu 房間創建成功!!", _room_id);}~room(){DBG_LOG("%lu 房間銷毀成功!!", _room_id);}/*添加白棋黑棋用戶,獲取房間id等接口*/uint64_t id() { return _room_id; }room_status statu() { return _status; }int player_count() { return _player_count; }void add_white_user(int uid){_white_id = uid;_player_count++;}void add_black_user(int uid){_black_id = uid;_player_count++;}int get_white_user() { return _white_id; }int get_black_user() { return _black_id; }bool five(int row, int col, int row_off, int col_off, int color){// row和col是下棋位置, row_off和col_off是偏移量,也是?向int count = 1;int search_row = row + row_off;int search_col = col + col_off;while (search_row >= 0 && search_row < BOARD_ROW &&search_col >= 0 && search_col < BOARD_COL &&_board[search_row][search_col] == color){// 同?棋?數量++count++;// 檢索位置繼續向后偏移search_row += row_off;search_col += col_off;}search_row = row - row_off;search_col = col - col_off;while (search_row >= 0 && search_row < BOARD_ROW &&search_col >= 0 && search_col < BOARD_COL &&_board[search_row][search_col] == color){// 同?棋?數量++count++;// 檢索位置繼續向后偏移search_row -= row_off;search_col -= col_off;}return (count >= 5);}int 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 == CHESS_WHITE ? _white_id : _black_id;}return 0;}/*處理下棋動作*/Json::Value handle_chess(Json::Value &req){Json::Value json_resp = req;// 2. 判斷房間中兩個玩家是否都在線,任意?個不在線,就是另??勝利。int chess_row = req["row"].asInt();int chess_col = req["col"].asInt();uint64_t cur_uid = req["uid"].asUInt64();if (_online_user->in_game_room(_white_id) == false){json_resp["result"] = true;json_resp["reason"] = "運?真好!對?掉線,不戰?勝!";json_resp["winner"] = (Json::UInt64)_black_id;return json_resp;}if (_online_user->in_game_room(_black_id) == false){json_resp["result"] = true;json_resp["reason"] = "運?真好!對?掉線,不戰?勝!";json_resp["winner"] = (Json::UInt64)_white_id;return json_resp;}// 3. 獲取?棋位置,判斷當前?棋是否合理(位置是否已經被占?)if (_board[chess_row][chess_col] != 0){json_resp["result"] = false;json_resp["reason"] = "當前位置已經有了其他棋?!";return json_resp;}int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;_board[chess_row][chess_col] = cur_color;// 4. 判斷是否有玩家勝利(從當前?棋位置開始判斷是否存在五星連珠)int winner_id = check_win(chess_row, chess_col, cur_color);if (winner_id != 0){json_resp["reason"] = "五星連珠,國服棋王,你無敵了!";}json_resp["result"] = true;json_resp["winner"] = (Json::UInt64)winner_id;return json_resp;}/*處理聊天動作*/Json::Value handle_chat(const Json::Value &req){Json::Value json_resp = req;std::string chat_message = req["message"].asString();if (chat_message.find("垃圾") != std::string::npos || chat_message.find("你干嘛") != std::string::npos){json_resp["result"] = false;json_resp["reason"] = "嘻嘻,請說喜歡你";return json_resp;}json_resp["result"] = true;return json_resp;}/*處理退出動作*/void handle_exit(int uid){Json::Value json_resp;// 如果是下棋狀態中退出,一方勝利if (_status == GAME_START){int winner_id = uid == _white_id ? _black_id : _white_id;json_resp["optype"] = "put_chess";json_resp["result"] = true;json_resp["reason"] = "對?掉線,不戰?勝!";json_resp["room_id"] = (Json::UInt64)_room_id;json_resp["uid"] = uid;json_resp["row"] = -1;json_resp["col"] = -1;json_resp["winner"] = winner_id;int loser_id = winner_id == _white_id ? _black_id : _white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_status = GAME_OVER;broadcast(json_resp);}// 房間中玩家數量--_player_count--;}/*總的請求處理函數,區分不同請求類型,調用不同函數執行對應響應,得到響應進行廣播*/void handle_request(Json::Value &req){// 1. 校驗房間號是否匹配Json::Value json_resp;uint64_t room_id = req["room_id"].asUInt64();if (room_id != _room_id){json_resp["optype"] = req["optype"].asString();json_resp["result"] = false;json_resp["reason"] = "房間號不匹配!";return broadcast(json_resp);}// 2. 根據不同的請求類型調?不同的處理函數if (req["optype"].asString() == "put_chess"){json_resp = handle_chess(req);if (json_resp["winner"].asUInt64() != 0){uint64_t winner_id = json_resp["winner"].asUInt64();uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_status = GAME_OVER;}}else if (req["optype"].asString() == "chat"){json_resp = handle_chat(req);}else{json_resp["optype"] = req["optype"].asString();json_resp["result"] = false;json_resp["reason"] = "未知請求類型";}std::string body;json_util::serialize(json_resp, body);DBG_LOG("房間-?播動作: %s", body.c_str());return broadcast(json_resp);}/*將指定的信息廣播給房間中的所有用戶,即返回響應給所有用戶*/void broadcast(const Json::Value &resp){// 1. 對要響應的信息進?序列化,將Json::Value中的數據序列化成為json格式字符串std::string body;json_util::serialize(resp, body);// 2. 獲取房間中所有??的通信連接// 3. 發送響應信息websocket_server::connection_ptr white_conn =_online_user->get_conn_from_game_room(_white_id);if (white_conn.get() != nullptr){white_conn->send(body);}else{DBG_LOG("房間-?棋玩家連接獲取失敗");}websocket_server::connection_ptr bconn = _online_user->get_conn_from_game_room(_black_id);if (bconn.get() != nullptr){bconn->send(body);}else{DBG_LOG("房間-?棋玩家連接獲取失敗");}return;}
};
using room_ptr = std::shared_ptr<room>;class room_manager
{
private:uint64_t _next_rid;std::mutex _mutex;user_table *_tb_user;online_manager *_online_user;std::unordered_map<uint64_t, room_ptr> _rooms; // 房間id到整個房間的映射std::unordered_map<int, uint64_t> _users; // 用戶id到房間id的映射public:room_manager(user_table *ut, online_manager *om): _next_rid(1000),_tb_user(ut),_online_user(om){DBG_LOG("房間管理模塊初始化完畢!");}~room_manager(){DBG_LOG("房間管理模塊即將銷毀!");}/*兩個用戶匹配成功的用戶創建房間*/room_ptr create_room(int uid1, int uid2){// 1. 校驗兩個??是否都還在游戲?廳中,只有都在才需要創建房間if (_online_user->in_game_hall(uid1) == false){DBG_LOG("??:%d 不在?廳中,創建房間失敗!", uid1);return room_ptr();}if (_online_user->in_game_hall(uid2) == false){DBG_LOG("??:%d 不在?廳中,創建房間失敗!", uid2);return room_ptr();}// 2. 創建房間,將??信息添加到房間中room_ptr rp(new room(_next_rid, _tb_user, _online_user)); // 智能指針管理指針對象,傳入指針進行構造rp->add_white_user(uid1);rp->add_black_user(uid2);// 3. 將房間信息管理起來_rooms.insert(std::make_pair(_next_rid, rp));_users.insert(std::make_pair(uid1, _next_rid));_users.insert(std::make_pair(uid2, _next_rid));_next_rid++;// 4. 返回房間信息return rp;}/*通過房間id獲取房間*/room_ptr get_room_by_rid(uint64_t room){std::unique_lock<std::mutex> lock(_mutex);auto rit = _rooms.find(room);if (rit == _rooms.end()){return room_ptr();}return rit->second; // 等價_rooms[room];}/*通過用戶id獲取房間*/room_ptr get_room_by_uid(int uid){std::unique_lock<std::mutex> lock(_mutex); // 加鎖??// 1. 通過??ID獲取房間IDauto uit = _users.find(uid);if (uit == _users.end()){return room_ptr();}uint64_t rid = uit->second;// 2. 通過房間ID獲取房間信息auto rit = _rooms.find(rid);if (rit == _rooms.end()){return room_ptr();}return rit->second;}/*通過房間id刪除房間*/void remove_room(uint64_t rid){// 因為房間信息,是通過shared_ptr在_rooms中進?管理,因此只要將shared_ptr從_rooms中移除// 則shared_ptr計數器==0,外界沒有對房間信息進?操作保存的情況下就會釋放// 1. 通過房間ID,獲取房間信息room_ptr rp = get_room_by_rid(rid);if (rp.get() == nullptr){return;}// 2. 通過房間信息,獲取房間中所有??的IDuint64_t uid1 = rp->get_white_user();uint64_t uid2 = rp->get_black_user();// 3. 移除房間管理中的??信息std::unique_lock<std::mutex> lock(_mutex);_users.erase(uid1);_users.erase(uid2);// 4. 移除房間管理信息_rooms.erase(rid);// auto it = _rooms.find(room);// if (it == _rooms.end())// {// return;// }// std::unique_lock<std::mutex> lock(_mutex);// _users.erase(_rooms[room]->get_black_user());// _users.erase(_rooms[room]->get_white_user());// _rooms.erase(room);}/*刪除房間中指定??,如果房間中沒有??了,則銷毀房間,??連接斷開時被調?*/void remove_room_by_user(int user){auto it = get_room_by_uid(user);if (it.get() == nullptr){return;}it->handle_exit(user);if (it->player_count() == 0){remove_room(it->id());}}
};
4.7 匹配管理模塊
我們將根據用戶得分維護三個匹配隊列,每次用戶匹配請求都在各自所屬的段位里面進行匹配。
匹配隊列類:包含好多用戶id,mutex和條件變量cond, 還有push,wait,pop,remove等接口。
匹配管理類:?包含三個匹配隊列,同時初始化三個匹配隊列的 線程入口函數,線程入口隊列函數不斷檢測隊列的大小是否超過2,超過則出隊列創建房間,為兩個玩家進行對戰操作.
#pragma once
#include "room.hpp"
#include <list>
#include <condition_variable>// 提供匹配隊列和 匹配隊列的管理類
// 匹配隊列中包含好多用戶id,mutex和條件變量cond, 還有push,wait,pop,remove等接口// 匹配隊列的管理類 ,包含三個匹配隊列,同時初始化三個匹配隊列的 線程入口函數
// 線程入口隊列函數不斷檢測隊列的大小是否超過2,超過則出隊列創建房間,為兩個玩家進行對戰操作
// 其提供add(uid)和del(uid)兩個函數// T就是int類型就是每個隊列中一個個的用戶id
template <class T>
class match_queue
{
private:std::list<T> _list; // 我們使用list是因為我們需要 remove某些用戶idstd::mutex _mutex;std::condition_variable _cond; //條件變量,在該條件變量下 進行wait阻塞等待public:match_queue(){}~match_queue(){}int size(){std::unique_lock<std::mutex> lock(_mutex);return _list.size();}bool empty(){std::unique_lock<std::mutex> lock(_mutex);return _list.empty();}/*阻塞隊列*/void wait(){std::unique_lock<std::mutex> lock(_mutex);_cond.wait(lock);}/*入隊數據,并喚醒線程*/void push(const T &data){std::unique_lock<std::mutex> lock(_mutex);_list.push_back(data);_cond.notify_all();}/*出隊數據*/bool pop(T &data){std::unique_lock<std::mutex> lock(_mutex);if (_list.empty()){return false;}data = _list.front();_list.pop_front();return true;}void remove(T &data){std::unique_lock<std::mutex> lock(_mutex);_list.remove(data);}
};// 需要了解網絡通信接口的格式,匹配成功時回復給兩個用戶什么信息
class matcher
{
private:/*普通選?匹配隊列*/match_queue<int> _q_normal;/*??匹配隊列*/match_queue<int> _q_high;/*?神匹配隊列*/match_queue<int> _q_super;/*對應三個匹配隊列的處理線程*/std::thread _th_normal;std::thread _th_high;std::thread _th_super;room_manager *_rm;user_table *_ut;online_manager *_om;void handle_match(match_queue<int> &mq){while(1){// 隊列人數小于2,則阻塞while(mq.size()<2){mq.wait();}// 出隊兩個玩家,int id1,id2;bool ret= mq.pop(id1);if(ret==false){continue;}ret= mq.pop(id2);if(ret==false){mq.push(id2);continue;}//檢測兩個玩家是否在線websocket_server::connection_ptr conn1=_om->get_conn_from_game_hall(id1);if(conn1.get()==nullptr){mq.push(id2);continue;}websocket_server::connection_ptr conn2=_om->get_conn_from_game_hall(id2);if(conn2.get()==nullptr){mq.push(id1);continue;}//為兩個玩家創建房間room_ptr rp= _rm->create_room(id1,id2);if(rp.get()==nullptr){mq.push(id1);mq.push(id2);continue;}//給兩個玩家返回響應Json::Value resp;resp["result"]=true;resp["optype"]="match_success";std::string body;json_util::serialize(resp,body);conn1->send(body);conn2->send(body);}}// 三個線程的入口函數void th_normal_entry(){handle_match(_q_normal);}void th_high_entry(){handle_match(_q_high);}void th_super_entry(){handle_match(_q_super);}public:matcher(room_manager *rm, user_table *ut, online_manager *om): _rm(rm), _ut(ut), _om(om),_th_normal(&matcher::th_normal_entry,this),_th_high(&matcher::th_high_entry,this),_th_super(&matcher::th_super_entry,this){DBG_LOG("游戲匹配模塊初始化完畢....");}bool add(int id){Json::Value user;bool ret = _ut->select_by_id(id, user);if (ret == false){DBG_LOG("獲取玩家:%d 信息失敗!!", id);return false;}int score = user["score"].asInt();if (score < 2000){_q_normal.push(id);}else if (score >= 2000 && score <= 3000){_q_high.push(id);}else{_q_super.push(id);}return true;}bool del(int id){Json::Value user;bool ret = _ut->select_by_id(id, user);if (ret == false){DBG_LOG("獲取玩家:%d 信息失敗!!", id);return false;}int score = user["score"].asInt();if (score < 2000){_q_normal.remove(id);}else if (score >= 2000 && score <= 3000){_q_high.remove(id);}else{_q_super.remove(id);}return true;}~matcher(){}
};
4.7 服務器模塊
最后服務器模塊應該包含前面所有的模塊,服務器類為第三方庫websocketpp的 websocket服務器,因此其成員應該有這些:
? std::string _web_root; ? // 靜態資源根?錄websocket_server _wssrv; // websocket_server對象user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;
接收到http或websocket連接請求,websocket請求是會調用相關的回調函數,我們只需要注冊這些回調函數即可,同上面所寫的websocket服務器框架相同
http請求,客戶端發送http請求只有 剛開始訪問服務器時,請求注冊或者登陸頁面或者根目錄(靜態資源請求),還有就是點擊注冊或登錄時(功能請求),還有剛進入游戲大廳時,請求用戶信息(功能請求).我們對這些請求進行判斷,執行對應的函數。
注意req獲得的uri是我們再http服務器所發送的/ 后面的資源路徑,它是不帶/的
void http_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();std::string method = req.get_method();if (method == "POST" && uri == "/reg"){return reg(conn);}else if (method == "POST" && uri == "/login"){return login(conn);}else if (method == "GET" && uri == "/info"){return info(conn);}else{return file_handler(conn);}}
客戶端向服務器發送websocket請求有兩次,第一次是大廳獲取用戶信息成功之后,會發送建立大廳長連接請求,第二次是進入游戲房間頁面之后,自動發送建立房間長連接請求,我們對此執行對應函數。
void wsopen_callback(websocketpp::connection_hdl hdl){// websocket長連接 建立成功之后 根據uri分辨是上面的哪一種websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建?了游戲?廳的?連接return wsopen_game_hall(conn);}else if (uri == "/room"){// 建?了游戲房間的?連接return wsopen_game_room(conn);}}
長連接關閉邏輯也同上
void wsclose_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建?了游戲?廳的?連接return wsclose_game_hall(conn);}else if (uri == "/room"){// 建?了游戲房間的?連接return wsclose_game_room(conn);}}
用戶長連接消息請求有兩種,一種是大廳發出的開始匹配請求,第二種是房間發出的下棋或者聊天請求。我們可以按照如下方式得到請求消息的Json::Value對象,注意這個與http請求獲取正文內容的方式有所不同。
std::string req_body = msg->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg){// websocket長連接通信處理回調函數// 1.判斷是哪里的請求websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 游戲?廳?連接的消息return wsmsg_game_hall(conn, msg);}else if (uri == "/room"){return wsmsg_game_room(conn, msg);}}
總的服務器server.hpp
#pragma once#include "room.hpp"
#include "online.hpp"
#include "session.hpp"
#include "matcher.hpp"
#include "db.hpp"
#include <string>#define WWWROOT "./wwwroot/"// 用戶先進行注冊(ajax請求),然后(跳轉)登錄(ajax請求),然后(跳轉)匹配大廳(ajax請求)
// 點擊開始匹配 進入匹配隊列,客戶端需要隔一段時間就問一下是否匹配成功// websocket服務器可以返回http響應,con->setStatus,也可以返回websocket響應(直接send)
class gobang_server
{
private:std::string _web_root; // 靜態資源根?錄 ./wwwroot/ ->./wwwroot/register.htmlwebsocket_server _wssrv; // websocket_server對象user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;// 靜態網頁的返回void file_handler(websocket_server::connection_ptr &conn){websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();std::string file_path = WWWROOT + uri;// 如果請求路徑是一個目錄,則在路徑后面加上loginif (file_path.back() == '/'){file_path += "login.html";}std::string body;bool ret = file_util::read(file_path, body);if (ret == false){std::string No_path = WWWROOT;No_path += "404.html";file_util::read(No_path, body);conn->set_status(websocketpp::http::status_code::not_found);conn->set_body(body);return;}// 5. 設置響應正?conn->set_body(body);conn->set_status(websocketpp::http::status_code::ok);}void http_resp(websocket_server::connection_ptr &conn, bool result,websocketpp::http::status_code::value code, const std::string &reason){Json::Value resp;resp["result"] = result;resp["reason"] = reason;std::string body;json_util::serialize(resp, body);conn->set_status(code);conn->append_header("Content-Type", "application/json");conn->set_body(body);return;}void reg(websocket_server::connection_ptr &conn){// ??注冊功能請求的處理websocketpp::http::parser::request req = conn->get_request();// 1. 獲取到請求正?std::string req_body = conn->get_request_body();// 2. 對正?進?json反序列化,得到??名和密碼Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DBG_LOG("反序列化注冊信息失敗");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "請求的正?格式錯誤");}// 3. 進?數據庫的??新增操作if (login_info["username"].isNull() ||login_info["password"].isNull()){DBG_LOG("??名密碼不完整");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "請輸???名/密碼");}ret = _ut.insert(login_info);if (ret == false){DBG_LOG("向數據庫插?數據失敗");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "??名已經被占?!");}// 如果成功了,則返回200return http_resp(conn, true, websocketpp::http::status_code::ok, "注冊??成功");}/*用戶登錄請求處理*/void login(websocket_server::connection_ptr &conn){// 1. 獲取請求正?,并進?json反序列化,得到??名和密碼std::string req_body = conn->get_request_body();Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DBG_LOG("反序列化登錄信息失敗");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "請求的正?格式錯誤");}// 2. 校驗正?完整性,進?數據庫的??信息驗證if (login_info["username"].isNull() ||login_info["password"].isNull()){DBG_LOG("??名密碼不完整");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "請輸???名/密碼");}ret = _ut.login(login_info);if (ret == false){// 1. 如果驗證失敗,則返回400DBG_LOG("??名密碼錯誤");return http_resp(conn, false,websocketpp::http::status_code::bad_request, "??名密碼錯誤");}// 如果創建成功,則創建一個會話,并通過set-cookie返回會話int uid = login_info["id"].asInt();session_ptr ssp = _sm.create_session(uid, LOGIN);if (ssp.get() == nullptr){DBG_LOG("創建會話失敗");return http_resp(conn, false,websocketpp::http::status_code::internal_server_error, "創建會話失敗");}_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);// 4. 設置響應頭部:Set-Cookie, 將sessionid通過cookie返回std::string cookie_session_id = "SSID=" + std::to_string(ssp->ssid());conn->append_header("Set-Cookie", cookie_session_id);return http_resp(conn, true, websocketpp::http::status_code::ok,"登錄成功");}bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val){// Cookie: SSID=XXX; path=/; Cookie之間以;作為間隔,// 1. 我們對字符串進?分割,得到各個單個的cookie信息std::string sep = ";";std::vector<std::string> arr;string_util::split(cookie_str, sep, arr);for (auto str : arr){// 2. 對單個cookie字符串,以 = 為間隔進?分割,得到key和valstd::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2){continue;}if (tmp_arr[0] == key){val = tmp_arr[1];return true;}}return false;}// 用戶會將 Cookie=abc 返回 先找cookie,再找cookie對應的SSID,再找SSID對應的會話,再找用戶信息返回,然后設置會話過期時間void info(websocket_server::connection_ptr &conn){// ??信息獲取功能請求的處理Json::Value err_resp;// 1. 獲取請求信息中的Cookie,從Cookie中獲取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){// 如果沒有cookie,返回錯誤:沒有cookie信息,讓客?端重新登錄return http_resp(conn, true,websocketpp::http::status_code::bad_request, "無cookie信息,請重新登錄");}// 1.5. 從cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false){// cookie中沒有ssid,返回錯誤:沒有ssid信息,讓客?端重新登錄return http_resp(conn, true,websocketpp::http::status_code::bad_request, "找不到cookie的對應ssid信息,請重新登錄");}// 2. 在session管理中查找對應的會話信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() == nullptr){// 沒有找到session,則認為登錄已經過期,需要重新登錄return http_resp(conn, true,websocketpp::http::status_code::bad_request, "登錄過期,請重新登錄");}// 3. 從數據庫中取出??信息,進?序列化發送給客?端uint64_t uid = ssp->get_user();Json::Value user_info;ret = _ut.select_by_id(uid, user_info);if (ret == false){// 獲取??信息失敗,返回錯誤:找不到??信息return http_resp(conn, true,websocketpp::http::status_code::bad_request, "找不到??信息,請重新登錄");}std::string body;json_util::serialize(user_info, body);conn->set_body(body);conn->append_header("Content-Type", "application/json");conn->set_status(websocketpp::http::status_code::ok);// 4. 刷新session的過期時間_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}// 一般的回調函數傳入一個websocket服務器和連接管理句柄(必須傳),我們有this可以訪問服務器// 通過 服務器和連接處理句柄 我們可以獲取這個連接,這個連接被我們傳入各個功能函數void http_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();std::string method = req.get_method();if (method == "POST" && uri == "/reg"){return reg(conn);}else if (method == "POST" && uri == "/login"){return login(conn);}else if (method == "GET" && uri == "/info"){return info(conn);}else{return file_handler(conn);}}// 用戶建立長連接之后,服務器使用send發送信息給客戶端void ws_resp(websocket_server::connection_ptr conn, Json::Value &resp){std::string body;json_util::serialize(resp, body);conn->send(body);}// 封裝從 客戶端的cookie 獲取session信息session_ptr get_session_by_cookie(websocket_server::connection_ptr conn){Json::Value err_resp;// 1. 獲取請求信息中的Cookie,從Cookie中獲取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){err_resp["optype"] = "hall_ready";err_resp["result"] = false;err_resp["reason"] = "沒有cookie信息,請重新登錄";ws_resp(conn, err_resp);return session_ptr();}std::string value;bool ret = get_cookie_val(cookie_str, "SSID", value);if (ret == false){err_resp["optype"] = "hall_ready";err_resp["result"] = false;err_resp["reason"] = "cookie中沒有用戶會話信息,請重新登錄";ws_resp(conn, err_resp);return session_ptr();}session_ptr ssp = _sm.get_session_by_ssid(std::stol(value));if (ssp.get() == nullptr){// 沒有找到session,則認為登錄已經過期,需要重新登錄err_resp["optype"] = "hall_ready";err_resp["reason"] = "沒有找到session信息,需要重新登錄";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}return ssp;}void wsopen_game_hall(websocket_server::connection_ptr conn){// 游戲?廳?連接建?成功Json::Value resp_json;// 1. 登錄驗證--判斷當前客?端是否已經成功登錄session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2. 判斷當前客?端是否是重復登錄if (_om.in_game_hall(ssp->get_user()) ||_om.in_game_room(ssp->get_user())){resp_json["optype"] = "hall_ready";resp_json["reason"] = "玩家重復登錄!";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 3. 將當前客?端以及連接加?到游戲?廳,游戲大廳維護了用戶id到連接的map_om.enter_game_hall(ssp->get_user(), conn);// 4. 給客?端響應游戲?廳連接建?成功resp_json["optype"] = "hall_ready";resp_json["reason"] = "游戲大廳進入成功!";resp_json["result"] = true;ws_resp(conn, resp_json);// 5. 記得將session設置為永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);}// 邏輯:大廳中加入匹配隊列,線程創建房間并返回前端match_success, 前端離開在線用戶管理模塊void wsopen_game_room(websocket_server::connection_ptr conn){// 1. 獲取當前客戶端的sessionsession_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 2.判斷該用戶是否在其它房間或者大廳中,如果是則出錯Json::Value resp_json;if (_om.in_game_hall(ssp->get_user()) || _om.in_game_room(ssp->get_user())){resp_json["optype"] = "room_ready";resp_json["reason"] = "玩家重復登錄!";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 3.判斷當前用戶是否創建好房間room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr){resp_json["optype"] = "room_ready";resp_json["reason"] = "沒有找到玩家的房間信息";resp_json["result"] = false;return ws_resp(conn, resp_json);}// 4. 將當前??添加到在線??管理的游戲房間中_om.enter_game_room(ssp->get_user(), conn);// 5. 將session重新設置為永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);// 6. 向前端回復房間準備完畢resp_json["optype"] = "room_ready";resp_json["result"] = true;resp_json["room_id"] = (Json::UInt64)rp->id();resp_json["uid"] = ssp->get_user();resp_json["white_id"] = rp->get_white_user();resp_json["black_id"] = rp->get_black_user();return ws_resp(conn, resp_json);}// 長連接建立有兩種,第一種是進入匹配隊列,第二種是進入游戲房間的時候void wsopen_callback(websocketpp::connection_hdl hdl){// websocket長連接 建立成功之后 根據uri分辨是上面的哪一種websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建?了游戲?廳的?連接return wsopen_game_hall(conn);}else if (uri == "/room"){// 建?了游戲房間的?連接return wsopen_game_room(conn);}}// 玩家離開掉網頁之后,會發送一個關掉網頁連接的請求,調用該函數void wsclose_game_hall(websocket_server::connection_ptr conn){// 游戲?廳?連接斷開的處理// 1. 登錄驗證--判斷當前客?端是否已經成功登錄session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 1. 將玩家從游戲?廳中移除_om.exit_game_hall(ssp->get_user());// 2. 將session恢復?命周期的管理,設置定時銷毀_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}void wsclose_game_room(websocket_server::connection_ptr conn){// 獲取會話信息,識別客?端session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return;}// 1. 將玩家從在線??管理中移除_om.exit_game_room(ssp->get_user());// 2. 將session回復?命周期的管理,設置定時銷毀_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);// 3. 將玩家從游戲房間中移除,房間中所有??退出了就會銷毀房間_rm.remove_room_by_user(ssp->get_user());}void wsclose_callback(websocketpp::connection_hdl hdl){websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 建?了游戲?廳的?連接return wsclose_game_hall(conn);}else if (uri == "/room"){// 建?了游戲房間的?連接return wsclose_game_room(conn);}}// 玩家進入大廳建立長連接,同時玩家開始/停止匹配請求時 調用該函數void wsmsg_game_hall(websocket_server::connection_ptr conn, websocket_server::message_ptr msg){Json::Value resp_json;// 1. ?份驗證,當前客?端到底是哪個玩家session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){return; // get_session_by_cookie內部已經返回了錯誤響應}// 2. 獲取請求信息Json::Value req_json;std::string req_body = msg->get_payload();bool ret = json_util::unserialize(req_body, req_json);if (ret == false){resp_json["result"] = false;resp_json["reason"] = "請求信息解析失敗";return ws_resp(conn, resp_json);}// 3.對請求進行處理if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start"){// 開始對戰匹配:通過匹配模塊,將??添加到匹配隊列中_mm.add(ssp->get_user());resp_json["optype"] = "match_start";resp_json["result"] = true;return ws_resp(conn, resp_json);}else if (!req_json["optype"].isNull() &&req_json["optype"].asString() == "match_stop"){// 停?對戰匹配:通過匹配模塊,將??從匹配隊列中移除_mm.del(ssp->get_user());resp_json["optype"] = "match_stop";resp_json["result"] = true;return ws_resp(conn, resp_json);}resp_json["optype"] = "unknow";resp_json["reason"] = "請求類型未知";resp_json["result"] = false;return ws_resp(conn, resp_json);}void wsmsg_game_room(websocket_server::connection_ptr conn, websocket_server::message_ptr msg){// 進入房間頁面,建立房間的長連接Json::Value resp_json;// 1. 獲取當前客?端的sessionsession_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr){DBG_LOG("房間-沒有找到會話信息");return;}// 2. 獲取客?端房間信息room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr){resp_json["optype"] = "unknow";resp_json["reason"] = "沒有找到玩家的房間信息";resp_json["result"] = false;DBG_LOG("房間-沒有找到玩家房間信息");return ws_resp(conn, resp_json);}// 3. 對消息進?反序列化Json::Value req_json;std::string req_body = msg->get_payload();bool ret = json_util::unserialize(req_body, req_json);if (ret == false){resp_json["optype"] = "unknow";resp_json["reason"] = "請求解析失敗";resp_json["result"] = false;DBG_LOG("房間-反序列化請求失敗");return ws_resp(conn, resp_json);}DBG_LOG("房間:收到房間請求,開始處理....");// 4. 通過房間模塊進?消息請求的處理return rp->handle_request(req_json);}void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg){// websocket長連接通信處理回調函數// 1.判斷是哪里的請求websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall"){// 游戲?廳?連接的消息return wsmsg_game_hall(conn, msg);}else if (uri == "/room"){return wsmsg_game_room(conn, msg);}}public:/*進?成員初始化,以及服務器回調函數的設置*/gobang_server(const std::string &host,const std::string &user,const std::string &pass,const std::string &dbname,uint16_t port = 3306,const std::string &wwwroot = WWWROOT) : _web_root(wwwroot), _ut(host, user, pass, dbname, port),_rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om){_wssrv.set_access_channels(websocketpp::log::alevel::none);_wssrv.init_asio();_wssrv.set_reuse_addr(true);_wssrv.set_http_handler(std::bind(&gobang_server::http_callback,this, std::placeholders::_1));_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback,this, std::placeholders::_1));_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this,std::placeholders::_1));_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this,std::placeholders::_1, std::placeholders::_2));}/*啟動服務器*/void start(int port){_wssrv.listen(port);_wssrv.start_accept();_wssrv.run();}
};
主函數gobang.cc
#include "room.hpp"
#include"session.hpp"#define HOST "127.0.0.1"
#define PORT 3306
#define USER "root"
#define PASSWD "123456"
#define DBNAME "gobang"
#include"matcher.hpp"
#include"server.hpp"int main()
{// user_table ut(HOST, USER, PASSWD, DBNAME, PORT);// match_queue<int> mq;// online_manager om;// room_manager rm(&ut,&om);// matcher mt(&rm,&ut,&om);gobang_server s(HOST, USER, PASSWD, DBNAME, PORT);s.start(7080);return 0;
}