中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

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

html5 css3網(wǎng)站模板百度流量推廣項(xiàng)目

html5 css3網(wǎng)站模板,百度流量推廣項(xiàng)目,網(wǎng)站流量站怎么做,南京門戶網(wǎng)站建設(shè)文章目錄 一、項(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. 安裝分布式版本控制工具 git7. 安裝 cmake8. 安裝 boost 庫9. 安裝 Jsoncpp 庫10. 安…

文章目錄

  • 一、項(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ā)階段:

  1. 環(huán)境搭建:在 Centos7.6 環(huán)境下安裝本項(xiàng)目會(huì)使用到的各種工具以及第三方庫。

  2. 前置知識(shí)了解:對(duì)項(xiàng)目中需要用到的一些知識(shí)進(jìn)行了解,學(xué)會(huì)它們的基本使用,比如 bind/WebSocketpp/HTML/JS/AJAX 等。

  3. 框架設(shè)計(jì):進(jìn)行項(xiàng)目模塊劃分,確定每一個(gè)模塊需要實(shí)現(xiàn)的功能。

  4. 模塊開發(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)閉連接。。image-20231114213053409

同時(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)文格式如下,大家了解即可:image-20231114214304820

WebSocket 相關(guān)接口

創(chuàng)建 WebSocket 對(duì)象:

var Socket = new WebSocket(url, [protocol]);

WebSocket 對(duì)象的相關(guān)事件:image-20231115141559205

WebSocket 對(duì)象的相關(guān)方法:image-20231115141628314

參考資料:

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ù)器的流程如下:

  1. 實(shí)例化一個(gè) websocketpp::server 對(duì)象。
  2. 設(shè)置日志等級(jí)。(本項(xiàng)目中我們使用自己封裝的日志函數(shù),所以這里設(shè)置日志等級(jí)為 none)
  3. 初始化 asio 調(diào)度器。
  4. 設(shè)置處理 http 請(qǐng)求、websocket 握手成功、websocket 連接關(guān)閉以及收到 websocket 消息的回調(diào)函數(shù)。
  5. 設(shè)置監(jiān)聽端口。
  6. 開始獲取 tcp 連接。
  7. 啟動(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ù)序列化的步驟如下:

  1. 將需要序列化的數(shù)據(jù)存儲(chǔ)在Json::Value對(duì)象中。
  2. 實(shí)例化StreamWriterBuilder工廠類對(duì)象。
  3. 使用StreamWriterBuilder工廠類對(duì)象實(shí)例化StreamWriter對(duì)象。
  4. 使用StreamWriter對(duì)象完成Json::Value中數(shù)據(jù)的序列化工作,并將序列化結(jié)果存放到ss中。

使用 JsonCpp 將數(shù)據(jù)反序列化的步驟如下:

  1. 實(shí)例化一個(gè) CharReaderBuilder 工廠類對(duì)象。
  2. 使用CharReaderBuilder對(duì)象實(shí)例化一個(gè)CharReader對(duì)象。
  3. 創(chuàng)建一個(gè)Json::Value對(duì)象,用于保存json格式字符串反序列化后的結(jié)果。
  4. 使用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;
}

image-20231114224725411

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é)束返回游戲大廳。

image-20231115153326176

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ù)。

image-20231115232406058

基于上面的原理,在本項(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)說過了,大體流程如下:

  1. 實(shí)例化一個(gè) websocketpp::server 對(duì)象。
  2. 設(shè)置日志等級(jí)。(本項(xiàng)目中我們使用自己封裝的日志函數(shù),所以這里設(shè)置日志等級(jí)為 none)
  3. 初始化 asio 調(diào)度器。
  4. 設(shè)置處理 http 請(qǐng)求、websocket 握手成功、websocket 連接關(guān)閉以及收到 websocket 消息的回調(diào)函數(shù)。
  5. 設(shè)置監(jiān)聽端口。
  6. 開始獲取 tcp 連接。
  7. 啟動(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;
}

image-20231117231554206

image-20231117231736208

打開瀏覽器,訪問 106.52.90.67:8081/register.html 進(jìn)行新用戶注冊(cè),注冊(cè)成功后瀏覽器彈出 “用戶注冊(cè)成功” 提示框,點(diǎn)擊確定會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁面。image-20231117232212365

此時(shí),打開 mysql 客戶端,可以看到 xiaowang 的用戶信息記錄被成功創(chuàng)建。image-20231117232414845

輸入用戶名密碼,點(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è)置為永久有效。image-20231117233507840

image-20231117233749295

然后,點(diǎn)擊開始匹配,該用戶會(huì)根據(jù)其天梯分?jǐn)?shù)被添加到對(duì)應(yīng)的匹配隊(duì)列中;點(diǎn)擊停止匹配,該用戶會(huì)從對(duì)應(yīng)的匹配隊(duì)列中移除??刂婆_(tái)提示相關(guān)信息。image-20231117234043403

此時(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 信息沖突)image-20231117234424839

image-20231117234454832

此時(shí),一方的聊天信息以及走棋信息都能被另一方知道。在游戲結(jié)束途中,如果一方退出,另一方直接獲勝;游戲結(jié)束后,用戶可以點(diǎn)擊 “返回大廳” 按鈕回到游戲大廳。image-20231117235106801

回到游戲大廳后,大廳界面顯示的玩家的比賽信息以及數(shù)據(jù)庫中玩家的比賽信息都會(huì)被更新。image-20231117235245962

image-20231117235319822


七、項(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


http://m.risenshineclean.com/news/60265.html

相關(guān)文章:

  • 鳥人 網(wǎng)站建設(shè)移動(dòng)網(wǎng)站推廣如何優(yōu)化
  • 建筑工程網(wǎng)上考試答案重慶seo黃智
  • 濰坊做網(wǎng)站的網(wǎng)絡(luò)公司品牌網(wǎng)絡(luò)推廣運(yùn)營公司
  • 重慶企業(yè)網(wǎng)站優(yōu)化營銷管理制度范本
  • 網(wǎng)站建設(shè)的淘寶模板南寧seo費(fèi)用服務(wù)
  • 圖片演示dw做網(wǎng)站手機(jī)如何創(chuàng)建網(wǎng)站
  • 網(wǎng)站ip訪問做圖表中國十大新聞網(wǎng)站排名
  • 石家莊網(wǎng)絡(luò)公司行業(yè)深圳百度seo怎么做
  • 微網(wǎng)站做的比較好搜索引擎營銷的主要方式有
  • 南京網(wǎng)站網(wǎng)站建設(shè)學(xué)校如何發(fā)布一個(gè)網(wǎng)站
  • 做金融必看網(wǎng)站谷歌在線瀏覽器免費(fèi)入口
  • 網(wǎng)站建設(shè)欄目說明百度一下就知道官網(wǎng)
  • 企業(yè)網(wǎng)站建設(shè)的思路最優(yōu)化方法
  • 一些做的好的網(wǎng)站東營百度推廣電話
  • 曲阜公司網(wǎng)站建設(shè)價(jià)格便宜ui設(shè)計(jì)培訓(xùn)班哪家好
  • 淘寶客網(wǎng)站還可以做嗎牛奶軟文廣告營銷
  • 長(zhǎng)沙今天最新招聘信息臺(tái)州關(guān)鍵詞優(yōu)化平臺(tái)
  • 阿里巴巴做網(wǎng)站的電話號(hào)碼西安百度推廣怎么做
  • 金融投資網(wǎng)站開發(fā)站長(zhǎng)工具是干嘛的
  • 產(chǎn)品經(jīng)理培訓(xùn)哪個(gè)機(jī)構(gòu)好湖南正規(guī)seo優(yōu)化
  • 做網(wǎng)站的人還能做什么公司網(wǎng)站建設(shè)要多少錢
  • 做網(wǎng)站怎樣連數(shù)據(jù)庫關(guān)鍵詞推廣效果分析
  • 做視頻網(wǎng)站視頻放在哪里域名怎么注冊(cè)
  • 如何做電影網(wǎng)站賺錢嗎刷百度指數(shù)
  • 營銷型網(wǎng)站建設(shè)公司云服務(wù)器
  • 網(wǎng)站建設(shè)捌金手指下拉一南寧推廣軟件
  • 瓊海網(wǎng)站建設(shè)太原seo報(bào)價(jià)
  • 目前最火的自媒體平臺(tái)seo網(wǎng)站關(guān)鍵詞排名優(yōu)化公司
  • 做韓國網(wǎng)站有哪些東西嗎手機(jī)網(wǎng)站排名優(yōu)化軟件
  • 響應(yīng)式網(wǎng)站底部菜單欄廣州競(jìng)價(jià)托管公司