html5 css3網(wǎng)站模板百度流量推廣項(xiàng)目
文章目錄
- 一、項(xiàng)目介紹
- 1. 項(xiàng)目簡(jiǎn)介
- 2. 開發(fā)環(huán)境
- 3. 核心技術(shù)
- 4. 開發(fā)階段
- 二、環(huán)境搭建
- 1. 安裝 wget 工具
- 2. 更換 yum 源
- 3. 安裝 lrzsz 傳輸工具
- 4. 安裝?版本 gcc/g++ 編譯器
- 5. 安裝 gdb 調(diào)試器
- 6. 安裝分布式版本控制工具 git
- 7. 安裝 cmake
- 8. 安裝 boost 庫
- 9. 安裝 Jsoncpp 庫
- 10. 安裝 MySQL 數(shù)據(jù)庫服務(wù)及開發(fā)包
- 11. 安裝 WebSocketpp 庫
- 三、前置知識(shí)了解
- 1. WebSocketpp
- 1.1 WebSocket 協(xié)議
- 1.2 WebSocketpp
- 2. JsonCpp
- 3. C++11
- 4. GDB
- 5. MySQL C API
- 6. HTML/CSS/JS/AJAX
- 6.1 HTML 簡(jiǎn)單了解
- 6.2 CSS 簡(jiǎn)單了解
- 6.3 JS 簡(jiǎn)單了解
- 6.4 AJAX 簡(jiǎn)單了解
- 四、框架設(shè)計(jì)
- 1. 項(xiàng)目模塊劃分
- 1.1 總體模塊劃分
- 1.2 業(yè)務(wù)處理子模塊劃分
- 2. 項(xiàng)目流程圖
- 2.1 用戶角度流程圖
- 2.2 服務(wù)器角度流程圖
- 五、模塊開發(fā)
- 1. 實(shí)用工具類模塊
- 1.1 日志宏封裝
- 1.2 MySQL C API 封裝
- 1.3 JsonCpp 封裝
- 1.4 String Split 封裝
- 1.5 File Read 封裝
- 2. 用戶數(shù)據(jù)管理模塊
- 2.1 用戶信息表
- 2.2 用戶數(shù)據(jù)管理類
- 3. 在線用戶管理模塊
- 4. 游戲房間管理模塊
- 5. 用戶 session 信息管理模塊
- 6. 匹配對(duì)戰(zhàn)管理模塊
- 7. 整合封裝服務(wù)器模塊
- 7.1 網(wǎng)絡(luò)通信接口設(shè)計(jì)
- 7.1.1 靜態(tài)資源請(qǐng)求
- 7.1.2 動(dòng)態(tài)功能請(qǐng)求
- 7.1.3 WebSocket 通信格式
- 7.2 服務(wù)器模塊實(shí)現(xiàn)
- 8. 前端界面模塊
- 8.1 用戶注冊(cè)界面
- 8.2 用戶登錄界面
- 8.3 游戲大廳界面
- 8.4 游戲房間界面
- 六、項(xiàng)目演示
- 七、項(xiàng)目擴(kuò)展
- 八、項(xiàng)目總結(jié)
一、項(xiàng)目介紹
1. 項(xiàng)目簡(jiǎn)介
本項(xiàng)目主要是實(shí)現(xiàn)一個(gè)網(wǎng)頁版的在線五子棋對(duì)戰(zhàn)游戲,它主要支持以下核心功能:
- 用戶數(shù)據(jù)管理:實(shí)現(xiàn)用戶注冊(cè)與登錄、用戶session信息管理、用戶比賽信息 (天梯分?jǐn)?shù)、比賽場(chǎng)次、獲勝場(chǎng)次) 管理等。
- 匹配對(duì)戰(zhàn)功能:實(shí)現(xiàn)兩個(gè)在線玩家在網(wǎng)頁端根據(jù)天梯分?jǐn)?shù)進(jìn)行對(duì)戰(zhàn)匹配,匹配成功后在游戲房間中進(jìn)行五子棋對(duì)戰(zhàn)的功能。
- 實(shí)時(shí)聊天功能:實(shí)現(xiàn)兩個(gè)玩家在游戲過程中能夠進(jìn)行實(shí)時(shí)聊天的功能。
2. 開發(fā)環(huán)境
本項(xiàng)目的開發(fā)環(huán)境如下:
- Linux:在 Centos7.6 環(huán)境下進(jìn)行數(shù)據(jù)庫部署與開發(fā)環(huán)境搭建。
- VSCode/Vim:通過 VSCode 遠(yuǎn)程連接服務(wù)器或直接使用 Vim 進(jìn)行代碼編寫與功能測(cè)試。
- g++/gdb:通過 g++/gdb 進(jìn)行代碼編譯與調(diào)試。
- Makefile:通過 Makefile 進(jìn)行項(xiàng)目構(gòu)建。
3. 核心技術(shù)
本項(xiàng)目所使用到的核心技術(shù)如下:
- HTTP/WebSocket:使用 HTTP/WebSocket 完成客戶端與服務(wù)器的短連接/長(zhǎng)連接通信。
- WebSocketpp:使用 WebSocketpp 實(shí)現(xiàn) WebSocket 協(xié)議的通信功能。
- JsonCpp:封裝 JsonCpp 完成網(wǎng)絡(luò)數(shù)據(jù)的序列與反序列功能。
- MySQL C API:封裝 MySQL C API 完成在 C++ 程序中訪問和操作 MySQL 數(shù)據(jù)庫的功能。
- C++11:使用 C++11 中的某些新特性完成代碼的編寫,例如 bind/shared_ptr/thread/mutex。
- BlockQueue:為不同段位的玩家設(shè)計(jì)不同的阻塞式匹配隊(duì)列來完成游戲的匹配功能。
- HTML/CSS/JS/AJAX:通過 HTML/CSS/JS 來構(gòu)建與渲染游戲前端頁面,以及通過 AJAX來向服務(wù)器發(fā)送 HTTP 客戶端請(qǐng)求。
4. 開發(fā)階段
本項(xiàng)目一共分為四個(gè)開發(fā)階段:
-
環(huán)境搭建:在 Centos7.6 環(huán)境下安裝本項(xiàng)目會(huì)使用到的各種工具以及第三方庫。
-
前置知識(shí)了解:對(duì)項(xiàng)目中需要用到的一些知識(shí)進(jìn)行了解,學(xué)會(huì)它們的基本使用,比如 bind/WebSocketpp/HTML/JS/AJAX 等。
-
框架設(shè)計(jì):進(jìn)行項(xiàng)目模塊劃分,確定每一個(gè)模塊需要實(shí)現(xiàn)的功能。
-
模塊開發(fā) && 功能測(cè)試:對(duì)各個(gè)子模塊進(jìn)行開發(fā)與功能測(cè)試,最后再將這些子模塊進(jìn)行整合并進(jìn)行整體功能測(cè)試。
二、環(huán)境搭建
1. 安裝 wget 工具
sudo yum install wget
2. 更換 yum 源
備份之前的 yum 源:
sudo mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak
更換 yum 源為國內(nèi)阿里的鏡像 yum 源:
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
sudo yum clean all
sudo yum makecache
安裝 scl 軟件源:
sudo yum install centos-release-scl-rh centos-release-scl
安裝 epel 軟件源:
sudo yum install epel-release
3. 安裝 lrzsz 傳輸工具
sudo yum install lrzsz
4. 安裝?版本 gcc/g++ 編譯器
安裝 devtoolset 高版本 gcc/g++ 編譯器:
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
將 devtoolset 加載配置指令添加到終端初始化配置文件中,使其在以后的所有新打開終端中有效:
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
重新加載終端配置文件:
source ~/.bashrc
5. 安裝 gdb 調(diào)試器
sudo yum install gdb
6. 安裝分布式版本控制工具 git
sudo yum install git
7. 安裝 cmake
sudo yum install cmake
8. 安裝 boost 庫
sudo yum install boost-devel.x86_64
9. 安裝 Jsoncpp 庫
sudo yum install jsoncpp-devel
10. 安裝 MySQL 數(shù)據(jù)庫服務(wù)及開發(fā)包
安裝 MySQL 環(huán)境:【MySQL】Linux 中 MySQL 環(huán)境的安裝與卸載
設(shè)置 MySQL 用戶與密碼:【MySQL】用戶與權(quán)限管理
11. 安裝 WebSocketpp 庫
從 github 官方倉庫克隆 WebSocketpp 庫:
git clone https://github.com/zaphoyd/websocketpp.git
由于 github 服務(wù)器在國外,所以可能會(huì)出現(xiàn) clone 失敗的情況,此時(shí)可以從 gitee 倉庫克隆 WebSocketpp 庫:
git clone https://gitee.com/freeasm/websocketpp.git
clone 成功后執(zhí)行如下指令來安裝 WebSocketpp 庫 (執(zhí)行 git clone 語句的目錄下):
cd websocketpp/
mkdir build
cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
sudo make install
驗(yàn)證 websocketpp 是否安裝成功 (build 目錄下):
cd ../examples/echo_server
當(dāng)前目錄下 ls 顯示:
CMakeLists.txt echo_handler.hpp echo_server.cpp SConscript
g++ 編譯 echo_server.cpp,如果編譯成功則說明安裝成功:
g++ -std=c++11 echo_server.cpp -o echo_server -lpthread -lboost_system
三、前置知識(shí)了解
1. WebSocketpp
1.1 WebSocket 協(xié)議
WebSocket 介紹
WebSocket 是從 HTML5 開始支持的?種網(wǎng)頁端和服務(wù)端保持長(zhǎng)連接的消息推送機(jī)制:
- 傳統(tǒng)的 web 程序都是屬于 “?問?答” 的形式,即客戶端給服務(wù)器發(fā)送?個(gè) HTTP 請(qǐng)求,然后服務(wù)器給客?端返回?個(gè) HTTP 響應(yīng)。這種情況下服務(wù)器是屬于被動(dòng)的一方,即如果客戶端不主動(dòng)發(fā)起請(qǐng)求,那么服務(wù)器也就?法主動(dòng)給客戶端響應(yīng)。
- 但是像網(wǎng)頁即時(shí)聊天或者五子棋游戲這樣的程序都是非常依賴 “消息推送” 的,即需要服務(wù)器主動(dòng)推動(dòng)消息到客戶端 (將一個(gè)客戶端發(fā)送的消息或下棋的動(dòng)作主動(dòng)發(fā)送給另一個(gè)客戶端)。那么如果只是使?原?的 HTTP 協(xié)議,要想實(shí)現(xiàn)消息推送?般就需要通過 “Ajax 輪詢” 的方式來實(shí)現(xiàn),而輪詢的成本是比較高的,并且客戶端也不能及時(shí)的獲取到消息的響應(yīng)。
為了解決上述兩個(gè)問題,有大佬就設(shè)計(jì)了一種新的應(yīng)用層協(xié)議 – WebSocket 協(xié)議。WebSocket 更接近于 TCP 這種級(jí)別的通信?式,?旦連接建立完成客戶端或者服務(wù)器都可以主動(dòng)的向?qū)Ψ桨l(fā)送數(shù)據(jù)。
原理解析
WebSocket 協(xié)議本質(zhì)上是?個(gè)基于 TCP 的協(xié)議。為了建??個(gè) WebSocket 連接,客戶端瀏覽器會(huì)通過 JavaScript 向服務(wù)器發(fā)出建立 WebSocket 連接的請(qǐng)求,這個(gè)連接請(qǐng)求本質(zhì)上仍然是一個(gè) HTTP 請(qǐng)求,但它包含了?些附加頭部信息,比如協(xié)議升級(jí)"Upgrade: WebSocket",服務(wù)器端解析這些附加的頭信息然后產(chǎn)生應(yīng)答信息返回給客戶端,客戶端和服務(wù)器端的 WebSocket 連接就建立起來了,雙方就可以通過這個(gè)連接通道自由的傳遞信息,并且這個(gè)連接會(huì)持續(xù)存在直到客戶端或者服務(wù)器端的某一方主動(dòng)的關(guān)閉連接。。
同時(shí),當(dāng)客戶端瀏覽器獲取到 Web Socket 連接后,之后的通信就不再通過 Ajax 構(gòu)建客戶端請(qǐng)求發(fā)送給服務(wù)器了,而是直接使用 WebSocket 的 send() 方法方法來向服務(wù)器發(fā)送數(shù)據(jù),并通過 onmessage 事件來接收服務(wù)器返回的數(shù)據(jù)。
報(bào)文格式
WebSocket 報(bào)文格式如下,大家了解即可:
WebSocket 相關(guān)接口
創(chuàng)建 WebSocket 對(duì)象:
var Socket = new WebSocket(url, [protocol]);
WebSocket 對(duì)象的相關(guān)事件:
WebSocket 對(duì)象的相關(guān)方法:
參考資料:
https://www.runoob.com/html/html5-websocket.html
https://www.bilibili.com/video/BV1684y1k7VP/?buvid=ZC4691C539D91BA74044%E2%80%A6&vd_source=cbc46a2fc528c4362ce79ac44dd49e2c
1.2 WebSocketpp
WebSocketpp 介紹
WebSocketpp 是?個(gè)跨平臺(tái)的開源 (BSD許可證) 頭部專?C++庫,它實(shí)現(xiàn)了RFC6455 (WebSocket協(xié)議) 和 RFC7692 (WebSocketCompression?Extensions)。它允許將 WebSocket 客戶端和服務(wù)器功能集成到 C++ 程序中。在最常見的配置中,全功能網(wǎng)絡(luò) I/O 由 Asio 網(wǎng)絡(luò)庫提供。
WebSocketpp 如要有以下特性:
- 事件驅(qū)動(dòng)的接口。
- ?持HTTP/HTTPS、WS/WSS、IPv6。
- 靈活的依賴管理 – Boost庫/C++11標(biāo)準(zhǔn)庫。
- 可移植性 – Posix/Windows、32/64bit、Intel/ARM。
- 線程安全。
WebSocketpp 同時(shí)支持 HTTP 和 Websocket 兩種網(wǎng)絡(luò)協(xié)議,比較適用于我們本次的項(xiàng)目,所以我們選用該庫作為項(xiàng)目的依賴庫,用來搭建 HTTP 和 WebSocket 服務(wù)器。
以下是 WebSocketpp 的一些相關(guān)網(wǎng)站:
github:https://github.com/zaphoyd/websocketpp
用戶手冊(cè):http://docs.websocketpp.org/
官網(wǎng):http://www.zaphoyd.com/websocketpp
WebSocketpp 的使用
WebSocketpp 常用接口及其功能介紹如下:
namespace websocketpp
{typedef lib::weak_ptr<void> connection_hdl;template <typename config>class endpoint : public config::socket_type{typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;typedef typename connection_type::ptr connection_ptr;typedef typename connection_type::message_ptr message_ptr;typedef lib::function<void(connection_hdl)> open_handler;typedef lib::function<void(connection_hdl)> close_handler;typedef lib::function<void(connection_hdl)> http_handler;typedef lib::function<void(connection_hdl, message_ptr)>message_handler;/* websocketpp::log::alevel::none 禁?打印所有?志*/void set_access_channels(log::level channels); /*設(shè)置?志打印等級(jí)*/void clear_access_channels(log::level channels); /*清除指定等級(jí)的?志*//*設(shè)置指定事件的回調(diào)函數(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請(qǐng)求回調(diào)處理函數(shù)*//*發(fā)送數(shù)據(jù)接?*/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 對(duì)應(yīng)連接的connection_ptr*/connection_ptr get_con_from_hdl(connection_hdl hdl);/*websocketpp基于asio框架實(shí)現(xiàn),init_asio?于初始化asio框架中的io_service調(diào)度器*/void init_asio();/*設(shè)置是否啟?地址重?*/void set_reuse_addr(bool value);/*設(shè)置endpoint的綁定監(jiān)聽端?*/void listen(uint16_t port);/*對(duì)io_service對(duì)象的run接?封裝,?于啟動(dòng)服務(wù)器*/std::size_t run();/*websocketpp提供的定時(shí)器,以毫秒為單位*/timer_ptr set_timer(long duration, timer_handler callback);};template <typename config>class server : public endpoint<connection<config>, config>{/*初始化并啟動(dòng)服務(wù)端監(jiān)聽連接的accept事件處理*/void start_accept();};template <typename config>class connection: public config::transport_type::transport_con_type,public config::connection_base{/*發(fā)送數(shù)據(jù)接?*/error_code send(std::string &payload, frame::opcode::valueop = frame::opcode::text);/*獲取http請(qǐng)求頭部*/std::string const &get_request_header(std::string const &key)/*獲取請(qǐng)求正?*/std::string const &get_request_body();/*設(shè)置響應(yīng)狀態(tài)碼*/void set_status(http::status_code::value code);/*設(shè)置http響應(yīng)正?*/void set_body(std::string const &value);/*添加http響應(yīng)頭部字段*/void append_header(std::string const &key, std::string const &val);/*獲取http請(qǐng)求對(duì)象*/request_type const &get_request();/*獲取connection_ptr 對(duì)應(yīng)的 connection_hdl */connection_hdl get_handle();};namespace http{namespace parser{class parser{std::string const &get_header(std::string const &key);}; class request : public parser{/*獲取請(qǐng)求?法*/std::string const &get_method();/*獲取請(qǐng)求uri接?*/std::string const &get_uri();};}};namespace message_buffer{/*獲取websocket請(qǐng)求中的payload數(shù)據(jù)類型*/frame::opcode::value get_opcode();/*獲取websocket中payload數(shù)據(jù)*/std::string const &get_payload();};namespace log{struct alevel{static level const none = 0x0;static level const connect = 0x1;static level const disconnect = 0x2;static level const control = 0x4;static level const frame_header = 0x8;static level const frame_payload = 0x10;static level const message_header = 0x20;static level const message_payload = 0x40;static level const endpoint = 0x80;static level const debug_handshake = 0x100;static level const debug_close = 0x200;static level const devel = 0x400;static level const app = 0x800;static level const http = 0x1000;static level const fail = 0x2000;static level const access_core = 0x00003003;static level const all = 0xffffffff;};}namespace http{namespace status_code{enum value{uninitialized = 0,continue_code = 100,switching_protocols = 101,ok = 200,created = 201,accepted = 202,non_authoritative_information = 203,no_content = 204,reset_content = 205,partial_content = 206,multiple_choices = 300,moved_permanently = 301,found = 302,see_other = 303,not_modified = 304,use_proxy = 305,temporary_redirect = 307,bad_request = 400,unauthorized = 401,payment_required = 402,forbidden = 403,not_found = 404,method_not_allowed = 405,not_acceptable = 406,proxy_authentication_required = 407,request_timeout = 408,conflict = 409,gone = 410,length_required = 411,precondition_failed = 412,request_entity_too_large = 413,request_uri_too_long = 414,unsupported_media_type = 415,request_range_not_satisfiable = 416,expectation_failed = 417,im_a_teapot = 418,upgrade_required = 426,precondition_required = 428,too_many_requests = 429,request_header_fields_too_large = 431,internal_server_error = 500,not_implemented = 501,bad_gateway = 502,service_unavailable = 503,gateway_timeout = 504,http_version_not_supported = 505,not_extended = 510,network_authentication_required = 511};}}namespace frame{namespace opcode{enum value{continuation = 0x0,text = 0x1,binary = 0x2,rsv3 = 0x3,rsv4 = 0x4,rsv5 = 0x5,rsv6 = 0x6,rsv7 = 0x7,close = 0x8,ping = 0x9,pong = 0xA,control_rsvb = 0xB,control_rsvc = 0xC,control_rsvd = 0xD,control_rsve = 0xE,control_rsvf = 0xF,};}}
}
使用 WebSocketpp 搭建一個(gè)簡(jiǎn)單服務(wù)器的流程如下:
- 實(shí)例化一個(gè) websocketpp::server 對(duì)象。
- 設(shè)置日志等級(jí)。(本項(xiàng)目中我們使用自己封裝的日志函數(shù),所以這里設(shè)置日志等級(jí)為 none)
- 初始化 asio 調(diào)度器。
- 設(shè)置處理 http 請(qǐng)求、websocket 握手成功、websocket 連接關(guān)閉以及收到 websocket 消息的回調(diào)函數(shù)。
- 設(shè)置監(jiān)聽端口。
- 開始獲取 tcp 連接。
- 啟動(dòng)服務(wù)器。
示例代碼如下:
#include <iostream>
#include <string>
#include <functional>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
using std::cout;
using std::endl;typedef websocketpp::server<websocketpp::config::asio> wsserver_t;void http_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {wsserver_t::connection_ptr conn = srv->get_con_from_hdl(hdl);std::cout << "body: " << conn->get_request_body() << std::endl;websocketpp::http::parser::request req = conn->get_request();std::cout << "method: " << req.get_method() << std::endl;std::cout << "uri: " << req.get_uri() << std::endl;// 響應(yīng)一個(gè)hello world頁面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);
}
void wsopen_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {cout << "websocket握手成功" << std::endl;
}
void wsclose_callback(wsserver_t *srv, websocketpp::connection_hdl hdl) {cout << "websocket連接關(guān)閉" << endl;
}
void wsmessage_callback(wsserver_t *srv, websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {wsserver_t::connection_ptr conn = srv->get_con_from_hdl(hdl);cout << "wsmsg: " << msg->get_payload() << endl;std::string rsp = "[server]# " + msg->get_payload();conn->send(rsp, websocketpp::frame::opcode::text);
}int main()
{// 1. 實(shí)例化server對(duì)象wsserver_t wssrv;// 2. 設(shè)置日志等級(jí)wssrv.set_access_channels(websocketpp::log::alevel::none);// 3. 初始化asio調(diào)度器wssrv.init_asio();// 4. 設(shè)置回調(diào)函數(shù)wssrv.set_http_handler(std::bind(http_callback, &wssrv, std::placeholders::_1));wssrv.set_open_handler(std::bind(wsopen_callback, &wssrv, std::placeholders::_1));wssrv.set_close_handler(std::bind(wsclose_callback, &wssrv, std::placeholders::_1));wssrv.set_message_handler(std::bind(wsmessage_callback, &wssrv, std::placeholders::_1, std::placeholders::_2));// 5. 設(shè)置監(jiān)聽端口wssrv.listen(8080);wssrv.set_reuse_addr(true);// 6. 開始獲取tcp連接wssrv.start_accept();// 7. 啟動(dòng)服務(wù)器wssrv.run();return 0;
}
2. JsonCpp
Json 數(shù)據(jù)格式
Json 是?種數(shù)據(jù)交換格式,它采?完全獨(dú)立于編程語?的?本格式來存儲(chǔ)和表示數(shù)據(jù)。
比如,們想表示?個(gè)同學(xué)的學(xué)?信息。在 C/C++ 中我們可能使用結(jié)構(gòu)體/類來表示:
typedef struct {char *name = "XXX";int age = 18;float score[3] = { 88.5, 99, 58 };
} stu;
而用 Json 數(shù)據(jù)格式表示如下:
{"姓名" : "xxX","年齡" : 18,"成績(jī)" : [88.5, 99, 58]
}
Json 的數(shù)據(jù)類型包括對(duì)象,數(shù)組,字符串,數(shù)字等:
- 對(duì)象:使用花括號(hào) {} 括起來的表示?個(gè)對(duì)象。
- 數(shù)組:使用中括號(hào) [] 括起來的表示?個(gè)數(shù)組。
- 字符串:使用常規(guī)雙引號(hào) “” 括起來的表示?個(gè)字符串。
- 數(shù)字:包括整形和浮點(diǎn)型,直接使用。
JsonCpp 介紹
Jsoncpp 庫主要是?于實(shí)現(xiàn) Json 格式數(shù)據(jù)的序列化和反序列化,它實(shí)現(xiàn)了將多個(gè)數(shù)據(jù)對(duì)象組織成為 json 格式字符串,以及將 Json 格式字符串解析得到多個(gè)數(shù)據(jù)對(duì)象的功能。
Json 數(shù)據(jù)對(duì)象類的部分表示如下:
class Json::Value{/*Value重載了[]和=,因此所有的賦值和獲取數(shù)據(jù)都可以通過簡(jiǎn)單的?式完成 val["name"] ="xx"*/Value &operator=(const Value &other); Value& operator[](const std::string& key); Value& operator[](const char* key);/*移除元素*/Value removeMember(const char* key); /*val["score"][0]*/const Value& operator[](ArrayIndex index) const; /*添加數(shù)組元素 -- val["score"].append(88)*/Value& append(const Value& value);/*獲取數(shù)組元素個(gè)數(shù) -- val["score"].size()*/ArrayIndex size() const; /*?于判斷是否存在某個(gè)字段*/bool isNull(); /*json格式數(shù)據(jù)轉(zhuǎn)string類型 -- string name = val["name"].asString()*/std::string asString() const; /*json格式數(shù)據(jù)轉(zhuǎn)C語言格式的字符串即char*類型 -- char *name = val["name"].asCString()*/const char* asCString() const;/*轉(zhuǎn)int -- int age = val["age"].asInt()*/Int asInt() const; /*轉(zhuǎn)無符號(hào)長(zhǎng)整型uint64_t -- uint64_t id = val["id"].asUInt64()*/Uint64 asUint64() const;/*轉(zhuǎn)浮點(diǎn)數(shù) -- float weight = val["weight"].asFloat()*/float asFloat() const; /*轉(zhuǎn)bool類型 -- bool ok = val["ok"].asBool()*/bool asBool() const;
};
Jsoncpp 庫主要借助三個(gè)類以及其對(duì)應(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;
}
使用 jsonCpp 將數(shù)據(jù)序列化的步驟如下:
- 將需要序列化的數(shù)據(jù)存儲(chǔ)在Json::Value對(duì)象中。
- 實(shí)例化StreamWriterBuilder工廠類對(duì)象。
- 使用StreamWriterBuilder工廠類對(duì)象實(shí)例化StreamWriter對(duì)象。
- 使用StreamWriter對(duì)象完成Json::Value中數(shù)據(jù)的序列化工作,并將序列化結(jié)果存放到ss中。
使用 JsonCpp 將數(shù)據(jù)反序列化的步驟如下:
- 實(shí)例化一個(gè) CharReaderBuilder 工廠類對(duì)象。
- 使用CharReaderBuilder對(duì)象實(shí)例化一個(gè)CharReader對(duì)象。
- 創(chuàng)建一個(gè)Json::Value對(duì)象,用于保存json格式字符串反序列化后的結(jié)果。
- 使用CharReader對(duì)象完成json格式字符串的反序列化工作。
示例代碼如下:
#include <iostream>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>
using std::cout;
using std::endl;/*使用jsonCpp完成數(shù)據(jù)的序列化工作*/
std::string serialize()
{// 1. 將需要序列化的數(shù)據(jù)存儲(chǔ)在Json::Value對(duì)象中Json::Value root;root["姓名"] = "小明";root["年齡"] = 18;root["成績(jī)"].append(80); //成績(jī)是數(shù)組類型root["成績(jī)"].append(90);root["成績(jī)"].append(100);// 2. 實(shí)例化StreamWriterBuilder工廠類對(duì)象Json::StreamWriterBuilder swb;// 3. 使用StreamWriterBuilder工廠類對(duì)象實(shí)例化StreamWriter對(duì)象Json::StreamWriter *sw = swb.newStreamWriter();// 4. 使用StreamWriter對(duì)象完成Json::Value中數(shù)據(jù)的序列化工作,并將序列化結(jié)果存放到ss中std::stringstream ss;int n = sw->write(root, &ss);if(n != 0){cout << "json serialize fail" << endl;delete sw;return ""; }delete sw;return ss.str();
}/*使用JsonCpp完成序列化數(shù)據(jù)的反序列化工作*/
void deserialize(const std::string &str)
{// 1. 實(shí)例化一個(gè)CharReaderBuilder工廠類對(duì)象Json::CharReaderBuilder crb;// 2. 使用CharReaderBuilder對(duì)象實(shí)例化一個(gè)CharReader對(duì)象Json::CharReader *cr = crb.newCharReader();// 3. 創(chuàng)建一個(gè)Json::Value對(duì)象,用于保存json格式字符串反序列化后的結(jié)果Json::Value root;// 4. 使用CharReader對(duì)象完成json格式字符串的反序列化工作std::string errmsg;bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &errmsg);if(ret == false){cout << "json deserialize fail: " << errmsg << endl;delete cr;return;}// 5. 依次打印Json::Value中的數(shù)據(jù)cout << "姓名: " << root["姓名"].asString() << endl;cout << "年齡: " << root["年齡"].asInt() << endl; int size = root["成績(jī)"].size();for(int i = 0; i < size; i++){cout << "成績(jī): " << root["成績(jī)"][i].asFloat() << endl;}
}int main()
{std::string str = serialize();cout << str << endl;deserialize(str);return 0;
}
3. C++11
C++11 bind 參考文章:
std::bind(一):包裝普通函數(shù)
std::bind(二):包裝成員函數(shù)
C++11 智能指針參考文章:
【C++】智能指針
C++11 線程庫/互斥鎖/條件變量參考文章:
【C++】C++11 線程庫
4. GDB
GDB 是一個(gè)強(qiáng)大的命令行式的源代碼級(jí)調(diào)試工具,可以用于分析和調(diào)試 C/C++ 等程序,在程序運(yùn)行時(shí)檢查變量的值、跟蹤函數(shù)調(diào)用、設(shè)置斷點(diǎn)以及其他調(diào)試操作。GDB 在服務(wù)器開發(fā)中使用非常廣泛,一個(gè)合格的后臺(tái)開發(fā)/服務(wù)器開發(fā)程序員應(yīng)該能夠使用 GDB 來調(diào)試程序。
由于 GDB 是純命令行的,所以我們需要學(xué)習(xí) GDB 相關(guān)的一些基本指令,下面是陳皓大佬編寫的關(guān)于 GDB 調(diào)試技巧的博客,供大家參考:
https://so.csdn.net/so/search?q=gdb&t=blog&u=haoel
https://coolshell.cn/articles/3643.html
5. MySQL C API
參考文章:
【MySQL】C語言連接數(shù)據(jù)庫
6. HTML/CSS/JS/AJAX
本項(xiàng)目中與前端有關(guān)的技術(shù)分別是HTML、CSS、JavaScript 和 AJAX:
- HTML:標(biāo)簽語言,用于渲染前端網(wǎng)頁。
- CSS:層疊樣式表,對(duì) HTML 標(biāo)簽進(jìn)行樣式修飾,使其更加好看。
- JavaScript:腳本語言,在 web 前端中主要用于控制頁面的渲染,使得前端靜態(tài)頁面能夠根據(jù)數(shù)據(jù)的變化而變化。
- AJAX:一個(gè)異步的 HTTP 客戶端,它可以異步的向服務(wù)器發(fā)送 HTTP 請(qǐng)求,獲取響應(yīng)并進(jìn)行處理。
注意:本項(xiàng)目中只是對(duì)上述這些前端技術(shù)進(jìn)行一個(gè)最基本的使用,目的是能夠通過它們做出一個(gè)簡(jiǎn)單的前端頁面。
6.1 HTML 簡(jiǎn)單了解
HTML 標(biāo)簽:HTML 代碼是由 “標(biāo)簽” 構(gòu)成的
- 標(biāo)簽名 (body) 放到 < > 中。
- 大部分標(biāo)簽成對(duì)出現(xiàn), 為開始標(biāo)簽, 為結(jié)束標(biāo)簽。
- 少數(shù)標(biāo)簽只有開始標(biāo)簽, 稱為 “單標(biāo)簽”。
- 開始標(biāo)簽和結(jié)束標(biāo)簽之間, 寫的是標(biāo)簽的內(nèi)容。(hello)
- 開始標(biāo)簽中可能會(huì)帶有 “屬性”, id 屬性相當(dāng)于給這個(gè)標(biāo)簽設(shè)置了一個(gè)唯一的標(biāo)識(shí)符。
body>hello</body>
<body id="myId">hello</body>
HTML 文件基本結(jié)構(gòu):
- html 標(biāo)簽是整個(gè) html 文件的根標(biāo)簽(最頂層標(biāo)簽)。
- head 標(biāo)簽中寫頁面的屬性。
- body 標(biāo)簽中寫的是頁面上顯示的內(nèi)容。
- title 標(biāo)簽中寫的是頁面的標(biāo)題。
<html><head><title>第一個(gè)頁面</title></head><body>hello world</body>
</html>
HTML 常見標(biāo)簽:
- 注釋標(biāo)簽:注釋不會(huì)顯示在界面上. 目的是提高代碼的可讀性。
<!-- 我是注釋 -->
- 標(biāo)題標(biāo)簽:標(biāo)題標(biāo)簽一共有六個(gè) – h1-h6,數(shù)字越大, 則字體越小。
<h1>hello</h1>
<h2>hello</h2>
<!-- ... -->
- 段落標(biāo)簽:p 標(biāo)簽表示一個(gè)段落。
<p>這是一個(gè)段落</p>
- 換行標(biāo)簽:br 是 break 的縮寫,表示換行。br 是一個(gè)單標(biāo)簽(不需要結(jié)束標(biāo)簽)。
<br/>
- 圖片標(biāo)簽 img:img 標(biāo)簽必須帶有 src 屬性表示圖片的路徑。
<img src="./tmp.jpg">
<img src="rose.jpg" alt="鮮花" title="這是一朵鮮花" width="500px" height="800px" border="5px">
- 超鏈接標(biāo)簽 a:a 標(biāo)簽必須具備 href,表示點(diǎn)擊后會(huì)跳轉(zhuǎn)到哪個(gè)頁面。同時(shí)可以指定 target 打開方式。(默認(rèn)是 _self,如果是 _blank 則用新的標(biāo)簽頁打開)
<!-- 外部鏈接 -->
<a href="http://www.github.com">github</a>
<!-- 內(nèi)部鏈接: 網(wǎng)站內(nèi)部頁面之間的鏈接 -->
<a href="2.html">點(diǎn)我跳轉(zhuǎn)到 2.html</a>
<!-- 下載鏈接: href 對(duì)應(yīng)的路徑是一個(gè)文件 -->
<a href="test.zip">下載文件</a>
- 列表標(biāo)簽:ul li 表示無序列表,ol li 表示有序列表,dl (總標(biāo)簽) dt (小標(biāo)題) dd (圍繞標(biāo)題來說明) 表示自定義列表。
<h3>無序列表</h3>
<ul><li>HTML</li><li>CSS</li><li>JS</li>
</ul>
<h3>有序列表</h3>
<ol><li>HTML</li><li>CSS</li><li>JS</li>
</ol>
<h3>自定義列表</h3>
<dl><dt>前端相關(guān):</dt><dd>HTML</dd><dd>CSS</dd><dd>JS</dd>
</dl>
-
表單標(biāo)簽 (重要):表單是讓用戶輸入信息的重要途徑,分成兩個(gè)部分 – 表單域和表單控件,其中表單域是包含表單元素的區(qū)域,重點(diǎn)是 form 標(biāo)簽;表單控件是輸入框、提交按鈕等,重點(diǎn)是 input 標(biāo)簽。
form 標(biāo)簽:描述了要把數(shù)據(jù)按照什么方式, 提交到哪個(gè)頁面中。
<form action="test.html">... [form 的內(nèi)容] </form>
input 標(biāo)簽:各種輸入控件, 單行文本框, 按鈕, 單選框, 復(fù)選框等。
<!-- 文本框 --> <input type="text"> <!-- 密碼框 --> <input type="password"> <!-- 單選框 --> <input type="radio" name="sex">男 <input type="radio" name="sex" checked="checked">女 <!-- 普通按鈕 --> <input type="button" value="我是個(gè)按鈕"> <!-- 提交按鈕 --> <form action="test.html"><input type="text" name="username"><input type="submit" value="提交"> </form>
-
無語義標(biāo)簽 div & span:div 標(biāo)簽, division 的縮寫, 含義是分割;span 標(biāo)簽, 含義是跨度。它們是兩個(gè)盒子,一般搭配 CSS 用于網(wǎng)頁布局。(div 是獨(dú)占一行的, 是一個(gè)大盒子;而span 不獨(dú)占一行, 是一個(gè)小盒子。)
<div><span>HTML</span><span>CSS</span><span>JS</span>
</div>
參考資料:
HTML 教程 – 菜鳥教程
6.2 CSS 簡(jiǎn)單了解
CSS (層疊樣式表) 能夠?qū)W(wǎng)頁中元素位置的排版進(jìn)行像素級(jí)精確控制, 實(shí)現(xiàn)美化頁面的效果, 能夠做到頁面的樣式和結(jié)構(gòu)分離。
CSS 基本語法規(guī)范是 選擇器 + {一條/N條聲明}:
- 選擇器決定針對(duì)誰修改。
- 聲明決定修改什么內(nèi)容。
- 聲明的屬性是鍵值對(duì),使用 “;” 區(qū)分鍵值對(duì), 使用 “:” 區(qū)分鍵和值。
/*對(duì)段落標(biāo)簽進(jìn)行樣式修飾*/
<style>p {/* 設(shè)置字體顏色 */color: red;/* 設(shè)置字體大小 */font-size: 30px;}
</style>
<p>hello</p>
選擇器的功能是選中頁面中指定的標(biāo)簽元素,然后對(duì)其進(jìn)行修飾。選擇器有很多種類,這里我們主要介紹基礎(chǔ)選擇器:
-
標(biāo)簽選擇器:標(biāo)簽選擇器的優(yōu)點(diǎn)是能快速為同一類型的標(biāo)簽都選擇出來,缺點(diǎn)是不能差異化選擇。
<!-- 對(duì)段落標(biāo)簽p進(jìn)行樣式修飾 --> <style> p {color: red; } </style><p>demo</p>
-
類選擇器:類選擇器的優(yōu)點(diǎn)是可以差異化表示不同的標(biāo)簽,同時(shí)一個(gè)類可以被多個(gè)標(biāo)簽使用。類選擇器就類似于我們給標(biāo)簽取了一個(gè)名字,然后對(duì)這個(gè)名字的所有標(biāo)簽統(tǒng)一進(jìn)行樣式修飾。
<style>.blue {color: blue;} </style><div class="blue">demo1</div> <p class="blue">demo2</p>
-
id 選擇器:和類選擇器類似,不同的是 id 是唯一的, 不能被多個(gè)標(biāo)簽使用。
<style>#ha {color: red;} </style><div id="ha">demo</div>
-
通配符選擇器:使用 * 的定義, 對(duì)所有的標(biāo)簽都有效。
CSS 的引入方式一般有三種:
-
內(nèi)部樣式表:直接寫在 style 標(biāo)簽中,嵌入到 html 內(nèi)部。(style 一般都是放到 head 標(biāo)簽中)
這樣做的優(yōu)點(diǎn)是能夠讓樣式和頁面結(jié)構(gòu)分離,缺點(diǎn)是分離的不夠徹底,在實(shí)際開發(fā)中并不常用。
<style>div {color: red;} </style>
-
行內(nèi)樣式表:通過 style 屬性, 來指定某個(gè)標(biāo)簽的樣式。
這種方法只適合于寫簡(jiǎn)單樣式,并且只針對(duì)某個(gè)標(biāo)簽生效,在實(shí)際開發(fā)中也不常用。
<div style="color:green">想要生活過的去, 頭上總得帶點(diǎn)綠</div>
-
外部樣式表 (重要):先 創(chuàng)建一個(gè) css 文件,然后使用 link 標(biāo)簽引入 css。
這樣做能夠讓讓樣式和頁面結(jié)構(gòu)徹底分離,即使是在 css 內(nèi)容很多的時(shí)候,這也是實(shí)際開發(fā)中最常用的方式。
<link rel="stylesheet" href="[CSS文件路徑]">
參考資料:
css 教程 – 菜鳥教程
css 選擇器參考手冊(cè) – W3school
6.3 JS 簡(jiǎn)單了解
JavaScript 的基本語法和 java 類似,所以我們不再單獨(dú)學(xué)習(xí)。這里我們主要學(xué)習(xí)如何使用 JavaScript 去渲染前端頁面,具體內(nèi)容如下:
- 如何使用 js 給按鈕添加點(diǎn)擊事件。
- 如何使用 js 去獲取以及設(shè)置一個(gè)頁面控件的內(nèi)容。
<body><input type="text" id="user_name"><input type="password" id="password"><!--為button按鈕添加點(diǎn)擊事件,調(diào)用登錄函數(shù)--><button id="submit" onclick="login()">提交</button><div><span>hello world</span><span>hello world</span></div>
</body>
<javascript>function login() {//獲取輸入框中的內(nèi)容var username = document.getElementById("user_name").value;var password = document.getElementById("password").value;//服務(wù)器用戶信息驗(yàn)證成功后提示登錄成功alert("登錄成功");//服務(wù)器用戶信息驗(yàn)證失敗后提示登錄失敗并清空輸入框內(nèi)容alert("登錄失敗");document.getElementById("user_name").value = "";document.getElementById("password").value = "";};//js相關(guān)的一些其他WebAPIfunction demo() {var div = getElementById("div");//讀取頁面內(nèi)容var msg = div.innerHTML;//向控制臺(tái)打印日志信息console.log(msg);//修改頁面內(nèi)容div.innerHTML = "<span>hello js</span>";}
</javascript>
參考資料:
JavaScript 教程 – 菜鳥教程
6.4 AJAX 簡(jiǎn)單了解
為了降低學(xué)習(xí)成本,這里我們并不使用 js 中原生的 AJAX,而是使用 jQuery 中的 AJAX:
-
jQuery 是一種基于JavaScript的開源庫。它簡(jiǎn)化了HTML文檔遍歷、事件處理、動(dòng)畫效果等操作。通過使用jQuery,開發(fā)者可以更輕松地操作DOM元素、處理事件、發(fā)送AJAX請(qǐng)求以及創(chuàng)建動(dòng)態(tài)效果,從而使網(wǎng)頁開發(fā)變得更加便捷和靈活。
-
jQuery AJAX 是指使用 jQuery 庫中提供的 AJAX 相關(guān)方法來進(jìn)行異步數(shù)據(jù)交互。通過使用 jQuery 提供的 AJAX 方法,開發(fā)者可以輕松地執(zhí)行諸如發(fā)送 GET 或 POST 請(qǐng)求、處理服務(wù)器響應(yīng)、以及執(zhí)行其他與異步數(shù)據(jù)交互相關(guān)的操作,簡(jiǎn)化了原生 JavaScript 中使用 XMLHttpRequest 對(duì)象進(jìn)行 AJAX 操作的復(fù)雜性。
<body><input type="text" id="user_name"><input type="password" id="password"><!--為button按鈕添加點(diǎn)擊事件,調(diào)用登錄函數(shù)--><button id="submit" onclick="login()">提交</button>
</body>
// 引用jQuery庫
<script src="jquery-1.10.2.min.js"></script>
<javascript>function login() {//獲取輸入框中的內(nèi)容var username = document.getElementById("user_name").value;var password = document.getElementById("password").value;// 通過ajax向服務(wù)器發(fā)送登錄請(qǐng)求$.ajax({// 請(qǐng)求類型 -- get/posttype: "post",// 請(qǐng)求資源路徑url: "http://106.52.90.67/login",// 請(qǐng)求的數(shù)據(jù)data: JSON.stringify(log_info),// 請(qǐng)求成功處理函數(shù)success: function(res) {alert("登錄成功");},// 請(qǐng)求失敗處理函數(shù)error: function(xhr) {document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(JSON.stringify(xhr));}})};
</javascript>
參考資料:
jQuery 安裝 – 菜鳥教程
jQuery Ajax 參考手冊(cè) – 菜鳥教程
四、框架設(shè)計(jì)
1. 項(xiàng)目模塊劃分
1.1 總體模塊劃分
本項(xiàng)目一共會(huì)劃分為三個(gè)大的模塊:
- 用戶數(shù)據(jù)管理模塊:基于 MySQL 數(shù)據(jù)庫進(jìn)行用戶數(shù)據(jù)的管理,包括用戶名、密碼、天梯分?jǐn)?shù)、比賽場(chǎng)次、獲勝場(chǎng)次等。
- 前端界面模塊:基于 HTTP/CSS/JS/AJAX 實(shí)現(xiàn)用戶注冊(cè)、登錄、游戲大廳和游戲房間前端界面的動(dòng)態(tài)控制以及與服務(wù)器的通信。
- 業(yè)務(wù)處理模塊:通過 WebSocketpp 相關(guān) API 搭建 WebSocket 服務(wù)器與客戶端瀏覽器進(jìn)行通信,接受客戶端請(qǐng)求并進(jìn)行業(yè)務(wù)處理。
1.2 業(yè)務(wù)處理子模塊劃分
由于項(xiàng)目需要實(shí)現(xiàn)用戶注冊(cè)、用戶登錄、用戶匹配對(duì)戰(zhàn)以及游戲內(nèi)實(shí)時(shí)聊天等不同的功能,所以需要對(duì)業(yè)務(wù)處理模塊進(jìn)行子模塊劃分,讓不同的子模塊負(fù)責(zé)不同的業(yè)務(wù)處理。
業(yè)務(wù)處理模塊具體的子模塊劃分如下:
- 網(wǎng)絡(luò)通信模塊:基于 websocketpp 庫實(shí)現(xiàn) Http&WebSocket 服務(wù)器的搭建,提供客戶端與服務(wù)器的網(wǎng)絡(luò)通信功能。
- 會(huì)話管理模塊:對(duì)客戶端的連接進(jìn)行 cookie&session 管理,實(shí)現(xiàn) HTTP 短連接下客戶端身份識(shí)別的功能。
- 在線用戶管理模塊:對(duì)進(jìn)行游戲大廳與游戲房間的用戶進(jìn)行在線管理,提供用戶在線判斷與用戶 WebSocket 長(zhǎng)連接獲取等功能。
- 游戲房間管理模塊:為匹配成功的用戶創(chuàng)建游戲房間,提供實(shí)時(shí)的五子棋對(duì)戰(zhàn)與聊天業(yè)務(wù)功能。
- 匹配對(duì)戰(zhàn)管理:根據(jù)天梯分?jǐn)?shù)為不同段位的玩家創(chuàng)建不同的匹配隊(duì)列,為匹配成功的用戶創(chuàng)建游戲房間并加入游戲房間。
2. 項(xiàng)目流程圖
2.1 用戶角度流程圖
從用戶/玩家的角度出發(fā),本項(xiàng)目的流程是 注冊(cè) -> 登錄 -> 對(duì)戰(zhàn)匹配 -> 游戲?qū)?zhàn)&實(shí)時(shí)聊天 -> 游戲結(jié)束返回游戲大廳。
2.2 服務(wù)器角度流程圖
從服務(wù)器角度出發(fā),本項(xiàng)目的流程如下:
- 服務(wù)器收到客戶端獲取注冊(cè)頁面請(qǐng)求,服務(wù)器響應(yīng)注冊(cè)頁面 register.html。
- 服務(wù)器收到客戶端用戶注冊(cè)請(qǐng)求,服務(wù)器根據(jù)用戶提交上來的注冊(cè)信息向數(shù)據(jù)庫中新增用戶,并返回注冊(cè)成功或失敗的響應(yīng)。
- 服務(wù)器收到客戶端獲取登錄頁面請(qǐng)求,服務(wù)器響應(yīng)登錄頁面 login.html。
- 服務(wù)器收到客戶端用戶登錄請(qǐng)求,服務(wù)器使用用戶提交上來的登錄信息與數(shù)據(jù)庫中的信息進(jìn)行比對(duì),并返回登錄成功或失敗的響應(yīng)。(注:用戶登錄成功后服務(wù)器會(huì)為用戶創(chuàng)建會(huì)話信息,并將用戶會(huì)話 id 添加到 http 頭部中進(jìn)行返回)
- 服務(wù)器收到客戶端獲取游戲大廳頁面請(qǐng)求,服務(wù)器響應(yīng)游戲大廳頁面 game_hall.html。
- 服務(wù)器收到客戶端獲取用戶詳細(xì)信息請(qǐng)求,服務(wù)器會(huì)取出請(qǐng)求頭部中的 cookie 信息獲取用戶 session 信息,cookie/session 不存在則返回失敗響應(yīng) (會(huì)話驗(yàn)證),存在則通過用戶 session 信息獲取用戶 id,再通過用戶 id 從數(shù)據(jù)庫中獲取用戶詳細(xì)信息并返回。
- 服務(wù)器收到客戶端建立游戲大廳 WebSocket 長(zhǎng)連接請(qǐng)求, 會(huì)話驗(yàn)證成功后,返回長(zhǎng)連接建立成功或失敗的響應(yīng)。(游戲大廳長(zhǎng)連接建立后,用戶會(huì)被加入到游戲大廳在線用戶管理中)
- 服務(wù)器收到客戶端開始/停止對(duì)戰(zhàn)匹配的請(qǐng)求,會(huì)話驗(yàn)證成功后,會(huì)根據(jù)用戶天梯分?jǐn)?shù)將用戶加入對(duì)應(yīng)的匹配隊(duì)列或從對(duì)應(yīng)的匹配隊(duì)列中移除并返回響應(yīng)。(游戲匹配成功后,服務(wù)器會(huì)為用戶創(chuàng)建游戲房間,并主動(dòng)給客戶端發(fā)送 match_success 響應(yīng))
- 游戲匹配成功后,服務(wù)器收到客戶端建立游戲房間長(zhǎng)連接請(qǐng)求,會(huì)話驗(yàn)證成功后,返回長(zhǎng)連接建立成功或失敗的響應(yīng)。(游戲房間長(zhǎng)連接建立后,用戶會(huì)被加入到游戲房間在線用戶管理中)
- 之后,開始游戲?qū)?zhàn)與實(shí)時(shí)聊天,服務(wù)器會(huì)收到或主動(dòng)向另一個(gè)客戶端推送下棋/聊天信息。
- 最后,當(dāng)游戲結(jié)束后,用戶會(huì)返回游戲大廳并重新建立游戲大廳長(zhǎng)連接。
五、模塊開發(fā)
1. 實(shí)用工具類模塊
在進(jìn)行具體的業(yè)務(wù)模塊開發(fā)之前,我們可以提前封裝實(shí)現(xiàn)?些項(xiàng)?中會(huì)用到的邊緣功能代碼,這樣以后在項(xiàng)目中有相應(yīng)需求時(shí)就可以直接使用了。
1.1 日志宏封裝
日志宏功能主要負(fù)責(zé)程序日志的打印,方便我們?cè)诔绦虺鲥e(cuò)時(shí)能夠快速定位錯(cuò)誤,以及在程序運(yùn)行過程中打印一些關(guān)鍵的提示信息。
logger.hpp:
#ifndef __LOGGER_HPP__
#define __LOGGER_HPP__
#include <cstdio>
#include <time.h>/*日志等級(jí)*/
enum {NORMAL, DEBUG, ERROR,FATAL
};/*將日志等級(jí)轉(zhuǎn)化為字符串*/
const char* level_to_stirng(int level) {switch (level){case NORMAL:return "NORMAL";case DEBUG:return "DEBUG";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return nullptr;}
}#define LOG(level, format, ...) do {\const char* levelstr = level_to_stirng(level); /*日志等級(jí)*/\time_t ts = time(NULL); /*時(shí)間戳*/\ struct tm *lt = localtime(&ts); /*格式化時(shí)間*/\ char buffer[32] = { 0 };\strftime(buffer, sizeof(buffer) - 1, "%y-%m-%d %H:%M:%S", lt); /*格式化時(shí)間到字符串*/\fprintf(stdout, "[%s][%s][%s:%d] " format "\n", levelstr, buffer, __FILE__, __LINE__, ##__VA_ARGS__); /*##解除必須傳遞可變參數(shù)的限制*/\
} while(0)
#endif
1.2 MySQL C API 封裝
MySQL C API 工具類主要是封裝部分C語言連接數(shù)據(jù)庫的接口,包括 MySQL 句柄的創(chuàng)建和銷毀,以及 sql 語句的執(zhí)行。
需要注意的是,我們并沒有封裝獲取 sql 查詢結(jié)果的相關(guān)接口,因?yàn)槭欠褚@取查詢結(jié)果、要獲取哪部分查詢結(jié)果以及以何種形式獲取查詢結(jié)果,這些都是與業(yè)務(wù)需求強(qiáng)相關(guān)的。
mysql_util:
/*MySQL C API工具類*/
class mysql_util {
public:/*創(chuàng)建MySQL句柄*/static MYSQL *mysql_create(const std::string &host, const std::string &user, const std::string &passwd, const std::string db = "gobang", uint16_t port = 4106) {/*初始化MYSQL句柄*/MYSQL *mysql = mysql_init(nullptr);if(mysql == nullptr) {LOG(FATAL, "mysql init failed");return nullptr;}/*連接MySQL數(shù)據(jù)庫*/mysql = mysql_real_connect(mysql, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0);if(mysql == nullptr) {LOG(FATAL, "mysql connect failed: %s", mysql_error(mysql));mysql_close(mysql);return nullptr;}/*設(shè)置客戶端字符集*/if(mysql_set_character_set(mysql, "utf8") != 0) {LOG(ERROR, "client character set failed: %s", mysql_error(mysql));}return mysql;}/*執(zhí)行sql語句*/static bool mysql_execute(MYSQL *mysql, const std::string &sql) {if(mysql_query(mysql, sql.c_str()) != 0) {LOG(ERROR, "sql query failed: %s", mysql_error(mysql));return false;}return true;}/*銷毀MySQL句柄*/static void mysql_destroy(MYSQL *mysql) {if(mysql != nullptr) {mysql_close(mysql);}}
};
1.3 JsonCpp 封裝
jsoncpp 工具類主要是完成數(shù)據(jù)的序列化與反序列化工作。
json_util:
/*jsoncpp工具類*/
class json_util {
public:/*序列化接口*/static bool serialize(Json::Value &root, std::string &str) {Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss;if(sw->write(root, &ss) != 0) {LOG(ERROR, "json serialize failed");return false;}str = ss.str();return true;}/*反序列化接口*/static bool deserialize(const std::string &str, Json::Value &root) {Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());std::string err;if(cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err) == false) {LOG(ERROR, "json deserialize failed: %s", err);return false;}return true;}
};
1.4 String Split 封裝
string split 主要是按照特定分隔符對(duì)字符串進(jìn)行分割,并將分割后的結(jié)果進(jìn)行返回。在本項(xiàng)目中,它的使用場(chǎng)景是分割請(qǐng)求頭部中的 cookie 信息,獲取 session id。
string_util:
/*字符串處理工具類*/
class string_util {
public:/*將源字符串按照特定分隔符分割為若干個(gè)子字符串*/static int split(const std::string &src, const std::string &sep, std::vector<std::string> &res) {// ..abc..de..efint index = 0, pos = 0;while(index < src.size()) {pos = src.find(sep, index);if(pos == std::string::npos) {res.push_back(src.substr(index));break;}if(index == pos) {index += sep.size();continue;}else {res.push_back(src.substr(index, pos - index));index = pos + sep.size();}}return res.size();}
};
1.5 File Read 封裝
file read 的作用是讀取指定文件中的內(nèi)容。
file_util:
/*讀取文件數(shù)據(jù)工具類*/
class file_util {
public:static bool read(const char* filename, std::string &data) {/*以二進(jìn)制形式打開文件*/std::ifstream ifs(filename, std::ios::binary);if(ifs.is_open() == false) {LOG(ERROR, "open %s file failed", filename);return false;}/*獲取文件大小*/size_t size;ifs.seekg(0, std::ios::end);size = ifs.tellg();ifs.seekg(0, std::ios::beg);/*讀取文件內(nèi)容*/data.resize(size);ifs.read(&data[0], size);if(ifs.good() == false) {LOG(ERROR, "read %s file content failed", filename);ifs.close();return false;}/*關(guān)閉文件*/ifs.close();return true;}
};
2. 用戶數(shù)據(jù)管理模塊
用戶數(shù)據(jù)管理模塊主要負(fù)責(zé)對(duì)數(shù)據(jù)庫中數(shù)據(jù)進(jìn)行統(tǒng)?的增刪查改管理,其他模塊對(duì)數(shù)據(jù)的操作都必須通過用戶數(shù)據(jù)管理模塊來完成。
2.1 用戶信息表
在本項(xiàng)目中,用戶數(shù)據(jù)主要包括用戶名、用戶密碼、用戶天梯分?jǐn)?shù)、用戶對(duì)戰(zhàn)場(chǎng)次以及用戶獲勝場(chǎng)次,我們可以在數(shù)據(jù)庫中創(chuàng)建一個(gè) user 表來保存用戶數(shù)據(jù)。其中,user 表中需要有一個(gè)自增主鍵 id 來唯一標(biāo)識(shí)一個(gè)用戶。
create database if not exists gobang;
use gobang;
create table if not exists user (id bigint unsigned primary key auto_increment key,username varchar(32) unique key not null,password varchar(64) not null,score int default 1000,total_count int default 0,win_count int default 0
);
2.2 用戶數(shù)據(jù)管理類
對(duì)于一般的數(shù)據(jù)庫來說,數(shù)據(jù)庫中有可能存在很多張表,而每張表中管理的數(shù)據(jù)以及要進(jìn)行的數(shù)據(jù)操作都各不相同,因此我們可以為每?張表中的數(shù)據(jù)操作都設(shè)計(jì)?個(gè)類,通過類實(shí)例化的對(duì)象來訪問這張數(shù)據(jù)庫表中的數(shù)據(jù)。這樣當(dāng)我們要訪問哪張表的時(shí)候,只需要使用對(duì)應(yīng)類實(shí)例化的對(duì)象即可。
對(duì)于本項(xiàng)目而言,目前數(shù)據(jù)庫中只有一張 user 表,所以我們需要為其設(shè)計(jì)一個(gè)類,它的主要功能如下:
- registers:完成新用戶注冊(cè),返回是否注冊(cè)成功。
- login:完成用戶登錄驗(yàn)證,如果登錄成功返回 true 并且填充用戶詳細(xì)信息。
- select_by_name:通過用戶名查找用戶詳細(xì)信息。
- select_by_id:通過用戶 id 查找用戶詳細(xì)信息。
- win:當(dāng)用戶對(duì)戰(zhàn)勝利后修改用戶數(shù)據(jù)庫數(shù)據(jù) – 天梯分?jǐn)?shù)、對(duì)戰(zhàn)場(chǎng)次、獲勝場(chǎng)次。
- lose:當(dāng)用戶對(duì)戰(zhàn)失敗后修改用戶數(shù)據(jù)庫數(shù)據(jù) – 天梯分?jǐn)?shù)、對(duì)戰(zhàn)場(chǎng)次。
db.hpp:
#ifndef __DB_HPP__
#define __DB_HPP__
#include "util.hpp"
#include <mutex>
#include <cassert>/*用戶數(shù)據(jù)管理模塊 -- 用于管理數(shù)據(jù)庫數(shù)據(jù),為數(shù)據(jù)庫中的每張表都設(shè)計(jì)一個(gè)類,然后通過類對(duì)象來操作數(shù)據(jù)庫表中的數(shù)據(jù)*/
/*用戶信息表*/
class user_table {
public:user_table(const std::string &host, const std::string &user, const std::string &passwd, \const std::string db = "gobang", uint16_t port = 4106) {_mysql = mysql_util::mysql_create(host, user, passwd, db, port);assert(_mysql != nullptr);LOG(DEBUG, "用戶數(shù)據(jù)管理模塊初識(shí)化完畢");}~user_table() {if(_mysql != nullptr) { mysql_util::mysql_destroy(_mysql);_mysql = nullptr;}LOG(DEBUG, "用戶數(shù)據(jù)管理模塊已被銷毀");}/*新用戶注冊(cè)*/bool registers(Json::Value &user) {if(user["username"].isNull() || user["password"].isNull()) {LOG(NORMAL, "please input username and password");return false; }// 由于用戶名有唯一鍵約束,所以不需要擔(dān)心用戶已被注冊(cè)的情況char sql[1024];
#define INSERT_USER "insert into user values(null, '%s', password('%s'), 1000, 0, 0)"sprintf(sql, INSERT_USER, user["username"].asCString(), user["password"].asCString());// LOG(DEBUG, "%s", sql);if(mysql_util::mysql_execute(_mysql, sql) == false) {LOG(NORMAL, "user register failed");return false;}LOG(NORMAL, "%s register success", user["username"].asCString());return true;}/*用戶登錄驗(yàn)證*/bool login(Json::Value &user) {// 與數(shù)據(jù)庫中的用戶名+密碼進(jìn)行比對(duì)// 注意:數(shù)據(jù)庫的password是經(jīng)過mysql password函數(shù)轉(zhuǎn)換后的,所以sql查詢時(shí)也需要對(duì)user["password"].asString()進(jìn)行轉(zhuǎn)化
#define SELECT_USER "select id, score, total_count, win_count from user where username = '%s' and password = password('%s')" char sql[1024];sprintf(sql, SELECT_USER, user["username"].asCString(), user["password"].asCString());MYSQL_RES *res = nullptr;{// mysql查詢與查詢結(jié)果的本地保存兩步操作需要加鎖,避免多線程使用同一句柄進(jìn)行操作的情況下發(fā)送結(jié)果集的數(shù)據(jù)覆蓋問題// 將鎖交給RAII unique_lock進(jìn)行管理std::unique_lock<std::mutex> lock(_mutex);if(mysql_util::mysql_execute(_mysql, sql) == false) return false;;// 獲取查詢到的結(jié)果--一行記錄res = mysql_store_result(_mysql);// 注意:當(dāng)mysql查詢結(jié)果為空時(shí),mysql_store_result也不會(huì)返回空,所以不能在這里判斷用戶名密碼是否正確if(res == nullptr) {LOG(NORMAL, "mysql store failed: ", mysql_error(_mysql));return false;}}int row_count = mysql_num_rows(res);int col_count = mysql_num_fields(res);// row_count 為0說明查詢不到與當(dāng)前用戶名+密碼匹配的數(shù)據(jù),即用戶名或密碼錯(cuò)誤if(row_count == 0) {LOG(NORMAL, "the username or password error, please input again");return false;}// 用戶名存在唯一鍵約束if(row_count > 1) {LOG(ERROR, "there are same user %s in the database", user["username"].asCString());return false;} LOG(NORMAL, "%s login success", user["username"].asCString());// 填充該用戶的其他詳細(xì)信息MYSQL_ROW row = mysql_fetch_row(res);user["id"] = std::stoi(row[0]);user["score"] = std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]); mysql_free_result(res); return true;}/*使用用戶名查找用戶的詳細(xì)信息*/bool select_by_name(const std::string &name, Json::Value &user) {
#define SELECT_BY_USERNAME "select id, score, total_count, win_count from user where username = '%s'"char sql[1024];sprintf(sql, SELECT_BY_USERNAME, name.c_str());MYSQL_RES *res = nullptr;{// 加鎖std::unique_lock<std::mutex> lock(_mutex);if(mysql_util::mysql_execute(_mysql, sql) == false) return false;// 獲取查詢到的結(jié)果--一行記錄res = mysql_store_result(_mysql);// 注意:當(dāng)mysql查詢結(jié)果為空時(shí),mysql_store_result也不會(huì)返回空,所以不能在這里判斷用戶是否存在if(res == nullptr) {LOG(DEBUG, "mysql store failed: ", mysql_error(_mysql));return false;}}int row_count = mysql_num_rows(res);int col_count = mysql_num_fields(res);// row_count為0說明查詢不到與當(dāng)前用戶名匹配的數(shù)據(jù),即用戶不存在if(row_count == 0) {LOG(DEBUG, "the user with name %s does not exist", name.c_str());return false;}// 用戶名存在唯一鍵約束if(row_count > 1) {LOG(ERROR, "there are same user name %s in the database", name.c_str());return false;} MYSQL_ROW row = mysql_fetch_row(res);// password是轉(zhuǎn)換后的,獲取無意義user["id"] = std::stoi(row[0]);user["username"] = name.c_str();user["score"] = std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}/*使用用戶ID查找用戶的詳細(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[1024];sprintf(sql, SELECT_BY_ID, id);MYSQL_RES *res = nullptr;{// 加鎖std::unique_lock<std::mutex> lock(_mutex);if(mysql_util::mysql_execute(_mysql, sql) == false) return false;// 獲取查詢到的結(jié)果--一行記錄res = mysql_store_result(_mysql);// 注意:當(dāng)mysql查詢結(jié)果為空時(shí),mysql_store_result也不會(huì)返回空,所以不能在這里判斷用戶是否存在if(res == nullptr) {LOG(DEBUG, "mysql store failed: ", mysql_error(_mysql));return false;}}int row_count = mysql_num_rows(res);int col_count = mysql_num_fields(res);// row_count為0說明查詢不到與當(dāng)前用戶名ID匹配的數(shù)據(jù),即用戶不存在if(row_count == 0) {LOG(DEBUG, "the user with ID %d does not exist", id);return false;}// 用戶名存在唯一鍵約束if(row_count > 1) {LOG(ERROR, "there are same user with ID %d in the database", id);return false;}MYSQL_ROW row = mysql_fetch_row(res);// password是轉(zhuǎn)換后的,獲取無意義user["id"] = (Json::UInt64)id;user["username"] = row[0];user["score"] = std::stoi(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true; }/*用戶對(duì)戰(zhàn)勝利,修改分?jǐn)?shù)以及比賽和勝利場(chǎng)次,勝利一場(chǎng)增加30分*/bool win(uint64_t id) {
#define UPDATE_WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id = %d"char sql[1024];sprintf(sql, UPDATE_WIN, id);if(mysql_util::mysql_execute(_mysql, sql) == false) {LOG(ERROR, "update the user info of win failed");return false;}return true;}/*用戶對(duì)戰(zhàn)失敗,修改分?jǐn)?shù)以及比賽場(chǎng)次*,失敗一場(chǎng)減少30分*/bool lose(uint64_t id) {
#define UPDATE_LOSE "update user set score=score-30, total_count=total_count+1 where id = %d"char sql[1024];sprintf(sql, UPDATE_LOSE, id);if(mysql_util::mysql_execute(_mysql, sql) == false) {LOG(ERROR, "update the user info of lose failed");return false;}return true;}
private:MYSQL *_mysql; // mysql操作句柄std::mutex _mutex; // 解決多線程使用同一類對(duì)象(句柄)訪問數(shù)據(jù)庫時(shí)可能發(fā)生的線程安全問題
};
#endif
3. 在線用戶管理模塊
在線用戶管理模塊主要管理兩類用戶 – 進(jìn)入游戲大廳的用戶與進(jìn)入游戲房間的用戶,因?yàn)橛脩糁挥羞M(jìn)入了游戲大廳或者游戲房間,其對(duì)應(yīng)的客戶端才會(huì)與服務(wù)器建立 WebSocket 長(zhǎng)連接。
此時(shí)我們需要將用戶 id 與用戶所對(duì)應(yīng)的 WebSocket 長(zhǎng)連接關(guān)聯(lián)起來,這樣我們就能夠通過用戶 id 找到用戶所對(duì)應(yīng)的連接,進(jìn)而實(shí)現(xiàn)服務(wù)器主動(dòng)向客戶端推送消息的功能:
-
在游戲大廳中,當(dāng)一個(gè)用戶開始匹配后,如果匹配成功,服務(wù)器需要主動(dòng)向客戶端推送匹配成功的消息。
-
在游戲房間中,當(dāng)一個(gè)玩家有下棋或者聊天動(dòng)作時(shí),服務(wù)器也需要將這些動(dòng)作主動(dòng)推送給另一個(gè)玩家。
需要注意的是,用戶在游戲大廳的長(zhǎng)連接與游戲房間的長(zhǎng)連接是不同的,所以我們需要分別建立游戲大廳用戶 id 與 WebSocket 長(zhǎng)連接的關(guān)聯(lián)關(guān)系以及游戲房間用戶 id 與 WebSocket 長(zhǎng)連接的關(guān)聯(lián)關(guān)系。
在線用戶管理類的主要功能如下:
- enter_game_hall:指定用戶進(jìn)入游戲大廳,此時(shí)需要建立用戶 id 與游戲大廳 WebSocket 長(zhǎng)連接的關(guān)聯(lián)關(guān)系。
- enter_game_hall:指定用戶進(jìn)入游戲房間,此時(shí)需要建立用戶 id 與游戲房間 WebSocket 長(zhǎng)連接的關(guān)聯(lián)關(guān)系。
- exit_game_hall:指定用戶離開游戲大廳,此時(shí)需要斷開用戶 id 與游戲大廳 WebSocket 長(zhǎng)連接的關(guān)聯(lián)關(guān)系。
- exit_game_room:指定用戶離開游戲房間,此時(shí)需要斷開用戶 id 與游戲房間 WebSocket 長(zhǎng)連接的關(guān)聯(lián)關(guān)系。
- is_in_game_hall:判斷指定用戶是否在游戲大廳中。
- is_in_game_room:判斷指定用戶是否在游戲房間中。
- get_conn_from_hall:獲取指定用戶的游戲大廳長(zhǎng)連接。
- get_conn_from_room:獲取指定用戶的游戲房間長(zhǎng)連接。
online.hpp:
#ifndef __ONLINE_HPP__
#define __ONLINE_HPP__
#include "util.hpp"
#include <mutex>
#include <unordered_map>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>typedef websocketpp::server<websocketpp::config::asio> wsserver_t;/*在線用戶管理模塊 -- 用于管理在游戲大廳以及游戲房間中的用戶,建立用戶id與websocket長(zhǎng)連接的對(duì)應(yīng)關(guān)系*/
class online_manager {
public:online_manager() { LOG(DEBUG, "在線用戶管理模塊初始化完畢"); }~online_manager() { LOG(DEBUG, "在線用戶管理模塊已被銷毀"); }/*用戶進(jìn)入游戲大廳(此時(shí)用戶websocket長(zhǎng)連接已建立好)*/void enter_game_hall(uint64_t uid, wsserver_t::connection_ptr conn) {std::unique_lock<std::mutex> lock(_mutex);_hall_user[uid] = conn;}/*用戶進(jìn)入游戲房間*/void enter_game_room(uint64_t uid, wsserver_t::connection_ptr conn) {std::unique_lock<std::mutex> lock(_mutex);_room_user[uid] = conn;}/*用戶離開游戲大廳(websocket長(zhǎng)連接斷開時(shí))*/void exit_game_hall(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);_hall_user.erase(uid);}/*用戶對(duì)戰(zhàn)結(jié)束離開游戲房間回到游戲大廳*/void exit_game_room(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);_room_user.erase(uid);}/*判斷當(dāng)前用戶是否在游戲大廳*/bool is_in_game_hall(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _hall_user.find(uid);if(it == _hall_user.end()) return false;return true;}/*判斷當(dāng)前用戶是否在游戲房間*/bool is_in_game_room(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _room_user.find(uid);if(it == _room_user.end()) return false;return true;}/*通過用戶id獲取游戲大廳用戶的通信連接*/wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _hall_user.find(uid);if(it == _hall_user.end()) return nullptr;return _hall_user[uid];}/*通過用戶id獲取游戲房間用戶的通信連接*/wsserver_t::connection_ptr get_conn_from_room(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _room_user.find(uid);if(it == _room_user.end()) return nullptr;return _room_user[uid];}
private:std::mutex _mutex; // 解決多線程模式下的線程安全問題std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user; // 建立游戲大廳用戶id與通信連接之間的關(guān)聯(lián)關(guān)系std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user; // 建立游戲房間用戶id與通信連接之間的關(guān)聯(lián)關(guān)系
};
#endif
4. 游戲房間管理模塊
游戲房間管理模塊就是設(shè)計(jì)一個(gè)房間類,能夠?qū)崿F(xiàn)房間的實(shí)例化;房間類主要是對(duì)匹配成功的玩家建立一個(gè)小范圍的關(guān)聯(lián)關(guān)系,當(dāng)一個(gè)房間中的玩家發(fā)生下棋或者聊天動(dòng)作時(shí),服務(wù)器能夠?qū)⑵鋸V播給房間中的其他玩家。
游戲房間類的具體功能如下:
- add_white_user:為房間添加白棋玩家。
- add_black_user:為白棋添加黑棋玩家。
- handler:總的動(dòng)作處理函數(shù),函數(shù)內(nèi)部會(huì)根據(jù)不同的動(dòng)作類型 (下棋/聊天) 調(diào)用不同的子函數(shù)進(jìn)行處理得到響應(yīng)。
- broadcast:將處理動(dòng)作得到的響應(yīng)廣播給房間中的其他玩家。
同時(shí),由于同一時(shí)間段內(nèi)進(jìn)行匹配或者正在對(duì)戰(zhàn)的玩家有很多,所以游戲房間可能會(huì)有多個(gè);那么我們就需要設(shè)計(jì)一個(gè)游戲房間管理類來對(duì)多個(gè)房間進(jìn)行管理。
游戲房間管理類的具體功能如下:
- create_room:為兩個(gè)玩家創(chuàng)建一個(gè)游戲房間。
- get_room_by_rid:通過房間 id 獲取房間信息。
- get_room_by_uid:通過玩家 id 獲取玩家所在房間的房間信息。
- remove_room:通過房間 id 銷毀房間。
- remove_room_user:移除房間中的指定玩家,若房間中沒有玩家了則直接銷毀房間。
最后,需要注意的是,在游戲房間管理模塊中,由于我們需要根據(jù)不同的消息類型來調(diào)用不同的函數(shù),進(jìn)而得到不同的響應(yīng),所以我們需要提前規(guī)定好 WebSocket (游戲房間中 WebSocket 長(zhǎng)連接已建立) 網(wǎng)絡(luò)通信中不同類型的消息的格式是怎樣的。這部分代碼會(huì)在服務(wù)器模塊的通信接口設(shè)計(jì)處給出,但為了便于理解,這里我們也放一份。
玩家下棋的消息:
// 玩家下棋消息
{"optype": "put_chess", // put_chess表示當(dāng)前請(qǐng)求是下棋操作"room_id": 222, // room_id 表?當(dāng)前動(dòng)作屬于哪個(gè)房間"uid": 1, // 當(dāng)前的下棋操作是哪個(gè)用戶發(fā)起的"row": 3, // 當(dāng)前下棋位置的?號(hào)"col": 2 // 當(dāng)前下棋位置的列號(hào)
}// 下棋成功后后臺(tái)回復(fù)的消息
{"optype": "put_chess","result": true,"reason": "下棋成功或游戲勝利或游戲失敗","room_id": 222,"uid": 1,"row": 3,"col": 2,"winner": 0 // 游戲獲勝者,0表示未分勝負(fù),!0表示已分勝負(fù)
}// 下棋失敗后后臺(tái)回復(fù)的消息
{"optype": "put_chess","result": false,"reason": "下棋失敗的原因","room_id": 222, "uid": 1,"row": 3,"col": 2,"winner": 0
}
玩家聊天的消息:
// 玩家聊天消息
{"optype": "chat", // chat表示當(dāng)前請(qǐng)求是下棋操作"room_id": 222, // room_id 表?當(dāng)前動(dòng)作屬于哪個(gè)房間"uid": 1, // 當(dāng)前的下棋操作是哪個(gè)用戶發(fā)起的"message": "你好" // 聊天消息的具體內(nèi)容
}// 聊天消息發(fā)送成功后臺(tái)回復(fù)的消息
{"optype": "chat","result": true,"room_id": 222,"uid": 1,"message": "你好"
}// 聊天消息發(fā)送失敗后臺(tái)回復(fù)的消息
{"optype": "chat","result": false,"reason": "錯(cuò)誤原因,比如消息中包含敏感詞","room_id": 222,"uid": 1,"message": "你好"
}
未知類型的消息:
{"optype": 消息的類型,"result": false,"reason": "未知類型的消息"
}
room.hpp:
#ifndef __ROOM_HPP__
#define __ROOM_HPP__
#include "util.hpp"
#include "db.hpp"
#include "online.hpp"
#include <vector>#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2typedef enum {GAME_START,GAME_OVER
} room_status;/*游戲房間管理模塊 -- 用于管理在游戲房間中產(chǎn)生的各種數(shù)據(jù)以及動(dòng)作,同時(shí)也包括對(duì)多個(gè)游戲房間本身的管理*/
/*游戲房間類*/
class room {
private:/*check_win子函數(shù),其中row/col表示下棋位置,row_off/col_off表示是否偏移*/bool five_piece(int row, int col, int row_off, int col_off, int color) {int count = 1; // 處理正方向int search_row = row + row_off;int search_col = col + col_off;while((search_row >= 0 && search_row < BOARD_ROW) && (search_col >= 0 && search_col < BOARD_COL)&& (_board[search_row][search_col] == color)) {++count;search_row += row_off;search_col += col_off;}// 處理反方向search_row = row - row_off;search_col = col - col_off;while((search_row >= 0 && search_row < BOARD_ROW) && (search_col >= 0 && search_col < BOARD_COL)&& (_board[search_row][search_col] == color)) {++count;search_row -= row_off;search_col -= col_off;}return count >= 5;}/*判斷是否有用戶勝利并返回winner_id (0表示沒有用戶勝利,非0表示有)*/uint64_t check_win(int chess_row, int chess_col, int cur_color) {uint64_t winner_id = cur_color == CHESS_WHITE ? _white_user_id : _black_user_id;// 橫行方向:當(dāng)前位置開始,行不變,列++/--if(five_piece(chess_row, chess_col, 0, 1, cur_color)) return winner_id;// 縱列方向:當(dāng)前位置開始,行++/--,列不變if(five_piece(chess_row, chess_col, 1, 0, cur_color)) return winner_id;// 正斜方向:當(dāng)前位置開始,行++列-- 以及 行--列++if(five_piece(chess_row, chess_col, 1, -1, cur_color)) return winner_id;// 反斜方向:當(dāng)前位置開始,行++列++ 以及 行--列--if(five_piece(chess_row, chess_col, 1, 1, cur_color)) return winner_id;// 沒有人獲勝返回0return 0;}/*用戶勝利或失敗后更新用戶數(shù)據(jù)庫信息*/void update_db_info(uint64_t winner_id, uint64_t loser_id) {_tb_user->win(winner_id);_tb_user->lose(loser_id);}
public:room(uint64_t room_id, user_table *tb_user, online_manager *online_user): _room_id(room_id), _statu(GAME_START), _tb_user(tb_user), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0)){LOG(DEBUG, "%d號(hào)房間創(chuàng)建成功", _room_id);}~room() { LOG(DEBUG, "%d號(hào)房間已被銷毀", _room_id); }/*添加白棋用戶*/void add_white_user(uint64_t id) {_white_user_id = id;++_player_count;}/*添加黑棋用戶*/void add_black_user(uint64_t id) {_black_user_id = id;++_player_count;}/*處理玩家下棋動(dòng)作并返回響應(yīng)*/Json::Value handler_chess(Json::Value &req) {Json::Value resp = req;// 判斷白棋與黑棋用戶是否在線,若一方不在線,另一方直接獲勝if(_online_user->is_in_game_room(_white_user_id) == false) {resp["result"] = true;resp["reason"] = "對(duì)方已掉線,游戲獲勝"; // 在黑棋的視角,白棋是"對(duì)方" resp["winner"] = (Json::UInt64)_black_user_id; // 白棋掉線,黑棋用戶}if(_online_user->is_in_game_room(_black_user_id) == false) {resp["result"] = true;resp["reason"] = "對(duì)方已掉線,游戲勝利"; resp["winner"] = (Json::UInt64)_white_user_id; }// 獲取下棋位置,判斷位置是否合理并下棋uint64_t cur_uid = req["uid"].asUInt64();int chess_row = req["row"].asInt();int chess_col = req["col"].asInt();if(_board[chess_row][chess_col] != 0) {resp["result"] = false;resp["reason"] = "該位置已被占用";return resp; }int cur_color = (cur_uid == _white_user_id ? CHESS_WHITE : CHESS_BLACK);_board[chess_row][chess_col] = cur_color;// 判斷是否有玩家獲勝(存在五星連珠的情況) 其中0表示沒有玩家勝利,非0表示勝利的玩家iduint64_t winner_id = check_win(chess_row, chess_col, cur_color);resp["result"] = true;resp["reason"] = "下棋成功"; resp["winner"] = (Json::UInt64)winner_id;if(winner_id != 0) { resp["reason"] = "五星連珠,游戲勝利"; }return resp;}/*處理玩家聊天動(dòng)作并返回響應(yīng)*/Json::Value handler_chat(Json::Value &req) {Json::Value resp = req;// 檢查消息中是否包含敏感詞std::string msg = req["message"].asString();size_t pos = msg.find("垃圾");if(pos != std::string::npos) {resp["result"] = false;resp["reason"] = "消息中包含敏感詞";return resp;}resp["reslut"] = true;return resp;}/*處理玩家退出動(dòng)作并返回響應(yīng)*/void handler_exit(uint64_t uid) {// 如果玩家在下棋中,則對(duì)方直接獲勝if(_statu == GAME_START) {Json::Value resp;resp["optype"] = "put_chess";resp["result"] = true;resp["reason"] = "對(duì)方已退出,游戲勝利";resp["room_id"] = (Json::UInt64)_room_id;resp["uid"] = (Json::UInt64)uid;resp["row"] = -1;resp["col"] = -1;resp["winner"] = (Json::UInt64)(uid == _white_user_id ? _black_user_id : _white_user_id);// 更新用戶數(shù)據(jù)庫信息與游戲房間的狀態(tài)uint64_t loser_id = uid;uint64_t winner_id = loser_id == _white_user_id ? _black_user_id : _white_user_id;update_db_info(winner_id, loser_id);_statu = GAME_OVER;// 將消息廣播給房間其他玩家broadcast(resp);}// 游戲結(jié)束正常退出直接更新玩家數(shù)量--_player_count;}/*總的動(dòng)作處理函數(shù),負(fù)責(zé)判斷動(dòng)作類型并調(diào)用對(duì)應(yīng)的處理函數(shù),得到處理響應(yīng)后將其廣播給房間中其他用戶*//*注意:玩家退出動(dòng)作屬于玩家斷開連接后調(diào)用的操作,不屬于handler的一種*/void handler(Json::Value &req) {Json::Value resp;// 判斷房間號(hào)是否匹配if(_room_id != req["room_id"].asUInt64()) {resp["optype"] = req["optype"].asString();resp["result"] = false;resp["reason"] = "房間號(hào)不匹配";broadcast(resp);return;}// 根據(jù)請(qǐng)求類型調(diào)用不同的處理函數(shù)std::string type = req["optype"].asString();if(type == "put_chess") {resp = handler_chess(req);// 判斷是否有玩家獲勝,如果有則需要更新用戶數(shù)據(jù)庫信息與游戲房間的狀態(tài)if(resp["winner"].asUInt64() != 0) {uint64_t winner_id = resp["winner"].asUInt64();uint64_t loser_id = (winner_id == _white_user_id ? _black_user_id : _white_user_id);update_db_info(winner_id, loser_id);_statu = GAME_OVER;}} else if(type == "chat") {resp = handler_chat(req);} else {resp["optype"] = req["optype"].asString();resp["result"] = false;resp["reason"] = "未知類型的消息";}// 將消息廣播給房間中的其他玩家broadcast(resp);}/*將動(dòng)作響應(yīng)廣播給房間中的其他玩家*/void broadcast(Json::Value &resp) {// 將Json響應(yīng)進(jìn)行序列化std::string body;json_util::serialize(resp, body);// 獲取房間中的所有玩家的通信連接wsserver_t::connection_ptr conn_white = _online_user->get_conn_from_room(_white_user_id);wsserver_t::connection_ptr conn_black = _online_user->get_conn_from_room(_black_user_id);// 如果玩家連接沒有斷開,則將消息廣播給他if(conn_white.get() != nullptr) {conn_white->send(body);}if(conn_black.get() != nullptr) {conn_black->send(body);}}
public:// 將部分成員變量設(shè)為public,供外部類訪問uint64_t _room_id; // 房間IDroom_status _statu; // 房間狀態(tài)int _player_count; // 玩家數(shù)量uint64_t _white_user_id; // 白棋玩家IDuint64_t _black_user_id; // 黑棋玩家ID
private:user_table *_tb_user; // 管理玩家數(shù)據(jù)的句柄online_manager *_online_user; // 管理玩家在線狀態(tài)的句柄 std::vector<std::vector<int>> _board; // 二維棋盤
};/*管理房間數(shù)據(jù)的智能指針*/
using room_ptr = std::shared_ptr<room>; /*游戲房間管理類*/
class room_manager {
public:room_manager(user_table *tb_user, online_manager *online_user): _next_rid(1), _tb_user(tb_user), _online_user(online_user) {LOG(DEBUG, "游戲房間管理模塊初始化成功");}~room_manager() { LOG(NORMAL, "游戲房間管理模塊已被銷毀"); }/*為兩個(gè)玩家創(chuàng)建房間,并返回房間信息*/room_ptr create_room(uint64_t uid1, uint64_t uid2) {// 判斷兩個(gè)玩家是否都處于游戲大廳中if(_online_user->is_in_game_hall(uid1) == false || _online_user->is_in_game_hall(uid2) == false) {LOG(DEBUG, "玩家不在游戲大廳中,匹配失敗");return room_ptr();}// 創(chuàng)建游戲房間,將用戶信息添加到房間中std::unique_lock<std::mutex> lock(_mutex);room_ptr rp(new room(_next_rid, _tb_user, _online_user));rp->add_white_user(uid1);rp->add_black_user(uid2);// 將游戲房間管理起來(建立房間id與房間信息以及玩家id與房間id的關(guān)聯(lián)關(guān)系)_rooms[_next_rid] = rp;_users[uid1] = _next_rid;_users[uid2] = _next_rid;// 更新下一個(gè)房間的房間id++_next_rid;// 返回房間信息return rp;}/*通過房間id獲取房間信息*/room_ptr get_room_by_rid(uint64_t rid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _rooms.find(rid);if(it == _rooms.end()) return room_ptr();return _rooms[rid];}/*通過用戶id獲取房間信息*/room_ptr get_room_by_uid(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);// 獲取房間idauto it1 = _users.find(uid);if(it1 == _users.end()) return room_ptr();uint64_t rid = _users[uid];// 獲取房間信息(這里不能直接調(diào)用get_room_by_rid,會(huì)造成死鎖)auto it2 = _rooms.find(rid);if(it2 == _rooms.end()) return room_ptr();return _rooms[rid];}/*通過房間id銷毀房間*/void remove_room(uint64_t rid) {// 通過房間id獲取房間信息room_ptr rp = get_room_by_rid(rid);if(rp.get() == nullptr) return;// 通過房間信息獲取房間中的玩家uint64_t white_user_id = rp->_white_user_id;uint64_t black_user_id = rp->_black_user_id;// 移除房間管理中的玩家信息std::unique_lock<std::mutex> lock(_mutex);_users.erase(white_user_id);_users.erase(black_user_id);// 移除房間管理信息 -- 移除房間對(duì)應(yīng)的shared_ptr(room_ptr)_rooms.erase(rid);}/*刪除房間中的指定用戶,若房間中沒有用戶則銷毀房間(用戶斷開websocket連接時(shí)調(diào)用)*/void remove_room_user(uint64_t uid) {// 通過玩家id獲取房間信息room_ptr rp = get_room_by_uid(uid);if(rp.get() == nullptr) return;// 玩家退出rp->handler_exit(uid);// 如果房間中沒有玩家了,則移除房間if(rp->_player_count == 0) remove_room(rp->_room_id);}
private:uint64_t _next_rid; //房間ID分配計(jì)數(shù)器std::mutex _mutex; user_table *_tb_user; // 管理玩家數(shù)據(jù)的句柄online_manager *_online_user; // 管理玩家在線狀態(tài)的句柄std::unordered_map<uint64_t, room_ptr> _rooms; // 建立房間id與房間信息的關(guān)聯(lián)關(guān)系std::unordered_map<uint64_t, uint64_t> _users; // 建立用戶id與房間id的關(guān)聯(lián)關(guān)系
};
#endif
5. 用戶 session 信息管理模塊
什么是 cookie&session:
- 在 web 開發(fā)中,由于 HTTP 是一種無狀態(tài)短連接的協(xié)議,這就導(dǎo)致一個(gè)用戶可能當(dāng)前登錄了,但過一會(huì)在進(jìn)行其他操作時(shí)原來的連接已經(jīng)斷開了,而我們又不知道新連接對(duì)應(yīng)的用戶是誰。這就導(dǎo)致要么我們頻繁讓用戶執(zhí)行登錄操作,完成身份認(rèn)證,要么不給用戶提供服務(wù),又或者在不確定當(dāng)前用戶身份和狀態(tài)的情況下為用戶提供服務(wù)。顯然這些方式都是不合理的。
- 為了解決這個(gè)問題,有大佬就提出了 cookie 的方案 – 客戶端在第一次登錄成功后,服務(wù)器會(huì)為響應(yīng)添加一個(gè) “Set-Cookie” 頭部字段,“Set-Cookie” 中包含了諸如 username&password 這類信息;客戶端收到響應(yīng)后會(huì)將 “Set-Cookie” 中的信息保存起來,并且之后發(fā)送新的請(qǐng)求時(shí)會(huì)自動(dòng)將 cookie 信息發(fā)送給服務(wù)器進(jìn)行身份與狀態(tài)驗(yàn)證,從而避免了用戶頻繁登錄的問題。
- 但是這樣簡(jiǎn)單的 cookie 機(jī)制會(huì)帶來安全問題,因?yàn)榭蛻舳丝赡軙?huì)自己偽造 “Set-Cookie” 信息,或者 HTTP 請(qǐng)求被中間人劫持導(dǎo)致 cookie 信息被篡改,所以大佬又提出了 session 機(jī)制。
- session 機(jī)制是指客戶端在第一次登錄成功后服務(wù)器會(huì)為客戶端實(shí)例化一個(gè) session (會(huì)話) 對(duì)象,該對(duì)象中保存了諸如用戶 id、用戶名、用戶密碼、用戶狀態(tài) (登錄/未登錄等) 這類信息,最重要的是服務(wù)器會(huì)為每一個(gè) session 對(duì)象,即每一個(gè)用戶分配一個(gè)唯一的 session id (ssid)。
此后,服務(wù)器與客戶端就通過 cookie 和 session 相結(jié)合的方式完成用戶身份與狀態(tài)的驗(yàn)證:
- 用戶首次登錄時(shí)服務(wù)器會(huì)為其實(shí)例化一個(gè) session 對(duì)象,然后將 ssid 添加到 “Set-Cookie” 頭部字段中響應(yīng)給客戶端。
- 客戶端收到響應(yīng)后會(huì)保存 cookie 信息,并且以后每次請(qǐng)求都自動(dòng)帶上 cookie 信息發(fā)送給服務(wù)器。
- 服務(wù)器收到新的客戶端請(qǐng)求后,會(huì)從請(qǐng)求頭部中獲取 cookie 信息,如果 cookie 信息中沒有 ssid 或者該 ssid 與服務(wù)器中所有的 session id 都不匹配時(shí),服務(wù)器會(huì)讓客戶端重新登錄并為其實(shí)例化 session 對(duì)象。如果服務(wù)器中存在與該 ssid 匹配的 session 對(duì)象,則為客戶端提供服務(wù)。
基于上面的原理,在本項(xiàng)目中,我們也需要設(shè)計(jì)一個(gè) session 類以及一個(gè) session 管理類,用來完成客戶端身份與狀態(tài)的驗(yàn)證以及 session 對(duì)象的管理。需要注意的是,session 對(duì)象不能一直存在,即當(dāng)用戶長(zhǎng)時(shí)間無操作后我們需要?jiǎng)h除服務(wù)器中該用戶對(duì)應(yīng)的 session 對(duì)象,因此我們需要使用 WebSocketpp 的定時(shí)器功能對(duì)每個(gè)創(chuàng)建的 session 對(duì)象進(jìn)行定時(shí)銷毀,否則也算是一種資源泄露。
session 類的具體功能如下:
- add_user:為 session 對(duì)象關(guān)聯(lián)具體的用戶。
- get_user:獲取 session 對(duì)象關(guān)聯(lián)的用戶。
- is_login:獲取用戶狀態(tài) (是否登錄)。
- get_ssid:獲取 session id。
- set_timer:設(shè)置 session 定時(shí)刪除任務(wù)。
- get_timer:獲取 session 關(guān)聯(lián)的定時(shí)器。
session 管理類的具體功能如下:
- create_session:為指定用戶創(chuàng)建 session 信息并返回 session 信息。
- get_session_by_ssid:通過 sessionID 獲取 session 信息。
- remove_session:通過 sessionID 刪除 session 信息。
- set_session_expire_time:設(shè)置 session 過期時(shí)間。
session.hpp:
#ifndef __SESSION_HPP__
#define __SESSION_HPP__
#include "online.hpp"
#include "logger.hpp"
#include <functional>typedef enum {UNLOGIN, LOGIN
} ss_statu;/*用戶session信息管理模塊 -- 用于http短連接通信情況下用戶狀態(tài)的管理(登錄/未登錄)*/
/*session 類*/
class session {
public:session(uint64_t ssid) : _ssid(ssid), _statu(LOGIN) { LOG(DEBUG, "session %d:%p 被創(chuàng)建", _ssid, this); }~session() { LOG(DEBUG, "session %d:%p 被刪除", _ssid, this); }/*添加用戶*/void add_user(uint64_t uid) { _uid = uid; }/*獲取用戶id*/uint64_t get_user() { return _uid; }/*獲取用戶狀態(tài)(檢查用戶是否已登錄)*/bool is_login() { return _statu == LOGIN; }/*獲取session id*/uint64_t get_ssid() { return _ssid; }/*設(shè)置session定時(shí)刪除任務(wù)*/void set_timer(const wsserver_t::timer_ptr &tp) { _tp = tp; }/*獲取session關(guān)聯(lián)的定時(shí)器*/wsserver_t::timer_ptr& get_timer() { return _tp; }
private:uint64_t _ssid; // session iduint64_t _uid; // session對(duì)應(yīng)的用戶idss_statu _statu; // 用戶狀態(tài)(登錄/未登錄)wsserver_t::timer_ptr _tp; // session關(guān)聯(lián)的定時(shí)器
};#define SESSION_TIMEOUT 30000 //30s
#define SESSION_FOREVER -1/*使用智能指針來管理session信息*/
using session_ptr = std::shared_ptr<session>;/*session 管理類*/
class session_manager {
public:session_manager(wsserver_t *server): _server(server), _next_ssid(1) {LOG(DEBUG, "用戶session管理模塊初始化成功");}~session_manager() { LOG(DEBUG, "用戶session管理模塊已被銷毀"); }/*為指定用戶創(chuàng)建session信息并返回*/session_ptr create_session(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);// 創(chuàng)建session信息session_ptr ssp(new session(_next_ssid));ssp->add_user(uid);// 建立sessionID與session信息的關(guān)聯(lián)關(guān)系_sessions[_next_ssid] = ssp;// 更新下一個(gè)session的id計(jì)數(shù)++_next_ssid;return ssp;}/*通過sessionID獲取session信息*/session_ptr get_session_by_ssid(uint64_t ssid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _sessions.find(ssid);if(it == _sessions.end()) return session_ptr();return _sessions[ssid];}/*刪除session信息*/void remove_session(uint64_t ssid) {std::unique_lock<std::mutex> lock(_mutex);_sessions.erase(ssid);}/*重新添加因cancel函數(shù)被刪除的_sessions成員*/void append_session(session_ptr ssp) {std::unique_lock<std::mutex> lock(_mutex);_sessions.insert(make_pair(ssp->get_ssid(), ssp)); // _sessions[ssp->get_ssid()] = ssp;}/*設(shè)置session過期時(shí)間(毫秒)*//*基于websocketpp定時(shí)器(timer_ptr)來完成對(duì)session生命周期的管理*/void set_session_expire_time(uint64_t ssid, int ms) {//當(dāng)客戶端與服務(wù)器建立http短連接通信(登錄/注冊(cè))時(shí),session應(yīng)該是臨時(shí)的,需要設(shè)置定時(shí)刪除任務(wù)//當(dāng)客戶端與服務(wù)器建立websocket長(zhǎng)連接通信(游戲大廳/游戲房間)時(shí),session應(yīng)該是永久的,直到websocket長(zhǎng)連接斷開session_ptr ssp = get_session_by_ssid(ssid);if(ssp.get() == nullptr) return;// 獲取session狀態(tài) -- session對(duì)象創(chuàng)建時(shí)默認(rèn)沒有關(guān)聯(lián)time_ptr,此時(shí)session是永久存在的(timer_ptr==nullptr)wsserver_t::timer_ptr tp = ssp->get_timer();// 1. 在session永久的情況下設(shè)置永久if(tp.get() == nullptr && ms == SESSION_FOREVER) return;// 2. 在session永久的情況下設(shè)置定時(shí)刪除任務(wù)else if(tp.get() == nullptr && ms != SESSION_FOREVER) {wsserver_t::timer_ptr tp_task = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tp_task); // 重新設(shè)置session關(guān)聯(lián)的定時(shí)器}// 3. 在session定時(shí)刪除的情況下設(shè)置永久(刪除定時(shí)任務(wù))else if(tp.get() != nullptr && ms == SESSION_FOREVER) {// 注意:websocketpp使用cancel函數(shù)刪除定時(shí)任務(wù)會(huì)導(dǎo)致定時(shí)任務(wù)直接被執(zhí)行,所以我們需要重新向_sessions中添加ssid與session_ptr// 同時(shí),由于這個(gè)定時(shí)任務(wù)不是立即被執(zhí)行的(服務(wù)器處理時(shí)才處理這個(gè)任務(wù)),所以我們不能在cancel函數(shù)后面直接重新添加session_ptr(這樣可能出現(xiàn)先添加、再刪除的情況)// 而是需要專門設(shè)置一個(gè)定時(shí)器來添加ssid與session_ptrtp->cancel();// 通過定時(shí)器來添加被刪除的_sessions成員_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp)); ssp->set_timer(wsserver_t::timer_ptr()); // 將session關(guān)聯(lián)的定時(shí)器設(shè)置為空(session永久有效)}// 4. 在session定時(shí)刪除的情況下重置刪除時(shí)間else {// 先刪除定時(shí)任務(wù)tp->cancel();_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp)); ssp->set_timer(wsserver_t::timer_ptr()); // 將session關(guān)聯(lián)的定時(shí)器設(shè)置為空(session永久有效)// 再重新添加定時(shí)任務(wù)wsserver_t::timer_ptr tp_task = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssid));ssp->set_timer(tp_task); // 重新設(shè)置session關(guān)聯(lián)的定時(shí)器}}
private:uint64_t _next_ssid; // sessionID計(jì)數(shù)器 std::mutex _mutex; std::unordered_map<uint64_t, session_ptr> _sessions; // 建立ssid與session信息之間的關(guān)聯(lián)關(guān)系wsserver_t *_server; // 服務(wù)器指針對(duì)象,用于設(shè)置定時(shí)任務(wù)
};
#endif
6. 匹配對(duì)戰(zhàn)管理模塊
匹配對(duì)戰(zhàn)管理模塊主要負(fù)責(zé)游戲大廳內(nèi)玩家開始匹配與取消匹配的功能,本模塊將玩家按照天梯分?jǐn)?shù)分為三個(gè)段位 (玩家的初始天梯分?jǐn)?shù)為1000分):
- 青銅:天梯分?jǐn)?shù)小于2000分。
- 黃金:天梯分?jǐn)?shù)大于等于2000分但小于3000分。
- 王者:天梯分?jǐn)?shù)大于等于3000分。
本模塊的設(shè)計(jì)思想是為不同段位的玩家分別設(shè)計(jì)一個(gè)匹配阻塞隊(duì)列:
- 當(dāng)有玩家開始匹配時(shí),服務(wù)器會(huì)將該玩家加入對(duì)應(yīng)的匹配隊(duì)列中,并喚醒該匹配隊(duì)列的線程。
- 當(dāng)有玩家取消匹配時(shí),會(huì)將該玩家從對(duì)應(yīng)的匹配隊(duì)列中移除.
- 當(dāng)某個(gè)匹配隊(duì)列中的玩家人數(shù)不足兩個(gè)時(shí),服務(wù)器會(huì)將該匹配隊(duì)列的線程阻塞,等待有新玩家加入匹配隊(duì)列時(shí)被喚醒。
- 當(dāng)某個(gè)匹配隊(duì)列中的玩家人數(shù)達(dá)到兩個(gè)時(shí),服務(wù)器會(huì)將隊(duì)頭的兩個(gè)玩家出隊(duì)列并給對(duì)應(yīng)的玩家推送匹配成功的信息,同時(shí)為匹配成功的玩家創(chuàng)建游戲房間。
最后,和游戲房間管理模塊一樣,這里我們也給出 WebSocket 通信的消息格式。
游戲匹配成功的消息:
{"optype": "match_success", //表?成匹配成功"result": true
}
matcher.hpp:
#ifndef __MATCHER_HPP__
#define __MATCHER_HPP__
#include "db.hpp"
#include "room.hpp"
#include "util.hpp"
#include "online.hpp"
#include <list>
#include <thread>
#include <mutex>
#include <condition_variable>/*用戶對(duì)戰(zhàn)匹配管理模塊 -- 將用戶按分?jǐn)?shù)分為青銅、黃金、王者三檔,并分別為它們?cè)O(shè)計(jì)一個(gè)匹配隊(duì)列,隊(duì)列元素>=2則匹配成功,否則阻塞*/
/*匹配隊(duì)列類*/
template <class T>
class match_queue {
public:match_queue() {}~match_queue() {}/*目標(biāo)元素入隊(duì)列,并喚醒線程*/void push(const T& data) {std::unique_lock<std::mutex> lock(_mutex);_list.push_back(data);LOG(DEBUG, "%d用戶加入匹配隊(duì)列", data);// 匹配隊(duì)列每新增一個(gè)元素,就喚醒對(duì)應(yīng)的匹配線程,判斷是否滿足匹配要求(隊(duì)列人數(shù)>=2)_cond.notify_all();}/*隊(duì)頭元素出隊(duì)列并返回隊(duì)頭元素*/bool pop(T& data) {std::unique_lock<std::mutex> lock(_mutex);if(_list.empty()) return false;data = _list.front();_list.pop_front();LOG(DEBUG, "%d用戶從匹配隊(duì)列中移除", data);return true;}/*移除隊(duì)列中的目標(biāo)元素*/void remove(const T& data) {std::unique_lock<std::mutex> lock(_mutex);_list.remove(data);LOG(DEBUG, "%d用戶從匹配隊(duì)列中移除", data);}/*阻塞線程*/void wait() {std::unique_lock<std::mutex> lock(_mutex);_cond.wait(lock);}/*獲取隊(duì)列元素個(gè)數(shù)*/int size() { std::unique_lock<std::mutex> lock(_mutex);return _list.size(); }/*判斷隊(duì)列是否為空*/bool empty() {std::unique_lock<std::mutex> lock(_mutex); return _list.empty();}
private:std::list<T> _list; // 使用雙向鏈表而不是queue充當(dāng)匹配隊(duì)列,便于用戶取消匹配時(shí)將該用戶從匹配隊(duì)列中移除std::mutex _mutex; // 實(shí)現(xiàn)線程安全std::condition_variable _cond; // 條件變量,當(dāng)向隊(duì)列中push元素時(shí)喚醒,用于阻塞消費(fèi)者
};/*匹配管理類*/
class matcher {
private:void handler_match(match_queue<uint64_t> &mq) {while(true) {// 檢查匹配條件是否滿足(人數(shù)>=2),不滿足則繼續(xù)阻塞while(mq.size() < 2) mq.wait();// 條件滿足,從隊(duì)列中取出兩個(gè)玩家uint64_t uid1, uid2;if(mq.pop(uid1) == false) continue;if(mq.pop(uid2) == false) {// 如果第二個(gè)玩家出隊(duì)列失敗,則需要將第一個(gè)玩家重新添加到隊(duì)列中this->add(uid1);continue;}// 檢查兩個(gè)玩家是否都處于大廳在線狀態(tài),若一方掉線,則需要將另一方重新添加到隊(duì)列wsserver_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);wsserver_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);if(conn1.get() == nullptr) {this->add(uid2);continue;}if(conn2.get() == nullptr) {this->add(uid1);continue;}// 為兩個(gè)玩家創(chuàng)建房間,失敗則重新添加到隊(duì)列room_ptr rp = _rm->create_room(uid1, uid2);if(rp.get() == nullptr) {this->add(uid1);this->add(uid2);continue;}// 給玩家返回匹配成功的響應(yīng)Json::Value resp;resp["optype"] = "match_success";resp["result"] = true;std::string body;json_util::serialize(resp, body);conn1->send(body);conn2->send(body);}}/*三個(gè)匹配隊(duì)列的線程入口*/void th_low_entry() { handler_match(_q_low); }void th_mid_entry() { handler_match(_q_mid); }void th_high_entry() { handler_match(_q_high); }
public:matcher(user_table *ut, online_manager *om, room_manager *rm): _ut(ut), _om(om), _rm(rm), _th_low(std::thread(&matcher::th_low_entry, this)),_th_mid(std::thread(&matcher::th_mid_entry, this)),_th_high(std::thread(&matcher::th_high_entry, this)) {LOG(DEBUG, "游戲?qū)?zhàn)匹配管理模塊初始化完畢");}~matcher() {LOG(DEBUG, "游戲?qū)?zhàn)匹配管理模塊已被銷毀");}/*添加用戶到匹配隊(duì)列*/bool add(uint64_t uid) {// 根據(jù)用戶id獲取用戶數(shù)據(jù)庫信息Json::Value user;if(_ut->select_by_id(uid, user) == false) {LOG(DEBUG, "查找玩家%d信息失敗", uid);return false;}// 根據(jù)用戶分?jǐn)?shù)將用戶添加到對(duì)應(yīng)的匹配隊(duì)列中去int score = user["score"].asInt();if(score < 2000) _q_low.push(uid);else if(score >= 2000 && score < 3000) _q_mid.push(uid);else _q_high.push(uid);return true;}/*將用戶從匹配隊(duì)列中移除*/bool remove(uint64_t uid) {// 根據(jù)用戶id獲取用戶數(shù)據(jù)庫信息Json::Value user;if(_ut->select_by_id(uid, user) == false) {LOG(DEBUG, "查找用戶%d信息失敗", uid);return false;}// 根據(jù)用戶分?jǐn)?shù)將用戶從對(duì)應(yīng)的匹配隊(duì)列中移除int score = user["score"].asInt();if(score < 2000) _q_low.remove(uid);else if(score >= 2000 && score < 3000) _q_mid.remove(uid);else _q_high.remove(uid);return true; }
private:// 三個(gè)匹配隊(duì)列(青銅/黃金/王者 -> low/mid/high)match_queue<uint64_t> _q_low;match_queue<uint64_t> _q_mid;match_queue<uint64_t> _q_high;// 三個(gè)管理匹配隊(duì)列的線程std::thread _th_low;std::thread _th_mid;std::thread _th_high;room_manager *_rm; // 游戲房間管理句柄online_manager *_om; // 在線用戶管理句柄user_table *_ut; // 用戶數(shù)據(jù)管理句柄
};
#endif
7. 整合封裝服務(wù)器模塊
服務(wù)器模塊是對(duì)當(dāng)前所實(shí)現(xiàn)的所有模塊進(jìn)行整合,并進(jìn)行服務(wù)器搭建的?個(gè)模塊。目的是封裝實(shí)現(xiàn)出?個(gè) gobang_server 的服務(wù)器模塊類,向外提供搭建五子棋對(duì)戰(zhàn)服務(wù)器的接口。程序員通過實(shí)例化服務(wù)器模塊類對(duì)象可以簡(jiǎn)便的完成服務(wù)器的搭建。
7.1 網(wǎng)絡(luò)通信接口設(shè)計(jì)
在實(shí)現(xiàn)具體的服務(wù)器類之前,我們需要對(duì) HTTP 網(wǎng)絡(luò)通信的通信接口格式進(jìn)行設(shè)計(jì),確保服務(wù)器能夠根據(jù)客戶端請(qǐng)求的格式判斷出這是一個(gè)什么類型請(qǐng)求,并在完成業(yè)務(wù)處理后給客戶端以特定格式的響應(yīng)。
本項(xiàng)目采用 RESTful 風(fēng)格通信接口:
- 資源定位:每個(gè)資源都有一個(gè)唯一的 URI 標(biāo)識(shí)符,比如 /login.html 表示獲取登錄頁面,/hall 表示進(jìn)入游戲大廳請(qǐng)求。
- 使用 HTTP 方法:使用 HTTP 的 GET 和 POST 方法來對(duì)資源進(jìn)行獲取與提交操作。
- 無狀態(tài)性:客戶端狀態(tài)信息由客戶端保存 (cookie&session),服務(wù)器不保存,客戶端的每個(gè)請(qǐng)求都是獨(dú)立的。
- 統(tǒng)一接口:使用統(tǒng)一的接口約束,包括使用標(biāo)準(zhǔn)的 HTTP 方法和狀態(tài)碼,使用標(biāo)準(zhǔn)的媒體類型 JSON 來傳輸數(shù)據(jù)。
本項(xiàng)目中客戶端的 HTTP 請(qǐng)求分為靜態(tài)資源請(qǐng)求與動(dòng)態(tài)功能請(qǐng)求,靜態(tài)資源請(qǐng)求指獲取游戲注冊(cè)頁面、登錄頁面等,動(dòng)態(tài)功能請(qǐng)求指用戶登錄/注冊(cè)請(qǐng)求、協(xié)議切換請(qǐng)求等。
7.1.1 靜態(tài)資源請(qǐng)求
靜態(tài)資源頁面,在后臺(tái)服務(wù)器上就是一個(gè)個(gè) HTML/CSS/JS 文件;而靜態(tài)資源請(qǐng)求,其實(shí)就是讓服務(wù)器把對(duì)應(yīng)的文件發(fā)送給客戶端。
獲取注冊(cè)界面:
// 客戶端請(qǐng)求
GET /register.html HTTP/1.1
報(bào)頭其他字段// 服務(wù)器響應(yīng)
// 響應(yīng)報(bào)頭
HTTP/1.1 200 OK
Content-Length: XXX
Content-Type: text/html
報(bào)頭其他字段
// 響應(yīng)正文
register.html文件中的數(shù)據(jù)
獲取登錄界面、游戲大廳頁面與游戲房間頁面類似:
// 客戶端請(qǐng)求
GET /login.html HTTP/1.1 or GET /game_hall.html HTTP/1.1 or GET /game_room.html HTTP/1.1
報(bào)頭其他字段// 服務(wù)器響應(yīng)
// 響應(yīng)報(bào)頭
HTTP/1.1 200 OK
Content-Length: XXX
Content-Type: text/html
報(bào)頭其他字段
// 響應(yīng)正文
login.html/game_hall/game_room文件中的數(shù)據(jù)
7.1.2 動(dòng)態(tài)功能請(qǐng)求
用戶注冊(cè)請(qǐng)求:
// 客戶端請(qǐng)求
// 請(qǐng)求報(bào)頭
POST /reg HTTP/1.1
Content-Type: application/json
Content-Length: XXX
// 請(qǐng)求正文 -- 序列化的用戶名和用戶密碼
{"username":"zhangsan", "password":"123456"}// 服務(wù)器成功的響應(yīng)
// 響應(yīng)報(bào)頭
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15
// 響應(yīng)正文
{"result":true, "reason": "用戶注冊(cè)成功"}// 服務(wù)器失敗的響應(yīng)
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: XXX
{"result":false, "reason": "錯(cuò)誤信息,比如該用戶名已被占用"}
用戶登錄請(qǐng)求:
// 客戶端請(qǐng)求
POST /login HTTP/1.1
Content-Type: application/json
Content-Length: XXX
{"username":"zhangsan", "password":"123456"}// 服務(wù)器成功的響應(yīng)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: XXX
{"result":true, "reason": "用戶登錄成功"}// 服務(wù)器失敗的響應(yīng)
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: XXX
{"result":false, "reason": "錯(cuò)誤信息,比如用戶名或密碼錯(cuò)誤"}
獲取玩家詳細(xì)信息請(qǐng)求:
// 客戶端請(qǐng)求
GET /info HTTP/1.1
Content-Type: application/json
Content-Length: 0// 服務(wù)器成功的響應(yīng)
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: XXX
{"id":1, "username":"zhangsan", "score":1000, "total_count":4, "win_count":2}// 服務(wù)器失敗的響應(yīng)
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: XXX
{"result":false, "reason": "錯(cuò)誤信息,比如用戶信息不存在"}
游戲大廳 WebSocket 長(zhǎng)連接協(xié)議切換請(qǐng)求
// 客戶端請(qǐng)求
/* ws://localhost:9000/match */
GET /hall HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......// 服務(wù)器成功的響應(yīng)
HTTP/1.1 101 Switching
...
游戲房間 WebSocket 長(zhǎng)連接協(xié)議切換請(qǐng)求
// 客戶端請(qǐng)求
/* ws://localhost:9000/match */
GET /room HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......// 服務(wù)器成功的響應(yīng)
HTTP/1.1 101 Switching
...
7.1.3 WebSocket 通信格式
上面我們提到的不管是靜態(tài)資源請(qǐng)求,還是動(dòng)態(tài)功能請(qǐng)求,它們本質(zhì)上都是 HTTP 請(qǐng)求,所以我們使用 RESTful 風(fēng)格的通信接口;但是當(dāng)玩家進(jìn)入游戲大廳或者游戲房間后,客戶端就會(huì)向服務(wù)器發(fā)送協(xié)議切換請(qǐng)求 (協(xié)議切換請(qǐng)求本身是 HTTP 請(qǐng)求),將 HTTP 短連接通信協(xié)議升級(jí)為 WebSocket 長(zhǎng)連接通信協(xié)議。
由于 WebSocket 協(xié)議是一種全雙工的持久連接協(xié)議,它允許在客戶端和服務(wù)器之間進(jìn)行雙向?qū)崟r(shí)通信,所以我們每次通信時(shí)直接使用 WebSocketpp::server 中的 send 接口向?qū)Ψ桨l(fā)送消息即可,而不再需要重新建立連接。
但是我們?nèi)匀恍枰孪纫?guī)定好發(fā)送消息中不同字段代表的含義,這樣才能正確區(qū)分收到的消息類型,從而根據(jù)消息不同的類型執(zhí)行不同的處理函數(shù)并返回不同的消息。
游戲大廳 WebSocket 握手成功后的回復(fù):
// 游戲大廳進(jìn)入成功
{"optype": "hall_ready", "result": true
}// 游戲大廳進(jìn)入失敗
{"optype": "hall_ready", "result": false,"reason": "失敗原因"
}
玩家開始匹配消息:
// 開始匹配消息
{"optype": "match_start"
}// 后臺(tái)正確處理后回復(fù)的消息
{"optype": "match_start""result": true,
}
玩家停止匹配消息:
// 停止匹配消息
{"optype": "match_stop"
}// 后臺(tái)正確處理后回復(fù)的消息
{"optype": "match_stop""result": true
}
游戲匹配成功后后臺(tái)回復(fù)的消息:
{"optype": "match_success", "result": true
}
游戲房間 WebSocket 握手成功后的回復(fù):
// 游戲房間創(chuàng)建成功
{"optype": "room_ready","result": true,"room_id": 222, //房間ID"uid": 1, //??ID"white_id": 1, //?棋ID"black_id": 2, //?棋ID
}// 游戲房間創(chuàng)建失敗
{"optype": "room_ready","result": false,"reason": "失敗原因"
}
玩家下棋的消息:
// 玩家下棋消息
{"optype": "put_chess", // put_chess表示當(dāng)前請(qǐng)求是下棋操作"room_id": 222, // room_id 表?當(dāng)前動(dòng)作屬于哪個(gè)房間"uid": 1, // 當(dāng)前的下棋操作是哪個(gè)用戶發(fā)起的"row": 3, // 當(dāng)前下棋位置的?號(hào)"col": 2 // 當(dāng)前下棋位置的列號(hào)
}// 下棋成功后后臺(tái)回復(fù)的消息
{"optype": "put_chess","result": true,"reason": "下棋成功或游戲勝利或游戲失敗","room_id": 222,"uid": 1,"row": 3,"col": 2,"winner": 0 // 游戲獲勝者,0表示未分勝負(fù),!0表示已分勝負(fù)
}// 下棋失敗后后臺(tái)回復(fù)的消息
{"optype": "put_chess","result": false,"reason": "下棋失敗的原因","room_id": 222, "uid": 1,"row": 3,"col": 2,"winner": 0
}
玩家聊天的消息:
// 玩家聊天消息
{"optype": "chat", // chat表示當(dāng)前請(qǐng)求是下棋操作"room_id": 222, // room_id 表?當(dāng)前動(dòng)作屬于哪個(gè)房間"uid": 1, // 當(dāng)前的下棋操作是哪個(gè)用戶發(fā)起的"message": "你好" // 聊天消息的具體內(nèi)容
}// 聊天消息發(fā)送成功后臺(tái)回復(fù)的消息
{"optype": "chat","result": true,"room_id": 222,"uid": 1,"message": "你好"
}// 聊天消息發(fā)送失敗后臺(tái)回復(fù)的消息
{"optype": "chat","result": false,"reason": "錯(cuò)誤原因,比如消息中包含敏感詞","room_id": 222,"uid": 1,"message": "你好"
}
未知類型的消息:
{"optype": 消息的類型,"result": false,"reason": "未知類型的消息"
}
7.2 服務(wù)器模塊實(shí)現(xiàn)
關(guān)于如何使用 WebSocketpp 來搭建一個(gè)服務(wù)器,我們?cè)谏厦媲爸弥R(shí)了解那里已經(jīng)說過了,大體流程如下:
- 實(shí)例化一個(gè) websocketpp::server 對(duì)象。
- 設(shè)置日志等級(jí)。(本項(xiàng)目中我們使用自己封裝的日志函數(shù),所以這里設(shè)置日志等級(jí)為 none)
- 初始化 asio 調(diào)度器。
- 設(shè)置處理 http 請(qǐng)求、websocket 握手成功、websocket 連接關(guān)閉以及收到 websocket 消息的回調(diào)函數(shù)。
- 設(shè)置監(jiān)聽端口。
- 開始獲取 tcp 連接。
- 啟動(dòng)服務(wù)器。
class gobang_server {
public:/*成員初始化與服務(wù)器回調(diào)函數(shù)設(shè)置*/gobang_server(const std::string &host, const std::string &user, const std::string &passwd, \const std::string db = "gobang", uint16_t port = 4106): _wwwroot(WWWROOT), _ut(host, user, passwd, db, port), _sm(&_wssrv), _rm(&_ut, &_om), _mm(&_ut, &_om, &_rm) {// 設(shè)置日志等級(jí)_wssrv.set_access_channels(websocketpp::log::alevel::none);// 初始化asio調(diào)度器_wssrv.init_asio();// 設(shè)置回調(diào)函數(shù)_wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1));_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1));_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));}/*啟動(dòng)服務(wù)器*/void start(uint16_t port) {// 設(shè)置監(jiān)聽端口_wssrv.listen(port);_wssrv.set_reuse_addr(true);// 開始獲取新連接_wssrv.start_accept();// 啟動(dòng)服務(wù)器_wssrv.run(); }
private:std::string _wwwroot; // 靜態(tài)資源根目錄user_table _ut; // 用戶數(shù)據(jù)管理模塊句柄session_manager _sm; // 用戶session信息管理模塊句柄online_manager _om; // 用戶在線信息管理模塊句柄room_manager _rm; // 游戲房間管理模塊句柄matcher _mm; // 用戶對(duì)戰(zhàn)匹配管理模塊句柄wsserver_t _wssrv; // websocketpp::server 句柄
};
我們的重難點(diǎn)在于如何實(shí)現(xiàn) http 請(qǐng)求、websocket 握手成功、websocket 連接關(guān)閉以及 websocket 消息這四個(gè)回調(diào)函數(shù)。具體實(shí)現(xiàn)如下:
/*
服務(wù)器模塊
通過對(duì)之前所有模塊進(jìn)行整合以及進(jìn)行服務(wù)器搭建,最終封裝實(shí)現(xiàn)出?個(gè)gobang_server的服務(wù)器模塊類,向外提供搭建五?棋對(duì)戰(zhàn)服務(wù)器的接?。
達(dá)到通過實(shí)例化的對(duì)象就可以簡(jiǎn)便的完成服務(wù)器搭建的目的
*/#ifndef __SERVER_HPP__
#define __SERVER_HPP__
#include "util.hpp"
#include "db.hpp"
#include "online.hpp"
#include "room.hpp"
#include "matcher.hpp"
#include "session.hpp"#define WWWROOT "./wwwroot"typedef websocketpp::server<websocketpp::config::asio> wsserver_t;class gobang_server {
private:/*http靜態(tài)資源請(qǐng)求處理函數(shù)(注冊(cè)界面、登錄界面、游戲大廳界面)*/void file_handler(wsserver_t::connection_ptr conn) {// 獲取http請(qǐng)求對(duì)象與請(qǐng)求uriwebsocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 根據(jù)uri組合出文件路徑,如果文件路徑是目錄(/結(jié)尾)則追加login.html,否則返回相應(yīng)界面std::string pathname = _wwwroot + uri;if(pathname.back() == '/') {pathname += "login.html";}// 讀取文件內(nèi)容,如果文件不存在,則返回404std::string body;if(file_util::read(pathname.c_str(), body) == false) {body += "<html><head><meta charset='UTF-8'/></head><body><h1> 404 Not Found </h1></body></html>";// 設(shè)置響應(yīng)狀態(tài)碼conn->set_status(websocketpp::http::status_code::not_found);}else conn->set_status(websocketpp::http::status_code::ok);// 添加響應(yīng)頭部conn->append_header("Content-Length", std::to_string(body.size()));// 設(shè)置響應(yīng)正文conn->set_body(body); }/*處理http響應(yīng)的子功能函數(shù)*/void http_resp(wsserver_t::connection_ptr conn, bool result, websocketpp::http::status_code::value code, const std::string &reason) {// 設(shè)置響應(yīng)正文及其序列化Json::Value resp;std::string resp_body;resp["result"] = result;resp["reason"] = reason;json_util::serialize(resp, resp_body);// 設(shè)置響應(yīng)狀態(tài)碼,添加響應(yīng)正文以及正文類型conn->set_status(code);conn->append_header("Content-Type", "application/json");conn->set_body(resp_body);}/*http動(dòng)態(tài)功能請(qǐng)求處理函數(shù) -- 用戶注冊(cè)*/void reg(wsserver_t::connection_ptr conn) {// 獲取json格式的請(qǐng)求正文std::string req_body = conn->get_request_body();// 將正文反序列化得到username和passwordJson::Value user_info;if(json_util::deserialize(req_body, user_info) == false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "請(qǐng)求正文格式錯(cuò)誤");}// 數(shù)據(jù)庫新增用戶if(user_info["username"].isNull() || user_info["password"].isNull()) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "請(qǐng)輸入用戶名/密碼");}if(_ut.registers(user_info) == false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "該用戶名已被占用");}return http_resp(conn, true, websocketpp::http::status_code::ok, "用戶注冊(cè)成功");}/*http動(dòng)態(tài)功能請(qǐng)求處理函數(shù) -- 用戶登錄*/void login(wsserver_t::connection_ptr conn) {// 獲取請(qǐng)求正文并反序列化std::string req_body = conn->get_request_body();Json::Value user_info;if(json_util::deserialize(req_body, user_info) == false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "請(qǐng)求正文格式錯(cuò)誤");}if(user_info["username"].isNull() || user_info["password"].isNull()) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "請(qǐng)輸入用戶名/密碼");}// 用戶登錄 -- 登錄失敗返回404if(_ut.login(user_info) == false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用戶名/密碼錯(cuò)誤");}// 登錄成功則為用戶創(chuàng)建session信息以及session生命周期session_ptr ssp = _sm.create_session(user_info["id"].asUInt64());if(ssp.get() == nullptr) {return http_resp(conn, false, websocketpp::http::status_code::internal_server_error, "用戶會(huì)話創(chuàng)建失敗");}_sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT);// 設(shè)置過響應(yīng)頭部 將cookie返回給客戶端std::string cookie_ssid = "SSID=" + std::to_string(ssp->get_ssid());conn->append_header("Set-Cookie", cookie_ssid);return http_resp(conn, true, websocketpp::http::status_code::ok, "用戶登錄成功");}/*從http請(qǐng)求頭部Cookie中獲取指定key對(duì)應(yīng)的value*/bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val) {// cookie_str格式:SSID=XXX; path=/XXX// 先以逗號(hào)為分割將cookie_str中的各個(gè)cookie信息分割開std::vector<std::string> cookies;string_util::split(cookie_str, ";", cookies);// 再以等號(hào)為分割將單個(gè)cookie中的key與val分割開,比對(duì)查找目標(biāo)key對(duì)應(yīng)的valfor(const auto cookie : cookies) {std::vector<std::string> kv;string_util::split(cookie, "=", kv);if(kv.size() != 2) continue;if(kv[0] == key) {val = kv[1];return true;}}return false;}/*http動(dòng)態(tài)功能請(qǐng)求處理函數(shù) -- 獲取用戶信息*/void info(wsserver_t::connection_ptr conn) {// 通過http請(qǐng)求頭部中的cookie字段獲取用戶ssidstd::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "找不到Cookie信息,請(qǐng)重新登錄");}std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "找不到Session信息,請(qǐng)重新登錄");}// 根據(jù)ssid_str獲取用戶Session信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "Session已過期,請(qǐng)重新登錄");}// 通過用戶session獲取用戶id,再根據(jù)用戶id獲取用戶詳細(xì)信息uint64_t uid = ssp->get_user(); Json::Value user;if(_ut.select_by_id(uid, user) == false) {return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用戶信息不存在");}// 返回用戶詳細(xì)信息std::string body;json_util::serialize(user, body);std::string resp_cookie = "SSID=" + ssid_str;conn->set_status(websocketpp::http::status_code::ok);conn->append_header("Content-Type", "application/json");conn->append_header("Set-Cookie", resp_cookie);conn->set_body(body);// 更新用戶session過期時(shí)間_sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT); }
private:/*************************************************************************************************//*http請(qǐng)求回調(diào)函數(shù)*//*************************************************************************************************/void http_callback(websocketpp::connection_hdl hdl) {wsserver_t::connection_ptr conn = _wssrv.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();// 根據(jù)不同的請(qǐng)求方法和請(qǐng)求路徑類型調(diào)用不同的處理函數(shù)// 動(dòng)態(tài)功能請(qǐng)求if(method == "POST" && uri == "/reg") reg(conn);else if(method == "POST" && uri == "/login") login(conn);else if(method == "GET" && uri == "/info") info(conn);// 靜態(tài)資源請(qǐng)求else file_handler(conn);}/*游戲大廳websocket長(zhǎng)連接建立后的響應(yīng)子函數(shù)*/void game_hall_resp(wsserver_t::connection_ptr conn, bool result, const std::string &reason = "") {Json::Value resp;resp["optype"] = "hall_ready";resp["result"] = result;// 只有錯(cuò)誤才返回錯(cuò)誤信息reasonif(result == false) resp["reason"] = reason;std::string body;json_util::serialize(resp, body);conn->send(body);}/*wsopen_callback子函數(shù) -- 游戲大廳websocket長(zhǎng)連接建立后的處理函數(shù)*/void wsopen_game_hall(wsserver_t::connection_ptr conn) {// 檢查用戶是否登錄 -- 檢查cookie&session信息// 通過http請(qǐng)求頭部中的cookie字段獲取用戶ssidstd::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) {return game_hall_resp(conn, false, "找不到Cookie信息,請(qǐng)重新登錄");}std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) {return game_hall_resp(conn, false, "找不到Session信息,請(qǐng)重新登錄");}// 根據(jù)ssid_str獲取用戶Session信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) {return game_hall_resp(conn, false, "Session已過期,請(qǐng)重新登錄");}// 通過用戶session獲取用戶iduint64_t uid = ssp->get_user();// 檢查用戶是否重復(fù)登錄 -- 用戶游戲大廳長(zhǎng)連接/游戲房間長(zhǎng)連接是否已經(jīng)存在if(_om.is_in_game_hall(uid) == true) {return game_hall_resp(conn, false, "玩家重復(fù)登錄");} // 將玩家及其連接加入到在線游戲大廳中_om.enter_game_hall(uid, conn);// 返回響應(yīng)game_hall_resp(conn, true);// 將用戶Session過期時(shí)間設(shè)置為永不過期_sm.set_session_expire_time(ssp->get_ssid(), SESSION_FOREVER);}/*游戲房間websocket長(zhǎng)連接建立后的響應(yīng)子函數(shù)*/void game_room_resp(wsserver_t::connection_ptr conn, bool result, const std::string &reason, uint64_t room_id = 0, uint64_t self_id = 0, uint64_t white_id = 0, uint64_t black_id = 0) {Json::Value resp;resp["optype"] = "room_ready";resp["result"] = result;// 如果成功返回room_id,self_id,white_id,black_id等信息,如果錯(cuò)誤則返回錯(cuò)誤信息if(result == true) {resp["room_id"] = (Json::UInt64)room_id;resp["uid"] = (Json::UInt64)self_id;resp["white_id"] = (Json::UInt64)white_id;resp["black_id"] = (Json::UInt64)black_id;}else resp["reason"] = reason; std::string body;json_util::serialize(resp, body);conn->send(body);} /*wsopen_callback子函數(shù) -- 游戲房間websocket長(zhǎng)連接建立后的處理函數(shù)*/void wsopen_game_room(wsserver_t::connection_ptr conn) {// 獲取cookie&session信息std::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) {return game_room_resp(conn, false, "找不到Cookie信息,請(qǐng)重新登錄");}std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) {return game_room_resp(conn, false, "找不到Session信息,請(qǐng)重新登錄");}// 根據(jù)ssid_str獲取用戶Session信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) {return game_room_resp(conn, false, "Session已過期,請(qǐng)重新登錄");} // 判斷用戶是否已經(jīng)處于游戲大廳/房間中了(在創(chuàng)建游戲房間長(zhǎng)連接之前,游戲大廳的長(zhǎng)連接已經(jīng)斷開了) -- 在線用戶管理if(_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())) {return game_room_resp(conn, false, "玩家重復(fù)登錄");} // 判斷游戲房間是否被創(chuàng)建 -- 游戲房間管理room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if(rp.get() == nullptr) {return game_room_resp(conn, false, "找不到房間信息");}// 將玩家加入到在線游戲房間中_om.enter_game_room(ssp->get_user(), conn);// 返回響應(yīng)信息game_room_resp(conn, true, "", rp->_room_id, ssp->get_user(), rp->_white_user_id, rp->_black_user_id);// 將玩家session設(shè)置為永不過期_sm.set_session_expire_time(ssp->get_ssid(), SESSION_FOREVER);}/*************************************************************************************************//*websocket長(zhǎng)連接建立之后的處理函數(shù)*//*************************************************************************************************/void wsopen_callback(websocketpp::connection_hdl hdl) {// 獲取通信連接、http請(qǐng)求對(duì)象和請(qǐng)求uriwsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 進(jìn)入游戲大廳與進(jìn)入游戲房間需要分別建立websocket長(zhǎng)連接if(uri == "/hall") wsopen_game_hall(conn);else if(uri == "/room") wsopen_game_room(conn);}/*wsclose_callback子函數(shù) -- 游戲大廳websocket長(zhǎng)連接斷開后的處理函數(shù)*/void wsclose_game_hall(wsserver_t::connection_ptr conn) {// 獲取cookie&session,如果不存在則說明websocket長(zhǎng)連接未建立(websocket長(zhǎng)連接建立后Session永久存在),直接返回std::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) return;std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) return;session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) return;// 將玩家從游戲大廳移除_om.exit_game_hall(ssp->get_user());// 將玩家session設(shè)置為定時(shí)刪除_sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT); } /*wsclose_callback子函數(shù) -- 游戲房間websocket長(zhǎng)連接斷開后的處理函數(shù)*/void wsclose_game_room(wsserver_t::connection_ptr conn) {// 獲取cookie&session,如果不存在直接返回std::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) return;std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) return;session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) return;// 將玩家從在線用戶管理的游戲房間中移除_om.exit_game_room(ssp->get_user());// 將玩家從游戲房間管理的房間中移除_rm.remove_room_user(ssp->get_user());// 設(shè)置玩家session為定時(shí)刪除_sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT); }/*************************************************************************************************//*websocket長(zhǎng)連接斷開之間的處理函數(shù)*//*************************************************************************************************/void wsclose_callback(websocketpp::connection_hdl hdl) {// 獲取通信連接、http請(qǐng)求對(duì)象和請(qǐng)求uriwsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 離開游戲大廳與離開游戲房間需要分別斷開websocket長(zhǎng)連接if(uri == "/hall") wsclose_game_hall(conn);else if(uri == "/room") wsclose_game_room(conn); }/*wsmsg_callback子函數(shù) -- 游戲大廳通信處理函數(shù)*/void wsmsg_game_hall(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {// 獲取cookie&session,如果不存在則返回錯(cuò)誤信息std::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) {return game_hall_resp(conn, false, "找不到Cookie信息,請(qǐng)重新登錄");}std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) {return game_hall_resp(conn, false, "找不到Session信息,請(qǐng)重新登錄");}session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) {return game_hall_resp(conn, false, "Session已過期,請(qǐng)重新登錄");}// 獲取請(qǐng)求信息 std::string req_msg_body = msg->get_payload(); Json::Value req_msg;if(json_util::deserialize(req_msg_body, req_msg) == false) {return game_hall_resp(conn, false, "請(qǐng)求信息解析失敗"); }// 處理請(qǐng)求信息 -- 開始對(duì)戰(zhàn)匹配與停止對(duì)戰(zhàn)匹配Json::Value resp = req_msg;std::string resp_body;// 開始對(duì)戰(zhàn)匹配請(qǐng)求則將用戶加入到匹配隊(duì)列中,取消對(duì)戰(zhàn)匹配請(qǐng)求則將用戶從匹配隊(duì)列中移除if(req_msg["optype"].isNull() == false && req_msg["optype"].asString() == "match_start") {_mm.add(ssp->get_user());resp["result"] = true;json_util::serialize(resp, resp_body);conn->send(resp_body);} else if(req_msg["optype"].isNull() == false && req_msg["optype"].asString() == "match_stop") {_mm.remove(ssp->get_user());resp["result"] = true;json_util::serialize(resp, resp_body);conn->send(resp_body);} else {resp["optype"] = req_msg["optype"].asString();resp["result"] = false;resp["reason"] = "未知類型的消息";json_util::serialize(resp, resp_body);conn->send(resp_body);} } /*wsmsg_callback子函數(shù) -- 游戲房間通信處理函數(shù)*/void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {// 獲取cookie&session,如果不存在則返回錯(cuò)誤信息std::string cookie_str = conn->get_request_header("Cookie");if(cookie_str.empty()) {return game_room_resp(conn, false, "找不到Cookie信息,請(qǐng)重新登錄");}std::string ssid_str;if(get_cookie_val(cookie_str, "SSID", ssid_str) == false) {return game_room_resp(conn, false, "找不到Session信息,請(qǐng)重新登錄");}session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if(ssp.get() == nullptr) {return game_room_resp(conn, false, "Session已過期,請(qǐng)重新登錄");}// 獲取房間信息room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if(rp.get() == nullptr) {return game_room_resp(conn, false, "找不到房間信息");}// 獲取請(qǐng)求信息 std::string req_msg_body = msg->get_payload(); Json::Value req_msg;if(json_util::deserialize(req_msg_body, req_msg) == false) {return game_room_resp(conn, false, "請(qǐng)求信息解析失敗"); }// 處理請(qǐng)求信息 -- 下棋動(dòng)作與聊天動(dòng)作rp->handler(req_msg);} /*************************************************************************************************//*websocket長(zhǎng)連接建立后通信的處理函數(shù)*//*************************************************************************************************/void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {// 獲取通信連接、http請(qǐng)求對(duì)象和請(qǐng)求uriwsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();// 游戲大廳通信處理與游戲房間通信處理if(uri == "/hall") wsmsg_game_hall(conn, msg);else if(uri == "/room") wsmsg_game_room(conn, msg); }
public:/*成員初始化與服務(wù)器回調(diào)函數(shù)設(shè)置*/gobang_server(const std::string &host, const std::string &user, const std::string &passwd, \const std::string db = "gobang", uint16_t port = 4106): _wwwroot(WWWROOT), _ut(host, user, passwd, db, port), _sm(&_wssrv), _rm(&_ut, &_om), _mm(&_ut, &_om, &_rm) {// 設(shè)置日志等級(jí)_wssrv.set_access_channels(websocketpp::log::alevel::none);// 初始化asio調(diào)度器_wssrv.init_asio();// 設(shè)置回調(diào)函數(shù)_wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1));_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1));_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));}/*啟動(dòng)服務(wù)器*/void start(uint16_t port) {// 設(shè)置監(jiān)聽端口_wssrv.listen(port);_wssrv.set_reuse_addr(true);// 開始獲取新連接_wssrv.start_accept();// 啟動(dòng)服務(wù)器_wssrv.run(); }
private:std::string _wwwroot; // 靜態(tài)資源根目錄user_table _ut; // 用戶數(shù)據(jù)管理模塊句柄session_manager _sm; // 用戶session信息管理模塊句柄online_manager _om; // 用戶在線信息管理模塊句柄room_manager _rm; // 游戲房間管理模塊句柄matcher _mm; // 用戶對(duì)戰(zhàn)匹配管理模塊句柄wsserver_t _wssrv; // websocketpp::server 句柄
};
#endif
8. 前端界面模塊
8.1 用戶注冊(cè)界面
register.html:
<!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>注冊(cè)</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/login.css">
</head>
<body><div class="nav">網(wǎng)絡(luò)五子棋對(duì)戰(zhàn)游戲 </div><div class="login-container"><!-- 登錄界面的對(duì)話框 --><div class="login-dialog"><!-- 提示信息 --><h3>注冊(cè)</h3><!-- 這個(gè)表示一行 --><div class="row"><span>用戶名</span><input type="text" id="user_name" name="username"></div><!-- 這是另一行 --><div class="row"><span>密碼</span><input type="password" id="password" name="password"></div><!-- 提交按鈕 --><div class="row"><!--給提交按鈕添加點(diǎn)擊事件 -- 調(diào)用注冊(cè)函數(shù)reg--><button id="submit" onclick="reg()">提交</button></div></div></div> <script src="js/jquery.min.js"></script><script>// 封裝實(shí)現(xiàn)注冊(cè)函數(shù)function reg() {// 獲取輸入框中的username和password,并將它們組織成json格式字符串var reg_info = {username: document.getElementById("user_name").value,password: document.getElementById("password").value};// 通過ajax向服務(wù)器發(fā)送注冊(cè)請(qǐng)求$.ajax({url: "/reg",type: "post",data: JSON.stringify(reg_info),// 請(qǐng)求失敗,清空輸入框中的內(nèi)容并提示錯(cuò)誤信息;請(qǐng)求成功,則返回用戶登錄頁面success: function(res) {if(res.result == false) {document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(res.reason);} else {alert(res.reason);window.location.assign("/login.html");}},error: function(xhr) {document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(JSON.stringify(xhr));}})}</script>
</body>
</html>
8.2 用戶登錄界面
login.html:
<!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>登錄</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/login.css">
</head>
<body><div class="nav">網(wǎng)絡(luò)五子棋對(duì)戰(zhàn)游戲</div><div class="login-container"><!-- 登錄界面的對(duì)話框 --><div class="login-dialog"><!-- 提示信息 --><h3>登錄</h3><!-- 這個(gè)表示一行 --><div class="row"><span>用戶名</span><input type="text" id="user_name"></div><!-- 這是另一行 --><div class="row"><span>密碼</span><input type="password" id="password"></div><!-- 提交按鈕 --><div class="row"><!--為按鈕添加點(diǎn)擊事件,調(diào)用登錄函數(shù)--><button id="submit" onclick="login()">提交</button></div></div></div><script src="./js/jquery.min.js"></script><script>function login() {// 獲取輸入框中的username和passwordvar log_info = {username: document.getElementById("user_name").value,password: document.getElementById("password").value};// 通過ajax向服務(wù)器發(fā)送登錄請(qǐng)求$.ajax({url: "/login",type: "post",data: JSON.stringify(log_info),// 請(qǐng)求成功返回游戲大廳頁面,請(qǐng)求失敗則清空輸入框中的內(nèi)容并提示錯(cuò)誤信息success: function(res) {alert("登錄成功");window.location.assign("/game_hall.html");},error: function(xhr) {document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(JSON.stringify(xhr));}})}</script>
</body>
</html>
8.3 游戲大廳界面
game_hall.html:
<!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>游戲大廳</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/game_hall.css">
</head>
<body><div class="nav">網(wǎng)絡(luò)五子棋對(duì)戰(zhàn)游戲</div><!-- 整個(gè)頁面的容器元素 --><div class="container"><!-- 這個(gè) div 在 container 中是處于垂直水平居中這樣的位置的 --><div><!-- 展示用戶信息 --><div id="screen"></div><!-- 匹配按鈕 --><div id="match-button">開始匹配</div></div></div><script src="./js/jquery.min.js"></script><script>ws_hdl = null;//設(shè)置離開當(dāng)前頁面后立即斷開websocket鏈接window.onbeforeunload = function () {ws_hdl.close();}// 獲取玩家信息展示在游戲大廳與websocket長(zhǎng)連接切換function get_user_info() {// 通過ajax向服務(wù)器發(fā)送獲取用戶信息請(qǐng)求$.ajax({url: "/info",type: "get",success: function(res) {var info_html = "<p>" + "姓名: " + res.username + " 積分:" + res.score + "</br>" + " 戰(zhàn)斗場(chǎng)次: " + res.total_count + " 勝利場(chǎng)次: " + res.win_count + "</p>";var screen_div = document.getElementById("screen");screen_div.innerHTML = info_html;// 獲取玩家信息成功之后將http短連接協(xié)議切換為websocket長(zhǎng)連接切換ws_url = "ws://" + location.host + "/hall";ws_hdl = new WebSocket(ws_url);// 為websocket各種觸發(fā)事件設(shè)置回調(diào)函數(shù)ws_hdl.onopen = ws_onopen;ws_hdl.onclose = ws_onclose;ws_hdl.onerror = ws_onerror;ws_hdl.onmessage = ws_onmessage;},// 獲取失敗則返回登錄頁面并提示錯(cuò)誤信息error: function(xhr) {alert(JSON.stringify(xhr));location.replace("/login.html");}})}// 匹配按鈕一共有兩種狀態(tài) -- 未開始匹配(unmatched)和匹配中(matching)var button_statu = "unmatched";// 為匹配按鈕添加點(diǎn)擊事件var button_ele = document.getElementById("match-button");button_ele.onclick = function() {// 在沒有匹配狀態(tài)下點(diǎn)擊按鈕,則發(fā)送開始匹配請(qǐng)求if(button_statu == "unmatched") {var req = { optype: "match_start" };ws_hdl.send(JSON.stringify(req));}// 在匹配狀態(tài)下點(diǎn)擊按鈕,則范式停止匹配請(qǐng)求else if(button_statu == "matching") {var req = { optype: "match_stop" };ws_hdl.send(JSON.stringify(req));}}function ws_onopen() {console.log("游戲大廳長(zhǎng)連接建立成功");}function ws_onclose() {console.log("游戲大廳長(zhǎng)連接斷開");}function ws_onerror() {console.log("游戲大廳長(zhǎng)連接建立出錯(cuò)");}// 服務(wù)器響應(yīng)處理函數(shù)function ws_onmessage(evt) {// 判斷請(qǐng)求是否被成功處理,如果處理失敗,則提示錯(cuò)誤信息并跳轉(zhuǎn)登錄頁面var resp = JSON.parse(evt.data);if(resp.result == false) {alert(evt.data)location.replace("/login.html");return;}// 根據(jù)不同的響應(yīng)類型進(jìn)行不同的操作(成功建立大廳長(zhǎng)連接、開始匹配、停止匹配、匹配成功以及未知響應(yīng)類型)if(resp.optype == "hall_ready") {} else if(resp.optype == "match_start") {console.log("玩家已成功加入匹配隊(duì)列");button_statu = "matching";button_ele.innerHTML = "匹配中... (點(diǎn)擊停止匹配)";} else if(resp.optype == "match_stop") {console.log("玩家已從匹配隊(duì)列中移除");button_statu = "unmatched";button_ele.innerHTML = "開始匹配";} else if(resp.optype == "match_success") {alert("匹配成功");location.replace("/game_room.html");}else {alert(evt.data);location.replace("/login.html");}}// 調(diào)用獲取玩家信息函數(shù)get_user_info();</script>
</body>
</html>
8.4 游戲房間界面
game_room.html:
<!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>游戲房間</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/game_room.css">
</head>
<body><div class="nav">網(wǎng)絡(luò)五子棋對(duì)戰(zhàn)游戲</div><div class="container"><div id="chess_area"><!-- 棋盤區(qū)域, 需要基于 canvas 進(jìn)行實(shí)現(xiàn) --><canvas id="chess" width="450px" height="450px"></canvas><!-- 顯示區(qū)域 --><div id="screen"> 等待玩家連接中... </div></div><div id="chat_area" width="400px" height="300px"><div id="chat_show"><p id="self_msg">你好!</p></br><p id="peer_msg">你好!</p></br></div><div id="msg_show"><input type="text" id="chat_input"><button id="chat_button">發(fā)送</button></div></div></div><script>let chessBoard = [];let BOARD_ROW_AND_COL = 15;let chess = document.getElementById('chess');//獲取chess控件區(qū)域2d畫布let context = chess.getContext('2d');// 將http協(xié)議切換為游戲房間的websocket長(zhǎng)連接協(xié)議var ws_url = "ws://" + location.host + "/room";var ws_hdl = new WebSocket(ws_url);// 設(shè)置離開當(dāng)前頁面立即斷開websocket連接window.onbeforeunload = function () {ws_hdl.close();}// 保存房間信息與是否輪到己方走棋var room_info;var is_me;function initGame() {initBoard();// 背景圖片let logo = new Image();logo.src = "image/sky.jpeg";logo.onload = function () {// 繪制圖片context.drawImage(logo, 0, 0, 450, 450);// 繪制棋盤drawChessBoard();}}function initBoard() {for (let i = 0; i < BOARD_ROW_AND_COL; i++) {chessBoard[i] = [];for (let j = 0; j < BOARD_ROW_AND_COL; j++) {chessBoard[i][j] = 0;}}}// 繪制棋盤網(wǎng)格線function drawChessBoard() {context.strokeStyle = "#BFBFBF";for (let i = 0; i < BOARD_ROW_AND_COL; i++) {//橫向的線條context.moveTo(15 + i * 30, 15);context.lineTo(15 + i * 30, 430); context.stroke();//縱向的線條context.moveTo(15, 15 + i * 30);context.lineTo(435, 15 + i * 30); context.stroke();}}//繪制棋子function oneStep(i, j, isWhite) {if (i < 0 || j < 0) return;context.beginPath();context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);context.closePath();//createLinearGradient() 方法創(chuàng)建放射狀/圓形漸變對(duì)象var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);// 區(qū)分黑白子if (!isWhite) {gradient.addColorStop(0, "#0A0A0A");gradient.addColorStop(1, "#636766");} else {gradient.addColorStop(0, "#D1D1D1");gradient.addColorStop(1, "#F9F9F9");}context.fillStyle = gradient;context.fill();}//棋盤區(qū)域的點(diǎn)擊事件chess.onclick = function (e) {// 如果當(dāng)前輪到對(duì)方走棋,則直接返回if(is_me == false) {return;}let x = e.offsetX;let y = e.offsetY;// 注意, 橫坐標(biāo)是列, 縱坐標(biāo)是行// 這里是為了讓點(diǎn)擊操作能夠?qū)?yīng)到網(wǎng)格線上let col = Math.floor(x / 30);let row = Math.floor(y / 30);if (chessBoard[row][col] != 0) {alert("當(dāng)前位置已有棋子");return;}// 發(fā)送走棋請(qǐng)求send_chess(row, col);}// 發(fā)送走棋請(qǐng)求(websocket長(zhǎng)連接通信,直接使用ws_hdl.send,而不是通過ajax)function send_chess(r, c) {var chess_info = {optype: "put_chess",room_id: room_info.room_id,uid: room_info.uid,row: r,col: c};ws_hdl.send(JSON.stringify(chess_info));console.log("click:" + JSON.stringify(chess_info));}// 聊天動(dòng)作// 給消息發(fā)送按鈕添加點(diǎn)擊事件var chat_button_div = document.getElementById("chat_button");chat_button_div.onclick = function() {// 獲取聊天輸入框中的消息var chat_msg = {optype: "chat",room_id: room_info.room_id,uid: room_info.uid,message: document.getElementById("chat_input").value};// 將消息發(fā)送給服務(wù)器ws_hdl.send(JSON.stringify(chat_msg)); } // websocket各種事件的執(zhí)行函數(shù)ws_hdl.onopen = function() {console.log("游戲房間長(zhǎng)連接建立成功");}ws_hdl.onclose = function() {console.log("游戲房間長(zhǎng)連接斷開");}ws_hdl.onerror = function() {console.log("游戲房間長(zhǎng)連接建立出錯(cuò)");}// 更新screen顯示的內(nèi)容function set_screen(me) {var screen_div = document.getElementById("screen");if(me) screen_div.innerHTML = "輪到己方走棋...";else screen_div.innerHTML = "輪到對(duì)方走棋...";}ws_hdl.onmessage = function(evt) {console.log("message:" + evt.data);var resp = JSON.parse(evt.data);// 收到room_ready響應(yīng)消息if(resp.optype == "room_ready") {// 保存房間信息與執(zhí)棋用戶room_info = resp; // 規(guī)定白棋先走is_me = (room_info.uid == room_info.white_id ? true : false);if(resp.result == false) {alert(resp.reason);location.replace("/login.html");} else {// 更新screen顯示的內(nèi)容set_screen(is_me);// 初始化游戲initGame();}}// 收到put_chess響應(yīng)消息else if(resp.optype == "put_chess") {// 判斷走棋是否成功if(resp.result == false) {alert(resp.reason);return;}// 下棋坐標(biāo)為-1表示對(duì)方掉線if(resp.row != -1 && resp.col != -1) {// 繪制棋子isWhite = (resp.uid == room_info.white_id ? true : false);oneStep(resp.col, resp.row, isWhite);// 更新棋盤chessBoard[resp.row][resp.col] = 1; }// 更新執(zhí)棋玩家is_me = !is_me;// 更新screen顯示的內(nèi)容set_screen(is_me);// 判斷是否有勝利者winner = resp.winner;if(winner == 0) return;// 更新screen信息var screen_div = document.getElementById("screen");if(winner == room_info.uid) screen_div.innerHTML = resp.reason;else screen_div.innerHTML = "游戲失敗,再接再厲";// 在chess_area區(qū)域下方添加返回大廳按鈕var chess_area_div = document.getElementById("chess_area");var button_div = document.createElement("div");button_div.innerHTML = "返回大廳";button_div.onclick = function() {ws_hdl.close();location.replace("/game_hall.html");}chess_area_div.appendChild(button_div);}// 收到chat響應(yīng)消息else if(resp.optype == "chat") {if(resp.result == false) {alert(resp.reason);document.getElementById("chat_input").value = "";return;}// 創(chuàng)建一個(gè)子控件,將消息內(nèi)嵌到其中var msg_div = document.createElement("p");msg_div.innerHTML = resp.message;// 添加屬性if(resp.uid == room_info.uid) msg_div.setAttribute("id", "self_msg");else msg_div.setAttribute("id", "peer_msg");// 添加換行var br_div = document.createElement("br");// 將消息與換行子控件渲染到聊天顯示框中var msg_show_div = document.getElementById("chat_show");msg_show_div.appendChild(msg_div);msg_show_div.appendChild(br_div);// 清空輸入框內(nèi)容document.getElementById("chat_input").value = "";}}</script>
</body>
</html>
六、項(xiàng)目演示
編譯 main.cc 得到可執(zhí)行程序 gobang 并運(yùn)行:
main.cc
#include "server.hpp"#define HOST "127.0.0.1"
#define USER "thj"
#define PASSWD "Abcd1234@"int main()
{gobang_server server(HOST, USER, PASSWD);server.start(8081);return 0;
}
打開瀏覽器,訪問 106.52.90.67:8081/register.html 進(jìn)行新用戶注冊(cè),注冊(cè)成功后瀏覽器彈出 “用戶注冊(cè)成功” 提示框,點(diǎn)擊確定會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁面。
此時(shí),打開 mysql 客戶端,可以看到 xiaowang 的用戶信息記錄被成功創(chuàng)建。
輸入用戶名密碼,點(diǎn)擊登錄,瀏覽器彈出 “登錄成功” 提示框,點(diǎn)擊自動(dòng)跳轉(zhuǎn)游戲大廳頁面,并且該用戶的詳細(xì)信息成功從數(shù)據(jù)庫獲取并展示在游戲大廳頁面;同時(shí),該用戶與服務(wù)器的通信協(xié)議由 HTTP 變?yōu)?WebSocket,控制臺(tái)打印 “游戲大廳長(zhǎng)連接建立成功” 日志;該用戶的 session 信息也被創(chuàng)建并且由于建立了 WebSocket 長(zhǎng)連接所以 session 被設(shè)置為永久有效。
然后,點(diǎn)擊開始匹配,該用戶會(huì)根據(jù)其天梯分?jǐn)?shù)被添加到對(duì)應(yīng)的匹配隊(duì)列中;點(diǎn)擊停止匹配,該用戶會(huì)從對(duì)應(yīng)的匹配隊(duì)列中移除??刂婆_(tái)提示相關(guān)信息。
此時(shí),我們?cè)儆昧硗庖粋€(gè)瀏覽器注冊(cè)一個(gè)用戶,登錄并開始匹配,由于新用戶天梯分?jǐn)?shù)默認(rèn)都是 1000,所以兩個(gè)玩家匹配成功,瀏覽器彈出 “匹配成功” 提示框,點(diǎn)擊確定自動(dòng)跳轉(zhuǎn)到游戲房間界面,此時(shí)原來游戲大廳的長(zhǎng)連接會(huì)斷開,游戲房間的長(zhǎng)連接會(huì)被創(chuàng)建。(使用不同的瀏覽器,防止 cookie 信息沖突)
此時(shí),一方的聊天信息以及走棋信息都能被另一方知道。在游戲結(jié)束途中,如果一方退出,另一方直接獲勝;游戲結(jié)束后,用戶可以點(diǎn)擊 “返回大廳” 按鈕回到游戲大廳。
回到游戲大廳后,大廳界面顯示的玩家的比賽信息以及數(shù)據(jù)庫中玩家的比賽信息都會(huì)被更新。
七、項(xiàng)目擴(kuò)展
我們上面實(shí)現(xiàn)的網(wǎng)絡(luò)五子棋其實(shí)只是一個(gè)最基礎(chǔ)的版本,或者說是一個(gè)重度刪減版,其實(shí)還可以對(duì)它進(jìn)行許多的擴(kuò)展,比如添加如下的一些功能:
-
實(shí)現(xiàn)局時(shí)與步時(shí)功能:我們可以設(shè)置一個(gè)玩家一局游戲能夠思考的總時(shí)間以及一步棋能夠思考的最長(zhǎng)時(shí)間;如果步時(shí)到了玩家仍未下棋,那么系統(tǒng)可以隨機(jī)落下一枚棋子。
-
實(shí)現(xiàn)棋譜保存與錄像回放功能:我們可以在數(shù)據(jù)庫中創(chuàng)建一個(gè)對(duì)戰(zhàn)表,用來存儲(chǔ)玩家的對(duì)戰(zhàn)數(shù)據(jù),即己方與對(duì)方下棋的步驟。
這樣玩家在對(duì)局結(jié)束后可以生成對(duì)局錄像回放 (將數(shù)據(jù)庫中該局對(duì)戰(zhàn)雙方的下棋步驟獲取出來,然后間隔一定時(shí)間依次顯示到前端頁面中),同時(shí),如果玩家游戲中途刷新界面或掉線重連后,我們也可以通過數(shù)據(jù)庫中的對(duì)戰(zhàn)數(shù)據(jù)讓其可以繼續(xù)對(duì)戰(zhàn)。
-
實(shí)現(xiàn)觀戰(zhàn)功能:我們可以在游戲大廳中顯示當(dāng)前正在對(duì)戰(zhàn)的所有游戲房間,然后家可以選中某個(gè)房間以觀眾的形式加入到房間中,實(shí)時(shí)的看到選手的對(duì)局情況。
-
實(shí)現(xiàn)人機(jī)對(duì)戰(zhàn)的功能:當(dāng)玩家長(zhǎng)時(shí)間匹配不到對(duì)手時(shí),我們可以為該玩家分配一個(gè) AI 對(duì)手與其進(jìn)行對(duì)戰(zhàn);同時(shí),在玩家游戲過程中,我們也可以提供類似 “托管” 的功能,由人機(jī)代替玩家來進(jìn)行對(duì)戰(zhàn)。
八、項(xiàng)目總結(jié)
本項(xiàng)目是一個(gè)業(yè)務(wù)型的項(xiàng)目,也是本人的第一個(gè)項(xiàng)目,在編程方面的難度其實(shí)并不是太大,主要是學(xué)習(xí)一個(gè)具體業(yè)務(wù)的整體工作邏輯是怎樣的 (從請(qǐng)求到業(yè)務(wù)處理再到響應(yīng)),以及前后端是如何配合進(jìn)行工作的 (HTML/CSS/JS/AJAX)。
在項(xiàng)目編寫過程中,相較于 C++、系統(tǒng)編程、網(wǎng)絡(luò)編程這些已經(jīng)學(xué)過的東西,其實(shí)前端以及 WebSocketpp 這方面的知識(shí)花費(fèi)的時(shí)間精力會(huì)要更多一些,因?yàn)檫@些技術(shù)都是第一次接觸,需要一邊查閱文檔一邊使用,很多地方出了 bug 也需要花很多時(shí)間才能修復(fù)。
下面是項(xiàng)目中一些需要特別注意的地方,也可以說是我自己踩過的坑:
- C語言可變參數(shù)與宏函數(shù):本項(xiàng)目日志宏封裝模塊中使用了一些C語言的知識(shí),包括可變參數(shù)、宏函數(shù)、預(yù)處理符號(hào) ## 以及格式化輸出函數(shù) fprintf 等,要注意正確使用它們。
- C++11 相關(guān):本項(xiàng)目中用到了一些 C++11 相關(guān)的知識(shí),包括函數(shù)綁定、智能指針、互斥鎖、條件變量等,其中要特別注意 bind 如何使用,包括如何使用 bind 固定參數(shù)、調(diào)整參數(shù)順序等。
- 動(dòng)靜態(tài)庫相關(guān):由于本項(xiàng)目中使用了一些第三方庫,包括 JsonCpp、WebSocketpp、MySQL C API 等,所以在 Makefile 中進(jìn)行編譯鏈接時(shí)需要使用 -l、-L、-I 選項(xiàng)來指定動(dòng)態(tài)庫名稱、動(dòng)態(tài)庫路徑以及庫頭文件路徑。
- WebSocketpp 相關(guān):由于本項(xiàng)目是使用 WebSocketpp 來進(jìn)行服務(wù)器搭建,所以要對(duì)其相關(guān)的接口及其使用有一定的了解,特別是其中的 cancel 函數(shù),需要充分了解它的特性才能夠正確的使用它。
源碼地址:
https://gitee.com/tian-hongjin/project-design/tree/master/gobang