国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁 > news >正文

棗莊做網(wǎng)站優(yōu)化網(wǎng)站客服系統(tǒng)

棗莊做網(wǎng)站優(yōu)化,網(wǎng)站客服系統(tǒng),做網(wǎng)站找人,手機(jī)app開發(fā)技術(shù)目錄 項(xiàng)目介紹 開發(fā)環(huán)境 核心技術(shù) 項(xiàng)目前置知識點(diǎn)介紹 Websocketpp 1. WebSocket基本認(rèn)識 2. WebSocket協(xié)議切換原理解析 3. WebSocket報(bào)文格式 4. Websocketpp介紹 5. 搭建一個(gè)簡單WebSocket服務(wù)器 JsonCpp 1. Json格式的基本認(rèn)識 2. JsonCpp介紹 3. 序列化與反序…

目錄

項(xiàng)目介紹

開發(fā)環(huán)境

核心技術(shù)

項(xiàng)目前置知識點(diǎn)介紹

Websocketpp

1. WebSocket基本認(rèn)識

2. WebSocket協(xié)議切換原理解析

3. WebSocket報(bào)文格式

4. Websocketpp介紹

5. 搭建一個(gè)簡單WebSocket服務(wù)器

JsonCpp

1. Json格式的基本認(rèn)識

2. JsonCpp介紹

3. 序列化與反序列化接口調(diào)用demo

MySQL API

1. MySQL數(shù)據(jù)庫的訪問操作流程

2. API介紹

3. API接口調(diào)用demo

項(xiàng)目結(jié)構(gòu)設(shè)計(jì)

項(xiàng)目模塊劃分

業(yè)務(wù)處理模塊的子模塊劃分

項(xiàng)目流程圖

用戶流程圖

服務(wù)器流程圖

項(xiàng)目類實(shí)現(xiàn)

工具類

日志宏封裝

MySQL_API封裝

Json格式數(shù)據(jù)的序列化和反序列封裝

字符串分割封裝

文件讀取封裝

數(shù)據(jù)管理類

數(shù)據(jù)庫設(shè)計(jì)

實(shí)現(xiàn)user_table類

在線用戶管理類實(shí)現(xiàn)

游戲房間管理類

房間類實(shí)現(xiàn)

房間管理類實(shí)現(xiàn)

| 房間類和房間管理類整合?|

session管理類

session的基本認(rèn)識

session類實(shí)現(xiàn)

session管理類實(shí)現(xiàn)

| session類和session管理類整合 |

玩家匹配管理類

匹配隊(duì)列類實(shí)現(xiàn)

匹配管理類實(shí)現(xiàn)

| 匹配隊(duì)列類和匹配管理類整合 |

?服務(wù)器類

Restful風(fēng)格的網(wǎng)絡(luò)通信接口設(shè)計(jì)

靜態(tài)資源請求與響應(yīng)格式

動態(tài)資源請求與響應(yīng)格式

客戶端對服務(wù)器的請求

服務(wù)器類實(shí)現(xiàn)

搭建基本的服務(wù)器框架

HTTP請求處理函數(shù)

靜態(tài)資源請求處理函數(shù)

用戶注冊請求處理函數(shù)

用戶登錄請求處理函數(shù)

獲取用戶信息請求處理函數(shù)

WebSocket長連接建立成功后的處理函數(shù)

用戶登錄驗(yàn)證函數(shù)(登錄成功則返回用戶session)

游戲大廳長連接建立成功的處理函數(shù)

游戲房間長連接建立成功的處理函數(shù)

WebSocket長連接斷開前的處理函數(shù)

游戲大廳長連接斷開的處理函數(shù)

游戲房間長連接斷開的處理函數(shù)

WebSocket長連接通信處理函數(shù)

游戲大廳請求處理函數(shù)(游戲匹配請求/停止匹配請求)

游戲房間請求處理函數(shù)(下棋請求/聊天請求)

| 服務(wù)器類所有函數(shù)整合 |

守護(hù)進(jìn)程化

項(xiàng)目源碼


項(xiàng)目介紹

本項(xiàng)目主要實(shí)現(xiàn)一個(gè)網(wǎng)頁版的五子棋對戰(zhàn)游戲,其當(dāng)前版本支持以下核心功能:

  • 用戶管理:實(shí)現(xiàn)用戶注冊、用戶登錄、獲取用戶信息、用戶游戲分?jǐn)?shù)記錄、用戶比賽場次記錄等。
  • 匹配對戰(zhàn):實(shí)現(xiàn)玩家在瀏覽器網(wǎng)頁端根據(jù)玩家的游戲分?jǐn)?shù)進(jìn)行匹配游戲?qū)κ?#xff0c;并進(jìn)行五子棋游戲?qū)?zhàn)的功能。
  • 實(shí)時(shí)聊天:實(shí)現(xiàn)在游戲房間內(nèi)對戰(zhàn)的兩個(gè)玩家可以進(jìn)行實(shí)時(shí)的聊天功能。

后續(xù)還可追加以下功能:

  • 落子計(jì)時(shí)
  • 棋局房間內(nèi)觀戰(zhàn)
  • 人機(jī)對戰(zhàn)

開發(fā)環(huán)境

  • Linux(CentOS-7.6)
  • Visual Studio Code/Vim
  • g++/gdb
  • Makefile

核心技術(shù)

  • HTTP/WebSocket
  • Websocketpp
  • JsonCpp
  • MySQL
  • C++11
  • BlockQueue
  • HTML/CSS/JS/AJAX

項(xiàng)目前置知識點(diǎn)介紹

Websocketpp

1. WebSocket基本認(rèn)識

WebSocket是從HTML5開始支持的一種網(wǎng)頁端和服務(wù)端保持長連接的消息推送機(jī)制。

WebSocket協(xié)議相較于HTTP協(xié)議的最大不同點(diǎn)在于,WebSocket協(xié)議支持服務(wù)端主動向客戶端發(fā)送消息,這是HTTP協(xié)議所不具備的!

HTTP本質(zhì)上就是一個(gè)“請求-響應(yīng)”協(xié)議,客戶端和服務(wù)器的通信屬于“一問一答”的形式。在HTTP協(xié)議下服務(wù)器是屬于被動的一方,如果客戶端不給服務(wù)器發(fā)送請求,服務(wù)器是無法主動給客戶端發(fā)送響應(yīng)的。

HTTP協(xié)議切換到WebSocket協(xié)議? >>? 短連接切換到長連接

本項(xiàng)目中的在線聊天功能以及實(shí)時(shí)顯示落子功能,都需要支持服務(wù)器主動給客戶端發(fā)送響應(yīng)信息,所以引入Websocketpp這個(gè)庫。

2. WebSocket協(xié)議切換原理解析

WebSocket協(xié)議本質(zhì)上是一個(gè)基于TCP的協(xié)議。為了建立一個(gè)WebSocket連接,客戶端瀏覽器首先要向服務(wù)器發(fā)起一個(gè)HTTP請求,這個(gè)請求和通常的HTTP請求不同,其中包含了些附加頭信息,通過這些附加頭信息完成握手過程并升級協(xié)議的過程。

3. WebSocket報(bào)文格式

重點(diǎn)關(guān)注以下字段:

  • FIN:WebSocket傳輸數(shù)據(jù)以消息為概念單位,一個(gè)消息有可能由一個(gè)或多個(gè)幀組成,FIN字段為1表示末尾幀。
  • RSV1~3:保留字段,只在擴(kuò)展時(shí)使用,若未啟用擴(kuò)展則應(yīng)置1,若收到不全為0的數(shù)據(jù)幀,且未協(xié)商擴(kuò)展則立即終止連接。
  • opcode:標(biāo)志當(dāng)前數(shù)據(jù)幀的類型。? ? ? ? ? ? ? ?
  • mask:表示Payload數(shù)據(jù)是否被編碼,若為1則必有Mask-Key,用于解碼Payload數(shù)據(jù)。僅客戶端發(fā)送給服務(wù)端的消息需要設(shè)置。
  • Payload length:數(shù)據(jù)載荷的長度,單位是字節(jié),有可能為7位、7+16位、7+64位。假設(shè)Payload length = x。
  • Mask-Key:當(dāng)mask為1時(shí)存在,長度為4字節(jié),解碼規(guī)則:DECODED[i] = ENCODED[i] ^ MASK[i % 4]。
  • Payload data:報(bào)文攜帶的載荷數(shù)據(jù)。

4. Websocketpp介紹

WebSocketpp是一個(gè)跨平臺的開源(BSD許可證)頭部專用C++庫,它實(shí)現(xiàn)了RFC6455(WebSocket 協(xié)議)和RFC7692(WebSocketCompression Extensions)。它允許將WebSocket客戶端和服務(wù)器功能集成到C++程序中。在最常見的配置中,全功能網(wǎng)絡(luò)I/O由Asio網(wǎng)絡(luò)庫提供。

項(xiàng)目內(nèi)常用Websocketpp常用接口:

日志相關(guān)接口:

void set_access_channels(log::level channels); //設(shè)置?志打印等級

回調(diào)函數(shù)相關(guān)接口:

針對不同的事件設(shè)置不同的處理函數(shù)。

搭建完WebSocket服務(wù)器后,給不同的事件設(shè)置不同的處理函數(shù)指針,這些指針指向指定的函數(shù)。當(dāng)服務(wù)器收到了指定數(shù)據(jù),觸發(fā)了指定事件后,就會通過函數(shù)指針去調(diào)用對應(yīng)的事件處理函數(shù)。

此時(shí)程序員只需要編寫對應(yīng)的業(yè)務(wù)處理函數(shù),并設(shè)置好對應(yīng)的函數(shù)指針的指向,即可做到當(dāng)對應(yīng)事件觸發(fā)時(shí),執(zhí)行對應(yīng)的業(yè)務(wù)函數(shù)。

void set_open_handler(open_handler h);       //websocket握?成功回調(diào)處理函數(shù)
void set_close_handler(close_handler h);     //websocket連接關(guān)閉回調(diào)處理函數(shù)
void set_message_handler(message_handler h); //websocket消息回調(diào)處理函數(shù)
void set_http_handler(http_handler h);       //http請求回調(diào)處理函數(shù)

通信連接相關(guān)接口:

// 給客戶端發(fā)送信息
void send(connection_hdl hdl, std::string& payload, frame::opcode::value op);
void send(connection_hdl hdl, void* payload, size_t len, frame::opcode::value op);// 關(guān)閉連接
void close(connection_hdl hdl, close::status::value code, std::string& reason);// 通過connection_hdl獲取對應(yīng)的connection_ptr
connection_ptr get_con_from_hdl(connection_hdl hdl);

其他服務(wù)器搭建的接口:

// 初始化asio框架
void init_asio();// 是否啟用地址
void set_reuse_addr(bool value);// 開始獲取新連接
void start_accept();// 設(shè)置endpoint的綁定監(jiān)聽端?
void listen(uint16_t port);// 啟動服務(wù)器
std::size_t run();// 設(shè)置定時(shí)任務(wù)
timer_ptr set_timer(long duration, timer_handler callback);

5. 搭建一個(gè)簡單WebSocket服務(wù)器

step1:實(shí)例化一個(gè)WebSocket的server對象。

step2:設(shè)置日志輸出等級。

step3:初始化asio框架中的調(diào)度器

step4:設(shè)置業(yè)務(wù)處理回調(diào)函數(shù)。

step5:設(shè)置監(jiān)聽端口。

step6:開始獲取tcp連接。

step7:啟動服務(wù)器。

#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>typedef websocketpp::server<websocketpp::config::asio> websocketsvr_t;void wsopen_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl)
{std::cout << "websocket建立連接" << std::endl;
}void wsclose_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl)
{std::cout << "websocket連接斷開" << std::endl;
}void wsmsg_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg)
{// 獲取通信連接websocketsvr_t::connection_ptr conn = wssvr->get_con_from_hdl(hdl);std::cout << "msg: " << msg->get_payload() << std::endl;// 將客戶端發(fā)送的信息作為響應(yīng)std::string resp = "client say: " + msg->get_payload();// 將響應(yīng)信息發(fā)送給客戶端conn->send(resp);
}// 給客戶端返回一個(gè)hello world頁面
void http_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl)
{// 獲取通信連接websocketsvr_t::connection_ptr conn = wssvr->get_con_from_hdl(hdl);// 打印請求正文std::cout << "body: " << conn->get_request_body() << std::endl;// 獲取http請求websocketpp::http::parser::request req = conn->get_request();// 打印請求方法和urlstd::cout << "method: " << req.get_method() << std::endl;std::cout << "uri: " << req.get_uri() << std::endl;// 設(shè)置響應(yīng)正文std::string body = "<html><body><h1>Hello World</h1></body></html>";conn->set_body(body);conn->append_header("Content-Type", "text/html");conn->set_status(websocketpp::http::status_code::ok);
}int main()
{// 1. 實(shí)例化server對象// 2. 設(shè)置日志等級// 3. 初始化asio調(diào)度器,設(shè)置地址重用// 4. 設(shè)置回調(diào)函數(shù)// 5. 設(shè)置監(jiān)聽端口// 6. 開始獲取新連接// 7. 啟動服務(wù)器// 1.websocketsvr_t wssvr;// 2.wssvr.set_access_channels(websocketpp::log::alevel::none); // 禁止打印所有日志// 3.wssvr.init_asio();wssvr.set_reuse_addr(true);// 4.wssvr.set_open_handler(std::bind(wsopen_callback, &wssvr, std::placeholders::_1));wssvr.set_close_handler(std::bind(wsclose_callback, &wssvr, std::placeholders::_1));wssvr.set_message_handler(std::bind(wsmsg_callback, &wssvr, std::placeholders::_1, std::placeholders::_2));wssvr.set_http_handler(std::bind(http_callback, &wssvr, std::placeholders::_1));// 5.wssvr.listen(8080);// 6.wssvr.start_accept();// 7.wssvr.run();return 0;
}

使用瀏覽器直接訪問主機(jī)ip和port:

下面寫一個(gè)簡單的前端客戶端界面,用于連接剛剛搭建的WebSocket服務(wù)器。

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Test Websocket</title>
</head><body><input type="text" id="message"><button id="submit">提交</button><script>// 創(chuàng)建 websocket 實(shí)例// ws://111.230.200.206:8080// 類?http// ws表示websocket協(xié)議// 111.230.200.206 表示服務(wù)器地址// 8080表示服務(wù)器綁定的端?let websocket = new WebSocket("ws://111.230.200.206:8080");// 處理連接打開的回調(diào)函數(shù)websocket.onopen = function () {alert("連接建?");}// 處理收到消息的回調(diào)函數(shù)// 控制臺打印消息websocket.onmessage = function (e) {alert("收到消息: " + e.data);}// 處理連接異常的回調(diào)函數(shù)websocket.onerror = function () {alert("連接異常");}// 處理連接關(guān)閉的回調(diào)函數(shù)websocket.onclose = function () {alert("連接關(guān)閉");}// 實(shí)現(xiàn)點(diǎn)擊按鈕后, 通過websocket實(shí)例向服務(wù)器發(fā)送請求let input = document.querySelector('#message');let button = document.querySelector('#submit');button.onclick = function () {alert("發(fā)送消息: " + input.value);websocket.send(input.value);}</script>
</body></html>

建立連接:

發(fā)送信息:

關(guān)閉瀏覽器:

實(shí)際上該項(xiàng)目五子棋對戰(zhàn)游戲后續(xù)的實(shí)現(xiàn)就是圍繞以上這四個(gè)函數(shù)來實(shí)現(xiàn)的!

JsonCpp

1. Json格式的基本認(rèn)識

Json是?種數(shù)據(jù)交換格式,它采用完全獨(dú)立于編程語言的文本格式來存儲和表示數(shù)據(jù)。

例如: 我們想表示一個(gè)同學(xué)的學(xué)生信息

| C++代碼 |

string name = "nK";
int age = 21;
float score[3] = {88.5, 99, 58};

| Json |

{"姓名": "nK","年齡": 21,"成績": [88.5, 99, 58]
}

Json的數(shù)據(jù)類型包括對象,數(shù)組,字符串,數(shù)字等。

  • 對象:使用花括號 {} 括起來的表示一個(gè)對象。
  • 數(shù)組:使用中括號 [] 括起來的表示一個(gè)數(shù)組。
  • 字符串:使用常規(guī)雙引號 "" 括起來的表示一個(gè)字符串。
  • 數(shù)字:包括整形和浮點(diǎn)型,直接使用。

2. JsonCpp介紹

Jsoncpp庫主要是用于實(shí)現(xiàn)Json格式數(shù)據(jù)的序列化和反序列化,它實(shí)現(xiàn)了將Json數(shù)據(jù)對象組織成為Json格式字符串,以及將Json格式字符串解析得到Json數(shù)據(jù)對象的功能。

| Json數(shù)據(jù)對象類的表示?|

class Json::Value
{Value& operator=(const Value& other);      // Value重載了[]和=,因此所有的賦值和獲取數(shù)據(jù)都可以通過Value& operator[](const std::string& key); // 簡單的?式完成 val["name"] = "xx";Value& operator[](const char* key);Value removeMember(const char* key);             // 移除元素const Value& operator[](ArrayIndex index) const; // val["score"][0]Value& append(const Value& value);               // 添加數(shù)組元素 val["score"].append(88);ArrayIndex size() const;                         // 獲取數(shù)組元素個(gè)數(shù) val["score"].size();bool isNull();                                   // ?于判斷是否存在某個(gè)字段std::string asString() const;                    // 轉(zhuǎn)string string name = val["name"].asString();const char* asCString() const;                   // 轉(zhuǎn)char* char* name = val["name"].asCString();int asInt() const;                               // 轉(zhuǎn)int int age = val["age"].asInt();float asFloat() const;                           // 轉(zhuǎn)float float weight = val["weight"].asFloat();bool asBool() const;                             // 轉(zhuǎn)bool bool ok = val["ok"].asBool();
};

| Json::Value對象特性?|

Json::Value root;
root["v1"] = 1;std::cout << root["v2"] << std::endl;

訪問Json::Value對象中一個(gè)不存在的字段,該字段以null返回。

JsonCpp庫主要借助三個(gè)類以及其對應(yīng)的少量成員函數(shù)完成序列化和反序列化的工作。

| 序列化接口 |

class JSON_API StreamWriter
{virtual int write(Value const& root, std::ostream* sout) = 0;
}class JSON_API StreamWriterBuilder : public StreamWriter::Factory
{virtual StreamWriter* newStreamWriter() const;
}

| 反序列化接口 |

class JSON_API CharReader
{virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, std::string* errs) = 0;
}class JSON_API CharReaderBuilder : public CharReader::Factory
{virtual CharReader* newCharReader() const;
}

3. 序列化與反序列化接口調(diào)用demo

下面是一個(gè)簡單的demo,調(diào)用JsonCpp庫中的序列化和反序列化接口對數(shù)據(jù)進(jìn)行序列化和反序列化操作。

#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <jsoncpp/json/json.h>// 序列化
std::string serialize(const Json::Value& root)
{// 1. 實(shí)例化一個(gè)StreamWriterBuilder工廠類對象Json::StreamWriterBuilder swb;// 2. 通過StreamWriterBuilder工廠類對象實(shí)例化一個(gè)StreamWriter對象Json::StreamWriter* sw = swb.newStreamWriter();// 3. 使用StreamWriter對象,對Json::Value對象中存儲的數(shù)據(jù)進(jìn)行序列化std::stringstream ss;sw->write(root, &ss);delete sw; // sw是new出來的記得釋放return ss.str();
}// 反序列化
Json::Value unserialize(const std::string& str)
{// 1. 實(shí)例化一個(gè)CharReaderBuilder工廠類對象Json::CharReaderBuilder crb;// 2. 通過CharReaderBuilder工廠類對象實(shí)例化一個(gè)CharReader對象Json::CharReader* cr = crb.newCharReader();// 3. 創(chuàng)建一個(gè)Json::Value對象存儲解析后的數(shù)據(jù)Json::Value root;// 4. 使用CharReader對象,對str字符串進(jìn)行Json格式的反序列化std::string err;cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);delete cr;return root;
}// 使用JosnCpp庫進(jìn)行多個(gè)數(shù)據(jù)對象的序列化與反序列化
int main()
{// 將需要進(jìn)行序列化的數(shù)據(jù)存儲到Json::Value對象中Json::Value student;student["name"] = "nK";student["age"] = 21;student["score"].append(98);student["score"].append(100);student["score"].append(80);std::string str = serialize(student);std::cout << "序列化結(jié)果:\n" << str << std::endl;Json::Value studentTmp = unserialize(str);// 輸出Json格式的數(shù)據(jù)std::cout << "反序列化結(jié)果:" << std::endl;std::cout << "name: " << studentTmp["name"].asString() << std::endl;std::cout << "age: " << studentTmp["age"].asInt() << std::endl;for (int i = 0; i < studentTmp["score"].size(); ++i){std::cout << "score" << studentTmp["score"][i].asFloat() << std::endl;}    return 0;
}

MySQL API

1. MySQL數(shù)據(jù)庫的訪問操作流程

① 客戶端初始化過程

  1. 初始化MySQL操作句柄。
  2. 連接MySQL服務(wù)器。
  3. 設(shè)置客戶端字符集
  4. 選擇想要操作的數(shù)據(jù)庫

② 客戶端對數(shù)據(jù)庫中數(shù)據(jù)的操作

  1. 執(zhí)行SQL語句
  2. 若SQL語句是查詢語句,則將查詢結(jié)果保存到本地
  3. 獲取查詢結(jié)果集中的結(jié)果條數(shù)
  4. 遍歷獲取結(jié)果集中的每一條數(shù)據(jù)進(jìn)行處理
  5. 釋放結(jié)果集
  6. 釋放MySQL操作句柄

2. API介紹

|?MySQL操作句柄初始化 |

MYSQL* mysql_init(MYSQL* mysql);

參數(shù)說明:

mysql為空則動態(tài)申請句柄空間進(jìn)行初始化

返回值:

成功返回句柄指針,失敗返回NULL

|?連接MySQL服務(wù)器 |

MYSQL* mysql_real_connect(MYSQL* mysql, const char* host, const char* user,const char* passwd, const char* db, unsigned int port,const char* unix_socket, unsigned long client_flag);

參數(shù)說明:

mysql ---- 初始化完成的句柄

host ---- 連接的MySQL服務(wù)器的地址

user ---- 連接的服務(wù)器的用戶名

passwd ---- 連接的服務(wù)器的密碼

db ---- 默認(rèn)選擇的數(shù)據(jù)庫名稱

port ---- 連接的服務(wù)器的端口:默認(rèn)是3306端口

unix_socket ---- 通信管道文件或者socket文件,通常設(shè)置為NULL

client_flag ---- 客戶端標(biāo)志位,通常置為0

返回值:

成功返回句柄指針,失敗返回NULL

|?設(shè)置當(dāng)前客戶端的字符集 |

int mysql_set_character_set(MYSQL* mysql, const char* csname);

參數(shù)說明:

mysql ---- 初始化完成的句柄

csname ---- 字符集名稱,通常為 "utf8"

返回值:

成功返回0, 失敗返回非0

|?選擇操作的數(shù)據(jù)庫 |

int mysql_select_db(MYSQL* mysql, const char* db);

參數(shù)說明:

mysql ---- 初始化完成的句柄

db ---- 要切換選擇的數(shù)據(jù)庫名稱

返回值:

成功返回0, 失敗返回非0

| 執(zhí)行SQL語句 |

int mysql_query(MYSQL* mysql, const char* stmt_str);

參數(shù)說明:

mysql ---- 初始化完成的句柄

stmt_str ---- 要執(zhí)?的SQL語句

返回值:

成功返回0, 失敗返回非0

| 保存查詢結(jié)果到本地 |

MYSQL_RES* mysql_store_result(MYSQL* mysql);

參數(shù)說明:

mysql ---- 初始化完成的句柄

返回值:

成功返回結(jié)果集的指針,失敗返回NULL

|?獲取結(jié)果集中的行數(shù) |

uint64_t mysql_num_rows(MYSQL_RES* result);

參數(shù)說明:

result ---- 保存到本地的結(jié)果集地址

返回值:

結(jié)果集中數(shù)據(jù)的條數(shù)

|?獲取結(jié)果集中的列數(shù) |

unsigned int mysql_num_fields(MYSQL_RES* result);

參數(shù)說明:

result ---- 保存到本地的結(jié)果集地址

返回值:

結(jié)果集中每一條數(shù)據(jù)的列數(shù)

|?遍歷結(jié)果集 |

MYSQL_ROW mysql_fetch_row(MYSQL_RES* result);

這個(gè)接口會保存當(dāng)前讀取結(jié)果位置,每次獲取的都是下?條數(shù)據(jù)。

參數(shù)說明:

result ---- 保存到本地的結(jié)果集地址

返回值:

實(shí)際上是一個(gè)char**的指針,將每一條數(shù)據(jù)做成了字符串指針數(shù)組

row[0] ---- 第0列

row[1] ---- 第1列......

|?釋放結(jié)果集?|

void mysql_free_result(MYSQL_RES* result);

參數(shù)說明:

result ---- 保存到本地的結(jié)果集地址

|?關(guān)閉數(shù)據(jù)庫客戶端連接,銷毀句柄 |

void mysql_close(MYSQL* mysql);

參數(shù)說明:

mysql ---- 初始化完成的句柄

|?獲取mysql接口執(zhí)行錯(cuò)誤原因 |

const char* mysql_error(MYSQL* mysql);

參數(shù)說明:

mysql ---- 初始化完成的句柄

返回值:

返回出錯(cuò)原因

3. API接口調(diào)用demo

#include <iostream>
#include <string>
#include <mysql/mysql.h>#define HOST "127.0.0.1"
#define PORT 3306
#define USER "root"
#define PASSWD ""
#define DBNAME "MySQL_API_study"int main()
{// 1. 初始化MySQL句柄MYSQL* mysql = mysql_init(NULL);if (mysql == NULL){std::cout << "MySQL init failed!" << std::endl;return -1;}// 2. 連接服務(wù)器if (mysql_real_connect(mysql, HOST, USER, PASSWD, DBNAME, PORT, NULL, 0) == NULL){std::cout << "connect MySQL server failed! " << mysql_error(mysql) << std::endl;mysql_close(mysql); // 退出前斷開連接,釋放mysql操作句柄return -1;}// 3. 設(shè)置客戶端字符集if (mysql_set_character_set(mysql, "utf8") != 0){std::cout << "set client character failed! " << mysql_error(mysql) << std::endl;mysql_close(mysql); // 退出前斷開連接,釋放mysql操作句柄return -1;}// 4. 選擇要操作的數(shù)據(jù)庫(這一步在連接MySQL服務(wù)器時(shí),函數(shù)參數(shù)中已經(jīng)設(shè)置過了)// 5. 執(zhí)行SQL語句// const char* sql = "insert into student values(null, 'nK', 21, 99.3, 100, 89.5);";// const char* sql = "update student set chinese=chinese + 30 where sn=1;";// const char* sql = "delete from student where sn=1;";const char* sql = "select * from student";int ret = mysql_query(mysql, sql);if (ret != 0){std::cout << "mysql query failed! " << mysql_error(mysql) << std::endl;mysql_close(mysql); // 退出前斷開連接,釋放mysql操作句柄return -1;}// 6. 若SQL語句是查詢語句,則將查詢結(jié)果保存到本地MYSQL_RES* res = mysql_store_result(mysql);if (res == NULL){mysql_close(mysql);return -1;}// 7. 獲取結(jié)果集中的結(jié)果條數(shù)int row = mysql_num_rows(res);int col = mysql_num_fields(res);// 8. 遍歷保存到本地的結(jié)果集for (int i = 0; i < row; ++i){MYSQL_ROW line = mysql_fetch_row(res);for (int j = 0; j < col; ++j){std::cout << line[j] << "\t";}std::cout << std::endl;}// 9. 釋放結(jié)果集mysql_free_result(res);// 10. 關(guān)閉連接,釋放句柄mysql_close(mysql);return 0;
}


項(xiàng)目結(jié)構(gòu)設(shè)計(jì)

項(xiàng)目模塊劃分

該項(xiàng)目要實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)版的在線五子棋匹配對戰(zhàn),由以下三個(gè)大模塊構(gòu)成。

  • 數(shù)據(jù)管理模塊:基于MySQL數(shù)據(jù)庫進(jìn)行用戶數(shù)據(jù)的管理。
  • 前端界面模塊:基于JS實(shí)現(xiàn)前端頁面(注冊,登錄,游戲大廳,游戲房間)的動態(tài)控制以及與服務(wù)器的通信。
  • 業(yè)務(wù)處理模塊:搭建WebSocket服務(wù)器與客戶端進(jìn)行通信,接收請求并進(jìn)行業(yè)務(wù)處理。

業(yè)務(wù)處理模塊的子模塊劃分

  • 網(wǎng)絡(luò)通信模塊:基于Websocketpp庫實(shí)現(xiàn)HTTP&WebSocket服務(wù)器的搭建,提供網(wǎng)絡(luò)通信功能。
  • 會話管理模塊:對客戶端的連接進(jìn)行Cookie&Session管理,實(shí)現(xiàn)HTTP短連接時(shí)客戶端身份識別功能。
  • 在線管理模塊:對進(jìn)?游戲?廳與游戲房間中用戶進(jìn)行管理,提供判斷用戶是否在線以及獲取用戶連接的功能。
  • 房間管理模塊:為匹配成功的用戶創(chuàng)建對戰(zhàn)房間,提供實(shí)時(shí)的五子棋對戰(zhàn)與聊天業(yè)務(wù)功能。
  • 用戶匹配模塊:根據(jù)天梯分?jǐn)?shù)不同進(jìn)行不同層次的玩家匹配,為匹配成功的玩家創(chuàng)建房間并加?房間。

項(xiàng)目流程圖

用戶流程圖

服務(wù)器流程圖


項(xiàng)目類實(shí)現(xiàn)

工具類

工具類主要是一些項(xiàng)目中會用到的邊緣功能代碼,提前實(shí)現(xiàn)好了就可以在項(xiàng)目中用到的時(shí)候直接使用了。

(工具類中的成員函數(shù)都用static修飾,目的是為了不實(shí)例化具體對象也能調(diào)用到類中的成員函數(shù))

日志宏封裝

主要是為了輸出程序運(yùn)行時(shí)的一些關(guān)鍵的日志信息,方便程序運(yùn)行出錯(cuò)時(shí)調(diào)試代碼。

#pragma once#include <iostream>
#include <ctime>#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL INF#define LOG(level, format, ...)                                                             \do                                                                                      \{                                                                                       \if (DEFAULT_LOG_LEVEL > level)                                                      \break;                                                                          \time_t t = time(NULL);                                                              \struct tm *lt = localtime(&t);                                                      \char buf[32] = {0};                                                                 \strftime(buf, 31, "%H:%M:%S", lt);                                                  \fprintf(stdout, "[%s %s:%d] " format "\n", buf, __FILE__, __LINE__, ##__VA_ARGS__); \} while (false)#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

MySQL_API封裝

該模塊封裝了對數(shù)據(jù)庫的三個(gè)操作:

  1. 數(shù)據(jù)庫的初始化&連接
  2. SQL語句的執(zhí)行
  3. MySQL操作句柄的銷毀

class mysql_util
{
public:static MYSQL* mysql_create(const std::string& host, const std::string& user, const std::string& passwd, const std::string& dbname, uint32_t port = 3306){// 1. 初始化MySQL句柄MYSQL *mysql = mysql_init(NULL);if (mysql == NULL){ELOG("MySQL init failed!");return nullptr;}// 2. 連接服務(wù)器if (mysql_real_connect(mysql, host.c_str(), user.c_str(), passwd.c_str(), dbname.c_str(), port, NULL, 0) == NULL){ELOG("connect MySQL server failed! %s", mysql_error(mysql));mysql_close(mysql); // 退出前斷開連接,釋放mysql操作句柄return nullptr;}// 3. 設(shè)置客戶端字符集if (mysql_set_character_set(mysql, "utf8") != 0){ELOG("set client character failed! %s", mysql_error(mysql));mysql_close(mysql); // 退出前斷開連接,釋放mysql操作句柄return nullptr;}return mysql;}static bool mysql_exec(MYSQL* mysql, const std::string& sql){int ret = mysql_query(mysql, sql.c_str());if (ret != 0){ELOG("%s", sql.c_str());ELOG("mysql query failed! %s", mysql_error(mysql));mysql_close(mysql); // 退出前斷開連接,釋放mysql操作句柄return false;}return true;}static void mysql_destroy(MYSQL* mysql){if (mysql != nullptr) mysql_close(mysql);}
};

Json格式數(shù)據(jù)的序列化和反序列封裝

  1. Json序列化:將Json::Value對象進(jìn)行序列化得到一個(gè)Json格式的字符串。
  2. Json反序列化:將Json格式的字符串反序列化得到一個(gè)Json::Value對象。
class json_util
{
public:static bool serialize(const Json::Value& root, std::string& str){// 1. 實(shí)例化一個(gè)StreamWriterBuilder工廠類對象Json::StreamWriterBuilder swb;// 2. 通過StreamWriterBuilder工廠類對象實(shí)例化一個(gè)StreamWriter對象std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());// 3. 使用StreamWriter對象,對Json::Value對象中存儲的數(shù)據(jù)進(jìn)行序列化std::stringstream ss;int ret = sw->write(root, &ss);if (ret != 0){ELOG("serialize failed!");return false;}str = ss.str();return true;}static bool unserialize(const std::string& str, Json::Value& root){// 1. 實(shí)例化一個(gè)CharReaderBuilder工廠類對象Json::CharReaderBuilder crb;// 2. 通過CharReaderBuilder工廠類對象實(shí)例化一個(gè)CharReader對象std::unique_ptr<Json::CharReader> cr(crb.newCharReader());// 3. 使用CharReader對象,對str字符串進(jìn)行Json格式的反序列化std::string err;bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);if (ret == false){ELOG("unserialize failed! %s", err.c_str());return false;}return true;}
};

字符串分割封裝

該模塊封裝字符串分割的功能,通過傳入的字符串和分割字符將字符串分割為若干份放入一個(gè)輸出數(shù)組中。

class string_util
{
public:static int split(const std::string& src, const std::string& sep, std::vector<std::string>& res){size_t start = 0, pos = 0;while (start < src.size()){pos = src.find(sep, start);if (pos == std::string::npos){res.push_back(src.substr(start));break;}if (pos == start){start += sep.size();continue;}res.push_back(src.substr(start, pos - start));start = pos + sep.size();}return res.size();}
};

?

循環(huán)中的這個(gè)if語句就是為了處理右側(cè)這種字符串中存在連續(xù)多個(gè)分割字符的情況。

文件讀取封裝

該模塊對讀取文件數(shù)據(jù)的操作進(jìn)行封裝,主要對于HTML文件數(shù)據(jù)進(jìn)行讀取。

讀取一個(gè)文件數(shù)據(jù)分為以下步驟:

  1. 打開文件
  2. 獲取文件大小
  3. 讀取文件中所有數(shù)據(jù)
  4. 關(guān)閉文件
class file_util
{
public:static bool read(const std::string& filename, std::string& body){// 1. 打開文件std::ifstream ifs(filename, std::ios::binary);if (ifs.is_open() == false){ELOG("open %s failed!", filename.c_str());return false;}// 2. 獲取文件大小size_t fsize = 0;ifs.seekg(0, std::ios::end); // 將文件指針移動到文件末尾處fsize = ifs.tellg(); // 返回當(dāng)前文件指針相較于文件開頭的偏移量(即當(dāng)前文件指針的位置相對于文件開頭的字節(jié)數(shù))ifs.seekg(0, std::ios::beg); // 將文件指針恢復(fù)到文件開頭處body.resize(fsize); // !將body擴(kuò)容至文件中數(shù)據(jù)的大小,否則后續(xù)的read(),無法將文件數(shù)據(jù)存放進(jìn)body中// 3. 讀取文件中所有數(shù)據(jù)ifs.read(&body[0], fsize);if (ifs.good() == false){ELOG("read %s content failed!", filename.c_str());ifs.close();return false;}// 4. 關(guān)閉文件ifs.close();return true;}
};

注意!不要忘記body.resize()這個(gè)操作,若沒有給body擴(kuò)容,則后續(xù)read(),則無法將文件中的數(shù)據(jù)放入body中!


數(shù)據(jù)管理類

數(shù)據(jù)管理類主要負(fù)責(zé)對于數(shù)據(jù)庫中數(shù)據(jù)進(jìn)行統(tǒng)一的增刪查改操作,其他模塊要對數(shù)據(jù)操作都必須通過數(shù)據(jù)管理類完成。

數(shù)據(jù)庫設(shè)計(jì)

設(shè)計(jì)一個(gè)用戶信息表,表中包括以下幾個(gè)數(shù)據(jù):

  • 用戶id
  • 用戶名
  • 用戶登錄密碼
  • 游戲積分
  • 游戲總場次
  • 游戲勝場次

創(chuàng)建一個(gè)名為online_gobang的數(shù)據(jù)庫。

在online_gobang中創(chuàng)建一個(gè)user表,表中包含以下6個(gè)數(shù)據(jù)。

實(shí)現(xiàn)user_table類

#pragma once#include <mutex>
#include <cassert>#include "util.hpp"class user_table
{
public:user_table(const std::string& host, const std::string& user, const std::string& password, const std::string& dbname, uint32_t port = 3306){_mysql = mysql_util::mysql_create(host, user, password, dbname, port);assert(_mysql != nullptr);}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql = nullptr;}// 注冊用戶bool signup(Json::Value& user){// 缺少用戶名或密碼,注冊失敗if (user["username"].isNull() || user["password"].isNull()){ELOG("missing username or password!");return false;}#define ADD_USER "insert into user values(null, '%s', password('%s'), 1000, 0, 0);"char sql[4096] = { 0 };sprintf(sql, ADD_USER, user["username"].asCString(), user["password"].asCString());bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DLOG("add user failed!");return false;}return true;}// 登錄驗(yàn)證,并返回詳細(xì)的用戶信息bool login(Json::Value& user){// 缺少用戶名或密碼,登錄失敗if (user["username"].isNull() || user["password"].isNull()){ELOG("missing username or password!");return false;}#define VERIFY_USER "select id, score, total_count, win_count from user where username='%s' and password=password('%s');"char sql[4096] = { 0 };sprintf(sql, VERIFY_USER, user["username"].asCString(), user["password"].asCString());MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DLOG("select user information failed!");return false;}// 保存查詢結(jié)果res = mysql_store_result(_mysql);int row = mysql_num_rows(res);if (res == nullptr || row == 0){DLOG("haven't user's information!");return false;}}MYSQL_ROW line = mysql_fetch_row(res); // 獲取一行查詢結(jié)果// 將用戶的詳細(xì)信息保存到形參user中user["id"] = (Json::UInt64)std::stol(line[0]);user["score"] = (Json::UInt64)std::stol(line[1]);user["total_count"] = std::stoi(line[2]);user["win_count"] = std::stoi(line[3]);mysql_free_result(res);return true;}// 通過用戶名獲取詳細(xì)的用戶詳細(xì)bool select_by_username(const std::string& username, Json::Value& user){
#define SELECT_BY_NAME "select id, score, total_count, win_count from user where username='%s';"char sql[4096] = { 0 };sprintf(sql, SELECT_BY_NAME, username.c_str());MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DLOG("select user information by username failed!");return false;}// 保存查詢結(jié)果res = mysql_store_result(_mysql);int row = mysql_num_rows(res);if (res == nullptr || row == 0){DLOG("haven't user's information!");return false;}}MYSQL_ROW line = mysql_fetch_row(res); // 獲取一行查詢結(jié)果// 將用戶的詳細(xì)信息保存到形參user中user["id"] = (Json::UInt64)std::stol(line[0]);user["username"] = username;user["score"] = (Json::UInt64)std::stol(line[1]);user["total_count"] = std::stoi(line[2]);user["win_count"] = std::stoi(line[3]);mysql_free_result(res);return true;}// 通過用戶id獲取詳細(xì)的用戶詳細(xì)bool select_by_id(uint64_t id, Json::Value& user){
#define SELECT_BY_ID "select username, score, total_count, win_count from user where id=%d;"char sql[4096] = { 0 };sprintf(sql, SELECT_BY_ID, id);MYSQL_RES* res = nullptr;{std::unique_lock<std::mutex> lock(_mtx);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DLOG("select user information by id failed!");return false;}// 保存查詢結(jié)果res = mysql_store_result(_mysql);int row = mysql_num_rows(res);if (res == nullptr || row == 0){DLOG("haven't user's information!");return false;}}MYSQL_ROW line = mysql_fetch_row(res); // 獲取一行查詢結(jié)果// 將用戶的詳細(xì)信息保存到形參user中user["id"] = (Json::UInt64)id;user["username"] = line[0];user["score"] = (Json::UInt64)std::stol(line[1]);user["total_count"] = std::stoi(line[2]);user["win_count"] = std::stoi(line[3]);mysql_free_result(res);return true;}// 玩家獲勝,分?jǐn)?shù)+30,總場+1,勝場+1bool victory(uint64_t id){// 根據(jù)id查詢是否有該玩家Json::Value val;if (select_by_id(id, val) == false){return false;}#define WIN_GAME "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id=%d;"char sql[4096] = { 0 };sprintf(sql, WIN_GAME, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DLOG("update winner's info failed!");return false;}return true;}// 玩家失敗,分?jǐn)?shù)-30,總場+1,其他不變bool defeat(uint64_t id){// 根據(jù)id查詢是否有該玩家Json::Value val;if (select_by_id(id, val) == false){return false;}#define LOSE_GAME "update user set score=score-30, total_count=total_count+1 where id=%d;"char sql[4096] = { 0 };sprintf(sql, LOSE_GAME, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false){DLOG("update loser's info failed!");return false;}return true;}private:MYSQL* _mysql; // mysql操作句柄std::mutex _mtx; // 互斥鎖,保證數(shù)據(jù)庫的訪問操作的安全性
};

該類可能在多線程中運(yùn)行,在數(shù)據(jù)庫中執(zhí)行查詢語句可能出現(xiàn)線程安全問題。?

MySQL提供的兩個(gè)API接口,mysql_query()和mysql_store_result(),這兩個(gè)接口單獨(dú)使用都是線程安全的,但是兩個(gè)組合在一起使用就可能會出現(xiàn)線程安全問題!

線程A在對user表進(jìn)行查詢操作,調(diào)用完mysql_query()后,還沒來得及調(diào)用my_store_result()將查詢結(jié)果保存到本地,就被掛起,切換執(zhí)行線程B,線程B對user表進(jìn)行了其他操作(增、刪、改),就會導(dǎo)致線程A的查詢結(jié)果遺失,再切換回線程A時(shí),繼續(xù)往下執(zhí)行,調(diào)用mysql_store_result(),就會失敗。

為了解決上述可能出現(xiàn)的線程安全問題,要給類中執(zhí)行查詢操作的區(qū)域加上互斥鎖進(jìn)行保護(hù),將上面代碼改為👇。


在線用戶管理類實(shí)現(xiàn)

在線用戶管理類,是對于當(dāng)前游戲?廳和游戲房間中的用戶進(jìn)行管理,主要是建立起用戶與Socket連接的映射關(guān)系,該類具有以下兩個(gè)功能:

  1. 能夠讓程序,根據(jù)用戶信息進(jìn)而找到能夠與用戶客戶端進(jìn)行通信的Socket連接,進(jìn)而實(shí)現(xiàn)與客戶端的通信。
  2. 判斷一個(gè)用戶是否在線,或判斷用戶是否已經(jīng)掉線。

在線用戶管理模塊管理的是這兩類用戶:a.進(jìn)入游戲大廳的用戶? b.進(jìn)入游戲房間的用戶。

當(dāng)客戶端建立WebSocket長連接時(shí),才能將用戶添加到游戲大廳或游戲房間中。

該類管理在線用戶的方法是:將用戶id和對應(yīng)的客戶端WebSocket長連接關(guān)聯(lián)起來。

實(shí)現(xiàn)在線用戶管理類的作用是:

  1. 當(dāng)用戶A執(zhí)行了一個(gè)業(yè)務(wù)操作(發(fā)送實(shí)時(shí)聊天信息/下棋操作),可以在在線用戶管理類中找到用戶A對應(yīng)的WebSocket長連接,將業(yè)務(wù)處理后的響應(yīng)發(fā)送給游戲房間內(nèi)的用戶B。
  2. 通過用戶id找到用戶的WebSocket長連接,從而實(shí)現(xiàn)給指定用戶的客戶端推送信息。
  3. 用戶A的WebSocket長連接關(guān)閉時(shí),會自動將用戶A的信息從在線用戶管理類中移除,即可以通過查找一個(gè)用戶是否還在在線用戶管理類中來判斷該用戶是否在線。
#pragma once#include <unordered_map>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#include <mutex>#include "logger.hpp"typedef websocketpp::server<websocketpp::config::asio> websocketsvr_t;class online_manager
{
public:// websocket連接建立的時(shí)候,才會加入游戲大廳的在線用戶管理void enter_game_hall(uint64_t userId, websocketsvr_t::connection_ptr& conn){std::unique_lock<std::mutex> lock(_mtx);_hall_users.insert({userId, conn});}// websocket連接建立的時(shí)候,才會加入游戲房間的在線用戶管理bool enter_game_room(uint64_t userId, websocketsvr_t::connection_ptr& conn){std::unique_lock<std::mutex> lock(_mtx);_room_users.insert({userId, conn});}// websocket連接斷開的時(shí)候,才會移除游戲大廳的在線用戶管理bool exit_game_hall(uint64_t userId){std::unique_lock<std::mutex> lock(_mtx);_hall_users.erase(userId);}// websocket連接斷開的時(shí)候,才會移除游戲房間的在線用戶管理bool exit_game_room(uint64_t userId){std::unique_lock<std::mutex> lock(_mtx);_room_users.erase(userId);}// 判斷指定用戶當(dāng)前是否在游戲大廳中bool is_in_game_hall(uint64_t userId){std::unique_lock<std::mutex> lock(_mtx);if (_hall_users.find(userId) == _hall_users.end()) return false;return true;}// 判斷指定用戶當(dāng)前是否在游戲房間中bool is_in_game_room(uint64_t userId){std::unique_lock<std::mutex> lock(_mtx);if (_room_users.find(userId) == _room_users.end()) return false;return true;}// 通過用戶id在游戲大廳用戶管理中獲取對應(yīng)用戶的通信連接websocketsvr_t::connection_ptr get_conn_from_hall(uint64_t userId){std::unique_lock<std::mutex> lock(_mtx);auto it = _hall_users.find(userId);if (it == _hall_users.end()) return websocketsvr_t::connection_ptr();return it->second;}// 通過用戶id在游戲房間用戶管理中獲取對應(yīng)用戶的通信連接websocketsvr_t::connection_ptr get_conn_from_room(uint64_t userId){std::unique_lock<std::mutex> lock(_mtx);auto it = _room_users.find(userId);if (it == _room_users.end()) return websocketsvr_t::connection_ptr();return it->second;}private:std::unordered_map<uint64_t, websocketsvr_t::connection_ptr> _hall_users; // 用于建立在游戲大廳的用戶的用戶id與通信連接的關(guān)系std::unordered_map<uint64_t, websocketsvr_t::connection_ptr> _room_users; // 用于建立在游戲房間的用戶的用戶id與通信連接的關(guān)系std::mutex _mtx;
};

在線用戶管理類中的所有成員函數(shù)都要加鎖保護(hù),因?yàn)樵擃惪赡茉诙嗑€程中運(yùn)行,加鎖保護(hù)以防止多個(gè)線程同時(shí)對成員變量_hall_users_room_users進(jìn)行操作,導(dǎo)致線程安全問題。


游戲房間管理類

游戲房間管理類由以下兩個(gè)類構(gòu)成,房間類房間管理類。

房間類實(shí)現(xiàn)

首先,要設(shè)計(jì)一個(gè)房間類,該類能夠?qū)嵗粋€(gè)游戲房間對象,游戲房間主要是對匹配成功的兩個(gè)玩家建立一個(gè)小范圍的關(guān)聯(lián)關(guān)系。房間中任意一個(gè)用戶做出的動作都會被廣播給房間中的所有用戶。

房間中用戶可執(zhí)行的動作包含以下兩種:

  1. 下棋
  2. 聊天
#define BOARD_ROWS 15
#define BOARD_COLS 15
#define WHITE_CHESS 1
#define BLACK_CHESS 2enum room_statu
{GAME_START,GAME_OVER
};class room
{
public:room(uint64_t room_id, user_table* user_table, online_manager* user_online):_room_id(room_id), _statu(GAME_START), _player_count(0), _user_table(user_table), _user_online(user_online), _board(BOARD_ROWS, std::vector<int>(BOARD_COLS, 0)){DLOG("%lu 房間創(chuàng)建成功", _room_id);}~room() { DLOG("%lu 房間銷毀成功", _room_id); }// 獲取游戲房間iduint64_t id() { return _room_id; }// 獲取游戲房間狀態(tài)room_statu statu() { return _statu; }// 獲取游戲房間的玩家數(shù)量int player_count() { return _player_count; }// 添加白棋玩家void add_white_player(uint64_t user_id){_white_id = user_id;++_player_count;}// 添加黑棋玩家void add_black_player(uint64_t user_id){_black_id = user_id;++_player_count;}// 獲取白棋玩家iduint64_t get_white_player() { return _white_id; }// 獲取黑棋玩家iduint64_t get_black_player() { return _black_id; }// 處理下棋動作Json::Value handle_chess(const Json::Value& req){Json::Value resp;uint64_t cur_user_id = req["uid"].asUInt64();int chess_row = req["row"].asInt();int chess_col = req["col"].asInt();// 1. 判斷走棋位置是否合理(是否越界,是否被占用)if (chess_row >= BOARD_ROWS || chess_col >= BOARD_COLS){resp["optype"] = "put_chess";resp["result"] = false;resp["reason"] = "下棋位置越界";return resp;}else if (_board[chess_row][chess_col] != 0){resp["optype"] = "put_chess";resp["result"] = false;resp["reason"] = "下棋位置被占用";return resp;}resp = req;// 2. 判斷房間中兩個(gè)玩家是否在線,若有一個(gè)退出,則判另一個(gè)獲勝// 判斷白棋玩家是否在線if (_user_online->is_in_game_room(_white_id) == false) // 白棋玩家掉線{resp["result"] = true;resp["reason"] = "白棋玩家掉線,黑棋玩家獲勝";resp["winner"] = (Json::UInt64)_black_id;return resp;}// 判斷黑棋玩家是否在線if (_user_online->is_in_game_room(_black_id) == false) // 黑棋玩家掉線{resp["result"] = true;resp["reason"] = "黑棋玩家掉線,白棋玩家獲勝";resp["winner"] = (Json::UInt64)_white_id;return resp;}// 3. 下棋int cur_chess_color = cur_user_id == _white_id ? WHITE_CHESS : BLACK_CHESS;_board[chess_row][chess_col] = cur_chess_color;// 4. 判斷是否有玩家勝利(從當(dāng)前走棋位置開始判斷,是否存在五星連珠)uint64_t winner_id = check_win(chess_row, chess_col, cur_chess_color);if (winner_id != 0) // winner_id 等于0表示沒有玩家獲勝{std::string reason = winner_id == _white_id ? "白棋五星連珠,白棋獲勝,游戲結(jié)束" : "黑棋五星連珠,黑棋獲勝,游戲結(jié)束";resp["result"] = true;resp["reason"] = reason;resp["winner"] = (Json::UInt64)winner_id;return resp;}// 沒有玩家獲勝,正常走棋resp["result"] = true;resp["reason"] = "正常走棋,游戲繼續(xù)";resp["winner"] = (Json::UInt64)winner_id;return resp;}// 處理聊天動作Json::Value handle_chat(const Json::Value& req){Json::Value resp;// 檢測消息中是否包含敏感詞std::string msg = req["message"].asString();if (have_sensitive_word(msg)){resp["optype"] = "chat";resp["result"] = false;resp["reason"] = "消息中包含敏感詞";return resp;}resp = req;resp["result"] = true;return resp;}// 處理玩家退出房間動作void handle_exit(uint64_t user_id){Json::Value resp;// 判斷玩家退出時(shí),房間狀態(tài)是否處于GAME_STARTif (_statu == GAME_START) // 游戲進(jìn)行中,玩家A退出,則判斷玩家B勝利{uint64_t winner_id = user_id == _white_id ? _black_id : _white_id;std::string reason = user_id == _white_id ? "白棋玩家退出游戲房間,黑棋玩家獲勝" : "黑棋玩家退出游戲房間,白棋玩家獲勝";resp["optype"] = "put_chess";resp["result"] = true;resp["reason"] = reason;resp["room_id"] = (Json::UInt64)_room_id;resp["uid"] = (Json::UInt64)user_id;resp["row"] = -1; // -1 表示玩家掉線,沒有走棋resp["col"] = -1; // -1 表示玩家掉線,沒有走棋resp["winner"] = (Json::UInt64)winner_id;// 更新數(shù)據(jù)庫中用戶信息表的相關(guān)信息uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;_user_table->victory(winner_id);_user_table->defeat(loser_id);_statu = GAME_OVER; // 更新游戲房間的狀態(tài)broadcast(resp); // 將處理信息廣播給房間的所有用戶}--_player_count; // 游戲房間中的玩家數(shù)量減一}// 總的請求處理函數(shù),在函數(shù)內(nèi)部區(qū)分請求類型,根據(jù)不同的請求調(diào)用不同的處理函數(shù),將得到的響應(yīng)進(jìn)行廣播void handle_request(const Json::Value& req){Json::Value resp;// 判斷req請求中的房間id與當(dāng)前房間id是否匹配uint64_t room_id = req["room_id"].asUInt64();if (room_id != _room_id){resp["optype"] = req["optype"];resp["result"] = false;resp["reason"] = "游戲房間id不匹配";}else{// 根據(jù)req["optype"]來調(diào)用不同的處理函數(shù)if (req["optype"].asString() == "put_chess"){resp = handle_chess(req);if (resp["winner"].asUInt64() != 0) // 說明有玩家獲勝{// 更新數(shù)據(jù)庫中用戶信息表的相關(guān)信息uint64_t winner_id = resp["winner"].asUInt64();uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;_user_table->victory(winner_id);_user_table->defeat(loser_id);// 更新游戲房間的狀態(tài)_statu = GAME_OVER;}}else if (req["optype"].asString() == "chat"){resp = handle_chat(req);}else{resp["optype"] = req["optype"];resp["result"] = false;resp["reason"] = "未知類型的請求";}}// 將處理信息廣播給房間的所有用戶broadcast(resp);}// 將指定的信息廣播給房間中所有玩家void broadcast(const Json::Value& resp){// 1. 對resp進(jìn)行序列化,將序列化結(jié)果保存到一個(gè)string中std::string resp_str;json_util::serialize(resp, resp_str);// 2. 獲取房間中白棋玩家和黑棋玩家的通信連接,并通過通信連接給玩家發(fā)送響應(yīng)信息websocketsvr_t::connection_ptr white_conn = _user_online->get_conn_from_room(_white_id);if (white_conn.get() != nullptr) white_conn->send(resp_str);websocketsvr_t::connection_ptr black_conn = _user_online->get_conn_from_room(_black_id);if (black_conn.get() != nullptr) black_conn->send(resp_str);}private:bool five_chess(int row, int col, int row_offset, int col_offset, int chess_color){int count = 1; // 將剛剛下的棋也包括在內(nèi)// 判斷方向1int serch_row = row + row_offset;int serch_col = col + col_offset;while (serch_row >= 0 && serch_row < BOARD_ROWS && serch_col >= 0 && serch_col <= BOARD_COLS && _board[serch_row][serch_col] == chess_color){++count; // 同色棋子數(shù)量++// 檢索位置繼續(xù)偏移serch_row += row_offset;serch_col += col_offset;}// 判斷方向2serch_row = row - row_offset;serch_col = col - col_offset;while (serch_row >= 0 && serch_row < BOARD_ROWS && serch_col >= 0 && serch_col <= BOARD_COLS && _board[serch_row][serch_col] == chess_color){++count; // 同色棋子數(shù)量++// 檢索位置繼續(xù)偏移serch_row -= row_offset;serch_col -= col_offset;}return count >= 5;}// 返回勝利玩家的id,沒有則返回0uint64_t check_win(int row, int col, int chess_color){// 在下棋的位置檢查四個(gè)方向是是否有五星連珠的情況(橫行,縱列,正斜,反斜)if ((five_chess(row, col, 0, 1, chess_color)) || (five_chess(row, col, 1, 0, chess_color)) || (five_chess(row, col, -1, -1, chess_color)) || (five_chess(row, col, -1, 1, chess_color))){return chess_color == WHITE_CHESS ? _white_id : _black_id;}return 0;}// 敏感詞檢測bool have_sensitive_word(const std::string& msg){for (const auto& word : _sensitive_words){// 聊天消息中包含敏感詞if (msg.find(word) != std::string::npos) return true;}return false;}private:uint64_t _room_id;                                // 游戲房間idroom_statu _statu;                                // 游戲房間的狀態(tài)int _player_count;                                // 游戲房間中玩家的數(shù)量uint64_t _white_id;                               // 白棋玩家的iduint64_t _black_id;                               // 黑棋玩家的iduser_table* _user_table;                          // 數(shù)據(jù)庫用戶信息表的操作句柄online_manager* _user_online;                     // 在線用戶管理句柄std::vector<std::vector<int>> _board;             // 棋盤static std::vector<std::string> _sensitive_words; // 聊天敏感詞(后期可補(bǔ)充)
};
std::vector<std::string> room::_sensitive_words = {"色情", "裸體", "性愛", "性交", "色情片","色情服務(wù)", "色情網(wǎng)站", "色情圖片", "色情小說","操", "滾", "傻逼", "蠢貨", "賤人", "混蛋","畜生", "白癡", "廢物", "黑鬼", "黃種人", "白豬","異教徒", "邪教", "基佬", "拉拉", "同性戀", "暴力","殺人", "打架", "戰(zhàn)斗", "毆打", "刺殺", "爆炸","恐怖襲擊", "毒品", "賭博", "販賣", "賄賂", "偷竊","搶劫"};

房間類中的成員函數(shù)handle_chess()和handle_chat以及handle_request()的參數(shù)都是Json::Value對象,以下列舉下棋聊天的請求格式。

| 下棋 |

{"optype": "put_chess", // put_chess表示當(dāng)前請求是下棋操作"room_id": 222, ? ? ? ?// room_id 表示當(dāng)前動作屬于哪個(gè)房間"uid": 1, ? ? ? ? ? ? ?// 當(dāng)前的下棋操作是哪個(gè)用戶發(fā)起的"row": 3, ? ? ? ? ? ? ?// 當(dāng)前下棋位置的行號"col": 2 ? ? ? ? ? ? ? // 當(dāng)前下棋位置的列號
}
{"optype": "put_chess","result": false,"reason": "走棋失敗具體原因...."
}
{"optype": "put_chess","result": true,"reason": "對放掉線,不戰(zhàn)而勝!" / "對方/己方五星連珠,戰(zhàn)無敵/雖敗猶榮!","room_id": 222,"uid": 1,"row": 3,"col": 2,"winner": 0 // 0 -- 未分勝負(fù), !0 -- 已分勝負(fù)(uid是誰,誰就贏了)
}

| 聊天 |

{"optype": "chat","room_id": 222,"uid": 1,"message": "快點(diǎn)!"
}
{"optype": "chat","result": false,"reason": "發(fā)送消息失敗的原因"
}
{"optype": "chat","result": true,"room_id": 222,"uid": 1,"message": "快點(diǎn)!"
}

房間管理類實(shí)現(xiàn)

該類負(fù)責(zé)對所有的游戲房間進(jìn)行管理,類中包括以下幾個(gè)對游戲房間的操作:

  1. 創(chuàng)建游戲房間
  2. 查找游戲房間(通過房間id查找,通過用戶id查找)
  3. 移除房間中的玩家
  4. 銷毀游戲房間
typedef std::shared_ptr<room> room_ptr;

由于項(xiàng)目中使用的room實(shí)例化對象是通過new出來的,所以不希望直接對指針進(jìn)行操作。為了避免對一個(gè)已經(jīng)釋放的room對象進(jìn)行操作,所以使用room_ptr來管理room對象的指針。只要shared_ptr計(jì)數(shù)器還沒有減到0,就不存在對空指針進(jìn)行訪問,從而避免了內(nèi)存訪問錯(cuò)誤的問題。

typedef std::shared_ptr<room> room_ptr;
class room_manager
{
public:room_manager(user_table* user_table, online_manager* user_online):_next_room_id(1), _user_table(user_table), _user_online(user_online){srand(time(nullptr)); // 用于將玩家1和玩家2隨機(jī)分配給白棋和黑棋DLOG("房間管理類初始化完畢");}~room_manager() { DLOG("房間管理類即將銷毀"); }room_ptr create_room(uint64_t user_id1, uint64_t user_id2){// 兩個(gè)玩家都在游戲大廳中匹配成功后才能創(chuàng)建房間// 1. 判斷玩家1和玩家2是否都在游戲大廳中if (_user_online->is_in_game_hall(user_id1) == false) // 玩家1不在游戲大廳中{DLOG("創(chuàng)建游戲房間失敗,玩家:%lu 不在游戲大廳中", user_id1);return room_ptr();}if (_user_online->is_in_game_hall(user_id2) == false) // 玩家2不在游戲大廳中{DLOG("創(chuàng)建游戲房間失敗,玩家:%lu 不在游戲大廳中", user_id2);return room_ptr();}std::unique_lock<std::mutex> lock(_mtx); // 對下面的操作進(jìn)行加鎖保護(hù)// 2. 創(chuàng)建房間,將用戶信息添加到房間中room_ptr proom(new room(_next_room_id++, _user_table, _user_online));// 玩家1和玩家2隨機(jī)匹配白棋和黑棋uint64_t white_id = rand() % 2 == 0 ? user_id1 : user_id2;uint64_t black_id = white_id == user_id1 ? user_id2 : user_id1;proom->add_white_player(white_id);proom->add_black_player(black_id);//-----------------------存疑?這里要不要調(diào)用_user_online->enter_game_room()?-----------------------// 3. 將房間信息管理起來_room_id_and_room.insert({proom->id(), proom});_user_id_and_room_id.insert({user_id1, proom->id()});_user_id_and_room_id.insert({user_id2, proom->id()});return proom;}room_ptr get_room_by_room_id(uint64_t room_id){std::unique_lock<std::mutex> lock(_mtx); // 對下面的操作進(jìn)行加鎖保護(hù)auto it = _room_id_and_room.find(room_id);if (it == _room_id_and_room.end()) // 沒找到房間號為id的房間{DLOG("不存在房間id為:%d 的房間", room_id);return room_ptr();}return it->second;}room_ptr get_room_by_user_id(uint64_t user_id){std::unique_lock<std::mutex> lock(_mtx); // 對下面的操作進(jìn)行加鎖保護(hù)auto it1 = _user_id_and_room_id.find(user_id);if (it1 == _user_id_and_room_id.end()){DLOG("不存在與id為:%d 的玩家匹配的房間");return room_ptr();}uint64_t room_id = it1->second;auto it2 = _room_id_and_room.find(room_id);if (it2 == _room_id_and_room.end()){DLOG("不存在房間id為:%d 的房間", room_id);return room_ptr();}return it2->second;}void remove_player_in_room(uint64_t user_id){// 1. 通過玩家id獲取游戲房間信息room_ptr proom = get_room_by_user_id(user_id);if (proom.get() == nullptr){DLOG("通過玩家id獲取游戲房間信息失敗");return;}// 2. 處理玩家退出房間動作proom->handle_exit(user_id);// 3. 判斷游戲房間中是否還有玩家,沒有則銷毀游戲房間if (proom->player_count() == 0) destroy_room(proom->id());}void destroy_room(uint64_t room_id){// 1. 通過房間id獲取游戲房間信息room_ptr proom = get_room_by_room_id(room_id);if (proom.get() == nullptr){DLOG("通過房間id獲取游戲房間信息失敗");return;}// 2. 通過游戲房間獲取白棋玩家id和黑棋玩家iduint64_t white_id = proom->get_white_player();uint64_t black_id = proom->get_black_player();{std::unique_lock<std::mutex> lock(_mtx); // 加鎖保護(hù)該作用域中的操作// 3. 將白棋玩家和黑棋玩家在“玩家id和游戲房間id的關(guān)聯(lián)關(guān)系”中移除_user_id_and_room_id.erase(white_id);_user_id_and_room_id.erase(black_id);// 4. 將游戲房間信息從房間管理中移除_room_id_and_room.erase(proom->id());}}private:uint64_t _next_room_id;                                      // 房間id分配器std::mutex _mtx;                                             // 互斥鎖user_table* _user_table;                                     // 數(shù)據(jù)庫用戶信息表的操作句柄online_manager* _user_online;                                // 在線用戶管理句柄std::unordered_map<uint64_t, room_ptr> _room_id_and_room;    // 游戲房間id和游戲房間的關(guān)聯(lián)關(guān)系std::unordered_map<uint64_t, uint64_t> _user_id_and_room_id; // 玩家id和游戲房間id的關(guān)聯(lián)關(guān)系
};

|?get_room_by_user_id()中的死鎖問題 |

在實(shí)現(xiàn)房間管理類中的get_room_by_user_id()時(shí),想著復(fù)用代碼,結(jié)果寫出來bug。

以上是存在bug的代碼。

由于_mtx是類的成員變量,在get_room_by_user_id()中獲取_mtx,在get_room_by_user_id()中調(diào)用了get_room_by_room_id(),而在get_room_by_room_id()中也要獲取_mtx,但問題是get_room_by_user_id()在調(diào)用get_room_by_room_id()時(shí)已經(jīng)持有了_mtx,因此在get_room_by_room_id()中再次嘗試獲取_mtx就會導(dǎo)致死鎖。

(上面代碼塊中的代碼已經(jīng)是解決死鎖bug之后的正確代碼)

| 房間類和房間管理類整合?|

#pragma once#include <vector>
#include <jsoncpp/json/json.h>
#include <string>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <cstdlib>
#include <ctime>#include "logger.hpp"
#include "db.hpp"
#include "online.hpp"
#include "util.hpp"#define BOARD_ROWS 15
#define BOARD_COLS 15
#define WHITE_CHESS 1
#define BLACK_CHESS 2enum room_statu
{GAME_START,GAME_OVER
};class room
{
public:room(uint64_t room_id, user_table* user_table, online_manager* user_online):_room_id(room_id), _statu(GAME_START), _player_count(0), _user_table(user_table), _user_online(user_online), _board(BOARD_ROWS, std::vector<int>(BOARD_COLS, 0)){DLOG("%lu 房間創(chuàng)建成功", _room_id);}~room() { DLOG("%lu 房間銷毀成功", _room_id); }// 獲取游戲房間iduint64_t id() { return _room_id; }// 獲取游戲房間狀態(tài)room_statu statu() { return _statu; }// 獲取游戲房間的玩家數(shù)量int player_count() { return _player_count; }// 添加白棋玩家void add_white_player(uint64_t user_id){_white_id = user_id;++_player_count;}// 添加黑棋玩家void add_black_player(uint64_t user_id){_black_id = user_id;++_player_count;}// 獲取白棋玩家iduint64_t get_white_player() { return _white_id; }// 獲取黑棋玩家iduint64_t get_black_player() { return _black_id; }// 處理下棋動作Json::Value handle_chess(const Json::Value& req){Json::Value resp;uint64_t cur_user_id = req["uid"].asUInt64();int chess_row = req["row"].asInt();int chess_col = req["col"].asInt();// 1. 判斷走棋位置是否合理(是否越界,是否被占用)if (chess_row >= BOARD_ROWS || chess_col >= BOARD_COLS){resp["optype"] = "put_chess";resp["result"] = false;resp["reason"] = "下棋位置越界";return resp;}else if (_board[chess_row][chess_col] != 0){resp["optype"] = "put_chess";resp["result"] = false;resp["reason"] = "下棋位置被占用";return resp;}resp = req;// 2. 判斷房間中兩個(gè)玩家是否在線,若有一個(gè)退出,則判另一個(gè)獲勝// 判斷白棋玩家是否在線if (_user_online->is_in_game_room(_white_id) == false) // 白棋玩家掉線{resp["result"] = true;resp["reason"] = "白棋玩家掉線,黑棋玩家獲勝";resp["winner"] = (Json::UInt64)_black_id;return resp;}// 判斷黑棋玩家是否在線if (_user_online->is_in_game_room(_black_id) == false) // 黑棋玩家掉線{resp["result"] = true;resp["reason"] = "黑棋玩家掉線,白棋玩家獲勝";resp["winner"] = (Json::UInt64)_white_id;return resp;}// 3. 下棋int cur_chess_color = cur_user_id == _white_id ? WHITE_CHESS : BLACK_CHESS;_board[chess_row][chess_col] = cur_chess_color;// 4. 判斷是否有玩家勝利(從當(dāng)前走棋位置開始判斷,是否存在五星連珠)uint64_t winner_id = check_win(chess_row, chess_col, cur_chess_color);if (winner_id != 0) // winner_id 等于0表示沒有玩家獲勝{std::string reason = winner_id == _white_id ? "白棋五星連珠,白棋獲勝,游戲結(jié)束" : "黑棋五星連珠,黑棋獲勝,游戲結(jié)束";resp["result"] = true;resp["reason"] = reason;resp["winner"] = (Json::UInt64)winner_id;return resp;}// 沒有玩家獲勝,正常走棋resp["result"] = true;resp["reason"] = "正常走棋,游戲繼續(xù)";resp["winner"] = (Json::UInt64)winner_id;return resp;}// 處理聊天動作Json::Value handle_chat(const Json::Value& req){Json::Value resp;// 檢測消息中是否包含敏感詞std::string msg = req["message"].asString();if (have_sensitive_word(msg)){resp["optype"] = "chat";resp["result"] = false;resp["reason"] = "消息中包含敏感詞";return resp;}resp = req;resp["result"] = true;return resp;}// 處理玩家退出房間動作void handle_exit(uint64_t user_id){Json::Value resp;// 判斷玩家退出時(shí),房間狀態(tài)是否處于GAME_STARTif (_statu == GAME_START) // 游戲進(jìn)行中,玩家A退出,則判斷玩家B勝利{uint64_t winner_id = user_id == _white_id ? _black_id : _white_id;std::string reason = user_id == _white_id ? "白棋玩家退出游戲房間,黑棋玩家獲勝" : "黑棋玩家退出游戲房間,白棋玩家獲勝";resp["optype"] = "put_chess";resp["result"] = true;resp["reason"] = reason;resp["room_id"] = (Json::UInt64)_room_id;resp["uid"] = (Json::UInt64)user_id;resp["row"] = -1; // -1 表示玩家掉線,沒有走棋resp["col"] = -1; // -1 表示玩家掉線,沒有走棋resp["winner"] = (Json::UInt64)winner_id;// 更新數(shù)據(jù)庫中用戶信息表的相關(guān)信息uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;_user_table->victory(winner_id);_user_table->defeat(loser_id);_statu = GAME_OVER; // 更新游戲房間的狀態(tài)broadcast(resp); // 將處理信息廣播給房間的所有用戶}--_player_count; // 游戲房間中的玩家數(shù)量減一}// 總的請求處理函數(shù),在函數(shù)內(nèi)部區(qū)分請求類型,根據(jù)不同的請求調(diào)用不同的處理函數(shù),將得到的響應(yīng)進(jìn)行廣播void handle_request(const Json::Value& req){Json::Value resp;// 判斷req請求中的房間id與當(dāng)前房間id是否匹配uint64_t room_id = req["room_id"].asUInt64();if (room_id != _room_id){resp["optype"] = req["optype"];resp["result"] = false;resp["reason"] = "游戲房間id不匹配";}else{// 根據(jù)req["optype"]來調(diào)用不同的處理函數(shù)if (req["optype"].asString() == "put_chess"){resp = handle_chess(req);if (resp["winner"].asUInt64() != 0) // 說明有玩家獲勝{// 更新數(shù)據(jù)庫中用戶信息表的相關(guān)信息uint64_t winner_id = resp["winner"].asUInt64();uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;_user_table->victory(winner_id);_user_table->defeat(loser_id);// 更新游戲房間的狀態(tài)_statu = GAME_OVER;}}else if (req["optype"].asString() == "chat"){resp = handle_chat(req);}else{resp["optype"] = req["optype"];resp["result"] = false;resp["reason"] = "未知類型的請求";}}// 將處理信息廣播給房間的所有用戶broadcast(resp);}// 將指定的信息廣播給房間中所有玩家void broadcast(const Json::Value& resp){// 1. 對resp進(jìn)行序列化,將序列化結(jié)果保存到一個(gè)string中std::string resp_str;json_util::serialize(resp, resp_str);// 2. 獲取房間中白棋玩家和黑棋玩家的通信連接,并通過通信連接給玩家發(fā)送響應(yīng)信息websocketsvr_t::connection_ptr white_conn = _user_online->get_conn_from_room(_white_id);if (white_conn.get() != nullptr) white_conn->send(resp_str);websocketsvr_t::connection_ptr black_conn = _user_online->get_conn_from_room(_black_id);if (black_conn.get() != nullptr) black_conn->send(resp_str);}private:bool five_chess(int row, int col, int row_offset, int col_offset, int chess_color){int count = 1; // 將剛剛下的棋也包括在內(nèi)// 判斷方向1int serch_row = row + row_offset;int serch_col = col + col_offset;while (serch_row >= 0 && serch_row < BOARD_ROWS && serch_col >= 0 && serch_col <= BOARD_COLS && _board[serch_row][serch_col] == chess_color){++count; // 同色棋子數(shù)量++// 檢索位置繼續(xù)偏移serch_row += row_offset;serch_col += col_offset;}// 判斷方向2serch_row = row - row_offset;serch_col = col - col_offset;while (serch_row >= 0 && serch_row < BOARD_ROWS && serch_col >= 0 && serch_col <= BOARD_COLS && _board[serch_row][serch_col] == chess_color){++count; // 同色棋子數(shù)量++// 檢索位置繼續(xù)偏移serch_row -= row_offset;serch_col -= col_offset;}return count >= 5;}// 返回勝利玩家的id,沒有則返回0uint64_t check_win(int row, int col, int chess_color){// 在下棋的位置檢查四個(gè)方向是是否有五星連珠的情況(橫行,縱列,正斜,反斜)if ((five_chess(row, col, 0, 1, chess_color)) || (five_chess(row, col, 1, 0, chess_color)) || (five_chess(row, col, -1, -1, chess_color)) || (five_chess(row, col, -1, 1, chess_color))){return chess_color == WHITE_CHESS ? _white_id : _black_id;}return 0;}// 敏感詞檢測bool have_sensitive_word(const std::string& msg){for (const auto& word : _sensitive_words){// 聊天消息中包含敏感詞if (msg.find(word) != std::string::npos) return true;}return false;}private:uint64_t _room_id;                                // 游戲房間idroom_statu _statu;                                // 游戲房間的狀態(tài)int _player_count;                                // 游戲房間中玩家的數(shù)量uint64_t _white_id;                               // 白棋玩家的iduint64_t _black_id;                               // 黑棋玩家的iduser_table* _user_table;                          // 數(shù)據(jù)庫用戶信息表的操作句柄online_manager* _user_online;                     // 在線用戶管理句柄std::vector<std::vector<int>> _board;             // 棋盤static std::vector<std::string> _sensitive_words; // 聊天敏感詞(后期可補(bǔ)充)
};
std::vector<std::string> room::_sensitive_words = {"色情", "裸體", "性愛", "性交", "色情片","色情服務(wù)", "色情網(wǎng)站", "色情圖片", "色情小說","操", "滾", "傻逼", "蠢貨", "賤人", "混蛋","畜生", "白癡", "廢物", "黑鬼", "黃種人", "白豬","異教徒", "邪教", "基佬", "拉拉", "同性戀", "暴力","殺人", "打架", "戰(zhàn)斗", "毆打", "刺殺", "爆炸","恐怖襲擊", "毒品", "賭博", "販賣", "賄賂", "偷竊","搶劫"};typedef std::shared_ptr<room> room_ptr;
class room_manager
{
public:room_manager(user_table* user_table, online_manager* user_online):_next_room_id(1), _user_table(user_table), _user_online(user_online){srand(time(nullptr)); // 用于將玩家1和玩家2隨機(jī)分配給白棋和黑棋DLOG("房間管理類初始化完畢");}~room_manager() { DLOG("房間管理類即將銷毀"); }room_ptr create_room(uint64_t user_id1, uint64_t user_id2){// 兩個(gè)玩家都在游戲大廳中匹配成功后才能創(chuàng)建房間// 1. 判斷玩家1和玩家2是否都在游戲大廳中if (_user_online->is_in_game_hall(user_id1) == false) // 玩家1不在游戲大廳中{DLOG("創(chuàng)建游戲房間失敗,玩家:%lu 不在游戲大廳中", user_id1);return room_ptr();}if (_user_online->is_in_game_hall(user_id2) == false) // 玩家2不在游戲大廳中{DLOG("創(chuàng)建游戲房間失敗,玩家:%lu 不在游戲大廳中", user_id2);return room_ptr();}std::unique_lock<std::mutex> lock(_mtx); // 對下面的操作進(jìn)行加鎖保護(hù)// 2. 創(chuàng)建房間,將用戶信息添加到房間中room_ptr proom(new room(_next_room_id++, _user_table, _user_online));// 玩家1和玩家2隨機(jī)匹配白棋和黑棋uint64_t white_id = rand() % 2 == 0 ? user_id1 : user_id2;uint64_t black_id = white_id == user_id1 ? user_id2 : user_id1;proom->add_white_player(white_id);proom->add_black_player(black_id);//-----------------------存疑?這里要不要調(diào)用_user_online->enter_game_room()?-----------------------// 3. 將房間信息管理起來_room_id_and_room.insert({proom->id(), proom});_user_id_and_room_id.insert({user_id1, proom->id()});_user_id_and_room_id.insert({user_id2, proom->id()});return proom;}room_ptr get_room_by_room_id(uint64_t room_id){std::unique_lock<std::mutex> lock(_mtx); // 對下面的操作進(jìn)行加鎖保護(hù)auto it = _room_id_and_room.find(room_id);if (it == _room_id_and_room.end()) // 沒找到房間號為id的房間{DLOG("不存在房間id為:%d 的房間", room_id);return room_ptr();}return it->second;}room_ptr get_room_by_user_id(uint64_t user_id){std::unique_lock<std::mutex> lock(_mtx); // 對下面的操作進(jìn)行加鎖保護(hù)auto it1 = _user_id_and_room_id.find(user_id);if (it1 == _user_id_and_room_id.end()){DLOG("不存在與id為:%d 的玩家匹配的房間");return room_ptr();}uint64_t room_id = it1->second;auto it2 = _room_id_and_room.find(room_id);if (it2 == _room_id_and_room.end()){DLOG("不存在房間id為:%d 的房間", room_id);return room_ptr();}return it2->second;}void remove_player_in_room(uint64_t user_id){// 1. 通過玩家id獲取游戲房間信息room_ptr proom = get_room_by_user_id(user_id);if (proom.get() == nullptr){DLOG("通過玩家id獲取游戲房間信息失敗");return;}// 2. 處理玩家退出房間動作proom->handle_exit(user_id);// 3. 判斷游戲房間中是否還有玩家,沒有則銷毀游戲房間if (proom->player_count() == 0) destroy_room(proom->id());}void destroy_room(uint64_t room_id){// 1. 通過房間id獲取游戲房間信息room_ptr proom = get_room_by_room_id(room_id);if (proom.get() == nullptr){DLOG("通過房間id獲取游戲房間信息失敗");return;}// 2. 通過游戲房間獲取白棋玩家id和黑棋玩家iduint64_t white_id = proom->get_white_player();uint64_t black_id = proom->get_black_player();{std::unique_lock<std::mutex> lock(_mtx); // 加鎖保護(hù)該作用域中的操作// 3. 將白棋玩家和黑棋玩家在“玩家id和游戲房間id的關(guān)聯(lián)關(guān)系”中移除_user_id_and_room_id.erase(white_id);_user_id_and_room_id.erase(black_id);// 4. 將游戲房間信息從房間管理中移除_room_id_and_room.erase(proom->id());}}private:uint64_t _next_room_id;                                      // 房間id分配器std::mutex _mtx;                                             // 互斥鎖user_table* _user_table;                                     // 數(shù)據(jù)庫用戶信息表的操作句柄online_manager* _user_online;                                // 在線用戶管理句柄std::unordered_map<uint64_t, room_ptr> _room_id_and_room;    // 游戲房間id和游戲房間的關(guān)聯(lián)關(guān)系std::unordered_map<uint64_t, uint64_t> _user_id_and_room_id; // 玩家id和游戲房間id的關(guān)聯(lián)關(guān)系
};

session管理類

session管理類由以下兩個(gè)類構(gòu)成,session類session管理類。

session的基本認(rèn)識

在Web開發(fā)中,HTTP協(xié)議是一種無狀態(tài)短鏈接協(xié)議,這就導(dǎo)致一個(gè)客戶端連接到服務(wù)器上之后,服務(wù)器不知道當(dāng)前的連接對應(yīng)的是哪個(gè)用戶,也不知道客戶端是否登錄成功,這時(shí)候?yàn)榭蛻舳颂峁┓?wù)是不合理的。因此,服務(wù)器為每個(gè)用戶瀏覽器創(chuàng)建?個(gè)會話對象(session對象),注意!一個(gè)瀏覽器獨(dú)占一個(gè)session對象(默認(rèn)情況下)。因此,在需要保存用戶數(shù)據(jù)時(shí),服務(wù)器程序可以把用戶數(shù)據(jù)寫到用戶瀏覽器獨(dú)占的session中,當(dāng)用戶使用瀏覽器訪問其它程序時(shí),其它程序可以從用戶的session中取出該用戶的數(shù)據(jù),識別該連接對應(yīng)的用戶,并為用戶提供服務(wù)。

| session工作原理 |

session類實(shí)現(xiàn)

session類用于保存用戶的狀態(tài)信息。

服務(wù)器管理的每一個(gè)session都會有過期的時(shí)間,超過了過期時(shí)間就會將對應(yīng)的session銷毀。每次客戶端與服務(wù)器通信后都要刷新session的過期時(shí)間。

為了實(shí)現(xiàn)銷毀服務(wù)器中的超時(shí)session,引入Websocketpp中的定時(shí)器👇。

|?Websocketpp中的定時(shí)器 |

Websocketpp中提供的定時(shí)器是基于boost::asio::steady_timer封裝實(shí)現(xiàn)的。

timer_ptr set_timer(long duration, timer_handler callback);

參數(shù)說明:

duration ---- 延遲多少毫秒后執(zhí)行callback

callback ---- 可調(diào)用對象,定時(shí)器倒計(jì)時(shí)結(jié)束后調(diào)用該函數(shù)

返回值:

返回一個(gè)句柄,如果不再需要定時(shí)器,可用該句柄取消定時(shí)器

void func(const std::string& str) { std::cout << str << std::endl; }websocketpp::server<websocketpp::config::asio> wssvr;
websocketpp::server<websocketpp::config::asio>::timer_ptr tp = wssvr->set_timer(5000, std::bind(func, "hello nK!"));
tp->cancel();

接收set_timer()的返回值來取消定時(shí)任務(wù),會導(dǎo)致定時(shí)任務(wù)被立即執(zhí)行。

#pragma once#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>#include "logger.hpp"enum session_statu
{LOGIN,NOTLOGIN
};typedef websocketpp::server<websocketpp::config::asio> websocketsvr_t;
class session
{
public:session(uint64_t session_id):_session_id(session_id){DLOG("session:%p 已創(chuàng)建", this);}~session() { DLOG("session:%p 已銷毀", this); }uint64_t get_session_id() { return _session_id; }void set_statu(session_statu statu) { _statu = statu; }void set_user(uint64_t user_id) { _user_id = user_id; }uint64_t get_user_id() { return _user_id; }void set_timer(const websocketsvr_t::timer_ptr& tp) { _tp = tp; }bool is_login() { return _statu == LOGIN; }private:uint64_t _session_id;          // session idsession_statu _statu;          // 用戶當(dāng)前的狀態(tài)信息(登錄/未登錄)uint64_t _user_id;             // 與session對應(yīng)的用戶idwebsocketsvr_t::timer_ptr _tp; // 與session關(guān)聯(lián)的定時(shí)器
};

session管理類實(shí)現(xiàn)

該類負(fù)責(zé)對所有的session進(jìn)行管理,類中包括以下幾個(gè)對session的操作:

  1. 創(chuàng)建session
  2. 重新添加一個(gè)已存在的session
  3. 通過sessin id獲取session
  4. 移除session
  5. 設(shè)置session過期時(shí)間

typedef std::shared_ptr<session> session_ptr;

由于項(xiàng)目中使用的session實(shí)例化對象是通過new出來的,所以不希望直接對指針進(jìn)行操作。為了避免對一個(gè)已經(jīng)釋放的session對象進(jìn)行操作,所以使用session_ptr來管理session對象的指針。只要shared_ptr計(jì)數(shù)器還沒有減到0,就不存在對空指針進(jìn)行訪問,從而避免了內(nèi)存訪問錯(cuò)誤的問題。

對于session的生命周期需要注意以下三點(diǎn)👇:

  1. 用戶登錄后,創(chuàng)建session,此時(shí)的session是臨時(shí)的,在指定時(shí)間內(nèi)無通信就被刪除。
  2. 進(jìn)入游戲大廳游戲房間,此時(shí)session的生命周期是永久的。
  3. 退出游戲大廳游戲房間,該session又被重新設(shè)置為臨時(shí)的,在指定時(shí)間內(nèi)無通信就被刪除。

#define SESSION_TEMOPRARY 30000 // session的生命周期為30s
#define SESSION_PERMANENT -1    // session的生命周期為永久typedef std::shared_ptr<session> session_ptr;
class session_manager
{
public:session_manager(websocketsvr_t* wssvr):_next_session_id(1), _wssvr(wssvr){DLOG("session管理類初始化完畢");}~session_manager() { DLOG("session管理類即將銷毀"); }// 創(chuàng)建sessionsession_ptr create_session(uint64_t user_id, session_statu statu){std::unique_lock<std::mutex> lock(_mtx);session_ptr psession(new session(_next_session_id++));psession->set_user(user_id);psession->set_statu(statu);_session_id_and_session.insert({psession->get_session_id(), psession});return psession;}// 向_session_id_and_session中重新添加一個(gè)已存在的sessionvoid append_session(const session_ptr& psession){std::unique_lock<std::mutex> lock(_mtx);_session_id_and_session.insert({psession->get_session_id(), psession});}// 通過sessin id獲取sessionsession_ptr get_session_by_session_id(uint64_t session_id){std::unique_lock<std::mutex> lock(_mtx);auto it = _session_id_and_session.find(session_id);if (it == _session_id_and_session.end()){DLOG("不存在session id為:%d 的session", session_id);return session_ptr();}return it->second;}// 移除sessionvoid remove_session(uint64_t session_id){std::unique_lock<std::mutex> lock(_mtx);_session_id_and_session.erase(session_id);}// 設(shè)置session過期時(shí)間void set_session_expiration_time(uint64_t session_id, int ms){// 依賴于websocketpp的定時(shí)器來實(shí)現(xiàn)對session生命周期的管理// 用戶登錄后,創(chuàng)建session,此時(shí)的session是臨時(shí)的,在指定時(shí)間內(nèi)無通信就被刪除// 進(jìn)入游戲大廳或游戲房間,這個(gè)session的生命周期是永久的// 退出游戲大廳或游戲房間,該session又被重新設(shè)置為臨時(shí)的,在指定時(shí)間內(nèi)無通信就被刪除// 獲取sessionsession_ptr psession = get_session_by_session_id(session_id);if (psession.get() == nullptr){DLOG("通過session id獲取session失敗");return;}websocketsvr_t::timer_ptr tp = psession->get_timer();// 1. 在session的生命周期為永久的情況下,將session的生命周期設(shè)置為永久if (tp.get() == nullptr && ms == SESSION_PERMANENT){// 無需處理return;}// 2. 在session的生命周期為永久的情況下,設(shè)置定時(shí)任務(wù):超過指定時(shí)間移除該sessionelse if (tp.get() == nullptr && ms != SESSION_PERMANENT){websocketsvr_t::timer_ptr tmp_tp = _wssvr->set_timer(ms, std::bind(&session_manager::remove_session, this, session_id));psession->set_timer(tmp_tp);}// 3. 在session設(shè)置了定時(shí)移除任務(wù)的情況下,將session的生命周期設(shè)置為永久else if (tp.get() != nullptr && ms == SESSION_PERMANENT){// 使用cancel()提前結(jié)束當(dāng)前的定時(shí)任務(wù),向session中添加一個(gè)空的定時(shí)器tp->cancel();psession->set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加該session_wssvr->set_timer(0, std::bind(&session_manager::append_session, this, psession));}// 4. 在session設(shè)置了定時(shí)移除任務(wù)的情況下,重置session的生命周期else if (tp.get() != nullptr && ms != SESSION_PERMANENT){// 使用cancel()提前結(jié)束當(dāng)前的定時(shí)任務(wù),向session中添加一個(gè)空的定時(shí)器tp->cancel();psession->set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加該session_wssvr->set_timer(0, std::bind(&session_manager::append_session, this, psession));// 給session設(shè)置新的定時(shí)任務(wù),并將定時(shí)器更新到session中websocketsvr_t::timer_ptr tmp_tp = _wssvr->set_timer(ms, std::bind(&session_manager::remove_session, this, session_id));psession->set_timer(tmp_tp);}}private:uint64_t _next_session_id;                                         // session id分配器std::mutex _mtx;                                                   // 互斥鎖std::unordered_map<uint64_t, session_ptr> _session_id_and_session; // session id和session的關(guān)聯(lián)關(guān)系websocketsvr_t* _wssvr;                                            // Websocket服務(wù)器對象
};

下面講解一下set_session_expiration_time()中的幾個(gè)細(xì)節(jié)問題👇。

1. 一個(gè)session中與之關(guān)聯(lián)的定時(shí)器如果是nullptr,則說明沒有給該session設(shè)置定時(shí)任務(wù),既該session的生命周期為永久。

2. 修改一個(gè)session的生命周期需要先調(diào)用cancel()取消該session原先的定時(shí)任務(wù)。

但是cancel()并不是立即執(zhí)行的,cancel()何時(shí)執(zhí)行由操作系統(tǒng)決定,這就導(dǎo)致了可能會出現(xiàn),先調(diào)用了_session_id_and_session.insert()然后操作系統(tǒng)才執(zhí)行了cancel(),就會出現(xiàn)意料之外的情況。

正確寫法👇。

將重新添加session這個(gè)操作也設(shè)置一個(gè)定時(shí)器來執(zhí)行,延遲時(shí)間為0秒。

cancel()由操作系統(tǒng)執(zhí)行,定時(shí)器也由操作系統(tǒng)執(zhí)行,且cancel()在新設(shè)置的定時(shí)器前面,所以可以保證canel()執(zhí)行完畢后,再向_session_id_and_session中重新添加session。

| session類和session管理類整合 |

#pragma once#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#include <mutex>
#include <unordered_map>
#include <memory>#include "logger.hpp"enum session_statu
{LOGIN,NOTLOGIN
};typedef websocketpp::server<websocketpp::config::asio> websocketsvr_t;
class session
{
public:session(uint64_t session_id):_session_id(session_id){DLOG("session:%p 已創(chuàng)建", this);}~session() { DLOG("session:%p 已銷毀", this); }uint64_t get_session_id() { return _session_id; }void set_statu(session_statu statu) { _statu = statu; }void set_user(uint64_t user_id) { _user_id = user_id; }uint64_t get_user_id() { return _user_id; }void set_timer(const websocketsvr_t::timer_ptr& tp) { _tp = tp; }websocketsvr_t::timer_ptr get_timer() { return _tp; }bool is_login() { return _statu == LOGIN; }private:uint64_t _session_id;          // session idsession_statu _statu;          // 用戶當(dāng)前的狀態(tài)信息(登錄/未登錄)uint64_t _user_id;             // 與session對應(yīng)的用戶idwebsocketsvr_t::timer_ptr _tp; // 與session關(guān)聯(lián)的定時(shí)器
};#define SESSION_TEMOPRARY 30000 // session的生命周期為30s
#define SESSION_PERMANENT -1    // session的生命周期為永久typedef std::shared_ptr<session> session_ptr;
class session_manager
{
public:session_manager(websocketsvr_t* wssvr):_next_session_id(1), _wssvr(wssvr){DLOG("session管理類初始化完畢");}~session_manager() { DLOG("session管理類即將銷毀"); }// 創(chuàng)建sessionsession_ptr create_session(uint64_t user_id, session_statu statu){std::unique_lock<std::mutex> lock(_mtx);session_ptr psession(new session(_next_session_id++));psession->set_user(user_id);psession->set_statu(statu);_session_id_and_session.insert({psession->get_session_id(), psession});return psession;}// 向_session_id_and_session中重新添加一個(gè)已存在的sessionvoid append_session(const session_ptr& psession){std::unique_lock<std::mutex> lock(_mtx);_session_id_and_session.insert({psession->get_session_id(), psession});}// 通過sessin id獲取sessionsession_ptr get_session_by_session_id(uint64_t session_id){std::unique_lock<std::mutex> lock(_mtx);auto it = _session_id_and_session.find(session_id);if (it == _session_id_and_session.end()){DLOG("不存在session id為:%d 的session", session_id);return session_ptr();}return it->second;}// 移除sessionvoid remove_session(uint64_t session_id){std::unique_lock<std::mutex> lock(_mtx);_session_id_and_session.erase(session_id);}// 設(shè)置session過期時(shí)間void set_session_expiration_time(uint64_t session_id, int ms){// 依賴于websocketpp的定時(shí)器來實(shí)現(xiàn)對session生命周期的管理// 用戶登錄后,創(chuàng)建session,此時(shí)的session是臨時(shí)的,在指定時(shí)間內(nèi)無通信就被刪除// 進(jìn)入游戲大廳或游戲房間,這個(gè)session的生命周期是永久的// 退出游戲大廳或游戲房間,該session又被重新設(shè)置為臨時(shí)的,在指定時(shí)間內(nèi)無通信就被刪除// 獲取sessionsession_ptr psession = get_session_by_session_id(session_id);if (psession.get() == nullptr){DLOG("通過session id獲取session失敗");return;}websocketsvr_t::timer_ptr tp = psession->get_timer();// 1. 在session的生命周期為永久的情況下,將session的生命周期設(shè)置為永久if (tp.get() == nullptr && ms == SESSION_PERMANENT){// 無需處理return;}// 2. 在session的生命周期為永久的情況下,設(shè)置定時(shí)任務(wù):超過指定時(shí)間移除該sessionelse if (tp.get() == nullptr && ms != SESSION_PERMANENT){websocketsvr_t::timer_ptr tmp_tp = _wssvr->set_timer(ms, std::bind(&session_manager::remove_session, this, session_id));psession->set_timer(tmp_tp);}// 3. 在session設(shè)置了定時(shí)移除任務(wù)的情況下,將session的生命周期設(shè)置為永久else if (tp.get() != nullptr && ms == SESSION_PERMANENT){// 使用cancel()提前結(jié)束當(dāng)前的定時(shí)任務(wù),向session中添加一個(gè)空的定時(shí)器tp->cancel();psession->set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加該session_wssvr->set_timer(0, std::bind(&session_manager::append_session, this, psession));}// 4. 在session設(shè)置了定時(shí)移除任務(wù)的情況下,重置session的生命周期else if (tp.get() != nullptr && ms != SESSION_PERMANENT){// 使用cancel()提前結(jié)束當(dāng)前的定時(shí)任務(wù),向session中添加一個(gè)空的定時(shí)器tp->cancel();psession->set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加該session_wssvr->set_timer(0, std::bind(&session_manager::append_session, this, psession));// 給session設(shè)置新的定時(shí)任務(wù),并將定時(shí)器更新到session中websocketsvr_t::timer_ptr tmp_tp = _wssvr->set_timer(ms, std::bind(&session_manager::remove_session, this, session_id));psession->set_timer(tmp_tp);}}private:uint64_t _next_session_id;                                         // session id分配器std::mutex _mtx;                                                   // 互斥鎖std::unordered_map<uint64_t, session_ptr> _session_id_and_session; // session id和session的關(guān)聯(lián)關(guān)系websocketsvr_t* _wssvr;                                            // Websocket服務(wù)器對象
};

玩家匹配管理類

玩家匹配管理類由以下兩個(gè)類構(gòu)成,匹配隊(duì)列類匹配管理類

根據(jù)玩家的分?jǐn)?shù)將所有的玩家分三個(gè)等級:

  • 青銅:score < 2000
  • 白銀:2000 <= score < 3000
  • 黃金:score >= 3000

分別為三個(gè)不同等級創(chuàng)建對應(yīng)的匹配隊(duì)列。

有玩家要進(jìn)行匹配時(shí),根據(jù)玩家的分?jǐn)?shù),將玩家的id加入到相應(yīng)等級的匹配隊(duì)列中。當(dāng)匹配隊(duì)列中的元素大于等于2時(shí),則說明此時(shí)該等級有兩個(gè)及以上的玩家正在進(jìn)行游戲匹配。從該匹配隊(duì)列中出隊(duì)兩個(gè)玩家id,為這兩個(gè)玩家創(chuàng)建一個(gè)游戲房間,將玩家加入該游戲房間后,向匹配成功的玩家發(fā)送游戲匹配成功的響應(yīng),至此玩家游戲匹配結(jié)束。

匹配隊(duì)列類實(shí)現(xiàn)

使用list來模擬queue,而不直接使用queue的原因是,后續(xù)操作中需要?jiǎng)h除匹配隊(duì)列中的指定元素,queue不支持遍歷,所以不使用原生的queue。

#pragma once#include <list>
#include <mutex>
#include <condition_variable>template<class T>
class match_queue
{
public:// 獲取隊(duì)列中元素個(gè)數(shù)int size(){std::unique_lock<std::mutex> lock(_mtx);return _list.size();}// 判斷隊(duì)列是否為空bool empty(){std::unique_lock<std::mutex> lock(_mtx);return _list.empty();}// 阻塞隊(duì)列所在的線程void wait(){std::unique_lock<std::mutex> lock(_mtx);_cond.wait(lock);}// 將數(shù)據(jù)入隊(duì),并喚醒線程void push(const T& data){std::unique_lock<std::mutex> lock(_mtx);_list.push_back(data);_cond.notify_all();}// 出隊(duì)并獲取數(shù)據(jù)bool pop(T& data){std::unique_lock<std::mutex> lock(_mtx);if (_list.empty() == true){return false;}data = _list.front();_list.pop_front();return true;}// 移除指定數(shù)據(jù)void remove(const T& data){std::unique_lock<std::mutex> lock(_mtx);_list.remove(data);}private:std::list<T> _list;            // 使用雙鏈表來模擬隊(duì)列(因?yàn)橛袆h除指定元素的需求)std::mutex _mtx;               // 互斥鎖,實(shí)現(xiàn)線程安全std::condition_variable _cond; // 條件變量,主要為了阻塞消費(fèi)者,消費(fèi)者是從隊(duì)列中拿數(shù)據(jù)的,當(dāng)隊(duì)列中元素 < 2時(shí)阻塞
};

在編寫匹配隊(duì)列類的時(shí)候我自己遇到了一個(gè)坑,就是下面左邊的這種寫法👇:?

錯(cuò)誤寫法中會導(dǎo)致死鎖問題。兩個(gè)成員函數(shù)pop()empty()都同時(shí)申請同一個(gè)鎖資源,即成員變量_mtx,就會出現(xiàn)爭奪鎖資源,導(dǎo)致死鎖。

匹配管理類實(shí)現(xiàn)

玩家進(jìn)入游戲大廳后向服務(wù)器發(fā)起Json::Value對象格式的游戲匹配請求。下面列舉了開始游戲匹配和停止游戲匹配的Json::Value格式👇。

| 開始游戲匹配?|

{"optype": "match_start"
}
// 服務(wù)器后臺正確處理后回復(fù)
{"optype": "match_start", // 表示成功加入匹配隊(duì)列"result": true
}// 服務(wù)器后臺處理出錯(cuò)后回復(fù)
{"optype": "match_start","result": false,"reason": "具體原因...."
}
// 匹配成功后給客戶端的響應(yīng)
{"optype": "match_success", // 表示匹配成功"result": true
}

| 停止游戲匹配 |

{"optype": "match_stop"
}
// 服務(wù)器后臺正確處理后回復(fù)
{"optype": "match_stop","result": true
}// 服務(wù)器后臺處理出錯(cuò)后回復(fù)
{"optype": "match_stop","result": false,"reason": "具體原因...."
}

class match_manager
{
public:match_manager(room_manager* room_manager, user_table* user_table, online_manager* user_online):_room_manager(room_manager), _user_table(user_table), _user_online(user_online), _bronze_thread(std::thread(&match_manager::bronze_thread_entrance, this)), _silver_thread(std::thread(&match_manager::silver_thread_entrance, this)), _gold_thread(std::thread(&match_manager::gold_thread_entrance, this)){DLOG("游戲匹配管理類初始化完畢");}~match_manager() { DLOG("游戲匹配管理類即將銷毀"); }// 將玩家添加到匹配隊(duì)列中bool add(uint64_t user_id){// 根據(jù)玩家的分?jǐn)?shù),將玩家添加到不同的匹配隊(duì)列中// 根據(jù)玩家id獲取玩家信息,讀取玩家的分?jǐn)?shù)Json::Value user;bool ret = _user_table->select_by_id(user_id, user);if (ret == false){DLOG("獲取玩家:%d 的信息失敗", user_id);return false;}// 根據(jù)分?jǐn)?shù)將玩家加入到對應(yīng)的匹配隊(duì)列中int score = user["score"].asInt();if (score < 2000){_bronze_queue.push(user_id);}else if (score >= 2000 && score < 3000){_silver_queue.push(user_id);}else if (score >= 3000){_gold_queue.push(user_id);}return true;}// 將玩家從匹配隊(duì)列中移除bool del(uint64_t user_id){Json::Value user;bool ret = _user_table->select_by_id(user_id, user);if (ret == false){DLOG("獲取玩家:%d 的信息失敗", user_id);return false;}int score = user["score"].asInt();if (score < 2000){_bronze_queue.remove(user_id);}else if (score >= 2000 && score < 3000){_silver_queue.remove(user_id);}else if (score >= 3000){_gold_queue.remove(user_id);}return true;}private:void match_handler(match_queue<uint64_t>& q){while (true) // 匹配線程需要一直處于運(yùn)行狀態(tài){while (q.size() < 2) // 匹配隊(duì)列中玩家不足兩人,阻塞線程{q.wait();}// 從匹配隊(duì)列中出隊(duì)兩個(gè)玩家uint64_t user_id1 = 0, user_id2 = 0;bool ret = q.pop(user_id1);if (ret == false) continue; // 第一個(gè)玩家出隊(duì)失敗,跳過后續(xù)代碼重新執(zhí)行上述代碼ret = q.pop(user_id2);if (ret == false) // 代碼執(zhí)行至此,說明第一個(gè)玩家已出隊(duì),第二個(gè)玩家出隊(duì)失敗,要將出隊(duì)的第一個(gè)玩家重新添加到匹配隊(duì)列中{this->add(user_id1);continue;}// 兩個(gè)玩家都出隊(duì)成功,則檢驗(yàn)兩個(gè)玩家是否都在游戲大廳,若玩家A掉線,則將玩家B重新添加到匹配隊(duì)列中websocketsvr_t::connection_ptr conn1 = _user_online->get_conn_from_hall(user_id1);if (conn1.get() == nullptr){this->add(user_id2);continue;}websocketsvr_t::connection_ptr conn2 = _user_online->get_conn_from_hall(user_id2);if (conn2.get() == nullptr){this->add(user_id1);continue;}// 判斷完兩個(gè)玩家都在線后,給兩個(gè)玩家創(chuàng)建游戲房間room_ptr proom = _room_manager->create_room(user_id1, user_id2);if (proom.get() == nullptr) // 創(chuàng)建游戲房間失敗{// 將兩個(gè)玩家重新放回匹配隊(duì)列中this->add(user_id1);this->add(user_id2);continue;}// 給游戲房間內(nèi)的兩個(gè)玩家返回響應(yīng)Json::Value resp;resp["optype"] = "match_success";resp["result"] = true;std::string resp_str;json_util::serialize(resp, resp_str);conn1->send(resp_str);conn2->send(resp_str);}}// 青銅匹配隊(duì)列處理線程void bronze_thread_entrance() { match_handler(_bronze_queue); }// 白銀匹配隊(duì)列處理線程void silver_thread_entrance() { match_handler(_silver_queue); }// 黃金匹配隊(duì)列處理線程void gold_thread_entrance() { match_handler(_gold_queue); }private:match_queue<uint64_t> _bronze_queue; // 青銅匹配隊(duì)列match_queue<uint64_t> _silver_queue; // 白銀匹配隊(duì)列match_queue<uint64_t> _gold_queue;   // 黃金匹配隊(duì)列std::thread _bronze_thread;          // 青銅線程,用來管理青銅匹配隊(duì)列的操作std::thread _silver_thread;          // 白銀線程,用來管理白銀匹配隊(duì)列的操作std::thread _gold_thread;            // 黃金線程,用來管理黃金匹配隊(duì)列的操作room_manager* _room_manager;         // 游戲房間管理句柄user_table* _user_table;             // 用戶數(shù)據(jù)管理句柄online_manager* _user_online;        // 在線用戶管理句柄
};

匹配管理類中的私有成員函數(shù)match_handler(),有以下幾點(diǎn)值得留意:

①:match_handler()是三個(gè)匹配線程執(zhí)行的函數(shù),該函數(shù)要處于一個(gè)死循環(huán)的狀態(tài),持續(xù)不斷的檢測是否有玩家進(jìn)入匹配隊(duì)列繼續(xù)游戲匹配。


②:匹配隊(duì)列中元素個(gè)數(shù)小于2,則說明匹配多列中的玩家不夠兩人,則需要將線程阻塞在當(dāng)前位置。


③:判斷匹配隊(duì)列中的元素使用了一個(gè)while循環(huán),這是因?yàn)槊看蜗蚱ヅ潢?duì)列中添加一個(gè)玩家(調(diào)用push())時(shí)都會喚醒線程,即線程從③的位置繼續(xù)執(zhí)行,回到while循環(huán)判斷,若此時(shí)匹配隊(duì)列元素大于等于2則向下執(zhí)行代碼,若匹配隊(duì)列中小于2則繼續(xù)阻塞線程,等待下次向匹配隊(duì)列添加玩家時(shí)才再次喚醒線程。

| 匹配隊(duì)列類和匹配管理類整合 |

#pragma once#include <list>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <string>#include "logger.hpp"
#include "room.hpp"
#include "db.hpp"
#include "online.hpp"
#include "util.hpp"template<class T>
class match_queue
{
public:// 獲取隊(duì)列中元素個(gè)數(shù)int size(){std::unique_lock<std::mutex> lock(_mtx);return _list.size();}// 判斷隊(duì)列是否為空bool empty(){std::unique_lock<std::mutex> lock(_mtx);return _list.empty();}// 阻塞隊(duì)列所在的線程void wait(){std::unique_lock<std::mutex> lock(_mtx);_cond.wait(lock);}// 將數(shù)據(jù)入隊(duì),并喚醒線程void push(const T& data){std::unique_lock<std::mutex> lock(_mtx);_list.push_back(data);_cond.notify_all();}// 出隊(duì)并獲取數(shù)據(jù)bool pop(T& data){std::unique_lock<std::mutex> lock(_mtx);if (_list.empty() == true){return false;}data = _list.front();_list.pop_front();return true;}// 移除指定數(shù)據(jù)void remove(const T& data){std::unique_lock<std::mutex> lock(_mtx);_list.remove(data);}private:std::list<T> _list;            // 使用雙鏈表來模擬隊(duì)列(因?yàn)橛袆h除指定元素的需求)std::mutex _mtx;               // 互斥鎖,實(shí)現(xiàn)線程安全std::condition_variable _cond; // 條件變量,主要為了阻塞消費(fèi)者,消費(fèi)者是從隊(duì)列中拿數(shù)據(jù)的,當(dāng)隊(duì)列中元素 < 2時(shí)阻塞
};class match_manager
{
public:match_manager(room_manager* room_manager, user_table* user_table, online_manager* user_online):_room_manager(room_manager), _user_table(user_table), _user_online(user_online), _bronze_thread(std::thread(&match_manager::bronze_thread_entrance, this)), _silver_thread(std::thread(&match_manager::silver_thread_entrance, this)), _gold_thread(std::thread(&match_manager::gold_thread_entrance, this)){DLOG("游戲匹配管理類初始化完畢");}~match_manager() { DLOG("游戲匹配管理類即將銷毀"); }// 將玩家添加到匹配隊(duì)列中bool add(uint64_t user_id){// 根據(jù)玩家的分?jǐn)?shù),將玩家添加到不同的匹配隊(duì)列中// 根據(jù)玩家id獲取玩家信息,讀取玩家的分?jǐn)?shù)Json::Value user;bool ret = _user_table->select_by_id(user_id, user);if (ret == false){DLOG("獲取玩家:%d 的信息失敗", user_id);return false;}// 根據(jù)分?jǐn)?shù)將玩家加入到對應(yīng)的匹配隊(duì)列中int score = user["score"].asInt();if (score < 2000){_bronze_queue.push(user_id);}else if (score >= 2000 && score < 3000){_silver_queue.push(user_id);}else if (score >= 3000){_gold_queue.push(user_id);}return true;}// 將玩家從匹配隊(duì)列中移除bool del(uint64_t user_id){Json::Value user;bool ret = _user_table->select_by_id(user_id, user);if (ret == false){DLOG("獲取玩家:%d 的信息失敗", user_id);return false;}int score = user["score"].asInt();if (score < 2000){_bronze_queue.remove(user_id);}else if (score >= 2000 && score < 3000){_silver_queue.remove(user_id);}else if (score >= 3000){_gold_queue.remove(user_id);}return true;}private:void match_handler(match_queue<uint64_t>& q){while (true) // 匹配線程需要一直處于運(yùn)行狀態(tài){while (q.size() < 2) // 匹配隊(duì)列中玩家不足兩人,阻塞線程{q.wait();}// 從匹配隊(duì)列中出隊(duì)兩個(gè)玩家uint64_t user_id1 = 0, user_id2 = 0;bool ret = q.pop(user_id1);if (ret == false) continue; // 第一個(gè)玩家出隊(duì)失敗,跳過后續(xù)代碼重新執(zhí)行上述代碼ret = q.pop(user_id2);if (ret == false) // 代碼執(zhí)行至此,說明第一個(gè)玩家已出隊(duì),第二個(gè)玩家出隊(duì)失敗,要將出隊(duì)的第一個(gè)玩家重新添加到匹配隊(duì)列中{this->add(user_id1);continue;}// 兩個(gè)玩家都出隊(duì)成功,則檢驗(yàn)兩個(gè)玩家是否都在游戲大廳,若玩家A掉線,則將玩家B重新添加到匹配隊(duì)列中websocketsvr_t::connection_ptr conn1 = _user_online->get_conn_from_hall(user_id1);if (conn1.get() == nullptr){this->add(user_id2);continue;}websocketsvr_t::connection_ptr conn2 = _user_online->get_conn_from_hall(user_id2);if (conn2.get() == nullptr){this->add(user_id1);continue;}// 判斷完兩個(gè)玩家都在線后,給兩個(gè)玩家創(chuàng)建游戲房間room_ptr proom = _room_manager->create_room(user_id1, user_id2);if (proom.get() == nullptr) // 創(chuàng)建游戲房間失敗{// 將兩個(gè)玩家重新放回匹配隊(duì)列中this->add(user_id1);this->add(user_id2);continue;}// 給游戲房間內(nèi)的兩個(gè)玩家返回響應(yīng)Json::Value resp;resp["optype"] = "match_success";resp["result"] = true;std::string resp_str;json_util::serialize(resp, resp_str);conn1->send(resp_str);conn2->send(resp_str);}}// 青銅匹配隊(duì)列處理線程void bronze_thread_entrance() { match_handler(_bronze_queue); }// 白銀匹配隊(duì)列處理線程void silver_thread_entrance() { match_handler(_silver_queue); }// 黃金匹配隊(duì)列處理線程void gold_thread_entrance() { match_handler(_gold_queue); }private:match_queue<uint64_t> _bronze_queue; // 青銅匹配隊(duì)列match_queue<uint64_t> _silver_queue; // 白銀匹配隊(duì)列match_queue<uint64_t> _gold_queue;   // 黃金匹配隊(duì)列std::thread _bronze_thread;          // 青銅線程,用來管理青銅匹配隊(duì)列的操作std::thread _silver_thread;          // 白銀線程,用來管理白銀匹配隊(duì)列的操作std::thread _gold_thread;            // 黃金線程,用來管理黃金匹配隊(duì)列的操作room_manager* _room_manager;         // 游戲房間管理句柄user_table* _user_table;             // 用戶數(shù)據(jù)管理句柄online_manager* _user_online;        // 在線用戶管理句柄
};

?服務(wù)器類

服務(wù)器類是對先前所實(shí)現(xiàn)的所有類的一個(gè)整合封裝,對外提供搭建五子棋對戰(zhàn)游戲服務(wù)器的接口的類。通過實(shí)例化出一個(gè)服務(wù)器對象,即可簡便的完成一個(gè)五子棋游戲服務(wù)器的搭建。

該項(xiàng)目只對后端C++代碼進(jìn)行講解和梳理,項(xiàng)目中涉及到的前端資源及代碼在這里獲取👉前端資源及代碼

👉在與服務(wù)器類文件同級目錄下創(chuàng)建一個(gè)名為wwwroot的文件夾,將前端資源及代碼放到wwwroot中即可👈

Restful風(fēng)格的網(wǎng)絡(luò)通信接口設(shè)計(jì)

客戶端和服務(wù)器之間存在多種請求和響應(yīng),客戶端發(fā)送的請求需要統(tǒng)一一種格式,同理服務(wù)器給客戶端發(fā)送的響應(yīng)也需要統(tǒng)一格式。

下面就來設(shè)計(jì)該項(xiàng)目中統(tǒng)一的網(wǎng)絡(luò)通信數(shù)據(jù)格式👇。

Restful風(fēng)格依托于HTTP協(xié)議來實(shí)現(xiàn)。

正文部分采用XMLJson格式進(jìn)行正文數(shù)據(jù)的格式組織。

GET - 獲取資源

POST - 新增資源

PUT - 更新資源

DELETE - 刪除資源

靜態(tài)資源請求與響應(yīng)格式

靜態(tài)資源頁面在服務(wù)器上就是一個(gè)html/css/js文件。服務(wù)器對于靜態(tài)資源請求的處理,其實(shí)就是讀取文件中的內(nèi)容再發(fā)送給客戶端。

| 注冊頁面請求與響應(yīng) |

請求:
GET /register.html HTTP/1.1響應(yīng):
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(響應(yīng)正文)register.html文件的內(nèi)容數(shù)據(jù)

?| 登錄頁面請求與響應(yīng) |

請求:
GET /login.html HTTP/1.1響應(yīng):
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(響應(yīng)正文)login.html文件的內(nèi)容數(shù)據(jù)

?| 游戲大廳頁面請求與響應(yīng) |

請求:
GET /game_hall.html HTTP/1.1響應(yīng):
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(響應(yīng)正文)game_hall.html文件的內(nèi)容數(shù)據(jù)

?| 游戲房間頁面請求與響應(yīng) |

請求:
GET /game_room.html HTTP/1.1響應(yīng):
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(響應(yīng)正文)game_room.html文件的內(nèi)容數(shù)據(jù)

動態(tài)資源請求與響應(yīng)格式

| 用戶注冊請求與響應(yīng) |

POST /signup HTTP/1.1
Content-Type: application/json
Content-Length: 32{"username":"xiaobai", "password":"123123"}
// 成功時(shí)的響應(yīng)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15{"result":true}
// 失敗時(shí)的響應(yīng)
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 43{"result":false, "reason":"用戶名已經(jīng)被占用"}

| 用戶登錄請求與響應(yīng) |

POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 32{"username":"xiaobai", "password":"123123"}
// 成功時(shí)的響應(yīng)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15{"result":true}
// 失敗時(shí)的響應(yīng)
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 43{"result":false, "reason":"用戶還未登錄"}

| 獲取客戶端信息請求與響應(yīng)?|

GET /userinfo HTTP/1.1
Content-Type: application/json
Content-Length: 0
// 成功時(shí)的響應(yīng)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 58{"id":1, "username":"xiaobai", "score":1000, "total_count":4, "win_count":2}
// 失敗時(shí)的響應(yīng)
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 43{"result":false, "reason":"用戶還未登錄"}

?| WebSocket長連接協(xié)議切換請求與響應(yīng)(進(jìn)入游戲大廳)|

GET /match HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......
// 響應(yīng)
HTTP/1.1 101 Switching
......

?| WebSocket長連接協(xié)議切換請求與響應(yīng)(進(jìn)入游戲房間)|

GET /game HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......
// 響應(yīng)
HTTP/1.1 101 Switching
......

👆上面的請求與響應(yīng)的格式是HTTP協(xié)議的格式。

WebSocket協(xié)議切換成功后,后續(xù)的請求與響應(yīng)格式改為Json格式👇。

| WebSocket連接建立成功后的響應(yīng) |

表示已經(jīng)成功進(jìn)入游戲大廳

{"optype": "hall_ready","result": true,"uid": 1
}

| WebSocket連接建立成功后的響應(yīng) |

表示已經(jīng)成功進(jìn)入游戲房間

{"optype": "room_ready","result": true,"room_id": 222, // 游戲房間id"self_id": 1,   // 當(dāng)前玩家id"white_id": 1,  // 白棋玩家id"black_id": 2   // 黑棋玩家id
}

| 游戲匹配請求與響應(yīng) |

{"optype": "match_start"
}
// 服務(wù)器后臺正確處理后回復(fù)
{"optype": "match_start", // 表示成功加入匹配隊(duì)列"result": true
}// 服務(wù)器后臺處理出錯(cuò)后回復(fù)
{"optype": "match_start","result": false,"reason": "具體原因...."
}
// 匹配成功后給客戶端的回復(fù)
{"optype": "match_success", // 表示匹配成功"result": true
}

| 停止匹配請求與響應(yīng)?|

{"optype": "match_stop"
}
// 服務(wù)器后臺正確處理后回復(fù)
{"optype": "match_stop","result": true
}// 服務(wù)器后臺處理出錯(cuò)后回復(fù)
{"optype": "match_stop","result": false,"reason": "具體原因...."
}

?| 下棋請求與響應(yīng)?|

{"optype": "put_chess", // put_chess表示當(dāng)前請求是下棋操作"room_id": 222, ? ? ? ?// room_id 表示當(dāng)前動作屬于哪個(gè)房間"uid": 1, ? ? ? ? ? ? ?// 當(dāng)前的下棋操作是哪個(gè)用戶發(fā)起的"row": 3, ? ? ? ? ? ? ?// 當(dāng)前下棋位置的行號"col": 2 ? ? ? ? ? ? ? // 當(dāng)前下棋位置的列號
}
{"optype": "put_chess","result": false,"reason": "走棋失敗具體原因...."
}
{"optype": "put_chess","result": true,"reason": "對放掉線,不戰(zhàn)而勝!" / "對方/己方五星連珠,戰(zhàn)無敵/雖敗猶榮!","room_id": 222,"uid": 1,"row": 3,"col": 2,"winner": 0 // 0 -- 未分勝負(fù), !0 -- 已分勝負(fù)(uid是誰,誰就贏了)
}

| 聊天請求與響應(yīng) |

{"optype": "chat","room_id": 222,"uid": 1,"message": "快點(diǎn)!"
}
{"optype": "chat","result": false,"reason": "發(fā)送消息失敗的原因"
}
{"optype": "chat","result": true,"room_id": 222,"uid": 1,"message": "快點(diǎn)!"
}

客戶端對服務(wù)器的請求

在搭建服務(wù)器之前,首先要清楚客戶端對服務(wù)器有哪些請求,以下列舉了用戶從開始注冊到游戲結(jié)束對服務(wù)器的所有請求👇。

HTTP請求:

  1. 客戶端從服務(wù)器獲取一個(gè)注冊頁面
  2. 客戶端給服務(wù)器發(fā)送一個(gè)用戶注冊請求(提交用戶名和密碼)
  3. 客戶端從服務(wù)器獲取一個(gè)登錄頁面
  4. 客戶端給服務(wù)器發(fā)送一個(gè)用戶登錄請求(提交用戶名和密碼)
  5. 客戶端從服務(wù)器獲取一個(gè)游戲大廳頁面
  6. 客戶端給服務(wù)器發(fā)送一個(gè)獲取個(gè)人信息的請求(展示個(gè)人信息)

WebSocket請求:

  1. 客戶端給服務(wù)器發(fā)送一個(gè)切換WebSocket協(xié)議通信的請求(建立游戲大廳長連接)
  2. 客戶端給服務(wù)器發(fā)送一個(gè)游戲匹配請求
  3. 客戶端給服務(wù)器發(fā)送一個(gè)停止匹配請求
  4. 對戰(zhàn)匹配成功,客戶端從服務(wù)器獲取一個(gè)游戲房間頁面
  5. 客戶端給服務(wù)器發(fā)送一個(gè)切換WebSocket協(xié)議通信的請求(建立游戲房間長連接)
  6. 客戶端給服務(wù)器發(fā)送一個(gè)下棋請求
  7. 客戶端給服務(wù)器發(fā)送一個(gè)聊天請求
  8. 游戲結(jié)果,返回游戲大廳(客戶端給服務(wù)器發(fā)送一個(gè)獲取游戲大廳頁面的請求)

服務(wù)器類實(shí)現(xiàn)

服務(wù)器類是對前面實(shí)現(xiàn)的所有類進(jìn)行整合并使用的類,是該項(xiàng)目中的重中之重!

通過服務(wù)器類可以實(shí)例化出一個(gè)服務(wù)器對象,調(diào)用服務(wù)器的接口函數(shù)即可將游戲服務(wù)器運(yùn)行起來。

由于服務(wù)器類中的成員函數(shù)繁多,下面將會一一實(shí)現(xiàn)服務(wù)器類中的成員函數(shù)。

搭建基本的服務(wù)器框架
#pragma once#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>#include "db.hpp"
#include "online.hpp"
#include "room.hpp"
#include "matcher.hpp"
#include "session.hpp"#define WWWROOT "./wwwroot/"class gobang_server
{
public:// 進(jìn)行成員變量初始化,以及設(shè)置服務(wù)器回調(diào)函數(shù)gobang_server(const std::string& host,const std::string& user,const std::string& password,const std::string& dbname,uint32_t port = 3306,const std::string& wwwroot = WWWROOT):_web_root(wwwroot), _user_table(host, user, password, dbname, port), _room_manager(&_user_table, &_user_online), _match_manager(&_room_manager, &_user_table, &_user_online), _session_manager(&_wssvr){_wssvr.set_access_channels(websocketpp::log::alevel::none);_wssvr.init_asio();_wssvr.set_reuse_addr(true);_wssvr.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1));_wssvr.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1));_wssvr.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));_wssvr.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));}// 啟動服務(wù)器void start(uint16_t port){_wssvr.listen(port);_wssvr.start_accept();_wssvr.run();}private:void wsopen_callback(websocketpp::connection_hdl hdl);void wsclose_callback(websocketpp::connection_hdl hdl);void wsmsg_callback(websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg);void http_callback(websocketpp::connection_hdl hdl);private:std::string _web_root;            // 靜態(tài)資源根目錄(./wwwroot/) 請求的url為 /register.html,會自動將url拼接到_web_root后,即./wwwroot/register.htmlwebsocketsvr_t _wssvr;            // WebSocket服務(wù)器user_table _user_table;           // 用戶數(shù)據(jù)管理模塊online_manager _user_online;      // 在線用戶管理模塊room_manager _room_manager;       // 游戲房間管理模塊match_manager _match_manager;     // 游戲匹配管理模塊session_manager _session_manager; // session管理模塊
};

博客的開頭也有講解如何搭建一個(gè)簡單的WebSocket服務(wù)器。在服務(wù)器類中只是把搭建WebSocket服務(wù)器的步驟拆分到了構(gòu)造函數(shù)start()中,搭建WebSocket服務(wù)器的本質(zhì)過程還是相同的。

后面主要圍繞wsopen_callback()wsclose_callback()wsmsg_callback()http_callback()這四個(gè)函數(shù)來做文章。

HTTP請求處理函數(shù)
void http_callback(websocketpp::connection_hdl hdl)
{websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string method = req.get_method();std::string uri = req.get_uri();if (method == "POST" && uri == "/signup"){signup_handler(conn);}else if (method == "POST" && uri == "/login"){login_handler(conn);}else if (method == "GET" && uri == "/userinfo"){info_handler(conn);}else{file_handler(conn);}
}

首先調(diào)用Websocketpp中的接口get_conn_from_hdl(),獲取連接(conn),通過conn獲取用戶發(fā)送的HTTP請求,解析HTTP請求,獲取HTTP首行中的請求方法uri,再根據(jù)請求方法uri的組合,調(diào)用不同的處理函數(shù)

實(shí)現(xiàn)以下函數(shù)接口以方便組織HTTP響應(yīng)返回給用戶端👇:

void http_resp(websocketsvr_t::connection_ptr& conn, bool result, const std::string& reason, websocketpp::http::status_code::value code)
{Json::Value resp;resp["result"] = result;resp["reason"] = reason;std::string resp_str;json_util::serialize(resp, resp_str);conn->set_status(code);conn->set_body(resp_str);conn->append_header("Content-Type", "application/json");
}

實(shí)現(xiàn)以下函數(shù)接口以方便組織WebSocket響應(yīng)返回給用戶端👇:

void websocket_resp(websocketsvr_t::connection_ptr& conn, Json::Value& resp)
{std::string resp_str;json_util::serialize(resp, resp_str);conn->send(resp_str);
}

根據(jù)請求方法uri的組合要實(shí)現(xiàn)以下幾個(gè)4個(gè)處理函數(shù)👇:

  1. 靜態(tài)資源請求處理函數(shù)(file_handler()
  2. 用戶注冊請求處理函數(shù)(signup_handler()
  3. 用戶登錄請求處理函數(shù)(login_handler()
  4. 獲取用戶信息請求處理函數(shù)(info_handler()

靜態(tài)資源請求處理函數(shù)
void file_handler(websocketsvr_t::connection_ptr& conn)
{// 1.獲取http請求的uri -- 資源路徑,了解客戶端請求的頁面文件名稱websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 2.組合出文件實(shí)際的路徑(相對根目錄 + uri)std::string real_path = _web_root + uri;// 3.如果請求的uri是個(gè)目錄,則增加一個(gè)后綴 -- login.htmlif (real_path.back() == '/') // 表示請求資源路徑是一個(gè)目錄{real_path += "login.html";}// 4.讀取文件內(nèi)容;若文件不存在,則返回404std::string body;bool ret = file_util::read(real_path, body);if (ret == false){body += "<html>";body += "<head>";body += "<meta charset='UTF-8'/>";body += "</head>";body += "<body>";body += "<h1> Not Found </h1>";body += "</body>";conn->set_status(websocketpp::http::status_code::value::not_found);conn->set_body(body);return;}// 5.設(shè)置響應(yīng)正文conn->set_status(websocketpp::http::status_code::value::ok);conn->set_body(body);return;
}

用戶注冊請求處理函數(shù)
void signup_handler(websocketsvr_t::connection_ptr& conn)
{// 1. 獲取HTTP請求正文std::string req_body = conn->get_request_body();// 2. 對HTTP請求正文進(jìn)行反序列化得到用戶名和密碼Json::Value signup_info;bool ret = json_util::unserialize(req_body, signup_info);if (ret == false){DLOG("反序列化注冊信息失敗");return http_resp(conn, false, "用戶注冊失敗", websocketpp::http::status_code::value::bad_request);}// 3. 進(jìn)行數(shù)據(jù)庫的用戶新增操作(成功返回200,失敗返回400)if (signup_info["username"].isNull() || signup_info["password"].isNull()){DLOG("缺少用戶名或密碼");return http_resp(conn, false, "缺少用戶名或密碼", websocketpp::http::status_code::value::bad_request);}ret = _user_table.signup(signup_info); // 在數(shù)據(jù)庫中新增用戶if (ret == false){DLOG("向數(shù)據(jù)庫中添加用戶失敗");return http_resp(conn, false, "用戶注冊失敗", websocketpp::http::status_code::value::bad_request);}// 用戶注冊成功,返回成功響應(yīng)return http_resp(conn, true, "用戶注冊成功", websocketpp::http::status_code::value::ok);
}

用戶登錄請求處理函數(shù)
void login_handler(websocketsvr_t::connection_ptr& conn)
{// 1. 獲取HTTP請求正文std::string req_body = conn->get_request_body();// 2. 對HTTP請求正文進(jìn)行反序列化得到用戶名和密碼Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DLOG("反序列化登錄信息失敗");return http_resp(conn, false, "用戶登錄失敗", websocketpp::http::status_code::value::bad_request);}// 3. 校驗(yàn)正文完整性,進(jìn)行數(shù)據(jù)庫的用戶信息驗(yàn)證(失敗返回400)if (login_info["username"].isNull() || login_info["password"].isNull()){DLOG("缺少用戶名或密碼");return http_resp(conn, false, "缺少用戶名或密碼", websocketpp::http::status_code::value::bad_request);}ret = _user_table.login(login_info); // 進(jìn)行登錄驗(yàn)證if (ret == false){DLOG("用戶登錄失敗");return http_resp(conn, false, "用戶登錄失敗", websocketpp::http::status_code::value::bad_request);}// 4. 用戶信息驗(yàn)證成功,則給客戶端創(chuàng)建sessionuint64_t user_id = login_info["id"].asUInt64();session_ptr psession = _session_manager.create_session(user_id, LOGIN);if (psession.get() == nullptr){DLOG("創(chuàng)建session失敗");return http_resp(conn, false, "創(chuàng)建session失敗", websocketpp::http::status_code::value::internal_server_error);}_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);// 5. 設(shè)置響應(yīng)頭部,Set-Cookie,將session id通過cookie返回std::string cookie_ssid = "SSID=" + std::to_string(psession->get_session_id());conn->append_header("Set-Cookie", cookie_ssid);return http_resp(conn, true, "用戶登錄成功", websocketpp::http::status_code::value::ok);
}

獲取用戶信息請求處理函數(shù)
// 通過HTTP請求頭部字段中的Cookie信息獲取session id
bool get_cookie_value(const std::string& cookie, const std::string& key, std::string& value)
{// Cookie: SSID=xxx; key=value; key=value; // 1. 以‘; ’作為間隔,對字符串進(jìn)行分割,得到各個(gè)單個(gè)的Cookie信息std::vector<std::string> cookie_arr;string_util::split(cookie, "; ", cookie_arr);// 2. 對單個(gè)的cookie字符串以‘=’為間隔進(jìn)行分割,得到key和valfor (const auto& str : cookie_arr){std::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2) continue;if (tmp_arr[0] == key){value = tmp_arr[1];return true;}}return false;
}void info_handler(websocketsvr_t::connection_ptr& conn)
{// 1. 獲取HTTP請求中的Cookie字段std::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){return http_resp(conn, false, "沒有Cookie信息", websocketpp::http::status_code::value::bad_request);}// 從Cookie中獲取session idstd::string session_id_str;bool ret = get_cookie_value(cookie_str, "SSID", session_id_str);if (ret == false){return http_resp(conn, false, "Cookie中沒有session id", websocketpp::http::status_code::value::bad_request);}// 2. 根據(jù)session id在session管理中獲取對應(yīng)的sessionsession_ptr psession = _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() == nullptr){return http_resp(conn, false, "session已過期", websocketpp::http::status_code::value::bad_request);}// 3. 通過session獲取對應(yīng)的user id,再從數(shù)據(jù)庫中獲取用戶信息,并序列化返回給客戶端uint64_t user_id = psession->get_user_id();Json::Value user_info;ret = _user_table.select_by_id(user_id, user_info);if (ret == false){return http_resp(conn, false, "獲取用戶信息失敗", websocketpp::http::status_code::value::bad_request);}std::string user_info_str;json_util::serialize(user_info, user_info_str);conn->set_status(websocketpp::http::status_code::value::ok);conn->set_body(user_info_str);conn->append_header("Content-Type", "application/json");// 4. 上述操作訪問了session,所以要刷新session的過期時(shí)間_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);
}

WebSocket長連接建立成功后的處理函數(shù)
void wsopen_callback(websocketpp::connection_hdl hdl)
{websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall") // 建立了游戲大廳的長連接{wsopen_game_hall(conn);}else if (uri == "/room") // 建立了游戲房間的長連接{wsopen_game_room(conn);}
}

根據(jù)HTTP請求首行中的uri來判斷,客戶端服務(wù)器建立的是游戲大廳的長連接還是游戲房間的長連接。

用戶登錄驗(yàn)證函數(shù)(登錄成功則返回用戶session)
session_ptr get_session_by_cookie(websocketsvr_t::connection_ptr conn)
{Json::Value resp;// 1. 獲取HTTP請求中的Cookie字段std::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "沒有Cookie信息";websocket_resp(conn, resp);return session_ptr();}// 從Cookie中獲取session idstd::string session_id_str;bool ret = get_cookie_value(cookie_str, "SSID", session_id_str);if (ret == false){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "Cookie中沒有session";websocket_resp(conn, resp);return session_ptr();}// 2. 根據(jù)session id在session管理中獲取對應(yīng)的sessionsession_ptr psession = _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() == nullptr){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "session已過期";websocket_resp(conn, resp);return session_ptr();}return psession;
}

游戲大廳長連接建立成功的處理函數(shù)
void wsopen_game_hall(websocketsvr_t::connection_ptr conn)
{Json::Value resp;// 1. 登錄驗(yàn)證(判斷當(dāng)前用戶是否登錄成功)session_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 判斷當(dāng)前用戶是否重復(fù)登錄if (_user_online.is_in_game_hall(psession->get_user_id()) || _user_online.is_in_game_room(psession->get_user_id())){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "用戶已登錄";return websocket_resp(conn, resp);}// 3. 將當(dāng)前用戶和對應(yīng)的連接加入到游戲大廳_user_online.enter_game_hall(psession->get_user_id(), conn);// 4. 給用戶響應(yīng)游戲大廳建立成功resp["optype"] = "hall_ready";resp["result"] = true;resp["uid"] = (Json::UInt64)psession->get_user_id();websocket_resp(conn, resp);// 5. 將session的生命周期設(shè)置為永久_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_PERMANENT);
}

游戲房間長連接建立成功的處理函數(shù)
void wsopen_game_room(websocketsvr_t::connection_ptr conn)
{Json::Value resp;// 1. 登錄驗(yàn)證(判斷當(dāng)前用戶是否登錄成功)session_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 判斷當(dāng)前用戶是否重復(fù)登錄if (_user_online.is_in_game_hall(psession->get_user_id()) || _user_online.is_in_game_room(psession->get_user_id())){resp["optype"] = "room_ready";resp["result"] = false;resp["reason"] = "用戶已登錄";return websocket_resp(conn, resp);}// 3. 判斷當(dāng)前用戶是否已經(jīng)創(chuàng)建好了房間room_ptr proom = _room_manager.get_room_by_user_id(psession->get_user_id());if (proom.get() == nullptr){resp["optype"] = "room_ready";resp["result"] = false;resp["reason"] = "通過用戶id獲取游戲房間失敗";return websocket_resp(conn, resp);}// 4. 將當(dāng)前用戶添加到在線用戶管理的游戲房間中的用戶管理中_user_online.enter_game_room(psession->get_user_id(), conn);// 5. 給用戶響應(yīng)房間創(chuàng)建完成resp["optype"] = "room_ready";resp["result"] = true;resp["room_id"] = (Json::UInt64)proom->id();resp["uid"] = (Json::UInt64)psession->get_user_id();resp["white_id"] = (Json::UInt64)proom->get_white_player();resp["black_id"] = (Json::UInt64)proom->get_black_player();websocket_resp(conn, resp);// 6. 將session的生命周期設(shè)置為永久_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_PERMANENT);
}

WebSocket長連接斷開前的處理函數(shù)
void wsclose_callback(websocketpp::connection_hdl hdl)
{websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall") // 游戲大廳長連接斷開{wscloes_game_hall(conn);}else if (uri == "/room") // 游戲房間長連接斷開{wscloes_game_room(conn);}
}

根據(jù)HTTP請求首行中的uri來判斷,客戶端斷開的是游戲大廳的長連接還是游戲房間的長連接

游戲大廳長連接斷開的處理函數(shù)
void wscloes_game_hall(websocketsvr_t::connection_ptr conn)
{// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 將用戶從游戲大廳中移除_user_online.exit_game_hall(psession->get_user_id());// 3. 將session的生命周期設(shè)為定時(shí)的,超時(shí)自動刪除_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);
}

游戲房間長連接斷開的處理函數(shù)
void wscloes_game_room(websocketsvr_t::connection_ptr conn)
{// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 將玩家從在線用戶管理中的游戲房間中的玩家中移除_user_online.exit_game_room(psession->get_user_id());// 3. 將玩家從游戲房間中移除(房間中所有玩家都退出了就會銷毀房間)_room_manager.remove_player_in_room(psession->get_user_id());// 4. 將session的生命周期設(shè)置為定時(shí)的,超時(shí)自動銷毀_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);
}

WebSocket長連接通信處理函數(shù)
void wsmsg_callback(websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg)
{websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall") // 游戲大廳請求{wsmsg_game_hall(conn, msg); // 游戲大廳請求處理函數(shù)}else if (uri == "/room") // 游戲房間請求{wsmsg_game_room(conn, msg); // 游戲房間請求處理函數(shù)}
}

根據(jù)HTTP請求首行中的uri來判斷,當(dāng)前請求是游戲大廳中的請求還是游戲房間中的請求。

游戲大廳請求處理函數(shù)(游戲匹配請求/停止匹配請求)
void wsmsg_game_hall(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg)
{Json::Value resp;// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 獲取WebSocket請求信息std::string req_str = msg->get_payload();Json::Value req;bool ret = json_util::unserialize(req_str, req);if (ret == false){resp["result"] = false;resp["reason"] = "解析請求失敗";return websocket_resp(conn, resp);}// 3. 對于請求分別進(jìn)行處理if (!req["optype"].isNull() && req["optype"].asString() == "match_start"){// 開始游戲匹配:通過匹配模塊,將玩家添加到匹配隊(duì)列中_match_manager.add(psession->get_user_id());resp["optype"] = "match_start";resp["result"] = true;return websocket_resp(conn, resp);}else if (!req["optype"].isNull() && req["optype"].asString() == "match_stop"){// 停止游戲匹配:通過匹配模塊,將玩家從匹配隊(duì)列中移除_match_manager.del(psession->get_user_id());resp["optype"] = "match_stop";resp["result"] = true;return websocket_resp(conn, resp);}resp["optype"] = "unknown";resp["result"] = false;resp["reason"] = "未知請求類型";return websocket_resp(conn, resp);
}

游戲房間請求處理函數(shù)(下棋請求/聊天請求)
void wsmsg_game_room(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg){Json::Value resp;// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 獲取用戶所在的游戲房間信息room_ptr proom = _room_manager.get_room_by_user_id(psession->get_user_id());if (proom.get() == nullptr){resp["optype"] = "unknown";resp["result"] = false;resp["reason"] = "通過用戶id獲取游戲房間失敗";return websocket_resp(conn, resp);}// 3. 對請求進(jìn)行反序列化std::string req_str = msg->get_payload();Json::Value req;bool ret = json_util::unserialize(req_str, req);if (ret == false){resp["optype"] = "unknown";resp["result"] = false;resp["reason"] = "解析請求失敗";return websocket_resp(conn, resp);}// 4. 通過游戲房間進(jìn)行游戲房間請求的處理return proom->handle_request(req);}

| 服務(wù)器類所有函數(shù)整合 |
#pragma once#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#include <vector>#include "db.hpp"
#include "online.hpp"
#include "room.hpp"
#include "matcher.hpp"
#include "session.hpp"#define WWWROOT "./wwwroot/"class gobang_server
{
public:// 進(jìn)行成員變量初始化,以及設(shè)置服務(wù)器回調(diào)函數(shù)gobang_server(const std::string& host,const std::string& user,const std::string& password,const std::string& dbname,uint32_t port = 3306,const std::string& wwwroot = WWWROOT):_web_root(wwwroot), _user_table(host, user, password, dbname, port), _room_manager(&_user_table, &_user_online), _match_manager(&_room_manager, &_user_table, &_user_online), _session_manager(&_wssvr){_wssvr.set_access_channels(websocketpp::log::alevel::none);_wssvr.init_asio();_wssvr.set_reuse_addr(true);_wssvr.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1));_wssvr.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1));_wssvr.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));_wssvr.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));}// 啟動服務(wù)器void start(uint16_t port){_wssvr.listen(port);_wssvr.start_accept();_wssvr.run();}private:// WebSocket長連接建立成功后的處理函數(shù)void wsopen_callback(websocketpp::connection_hdl hdl){websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall") // 建立了游戲大廳的長連接{wsopen_game_hall(conn);}else if (uri == "/room") // 建立了游戲房間的長連接{wsopen_game_room(conn);}}// WebSocket長連接斷開前的處理函數(shù)void wsclose_callback(websocketpp::connection_hdl hdl){websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall") // 游戲大廳長連接斷開{wscloes_game_hall(conn);}else if (uri == "/room") // 游戲房間長連接斷開{wscloes_game_room(conn);}}// WebSocket長連接通信處理void wsmsg_callback(websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg){websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();if (uri == "/hall") // 游戲大廳請求{wsmsg_game_hall(conn, msg); // 游戲大廳請求處理函數(shù)}else if (uri == "/room") // 游戲房間請求{wsmsg_game_room(conn, msg); // 游戲房間請求處理函數(shù)}}void http_callback(websocketpp::connection_hdl hdl){websocketsvr_t::connection_ptr conn = _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string method = req.get_method();std::string uri = req.get_uri();if (method == "POST" && uri == "/signup"){signup_handler(conn);}else if (method == "POST" && uri == "/login"){login_handler(conn);}else if (method == "GET" && uri == "/userinfo"){info_handler(conn);}else{file_handler(conn);}}private:// 組織HTTP響應(yīng)void http_resp(websocketsvr_t::connection_ptr& conn, bool result, const std::string& reason, websocketpp::http::status_code::value code){Json::Value resp;resp["result"] = result;resp["reason"] = reason;std::string resp_str;json_util::serialize(resp, resp_str);conn->set_status(code);conn->set_body(resp_str);conn->append_header("Content-Type", "application/json");}// 組織WebSocket響應(yīng)void websocket_resp(websocketsvr_t::connection_ptr& conn, Json::Value& resp){std::string resp_str;json_util::serialize(resp, resp_str);conn->send(resp_str);}// 靜態(tài)資源請求的處理void file_handler(websocketsvr_t::connection_ptr& conn){// 1.獲取http請求的uri -- 資源路徑,了解客戶端請求的頁面文件名稱websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 2.組合出文件實(shí)際的路徑(相對根目錄 + uri)std::string real_path = _web_root + uri;// 3.如果請求的uri是個(gè)目錄,則增加一個(gè)后綴 -- login.htmlif (real_path.back() == '/') // 表示請求資源路徑是一個(gè)目錄{real_path += "login.html";}// 4.讀取文件內(nèi)容;若文件不存在,則返回404std::string body;bool ret = file_util::read(real_path, body);if (ret == false){body += "<html>";body += "<head>";body += "<meta charset='UTF-8'/>";body += "</head>";body += "<body>";body += "<h1> Not Found </h1>";body += "</body>";conn->set_status(websocketpp::http::status_code::value::not_found);conn->set_body(body);return;}// 5.設(shè)置響應(yīng)正文conn->set_status(websocketpp::http::status_code::value::ok);conn->set_body(body);return;}// 用戶注冊請求的處理void signup_handler(websocketsvr_t::connection_ptr& conn){// 1. 獲取HTTP請求正文std::string req_body = conn->get_request_body();// 2. 對HTTP請求正文進(jìn)行反序列化得到用戶名和密碼Json::Value signup_info;bool ret = json_util::unserialize(req_body, signup_info);if (ret == false){DLOG("反序列化注冊信息失敗");return http_resp(conn, false, "用戶注冊失敗", websocketpp::http::status_code::value::bad_request);}// 3. 進(jìn)行數(shù)據(jù)庫的用戶新增操作(成功返回200,失敗返回400)if (signup_info["username"].isNull() || signup_info["password"].isNull()){DLOG("缺少用戶名或密碼");return http_resp(conn, false, "缺少用戶名或密碼", websocketpp::http::status_code::value::bad_request);}ret = _user_table.signup(signup_info); // 在數(shù)據(jù)庫中新增用戶if (ret == false){DLOG("向數(shù)據(jù)庫中添加用戶失敗");return http_resp(conn, false, "用戶注冊失敗", websocketpp::http::status_code::value::bad_request);}// 用戶注冊成功,返回成功響應(yīng)return http_resp(conn, true, "用戶注冊成功", websocketpp::http::status_code::value::ok);}// 用戶登錄請求的處理void login_handler(websocketsvr_t::connection_ptr& conn){// 1. 獲取HTTP請求正文std::string req_body = conn->get_request_body();// 2. 對HTTP請求正文進(jìn)行反序列化得到用戶名和密碼Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false){DLOG("反序列化登錄信息失敗");return http_resp(conn, false, "用戶登錄失敗", websocketpp::http::status_code::value::bad_request);}// 3. 校驗(yàn)正文完整性,進(jìn)行數(shù)據(jù)庫的用戶信息驗(yàn)證(失敗返回400)if (login_info["username"].isNull() || login_info["password"].isNull()){DLOG("缺少用戶名或密碼");return http_resp(conn, false, "缺少用戶名或密碼", websocketpp::http::status_code::value::bad_request);}ret = _user_table.login(login_info); // 進(jìn)行登錄驗(yàn)證if (ret == false){DLOG("用戶登錄失敗");return http_resp(conn, false, "用戶登錄失敗", websocketpp::http::status_code::value::bad_request);}// 4. 用戶信息驗(yàn)證成功,則給客戶端創(chuàng)建sessionuint64_t user_id = login_info["id"].asUInt64();session_ptr psession = _session_manager.create_session(user_id, LOGIN);if (psession.get() == nullptr){DLOG("創(chuàng)建session失敗");return http_resp(conn, false, "創(chuàng)建session失敗", websocketpp::http::status_code::value::internal_server_error);}_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);// 5. 設(shè)置響應(yīng)頭部,Set-Cookie,將session id通過cookie返回std::string cookie_ssid = "SSID=" + std::to_string(psession->get_session_id());conn->append_header("Set-Cookie", cookie_ssid);return http_resp(conn, true, "用戶登錄成功", websocketpp::http::status_code::value::ok);}// 通過HTTP請求頭部字段中的Cookie信息獲取session idbool get_cookie_value(const std::string& cookie, const std::string& key, std::string& value){// Cookie: SSID=xxx; key=value; key=value; // 1. 以‘; ’作為間隔,對字符串進(jìn)行分割,得到各個(gè)單個(gè)的Cookie信息std::vector<std::string> cookie_arr;string_util::split(cookie, "; ", cookie_arr);// 2. 對單個(gè)的cookie字符串以‘=’為間隔進(jìn)行分割,得到key和valfor (const auto& str : cookie_arr){std::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2) continue;if (tmp_arr[0] == key){value = tmp_arr[1];return true;}}return false;}// 獲取用戶信息請求的處理void info_handler(websocketsvr_t::connection_ptr& conn){// 1. 獲取HTTP請求中的Cookie字段std::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){return http_resp(conn, false, "沒有Cookie信息", websocketpp::http::status_code::value::bad_request);}// 從Cookie中獲取session idstd::string session_id_str;bool ret = get_cookie_value(cookie_str, "SSID", session_id_str);if (ret == false){return http_resp(conn, false, "Cookie中沒有session id", websocketpp::http::status_code::value::bad_request);}// 2. 根據(jù)session id在session管理中獲取對應(yīng)的sessionsession_ptr psession = _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() == nullptr){return http_resp(conn, false, "session已過期", websocketpp::http::status_code::value::bad_request);}// 3. 通過session獲取對應(yīng)的user id,再從數(shù)據(jù)庫中獲取用戶信息,并序列化返回給客戶端uint64_t user_id = psession->get_user_id();Json::Value user_info;ret = _user_table.select_by_id(user_id, user_info);if (ret == false){return http_resp(conn, false, "獲取用戶信息失敗", websocketpp::http::status_code::value::bad_request);}std::string user_info_str;json_util::serialize(user_info, user_info_str);conn->set_status(websocketpp::http::status_code::value::ok);conn->set_body(user_info_str);conn->append_header("Content-Type", "application/json");// 4. 上述操作訪問了session,所以要刷新session的過期時(shí)間_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);}// 用于驗(yàn)證用戶是否登錄成功,登錄成功則返回用戶sessionsession_ptr get_session_by_cookie(websocketsvr_t::connection_ptr conn){Json::Value resp;// 1. 獲取HTTP請求中的Cookie字段std::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "沒有Cookie信息";websocket_resp(conn, resp);return session_ptr();}// 從Cookie中獲取session idstd::string session_id_str;bool ret = get_cookie_value(cookie_str, "SSID", session_id_str);if (ret == false){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "Cookie中沒有session";websocket_resp(conn, resp);return session_ptr();}// 2. 根據(jù)session id在session管理中獲取對應(yīng)的sessionsession_ptr psession = _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() == nullptr){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "session已過期";websocket_resp(conn, resp);return session_ptr();}return psession;}// 游戲大廳長連接建立成功的處理函數(shù)void wsopen_game_hall(websocketsvr_t::connection_ptr conn){Json::Value resp;// 1. 登錄驗(yàn)證(判斷當(dāng)前用戶是否登錄成功)session_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 判斷當(dāng)前用戶是否重復(fù)登錄if (_user_online.is_in_game_hall(psession->get_user_id()) || _user_online.is_in_game_room(psession->get_user_id())){resp["optype"] = "hall_ready";resp["result"] = false;resp["reason"] = "用戶已登錄";return websocket_resp(conn, resp);}// 3. 將當(dāng)前用戶和對應(yīng)的連接加入到游戲大廳_user_online.enter_game_hall(psession->get_user_id(), conn);// 4. 給用戶響應(yīng)游戲大廳建立成功resp["optype"] = "hall_ready";resp["result"] = true;resp["uid"] = (Json::UInt64)psession->get_user_id();websocket_resp(conn, resp);// 5. 將session的生命周期設(shè)置為永久_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_PERMANENT);}// 游戲房間長連接建立成功的處理函數(shù)void wsopen_game_room(websocketsvr_t::connection_ptr conn){Json::Value resp;// 1. 登錄驗(yàn)證(判斷當(dāng)前用戶是否登錄成功)session_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 判斷當(dāng)前用戶是否重復(fù)登錄if (_user_online.is_in_game_hall(psession->get_user_id()) || _user_online.is_in_game_room(psession->get_user_id())){resp["optype"] = "room_ready";resp["result"] = false;resp["reason"] = "用戶已登錄";return websocket_resp(conn, resp);}// 3. 判斷當(dāng)前用戶是否已經(jīng)創(chuàng)建好了房間room_ptr proom = _room_manager.get_room_by_user_id(psession->get_user_id());if (proom.get() == nullptr){resp["optype"] = "room_ready";resp["result"] = false;resp["reason"] = "通過用戶id獲取游戲房間失敗";return websocket_resp(conn, resp);}// 4. 將當(dāng)前用戶添加到在線用戶管理的游戲房間中的用戶管理中_user_online.enter_game_room(psession->get_user_id(), conn);// 5. 給用戶響應(yīng)房間創(chuàng)建完成resp["optype"] = "room_ready";resp["result"] = true;resp["room_id"] = (Json::UInt64)proom->id();resp["uid"] = (Json::UInt64)psession->get_user_id();resp["white_id"] = (Json::UInt64)proom->get_white_player();resp["black_id"] = (Json::UInt64)proom->get_black_player();websocket_resp(conn, resp);// 6. 將session的生命周期設(shè)置為永久_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_PERMANENT);}// 游戲大廳長連接斷開的處理函數(shù)void wscloes_game_hall(websocketsvr_t::connection_ptr conn){// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 將用戶從游戲大廳中移除_user_online.exit_game_hall(psession->get_user_id());// 3. 將session的生命周期設(shè)為定時(shí)的,超時(shí)自動刪除_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);}// 游戲房間長連接斷開的處理函數(shù)void wscloes_game_room(websocketsvr_t::connection_ptr conn){// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 將玩家從在線用戶管理中的游戲房間中的玩家中移除_user_online.exit_game_room(psession->get_user_id());// 3. 將玩家從游戲房間中移除(房間中所有玩家都退出了就會銷毀房間)_room_manager.remove_player_in_room(psession->get_user_id());// 4. 將session的生命周期設(shè)置為定時(shí)的,超時(shí)自動銷毀_session_manager.set_session_expiration_time(psession->get_session_id(), SESSION_TEMOPRARY);}// 游戲大廳請求處理函數(shù)(游戲匹配請求/停止匹配請求)void wsmsg_game_hall(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg){Json::Value resp;// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 獲取WebSocket請求信息std::string req_str = msg->get_payload();Json::Value req;bool ret = json_util::unserialize(req_str, req);if (ret == false){resp["result"] = false;resp["reason"] = "解析請求失敗";return websocket_resp(conn, resp);}// 3. 對于請求分別進(jìn)行處理if (!req["optype"].isNull() && req["optype"].asString() == "match_start"){// 開始游戲匹配:通過匹配模塊,將玩家添加到匹配隊(duì)列中_match_manager.add(psession->get_user_id());resp["optype"] = "match_start";resp["result"] = true;return websocket_resp(conn, resp);}else if (!req["optype"].isNull() && req["optype"].asString() == "match_stop"){// 停止游戲匹配:通過匹配模塊,將玩家從匹配隊(duì)列中移除_match_manager.del(psession->get_user_id());resp["optype"] = "match_stop";resp["result"] = true;return websocket_resp(conn, resp);}resp["optype"] = "unknown";resp["result"] = false;resp["reason"] = "未知請求類型";return websocket_resp(conn, resp);}// 游戲房間請求處理函數(shù)(下棋請求/聊天請求)void wsmsg_game_room(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg){Json::Value resp;// 1. 獲取用戶的sessionsession_ptr psession = get_session_by_cookie(conn);if (psession.get() == nullptr) return;// 2. 獲取用戶所在的游戲房間信息room_ptr proom = _room_manager.get_room_by_user_id(psession->get_user_id());if (proom.get() == nullptr){resp["optype"] = "unknown";resp["result"] = false;resp["reason"] = "通過用戶id獲取游戲房間失敗";return websocket_resp(conn, resp);}// 3. 對請求進(jìn)行反序列化std::string req_str = msg->get_payload();Json::Value req;bool ret = json_util::unserialize(req_str, req);if (ret == false){resp["optype"] = "unknown";resp["result"] = false;resp["reason"] = "解析請求失敗";return websocket_resp(conn, resp);}// 4. 通過游戲房間進(jìn)行游戲房間請求的處理return proom->handle_request(req);}private:std::string _web_root;            // 靜態(tài)資源根目錄(./wwwroot/) 請求的url為 /register.html,會自動將url拼接到_web_root后,即./wwwroot/register.htmlwebsocketsvr_t _wssvr;            // WebSocket服務(wù)器user_table _user_table;           // 用戶數(shù)據(jù)管理模塊online_manager _user_online;      // 在線用戶管理模塊room_manager _room_manager;       // 游戲房間管理模塊match_manager _match_manager;     // 游戲匹配管理模塊session_manager _session_manager; // session管理模塊
};

項(xiàng)目做到這里已經(jīng)實(shí)現(xiàn)了實(shí)時(shí)聊天的在線匹配五子棋對戰(zhàn)游戲的基本功能了。但是每次啟動游戲服務(wù)器都需要連接Linux服務(wù)器主機(jī),在終端上運(yùn)行該程序,才能運(yùn)行該游戲服務(wù)器,不算真正意義上的網(wǎng)絡(luò)游戲。

為了實(shí)現(xiàn)將程序脫離主機(jī)連接和手動啟動服務(wù)器,我們要將該程序進(jìn)行守護(hù)進(jìn)程化。

守護(hù)進(jìn)程化

?守護(hù)進(jìn)程是一種在后臺運(yùn)行的服務(wù)進(jìn)程,沒有控制終端,因此它們獨(dú)立于任何用戶會話。

在Linux中,daemon()用于將一個(gè)進(jìn)程變成守護(hù)進(jìn)程。

int daemon(int nochdir, int noclose);

參數(shù)說明:

nochdir ----?如果這個(gè)參數(shù)為0,daemon()會將當(dāng)前工作目錄更改為根目錄,這是因?yàn)槭刈o(hù)進(jìn)程不應(yīng)該與某個(gè)具體的文件系統(tǒng)掛載點(diǎn)關(guān)聯(lián)。如果你不希望改變當(dāng)前工作目錄,可以將這個(gè)參數(shù)設(shè)置為非零值

noclose ----?如果這個(gè)參數(shù)為0,daemon()會將標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤重定向到 /dev/null,這意味著守護(hù)進(jìn)程不會在終端上產(chǎn)生輸出。如果你希望保持這些文件描述符不變,可以將這個(gè)參數(shù)設(shè)置為非零值。

返回值:

返回0表示成功,返回-1表示失敗,并設(shè)置errno以指示錯(cuò)誤原因

由于要將程序設(shè)置為守護(hù)進(jìn)程,則先前輸出在終端上的日志信息需要寫入一個(gè)文件中!

在src目錄下創(chuàng)建一個(gè)log.txt文件,將日志信息寫入到log.txt中。

修改日志宏以達(dá)到上述要求👇:

#pragma once#include <iostream>
#include <ctime>
#include <cstdio>#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL INF#define LOG(level, format, ...)                                                                 \do                                                                                          \{                                                                                           \if (DEFAULT_LOG_LEVEL > level)                                                          \break;                                                                              \time_t t = time(NULL);                                                                  \struct tm *lt = localtime(&t);                                                          \char buf[32] = {0};                                                                     \strftime(buf, 31, "%H:%M:%S", lt);                                                      \FILE* pf_log = fopen("./log.txt", "a");                                                 \if (pf_log)                                                                             \{                                                                                       \fprintf(pf_log, "[%s %s:%d] " format "\n", buf, __FILE__, __LINE__, ##__VA_ARGS__); \fclose(pf_log);                                                                     \}                                                                                       \else                                                                                    \{                                                                                       \fprintf(stderr, "Failed to open log file\n");                                       \}                                                                                       \} while (false)#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

使用fopen()打開log.txt,再調(diào)用fprintf(),將日志信息格式化寫入log.txt中。

創(chuàng)建一個(gè)online_gobang.cpp文件,文件中實(shí)例化一個(gè)服務(wù)器類(gobang_server)對象,并運(yùn)行服務(wù)器。

#include <unistd.h>#include "server.hpp"int main()
{// 將游戲服務(wù)器設(shè)置為守護(hù)進(jìn)程if (daemon(1, 0) == -1) {perror("daemon");exit(EXIT_FAILURE);}gobang_server server("127.0.0.1", "root", "", "online_gobang", 3306);server.start(8080);return 0;
}

在main()中的開頭調(diào)用daemon(),將該程序設(shè)置為守護(hù)進(jìn)程。

注意不要改變程序的工作目錄,因?yàn)槌绦蛑姓{(diào)用的前端資源和log.txt都在同級目錄下。

在終端下運(yùn)行編譯生成的可執(zhí)行程序即是將游戲服務(wù)器運(yùn)行起來了!

在瀏覽器中訪問 → 主機(jī)id:目標(biāo)端口號,即可體驗(yàn)在線五子棋對戰(zhàn)游戲了!


項(xiàng)目源碼

項(xiàng)目完整源代碼👉https://github.com/NICK03nK/Project/tree/main/Gobang_online_AG

http://aloenet.com.cn/news/34498.html

相關(guān)文章:

  • 萬網(wǎng)域名申請網(wǎng)站全自動推廣引流軟件
  • 網(wǎng)站上的logo怎么做今日國內(nèi)新聞
  • 網(wǎng)站設(shè)計(jì)聯(lián)盟西安seo學(xué)院
  • 網(wǎng)站開發(fā)wbs工作分解結(jié)構(gòu)北京互聯(lián)網(wǎng)公司有哪些
  • wordpress背景圖更改網(wǎng)站自然優(yōu)化
  • 北京網(wǎng)站如何制作seo網(wǎng)站關(guān)鍵詞優(yōu)化快速官網(wǎng)
  • wordpress 入侵視頻優(yōu)化營商環(huán)境條例全文
  • 怎么設(shè)計(jì)app太原seo排名外包
  • 做app好還是響應(yīng)式網(wǎng)站深圳企業(yè)黃頁網(wǎng)
  • 萊蕪營銷型網(wǎng)站制作廣東省各城市疫情搜索高峰進(jìn)度
  • 網(wǎng)絡(luò)營銷案例分析200字關(guān)鍵詞seo如何優(yōu)化
  • 網(wǎng)站整體運(yùn)營思路互聯(lián)網(wǎng)推廣引流
  • 中國招標(biāo)投標(biāo)網(wǎng)查詢平臺站長之家 seo查詢
  • 網(wǎng)站輪播廣告代碼怎樣搭建一個(gè)網(wǎng)站
  • 無錫 網(wǎng)站建設(shè)公司廣州做seo整站優(yōu)化公司
  • 榆林市網(wǎng)站建設(shè)網(wǎng)站開發(fā)工具
  • 深圳專業(yè)網(wǎng)站制作費(fèi)用怎么建一個(gè)自己的網(wǎng)站
  • 政務(wù)公開政府網(wǎng)站建設(shè)管理百度刷搜索詞
  • wordpress 首頁調(diào)用頁面標(biāo)題城關(guān)網(wǎng)站seo
  • 廣州 網(wǎng)站建設(shè)網(wǎng)絡(luò)推廣網(wǎng)頁設(shè)計(jì)免費(fèi)推廣廣告鏈接
  • 學(xué)校網(wǎng)站推廣seo關(guān)鍵詞排名優(yōu)化怎樣
  • 國外炫酷網(wǎng)站外貿(mào)平臺app
  • 網(wǎng)業(yè)端云服務(wù)武漢seo網(wǎng)站排名優(yōu)化
  • 減肥網(wǎng)站源碼seo需求
  • 國內(nèi)最好的crm軟件南昌seo技術(shù)外包
  • 個(gè)人備案網(wǎng)站做企業(yè)網(wǎng)可以嗎北京搜索引擎優(yōu)化主管
  • 網(wǎng)站專題建設(shè)合同2024新聞熱點(diǎn)事件
  • wordpress布置網(wǎng)站教程友情鏈接只有鏈接
  • seo搜索引擎優(yōu)化技術(shù)教程關(guān)鍵詞排名優(yōu)化提升培訓(xùn)
  • 網(wǎng)站信息資料庫建設(shè)品牌seo是什么