当前位置: 首页 > news >正文

在线五子棋对战项目

在线五子棋对战

本篇博客只为记录实现五子棋项目的知识记录和代码的实现,不具备教学意义。


目录

  • 在线五子棋对战
    • 1. 项目介绍
    • 2. 前置知识了解
      • 2.1 `Mysql`的配置
        • 2.1.1 配置`/etc/my.cnf`字符集
        • 2.1.2 启动`Mysql`服务
        • 2.1.3 获取`Mysql`临时密码
        • 2.1.4 设置`Mysql`数据库密码
        • 2.1.5 登录查看`Mysql`字符集是否正常
      • 2.2 `Websocketpp`
        • 2.2.1 `Websocket`介绍
        • 2.2.2 原理分析
        • 2.2.3 报文格式
        • 2.2.4 `Websocketpp`介绍
        • 2.2.5 `Websocketpp`使用
      • 2.3 `JsonCpp`
        • 2.3.1 `Json`数据格式
        • 2.3.2 `JsonCpp`介绍
        • 2.3.3 代码示例
      • 2.4 `Mysql API`
        • 2.4.1 `Mysql API`介绍
        • 2.4.2 `Mysql API`使用
      • 2.5 前端知识
        • 2.5.1 HTML基础
        • 2.5.2 CSS基础
        • 2.5.3 JavaScript基础
        • 2.5.4 AJAX基础
    • 3. 项目结构设计
      • 3.1 项目模块划分
      • 3.2 业务处理模块的子模块划分
      • 3.3 项目流程图
    • 4. 项目实现
      • 4.1 实用工具类模块实现
      • 4.2 数据管理模块实现
      • 4.3 在线用户管理模块实现
      • 4.4 游戏房间管理模块
      • 4.5 会话管理模块实现
        • 4.5.1 session介绍
        • 4.5.2 session实现
      • 4.6 用户匹配模块实现
      • 4.7 网络通信模块实现
      • 4.8 前端交互界面实现
        • 4.8.1 登录⻚⾯: `login.html`
        • 4.8.2 注册页面:`register.html`
        • 4.8.3 游戏大厅页面:`game_hall.html`
        • 4.8.4 游戏房间页面:`game_room.html`
      • 4.9 main函数和Makefile文件


1. 项目介绍

  • 本项目主要实现一个网页版的五子棋对战游戏,其主要支持以下核心功能:

    • 用户管理:实现用户注册、用户登录、获取用户信息、用户天梯分数记录、用户比赛场次记录等功能;
    • 匹配对战:实现两个玩家在网页端根据天梯分数匹配游戏对手,并进行五子棋游戏对战的功能;
    • 聊天功能:实现两个玩家在下棋的同时可以进行实时聊天的功能。

  • 开发环境:

    • Linux(ubuntu-22.04)
    • VSCode
    • g++
    • Makefile

  • 核心技术:

    • HTTP/WebSocket
    • Websocket++
    • JsonCpp
    • Mysql
    • C++11
    • BlockQueue
    • HTML/CSS/JS/AJAX

2. 前置知识了解

2.1 Mysql的配置

2.1.1 配置/etc/my.cnf字符集
sudo vim /etc/my.cnf
# For advice on how to change settings please see
# http://dev.mysql.com/doc/refman/5.7/en/server-configuration-defaults.html
[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
character-set-server=utf8
#
# Remove leading # and set to the amount of RAM for the most important data
# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.
# innodb_buffer_pool_size = 128M
#
# Remove leading # to turn on a very important data integrity option: logging
# changes to the binary log between backups.
# log_bin
#
# Remove leading # to set options mainly useful for reporting servers.
# The server defaults are faster for transactions and fast SELECTs.
# Adjust sizes as needed, experiment to find the optimal values.
# join_buffer_size = 128M
# sort_buffer_size = 2M
# read_rnd_buffer_size = 2M
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

2.1.2 启动Mysql服务
sudo systemctl start mysqld
sudo systemctl status mysqld
mysqld.service - MySQL ServerLoaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor 
preset: disabled)Active: active (running) since Mon 2023-04-17 17:54:00 CST; 9min agoDocs: man:mysqld(8)http://dev.mysql.com/doc/refman/en/using-systemd.htmlProcess: 20047 ExecStart=/usr/sbin/mysqld --daemonize --pidfile=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS)Process: 19988 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, 
status=0/SUCCESS)Main PID: 20051 (mysqld)Tasks: 28Memory: 189.2MCGroup: /system.slice/mysqld.service└─20051 /usr/sbin/mysqld --daemonize --pidfile=/var/run/mysqld/mysqld.pid
Apr 17 17:53:59 VM-8-12-centos systemd[1]: Starting MySQL Server...
Apr 17 17:54:00 VM-8-12-centos systemd[1]: Started MySQL Server.

2.1.3 获取Mysql临时密码
sudo grep 'temporary password' /var/log/mysqld.log

2.1.4 设置Mysql数据库密码
mysql -uroot -p
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.7.41 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> set global validate_password_policy=0;
Query OK, 0 rows affected (0.00 sec)
mysql> set global validate_password_length=1;
Query OK, 0 rows affected (0.00 sec)
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'qwer@wu.888';
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

2.1.5 登录查看Mysql字符集是否正常
mysql -uroot -p
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 4
Server version: 5.7.41 MySQL Community Server (GPL)
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show variables like '%chara%';
+--------------------------+----------------------------+
| Variable_name 		| Value 					|
+--------------------------+----------------------------+
| character_set_client | utf8 						| --客⼾端使⽤的字符集
| character_set_connection | utf8 					| --客⼾端连接时使⽤的字符集
| character_set_database | utf8 					| --数据库创建默认字符集
| character_set_filesystem | binary 				| --⽂件系统编码格式
| character_set_results | utf8 						| --服务器返回结果时的字符集
| character_set_server | utf8 						| --存储系统元数据的字符集
| character_set_system | utf8 						| --系统使⽤的编码格式,不影响
| character_sets_dir | /usr/share/mysql/charsets/ 	  |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)
mysql> quit

2.2 Websocketpp

2.2.1 Websocket介绍

WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的消息推送机制。

  • 传统的 web 程序都是属于 “一问一答” 的形式,即客户端给服务器发送了一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应。
  • 像网页即时聊天或者我们做的五子棋游戏这样的程序都是非常依赖 “消息推送” 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 “轮询” 的方式实现,而轮询的成本比较高并且也不能及时的获取到消息的响应。
  • 基于上述两个问题,就产生了 WebSocket 协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。

2.2.2 原理分析

WebSocket 协议本质上是⼀个基于 TCP 的协议。为了建⽴⼀个 WebSocket 连接,客⼾端浏览器⾸先要向服务器发起⼀个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了⼀些附加头信息,通过这个附加头信息完成握⼿过程并升级协议的过程。

在这里插入图片描述

具体协议升级的过程:

在这里插入图片描述


2.2.3 报文格式

在这里插入图片描述

  • FIN: WebSocket传输数据以消息为概念单位,⼀个消息有可能由⼀个或多个帧组成,FIN字段为1表⽰末尾帧。

  • RSV1~3:保留字段,只在扩展时使⽤,若未启⽤扩展则应置1,若收到不全为0的数据帧,且未协商扩展则⽴即终⽌连接。

  • opcode: 标志当前数据帧的类型:

    • 0x0表⽰这是个延续帧,当opcode为0表⽰本次数据传输采⽤了数据分⽚,当前收到的帧为其中⼀个分⽚;
    • 0x1表⽰这是⽂本帧;
    • 0x2表⽰这是⼆进制帧;
    • 0x3-0x7保留,暂未使⽤;
    • 0x8表⽰连接断开;
    • 0x9表⽰ping帧;
    • 0xa表⽰pong帧;
    • 0xb-0xf保留,暂未使⽤。
  • mask:表⽰Payload数据是否被编码,若为1则必有Mask-Key,⽤于解码Payload数据。仅客⼾端发送给服务端的消息需要设置。

  • Payload length:数据载荷的⻓度,单位是字节,有可能为7位、7+16位、7+64位。假设Payload length = x:

    • x为0~126时数据的⻓度为x字节;
    • x为126时后续2个字节代表⼀个16位的⽆符号整数,该⽆符号整数的值为数据的⻓度;
    • x为127时后续8个字节代表⼀个64位的⽆符号整数(最⾼位为0),该⽆符号整数的值为数据的⻓度。
  • Mask-Key:当mask为1时存在,⻓度为4字节,解码规则:DECODED[i] = ENCODED[i] ^ MASK[i % 4]

  • Payload data: 报⽂携带的载荷数据。


2.2.4 Websocketpp介绍
  • WebSocketpp 是一个跨平台的开源(BSD许可证)头部专用 C++ 库,它实现了 RFC6455(WebSocket 协议)和 RFC7692(WebSocket Compression Extensions),允许将 WebSocket 客户端和服务器功能集成到 C++ 程序中。在最常见的配置中,全功能网络 I/O 由 Asio 网络库提供。

  • WebSocketpp 的主要特性包括:

    • 事件驱动的接口
    • 支持 HTTP/HTTPSWS/WSSIPv6
    • 灵活的依赖管理(Boost 库/C++11 标准库)
    • 可移植性(Posix/Windows32/64bitIntel/ARM
    • 线程安全
  • WebSocketpp 同时支持 HTTPWebSocket 两种网络协议,比较适用于我们本次的项目,因此我们选用该库作为项目的依赖库,用来搭建 HTTPWebSocket 服务器。


2.2.5 Websocketpp使用
  • Websocketpp常用接口介绍:

    namespace websocketpp {// 连接句柄类型定义,使用弱指针避免循环引用typedef lib::weak_ptr<void> connection_hdl;// 端点类模板,作为WebSocket通信的核心端点template <typename config>class endpoint : public config::socket_type {public:// 定时器指针类型定义typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;// 连接指针类型定义typedef typename connection_type::ptr connection_ptr;// 消息指针类型定义typedef typename connection_type::message_ptr message_ptr;// 各种回调函数类型定义typedef lib::function<void(connection_hdl)> open_handler;typedef lib::function<void(connection_hdl)> close_handler;typedef lib::function<void(connection_hdl)> http_handler;typedef lib::function<void(connection_hdl, message_ptr)> message_handler;/* websocketpp::log::alevel::none 禁⽌打印所有⽇志 */// 设置日志打印等级void set_access_channels(log::level channels);// 清除指定等级的日志void clear_access_channels(log::level channels);/* 设置指定事件的回调函数 */void set_open_handler(open_handler h);    // websocket握⼿成功回调处理函数void set_close_handler(close_handler h);  // websocket连接关闭回调处理函数void set_message_handler(message_handler h); // websocket消息回调处理函数void set_http_handler(http_handler h);    // http请求回调处理函数/* 发送数据接⼝ */// 发送字符串数据void 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);/* 关闭连接接⼝ */void close(connection_hdl hdl, close::status::value code, std::string& reason);/* 获取connection_hdl 对应连接的connection_ptr */connection_ptr get_con_from_hdl(connection_hdl hdl);/* * websocketpp基于asio框架实现,init_asio⽤于初始化asio框架中的io_service调度器* 必须在调用其他方法前初始化*/void init_asio();// 设置是否启用地址重用(SO_REUSEADDR)void set_reuse_addr(bool value);// 设置endpoint的绑定监听端口void listen(uint16_t port);/* * 对io_service对象的run接⼝封装,⽤于启动服务器* 会阻塞当前线程直到所有工作完成*/std::size_t run();/* * websocketpp提供的定时器,以毫秒为单位* duration: 定时器超时时间(毫秒)* callback: 超时回调函数*/timer_ptr set_timer(long duration, timer_handler callback);};// 服务器类模板template <typename config>class server : public endpoint<connection<config>, config> {public:/* 初始化并启动服务端监听连接的accept事件处理 */void start_accept();};// 连接类模板,表示一个WebSocket连接template <typename config>class connection : public config::transport_type::transport_con_type,public config::connection_base {public:/* 发送数据接⼝ */error_code send(std::string& payload, frame::opcode::value op = frame::opcode::text);/* 获取http请求头部字段值 */std::string const& get_request_header(std::string const& key);/* 获取http请求正文 */std::string const& get_request_body();/* 设置http响应状态码 */void set_status(http::status_code::value code);/* 设置http响应正文 */void set_body(std::string const& value);/* 添加http响应头部字段 */void append_header(std::string const& key, std::string const& val);/* 获取http请求对象 */request_type const& get_request();/* 获取connection_ptr对应的connection_hdl */connection_hdl get_handle();};namespace http {namespace parser {// HTTP解析器基类class parser {public:// 获取请求头部字段值std::string const& get_header(std::string const& key);};// HTTP请求解析器class request : public parser {public:/* 获取请求方法(GET/POST等) */std::string const& get_method();/* 获取请求URI */std::string const& get_uri();};};};namespace message_buffer {/* 获取websocket请求中的payload数据类型 */frame::opcode::value get_opcode();/* 获取websocket中payload数据 */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;                // HTTP相关日志static level const fail = 0x2000;                // 失败日志static level const access_core = 0x00003003;     // 核心访问日志static level const all = 0xffffffff;             // 所有日志};}namespace http {namespace status_code {// HTTP状态码枚举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 {// WebSocket帧操作码枚举enum value {continuation = 0x0,  // 延续帧text = 0x1,          // 文本帧binary = 0x2,        // 二进制帧rsv3 = 0x3,          // RSV3 (保留)rsv4 = 0x4,          // RSV4 (保留)rsv5 = 0x5,          // RSV5 (保留)rsv6 = 0x6,          // RSV6 (保留)rsv7 = 0x7,          // RSV7 (保留)close = 0x8,         // 关闭帧ping = 0x9,          // ping帧pong = 0xA,          // pong帧control_rsvb = 0xB,  // 控制帧RSVB (保留)control_rsvc = 0xC,  // 控制帧RSVC (保留)control_rsvd = 0xD,  // 控制帧RSVD (保留)control_rsve = 0xE,  // 控制帧RSVE (保留)control_rsvf = 0xF   // 控制帧RSVF (保留)};}}
    }
    
  • 简易的http/Websocketpp服务器实现

    • 服务器

      #include <iostream>
      #include <websocketpp/config/asio_no_tls.hpp>
      #include <websocketpp/server.hpp>using namespace std;// 定义WebSocket服务器类型和消息指针类型
      typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
      typedef websocketsvr::message_ptr message_ptr;// 使用占位符简化回调函数绑定
      using websocketpp::lib::placeholders::_1;
      using websocketpp::lib::placeholders::_2;
      using websocketpp::lib::bind;// websocket连接成功的回调函数
      void OnOpen(websocketsvr *server, websocketpp::connection_hdl hdl) {cout << "连接成功" << endl;// 可以在这里保存连接句柄,用于后续向特定客户端发送消息
      }// websocket连接关闭的回调函数
      void OnClose(websocketsvr *server, websocketpp::connection_hdl hdl) {cout << "连接关闭" << endl;// 可以在这里清理与该连接相关的资源
      }// websocket连接收到消息的回调函数
      void OnMessage(websocketsvr *server, websocketpp::connection_hdl hdl, message_ptr msg) {cout << "收到消息: " << msg->get_payload() << endl;// 收到消息将相同的消息发回给websocket客户端(实现echo功能)// 参数说明:// hdl - 连接句柄,指定要发送给哪个客户端// msg->get_payload() - 获取消息内容// websocketpp::frame::opcode::text - 指定消息类型为文本server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
      }// websocket连接异常的回调函数
      void OnFail(websocketsvr *server, websocketpp::connection_hdl hdl) {cout << "连接异常" << endl;// 可以在这里记录错误日志或尝试重新连接
      }// 处理http请求的回调函数 返回一个html欢迎页面
      void OnHttp(websocketsvr *server, websocketpp::connection_hdl hdl) {cout << "处理http请求" << endl;// 从连接句柄获取连接指针websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);// 构建HTML响应内容std::stringstream ss;ss << "<!doctype html><html><head>"<< "<title>hello websocket</title><body>"<< "<h1>hello websocketpp</h1>"<< "</body></head></html>";// 设置响应内容和状态码con->set_body(ss.str());con->set_status(websocketpp::http::status_code::ok);
      }int main() {// 创建WebSocket服务器实例websocketsvr server;// 设置日志级别// all表示打印全部级别日志// none表示什么日志都不打印server.set_access_channels(websocketpp::log::alevel::none);// 初始化ASIO网络库server.init_asio();// 注册各种事件的处理函数server.set_http_handler(bind(&OnHttp, &server, ::_1));    // HTTP请求处理server.set_open_handler(bind(&OnOpen, &server, ::_1));    // WebSocket连接打开server.set_close_handler(bind(&OnClose, &server, _1));     // WebSocket连接关闭server.set_message_handler(bind(&OnMessage, &server, _1, _2)); // WebSocket消息接收// 监听8888端口server.listen(8888);// 开始接受TCP连接server.start_accept();// 启动服务器事件循环server.run();return 0;
      }
      
    • 客户端

      <!DOCTYPE html>
      <html lang="en">
      <head><!-- 基础元数据 --><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><!-- 响应式视口设置 --><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Test Websocket</title>
      </head>
      <body><!-- 消息输入框 --><input type="text" id="message"><!-- 消息提交按钮 --><button id="submit">提交</button><script>/*** WebSocket 客户端实现* 协议格式说明:ws://[服务器地址]:[端口]* 示例连接本地测试服务器:ws://192.168.51.100:8888*/let websocket = new WebSocket("ws://192.168.51.100:8888");// 连接成功回调websocket.onopen = function() {console.log("WebSocket连接已建立");}// 消息接收回调// e.data 包含服务器发送的消息内容websocket.onmessage = function(e) {console.log("收到服务器消息: " + e.data);}// 连接错误回调websocket.onerror = function() {console.error("WebSocket连接发生错误");}// 连接关闭回调websocket.onclose = function() {console.warn("WebSocket连接已关闭");}// DOM元素获取let input = document.querySelector('#message');let button = document.querySelector('#submit');// 按钮点击事件处理button.onclick = function() {// 验证输入内容是否为空if(!input.value.trim()) {console.warn("不能发送空消息");return;}console.log("正在发送消息: " + input.value);// 通过WebSocket发送消息到服务器websocket.send(input.value);// 清空输入框(可选)input.value = '';}</script>
      </body>
      </html>
      

2.3 JsonCpp

2.3.1 Json数据格式

Json是一种数据交换格式,它采用完全独立于编程语言的文本格式来存储和表示数据。

  • C语言表示:

    char *name = "xx";
    int age = 18;
    float score[3] = {88.5, 99, 58};
    
  • Json表示

    {"姓名" : "xx","年龄" : 18,"成绩" : [88.5, 99, 58]
    }
    [{"姓名":"⼩明", "年龄":18, "成绩":[23, 65, 78]},{"姓名":"⼩红", "年龄":19, "成绩":[88, 95, 78]}
    ]
    
  • Json 的数据类型包括对象,数组,字符串,数字等:

    • 对象:使⽤花括号 {} 括起来的表示⼀个对象
    • 数组:使⽤中括号 [] 括起来的表示⼀个数组
    • 字符串:使⽤常规双引号 "" 括起来的表示⼀个字符串
    • 数字:包括整形和浮点型,直接使用

2.3.2 JsonCpp介绍
  • Jsoncpp 库主要是用于实现 Json 格式数据的序列化和反序列化,它实现了将多个数据对象组织成为 json 格式字符串,以及将 Json 格式字符串解析得到多个数据对象的功能。

  • Json 数据对象类的表示:

class Json::Value{Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过Value& operator[](const std::string& key);//简单的⽅式完成 val["name"] = "xx";Value& operator[](const char* key);Value removeMember(const char* key);//移除元素const Value& operator[](ArrayIndex index) const; //val["score"][0]Value& append(const Value& value);//添加数组元素val["score"].append(88); ArrayIndex size() const;//获取数组元素个数 val["score"].size();bool isNull(); //⽤于判断是否存在某个字段std::string asString() const;//转string string name = val["name"].asString();const char* asCString() const;//转char* char *name = val["name"].asCString();Int asInt() const;//转int int age = val["age"].asInt();float asFloat() const;//转float float weight = val["weight"].asFloat();bool asBool() const;//转 bool bool ok = val["ok"].asBool();
};
  • Jsoncpp 库主要借助三个类以及其对应的少量成员函数完成序列化及反序列化,包括序列化接口和反序列化接口。

2.3.3 代码示例
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{// 序列化Json::Value stu;stu["name"] = "zhangsan";stu["age"] = 19; stu["socre"].append(77.5);stu["socre"].append(88);stu["socre"].append(99.5);Json::StreamWriterBuilder swb;std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());std::stringstream ss; int ret = sw->write(stu, &ss);if (ret != 0) {std::cout << "Serialize failed!\n";return -1; } std::cout << "序列化结果:\n" << ss.str() << std::endl;// 反序列化std::string str = ss.str();Json::Value root;Json::CharReaderBuilder crb;std::unique_ptr<Json::CharReader> cr(crb.newCharReader());bool ret1 = cr->parse(str.c_str(), str.c_str() + str.size(), &root, nullptr);if (!ret1) {std::cout << "UnSerialize failed!" << std::endl;return -1;}std::cout << "反序列化结果:\n"<< "name:" << root["name"].asString() << "\n"<< "age:" << root["age"].asInt() << "\n"<< "socre:" << root["socre"][0].asFloat() << " " << root["socre"][1].asInt()<< " " << root["socre"][2].asFloat() << "\n";return 0;
}
[zsc@node test_jsoncpp]$ g++ test_json.cpp -o test_json -ljsoncpp -std=c++11
[zsc@node test_jsoncpp]$ ./test_json 
序列化结果:
{"age" : 19,"name" : "zhangsan","socre" : [77.5,88,99.5]
}
反序列化结果:
name:zhangsan
age:19
socre:77.5 88 99.5

2.4 Mysql API

2.4.1 Mysql API介绍

MySQLC/S 模式, C API 其实就是⼀个 MySQL 客⼾端,提供⼀种⽤ C 语⾔代码操作数据库的流程。

/* MySQL数据库操作API封装 *//*** 初始化MySQL操作句柄* @param mysql 为空则动态申请句柄空间进行初始化* @return 成功返回句柄指针,失败返回NULL*/
MYSQL *mysql_init(MYSQL *mysql);/*** 连接MySQL服务器* @param mysql 初始化完成的句柄* @param host 连接的MySQL服务器地址(IP或域名)* @param user 连接的服务器的用户名* @param passwd 连接的服务器的密码* @param db 默认选择的数据库名称* @param port 连接的服务器的端口(默认0表示3306端口)* @param unix_socket 通信管道文件或socket文件(通常置NULL)* @param client_flag 客户端标志位(通常置0)* @return 成功返回句柄指针,失败返回NULL* @note 这是建立数据库连接的关键函数,必须先调用mysql_init初始化*/
MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user,const char *passwd, const char *db, unsigned int port,const char *unix_socket, unsigned long client_flag);/*** 设置当前客户端的字符集* @param mysql 初始化完成的句柄* @param csname 字符集名称(推荐使用"utf8"或"utf8mb4")* @return 成功返回0,失败返回非0* @important 必须在连接后立即设置,防止中文乱码问题*/
int mysql_set_character_set(MYSQL *mysql, const char *csname);/*** 选择操作的数据库* @param mysql 初始化完成的句柄* @param db 要切换选择的数据库名称* @return 成功返回0,失败返回非0*/
int mysql_select_db(MYSQL *mysql, const char *db);/*** 执行SQL语句* @param mysql 初始化完成的句柄* @param stmt_str 要执行的SQL语句字符串* @return 成功返回0,失败返回非0* @warning 不要用此函数执行包含二进制数据的语句,应使用预处理语句*/
int mysql_query(MYSQL *mysql, const char *stmt_str);/*** 保存查询结果到本地* @param mysql 初始化完成的句柄* @return 成功返回结果集指针,失败返回NULL* @note 使用后必须调用mysql_free_result释放内存*/
MYSQL_RES *mysql_store_result(MYSQL *mysql);/*** 获取结果集中的行数* @param result 保存到本地的结果集地址* @return 结果集中数据的条数*/
uint64_t mysql_num_rows(MYSQL_RES *result);/*** 获取结果集中的列数* @param result 保存到本地的结果集地址* @return 结果集中每一条数据的列数*/
unsigned int mysql_num_fields(MYSQL_RES *result);/*** 遍历结果集(会自动记录读取位置)* @param result 保存到本地的结果集地址* @return 字符串指针数组(row[0]表示第0列,row[1]表示第1列...)* @note 返回NULL表示已到达结果集末尾*/
MYSQL_ROW mysql_fetch_row(MYSQL_RES *result);/*** 释放结果集内存* @param result 保存到本地的结果集地址* @important 必须调用以避免内存泄漏*/
void mysql_free_result(MYSQL_RES *result);/*** 关闭数据库连接并销毁句柄* @param mysql 初始化完成的句柄* @important 程序退出前必须调用以释放资源*/
void mysql_close(MYSQL *mysql);/*** 获取MySQL错误原因描述* @param mysql 初始化完成的句柄* @return 错误描述字符串* @note 可用于调试和错误日志记录*/
const char *mysql_error(MYSQL *mysql);

2.4.2 Mysql API使用

下面我们使用C API来实现MySQL的增删查改操作

  • 创建测试数据库

    create database if not exists test_db;
    use test_db;
    create table stu(id int primary key auto_increment, -- 学⽣idage int, -- 学⽣年龄name varchar(32) -- 学⽣姓名
    );
    
  • 连接Mysql服务,进入shell并执行sql语句

    [zsc@node test_mysql]$ mysql -uroot -p123456
    MariaDB [(none)]> create database if not exists test_db;
    Query OK, 1 row affected (0.01 sec)
    MariaDB [(none)]> use test_db;
    Database changed
    MariaDB [test_db]> create table stu(-> id int primary key auto_increment, -- 学⽣id-> age int, -- 学⽣年龄-> name varchar(32) -- 学⽣姓名-> );
    Query OK, 0 rows affected (0.02 sec)
    
  • 实现增删查改操作

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <mysql/mysql.h>// 数据库连接配置
    #define HOST "127.0.0.1"      // MySQL服务器地址
    #define USER "root"           // 数据库用户名
    #define PASSWD "123456"       // 数据库密码
    #define DBNAME "test_db"      // 数据库名称/*** 向stu表添加数据* @param mysql MySQL连接句柄*/
    void add(MYSQL *mysql) {// SQL插入语句:插入两条学生记录char *sql = "insert into stu values(null, 18, '张三'), (null, 17, '李四');";int ret = mysql_query(mysql, sql);if (ret != 0) {printf("mysql query error:%s\n", mysql_error(mysql));return;} return;
    }/*** 从stu表删除数据* @param mysql MySQL连接句柄*/
    void del(MYSQL *mysql) {// SQL删除语句:删除姓名为'张三'的记录char *sql = "delete from stu where name='张三';";int ret = mysql_query(mysql, sql);if (ret != 0) {printf("mysql query error:%s\n", mysql_error(mysql));return;}return;
    }/*** 修改stu表数据* @param mysql MySQL连接句柄*/
    void mod(MYSQL *mysql) {// SQL更新语句:将'张三'的年龄改为15char *sql = "update stu set age=15 where name='张三';";int ret = mysql_query(mysql, sql);if (ret != 0) {printf("mysql query error:%s\n", mysql_error(mysql));return;}return;
    }/*** 查询stu表数据并打印结果* @param mysql MySQL连接句柄*/
    void get(MYSQL *mysql) {// SQL查询语句:查询所有学生记录char *sql = "select * from stu;";int ret = mysql_query(mysql, sql);if (ret != 0) {printf("mysql query error:%s\n", mysql_error(mysql));return;} // 获取查询结果集MYSQL_RES *res = mysql_store_result(mysql);if (res == NULL) {printf("mysql store result error:%s\n", mysql_error(mysql));return;} // 获取行数和列数int row = mysql_num_rows(res);int col = mysql_num_fields(res);// 打印表头printf("%10s%10s%10s\n", "ID", "年龄", "姓名");// 遍历并打印每一行数据for (int i = 0; i < row; i++) {MYSQL_ROW row_data = mysql_fetch_row(res);for (int i = 0; i < col; i++) {printf("%10s", row_data[i]);} printf("\n");} // 释放结果集内存mysql_free_result(res);return;
    } int main()
    {// 初始化MySQL句柄MYSQL *mysql = mysql_init(NULL);if (mysql == NULL) {printf("init mysql handle failed!\n");return -1;}// 连接MySQL数据库if (mysql_real_connect(mysql, HOST, USER, PASSWD, DBNAME, 0, NULL, 0) == NULL) {printf("mysql connect error:%s\n", mysql_error(mysql));return -1;}// 设置字符集为UTF-8mysql_set_character_set(mysql, "utf8");// 测试添加功能printf("===================== add =========================\n");add(mysql);get(mysql);// 测试修改功能printf("===================== mod =========================\n");mod(mysql);get(mysql);// 测试删除功能printf("===================== del =========================\n");del(mysql);get(mysql);// 关闭数据库连接mysql_close(mysql);return 0;
    }
    
    [zsc@node test_mysql]$ g++ test_mysql.cpp -o test_mysql -L/usr/lib64/mysql -
    lmysqlclient
    [zsc@node test_mysql]$ ./test_mysql 
    ===================== add =========================ID 年龄 姓名11 18 张三12 17 李四
    ===================== mod =========================ID 年龄 姓名11 15 张三12 17 李四
    ===================== del =========================ID 年龄 姓名12 17 李四
    

2.5 前端知识

2.5.1 HTML基础
  • 认识HTML标签

    HTML 代码是由 “标签” 构成的,形如:

    <body>hello</body>
    
    • 标签名(body)放到 < > 中

    • 大部分标签成对出现,<body> 为开始标签,</body> 为结束标签

    • 少数标签只有开始标签,称为 “单标签”

    • 开始标签和结束标签之间写的是标签的内容(hello)

    • 开始标签中可能会带有 “属性”,id 属性相当于给这个标签设置了一个唯一的标识符(身份证号码),形如:

      <body id="myId">hello</body>
      
  • HTML文件基本结构

    <html><head><title>第一个页面</title></head><body>hello world</body>
    </html>
    
    • html 标签是整个 html 文件的根标签(最顶层标签)

    • head 标签中写页面的属性

    • body 标签中写的是页面上显示的内容

    • title 标签中写的是页面的标题

    • 标签层次结构:

      • head 和 body 是 html 的子标签(html 就是 head 和 body 的父标签)
      • title 是 head 的子标签,head 是 title 的父标签
      • head 和 body 之间是兄弟关系
    • 标签之间的结构关系,构成了一个DOM树。

      在这里插入图片描述

  • HTML常见标签

    • 注释标签:<!-- -->

      <!-- 我是注释 -->
      

      ctrl + / 快捷键可以快速进行注释/取消注释

    • 标题标签:<h1></h1>

      有六个, 从 h1 - h6,数字越大,则字体越小。

      <h1>hello</h1>
      <h2>hello</h2>
      <h3>hello</h3>
      <h4>hello</h4>
      <h5>hello</h5>
      <h6>hello</h6>
      
    • 段落标签:<p> </p>

      p标签表示一个段落

      <p>这是一个段落</p>
      
      • p 标签之间存在一个空隙
      • 当前的 p 标签描述的段落, 前面还没有缩进
      • 自动根据浏览器宽度来决定排版
      • html 内容首尾处的换行,空格均无效
      • 在 html 中文字之间输入的多个空格只相当于一个空格
      • html 中直接输入换行不会真的换行,而是相当于一个空格
    • 换行标签:<br/>

      • br 是 break 的缩写,表示换行
      • br 是一个单标签(不需要结束标签)
      • br 标签不像 p 标签那样带有一个很大的空隙
    • 格式化标签:(了解)

      • 加粗: strong 标签 和 b 标签

      • 倾斜: em 标签 和 i 标签

      • 删除线: del 标签 和 s 标签

      • 下划线: ins 标签 和 u 标签

      • <strong>strong 加粗</strong>
        <b>b 加粗</b>
        <em>倾斜</em>
        <i>倾斜</i>
        <del>删除线</del>
        <s>删除线</s>
        <ins>下划线</ins>
        <u>下划线</u>
        
    • 图片标签:<img src="图片路径">

      img 标签必须带有 src 属性, 表示图片的路径

      <img src="rose.jpg">
      
      • 此时要把 rose.jpg 这个图片文件放到和 html 中的同级目录中

      • img 标签的其他属性:

        • alt 是替换文本,当图片不能正确显示时会显示替换文字;

        • title 是提示文本,鼠标悬停在图片上时会出现提示;

        • width/height 控制宽度和高度,通常只需调整其中一个,另一个会等比例缩放,否则可能导致图片失衡;

        • border 设定边框宽度(以像素为单位),但一般建议使用 CSS 控制。

        • 注意:1. 属性可以有多个,但必须写在标签内;2. 属性之间用空格(或换行)分隔,顺序无关;3. 属性以“键=值”格式表示。

        • <img src="rose.jpg" alt="鲜花" title="这是一朵鲜花" width="500px" height="800px"
          border="5px">
          
    • 超链接标签:<a href="网址">链接名</a>

      • href:必须具备,表示点击后会跳转到哪个页面。

      • target:打开方式,默认是 _self,如果是 _blank 则用新的标签页打开

      • <a href="http://www.baidu.com">百度</a>
        
      • 如果是网站内部页面之间的连接,直接写文件的相对路径即可。

      • 其他链接形式:

        • 空链接:使用 # 在 href 中占位;<a href="#">空链接</a>

        • 下载链接: href 对应的路径是一个文件 (可以使用 zip 文件);<a href="test.zip">下载文件</a>

        • 网页元素链接:可以给图片等任何元素添加链接(把元素放到 a 标签中);

          <a href="http://www.sogou.com"><img src="rose.jpg" alt="">
          </a>
          
        • 锚点链接:可以快速定位到页面中的某个位置

          <a href="#one">第一集</a>
          <a href="#two">第二集</a>
          <a href="#three">第三集</a>
          <p id="one">第一集剧情 <br>第一集剧情 <br>...
          </p>
          <p id="two">第二集剧情 <br>第二集剧情 <br>...
          </p>
          <p id="three">第三集剧情 <br>第三集剧情 <br>...
          </p>
          
    • 表单标签:

      • 表单是让用户输入信息的重要途径,分成两个部分:

        • 表单域是包含表单元素的区域,重点是 form 标签;

        • 表单控件包括输入框、提交按钮等,重点是 input 标签。

      • form标签

        <form action="test.html">... [form 的内容]
        </form>
        

        描述了要把数据按照什么方式,提交到哪个页面中。(表单控件存放的位置)

      • input标签

        • 各种输入控件,单行文本框、按钮、单选框、复选框。

        • type(必须有),取值种类很多,如 buttoncheckboxtextfileimagepasswordradio 等;

        • name:给 input 起了个名字,尤其是对于单选按钮,具有相同的 name 才能多选一;

        • valueinput 中的默认值;checked:默认被选中(用于单选按钮和多选按钮);

        • maxlength:设定最大长度。

        • 文本框:

          <input type="text">
          
        • 密码框:

          <input type="password">
          
        • 单选框:

          性别: 
          <input type="radio" name="sex"><input type="radio" name="sex" checked="checked">
        • 复选框:

          爱好:
          <input type="checkbox"> 吃饭 <input type="checkbox"> 睡觉 <input type="checkbox">打游戏
          
        • 普通按钮:

          <input type="button" value="我是个按钮" onclick="alert('hello')">
          
        • 提交按钮:

          <form action="test.html"><input type="text" name="username"><input type="submit" value="提交">
          </form>
          

          提交按钮必须放到 form 标签内,点击后就会尝试给服务器发送。

        • 清空按钮:

          <form action="test.html"><input type="text" name="username"><input type="submit" value="提交"><input type="reset" value="清空">
          </form>
          

          清空按钮必须放在 form 中,点击后会将 form 内所有的用户输入内容重置。

        • 选择文件:

          <input type="file">
          

          点击选择文件,会弹出对话框,选择文件。

    • 无语义标签:div&span

      div 标签,division 的缩写,含义是分割;

      span 标签,含义是跨度。

      它们就是两个盒子,用于网页布局。

      div 是独占一行的,是一个大盒子;span 不独占一行,是一个小盒子。

      <div><span>咬人猫</span><span>咬人猫</span><span>咬人猫</span>
      </div>
      <div><span>兔总裁</span><span>兔总裁</span><span>兔总裁</span>
      </div>
      <div><span>阿叶君</span><span>阿叶君</span><span>阿叶君</span>
      </div>
      

2.5.2 CSS基础
  • 认识CSS

    层叠样式表 (Cascading Style Sheets)。

    CSS 能够对网页中元素位置的排版进行像素级精确控制,实现美化页面的效果,能够做到页面的样式和结构分离。

  • 基本语法规范

    • 在style标签中使用选择器 + {一条/N条声明}

      • 选择器决定针对谁修改 (找谁)
      • 声明决定修改啥 (干啥)
      • 声明的属性是键值对,使用 ; 区分键值对,使用 : 区分键和值。
      • 注意:CSS 要写到 style 标签中,style 标签可以放到页面任意位置,一般放到 head 标签内,CSS 使用 /* */ 作为注释 (使用 ctrl + / 快速切换)。
    • <style>p {/* 设置字体颜色 */color: red;/* 设置字体大小 */font-size: 30px;}
      </style>
      <p>hello</p>
      
  • 基础选择器

    • 标签选择器:

      <style>
      p {color: red;
      }
      div {color: green;
      }
      </style>
      <p>咬人猫</p>
      <p>咬人猫</p>
      <p>咬人猫</p>
      <div>阿叶君</div>
      <div>阿叶君</div>
      <div>阿叶君</div>
      
    • 类选择器:

      <style>.blue {color: blue;}
      </style>
      <div class="blue">咬人猫</div>
      <div>咬人猫</div
      

      语法细节:

      • 类名用 . 开头的
      • 下方的标签使用 class 属性来调用
      • 一个类可以被多个标签使用,一个标签也能使用多个类(多个类名要使用空格分割,这种做法可以让代码更好复用)
      • 如果是长的类名,可以使用 - 分割。
      • 不要使用纯数字,或者中文,以及标签名来命名类名
    • id选择器:

      • CSS 中使用 # 开头表示 id 选择器
      • id 选择器的值和 html 中某个元素的 id 值相同
      • html 的元素 id 不必带 #
      • id 是唯一的, 不能被多个标签使用 (是和 类选择器 最大的区别)
      <style>#ha {color: red;}
      </style>
      <div id="ha">蛤蛤蛤</div>
      
    • 通配符选择器:

      使用 * 的定义,选取所有的标签

      * {
      color: red;
      }
      

2.5.3 JavaScript基础
  • 认识JavaScript

    • JavaScript(简称JS)是世界上最流行的编程语言之一,是一个脚本语言,通过解释器运行,主要在客户端(浏览器)上运行,现在也可以基于Node.js在服务器端运行。JavaScript能做的事情包括网页开发(更复杂的特效和用户交互)、网页游戏开发、服务器开发(Node.js)、桌面程序开发(Electron,VSCode就是这么来的)以及手机App开发。
    • 浏览器分成渲染引擎 + JS 引擎。渲染引擎:解析 html + CSS,俗称"内核"。JS 引擎:也就是 JS 解释器,典型的就是 Chrome 中内置的 V8。JS 引擎逐行读取 JS 代码内容,然后解析成二进制指令,再执行
  • JavaScript的书写形式

    • 行内式:

      直接嵌入到html元素内部

      <input type="button" value="点我一下" onclick="alert('haha')">
      
    • 内嵌式:

      写到script标签中,切记script标签要放到语法树的最后,通常是body的下面。因为页面是先渲染再运行脚本。

      <body><h1 id="h1_title">hello world</h1><from action='http://43.138.218.166:8888/login' method='post'><input type='text' id='username' name='username'><input type='password' id='password' name='password'><input type='submit' id='submit' name='submit' value='提交'></from><!--要运行脚本的按钮,设置点击事件,点击则运行脚本中的test函数--><button onclick="test()">普通的按钮</button>
      </body>
      <script>//function代表定义一个函数function test(){//var代表定义一个动态类型的变量var h1 = document.getElementById("h1_title");//通过id获取标签值传给h1h1.innerHTML = "HELLO WORLD";//通过innerHTML来修改标签的值var input1 = document.getElementById("username");//通过id获取表单控件值传给input1input.value = "";//对于表单控件类型的变量直接使用.value来修改控件的值}
      </script>
      
    • 外部式:

      写到单独的 .js 文件中, 这种情况下 script 标签中间不能写代码,必须空着(写了代码也不会执行)。

      <script src="hello.js"></script>
      
      alert("hehe");
      
    • 注释和C语言一样


2.5.4 AJAX基础
  • ajax使用的是jequery的。ajax用于异步的http客户端向服务器发送http请求的,作用在script标签上。ajax规定响应链接和来源链接必须相同。ajax传输json对象。

  • 语法格式

    $.ajax({

    • 类型
    • 地址
    • 数据
    • 请求成功的回调函数,回调函数固定有三个参数,res是响应正文,statu是状态(t/f),xhr是详细的响应信息
    • 请求失败的回调函数,回调函数固定有三个参数,xhr是详细的响应信息,statu是状态(t/f),error

    });

<body><h1 id="h1_title">hello world</h1><from action='http://43.138.218.166:8888/login' method='post'><input type='text' id='username' name='username'><input type='password' id='password' name='password'><input type='submit' id='submit' name='submit' value='提交'></from><!--要运行脚本的按钮,设置点击事件,点击则运行脚本中的test函数--><button onclick="test()">普通的按钮</button>
</body>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script>//function代表定义一个函数function test(){//var代表定义一个动态类型的变量,这里是一个json对象var login_info = {username:document.getElementById("username").value,password:document.getElementById("password").value}//ajax$.ajax({type:"post",url:"http://43.138.218.166:8888/login",data:JSON.stringify(login_info),success:function(res,statu,xhr){alert(res);},error:function(xhr){alert(JSON.stringify(xhr));}});}
</script>

3. 项目结构设计

3.1 项目模块划分

项目的实现划分为三个大模块来进行:

  • 数据管理模块基于MySQL数据库进行用户数据的管理;
  • 前端交互界面模块基于JS实现前端页面(注册、登录、游戏大厅、游戏房间)的动态控制以及与服务器的通信;
  • 业务处理模块搭建WebSocket服务器与客户端进行通信,接收请求并进行业务处理。【完成在线五子棋对战功能的核心模块】

3.2 业务处理模块的子模块划分

  • 网络通信模块:基于websocketpp库实现Http&WebSocket服务器的搭建,提供网络通信功能;
  • 会话管理模块:对客户端的连接进行cookie&session管理,实现http短连接时客户端身份识别功能;
  • 在线管理模块:对进入游戏大厅与游戏房间中的用户进行管理,提供用户是否在线以及获取用户连接的功能;
  • 房间管理模块:为匹配成功的用户创建对战房间,提供实时的五子棋对战与聊天业务功能;
  • 用户匹配模块:根据天梯分数不同进行不同层次的玩家匹配,为匹配成功的玩家创建房间并加入房间。

3.3 项目流程图

  • 玩家用户角度流程图:

  • 在这里插入图片描述

  • 服务器流程结构图

  • 在这里插入图片描述


4. 项目实现

4.1 实用工具类模块实现

  • logger.hpp

    #ifndef __M_LOGGER_H__
    #define __M_LOGGER_H__
    #include<stdio.h>
    #include<time.h>
    #include<string.h>#define INF 0
    #define DBG 1
    #define ERR 2
    #define DEFAULT_LOG_LEVEL INF//宏定义日志工具:实现程序日志的打印
    //1.根据日志等级判断是否需要输出到标准输出流
    //2.通过时间戳等函数获得当地时间
    //3.通过宏定义将输出的日志信息借助fprintf按照指定格式输出到标准输出流中
    //注:使用换行符时尽量避免空格注释等
    #define LOG(level,format,...) do{\if(DEFAULT_LOG_LEVEL > level)break;\time_t t = time(NULL);\struct tm *lt = localtime(&t);\char buf[32]={0};\strftime(buf,31,"%H:%M:%S",lt);\fprintf(stdout,"%s %s:%d" format "\n",buf,__FILE__,__LINE__,##__VA_ARGS__);\
    }while(0)//设置等级日志
    #define ILOG(format,...) LOG(INF,format,##__VA_ARGS__)
    #define DLOG(format,...) LOG(DBG,format,##__VA_ARGS__)
    #define ELOG(format,...) LOG(ERR,format,##__VA_ARGS__)#endif
    
  • util.hpp

    #ifndef __M_UTIL_H__
    #define __M_UTIL_H__
    #include "logger.hpp"
    #include <iostream>
    #include <sstream>
    #include <fstream>
    #include <string>
    #include <memory>
    #include <vector>
    #include <cstdint>
    #include <jsoncpp/json/json.h>
    #include <mysql/mysql.h>
    #include<unordered_map>
    #include<mutex>#include <websocketpp/server.hpp>
    //WebSocket++ 的核心服务端实现,提供了 WebSocket 服务端的基本功能(如连接管理、消息处理等)。
    #include <websocketpp/config/asio_no_tls.hpp>
    //指定使用 Asio 作为网络底层库(Boost.Asio 或 Standalone Asio),并禁用 TLS/SSL 加密(no_tls 表示不启用 HTTPS/WSS)。
    typedef websocketpp::server<websocketpp::config::asio> wsserver_t;
    //这是 WebSocket++ 的模板类,它表示一个基于 Asio 的非加密 WebSocket 服务端实例,重命名为wsserver_t//这是一个工具类文件,包含工程中其他文件所需要的头文件和函数
    //为了让外界使用成员函数时不用实例化对象,因此将成员函数都设置为静态//mysql_util:数据库的连接&初始化,句柄的销毁,语句的执行
    //初始化过程:
    //1.初始化mysql操作句柄
    //2.连接mysql服务器
    //3.设置客户端的字符集
    //4.选择想要操作的数据库(只有一个就不实现了)
    //语句的执行(不包含查询,查询需要将结果保存到本地,获取结果条数,遍历结果集)
    //句柄的销毁class mysql_util{public://初始化过程:static MYSQL *mysql_create(const std::string &host,const std::string &username,const std::string &password,const std::string &dbname,uint16_t port = 3306){//1.初始化mysql操作句柄MYSQL *mysql = mysql_init(NULL);if(mysql == NULL){ELOG("mysql init failed!");return NULL;}//2.连接mysql服务器if(mysql_real_connect(mysql,host.c_str(),username.c_str(),password.c_str(),dbname.c_str(),port,NULL,0) == NULL){ELOG("connect mysql server failed : %s",mysql_error(mysql));//如果连接失败直接就把mysql句柄销毁了mysql_close(mysql);return NULL;}//3.设置客户端的字符集if(mysql_set_character_set(mysql,"utf8")!= 0){ELOG("set client character failed : %s",mysql_error(mysql));return NULL;}return mysql;}//语句的执行(不包含查询,查询需要将结果保存到本地,获取结果条数,遍历结果集)static bool mysql_exec(MYSQL *mysql,const std::string &sql){int ret = mysql_query(mysql,sql.c_str());if(ret != 0){ELOG("%s\n",sql.c_str());ELOG("mysql query failed : %s\n",mysql_error(mysql));return false;}return true;}//句柄的销毁static void mysql_destroy(MYSQL *mysql){if(mysql != NULL){mysql_close(mysql);}return ;}};//json_util:封装实现json的序列化和反序列化
    //序列化过程:
    //1. 将需要进行序列化的数据,存储在Json::Value 对象中
    //2. 实例化一个StreamWriterBuilder工厂类对象
    //3. 通过StreamWriterBuilder工厂类对象生产一个StreamWriter对象
    //4. 使用StreamWriter对象,对Json::Value中存储的数据进行序列化
    //反序列化过程:
    //1. 实例化一个CharReaderBuilder工厂类对象
    //2. 使用CharReaderBuilder工厂类生产一个CharReader对象
    //3. 定义一个Json::Value对象存储解析后的数据
    //4. 使用CharReader对象进行json格式字符串str的反序列化
    // parse(char *start,  char *end,  Json::Value *val,  string *err);
    //5. 逐个元素去访问Json::Value中的数据
    class json_util{public://序列化过程:static bool serialize(const Json::Value &root,std::string &str){//1. 实例化一个StreamWriterBuilder工厂类对象Json::StreamWriterBuilder swb;//2. 通过StreamWriterBuilder工厂类对象生产一个StreamWriter对象(通过智能指针进行维护)std::unique_ptr<Json::StreamWriter>sw(swb.newStreamWriter());std::stringstream ss;//用于在内存中对字符串进行流式操作的类//3. 使用StreamWriter对象,对Json::Value中存储的数据进行序列化int ret = sw->write(root,&ss);if(ret != 0){ELOG("json serialize failed!!");return false;}str = ss.str();return true;}//反序列化过程:static bool unserialize(const std::string &str,Json::Value &root){//1. 实例化一个CharReaderBuilder工厂类对象Json::CharReaderBuilder crb;//2. 使用CharReaderBuilder工厂类生产一个CharReader对象std::unique_ptr<Json::CharReader> cr(crb.newCharReader());std::string err;//3. 使用CharReader对象进行json格式字符串str的反序列化bool ret = cr->parse(str.c_str(),str.c_str() + str.size(),&root,&err);// parse(char *start,  char *end,  Json::Value *val,  string *err);if(ret == false){ELOG("json unserialize failed: %s",err.c_str());return false;}return true;}
    };//string_util:主要是封装实现字符串的分割功能
    class string_util{public:static int split(const std::string &src,const std::string &sep,std::vector<std::string>&res){size_t pos,idx = 0;while(idx < src.size()){pos = src.find(sep,idx);if(pos == std::string::npos){//在字符串中没有找到间隔字符,跳出循环res.push_back(src.substr(idx));break;}if(pos == idx){//防止有连续间隔字符出现idx += sep.size();continue;}res.push_back(src.substr(idx,pos-idx));idx = pos + sep.size();}return res.size();}
    };//file_util:主要封装了文件数据的读取功能(对于html文件数据进行读取)class file_util {
    public:/*** @brief 读取文件内容到字符串* @param filename 要读取的文件路径* @param body 用于存储文件内容的字符串引用*/static bool read(const std::string &filename, std::string &body) {// 以二进制模式打开文件// std::ios::binary 表示以二进制方式打开,避免文本模式下的字符转换std::ifstream ifs(filename, std::ios::binary);// 检查文件是否成功打开if (ifs.is_open() == false) {// 使用错误日志记录文件打开失败// ELOG可能是项目中自定义的日志宏// filename.c_str() 将C++字符串转换为C风格字符串ELOG("%s file open failed!!", filename.c_str());return false;}// 获取文件大小size_t fsize = 0;// 将文件指针移动到文件末尾ifs.seekg(0, std::ios::end);// 获取当前指针位置(即文件大小)fsize = ifs.tellg();// 将文件指针移回文件开头ifs.seekg(0, std::ios::beg);// 调整body字符串的大小以容纳整个文件内容// resize()会分配足够的空间,但不初始化内容body.resize(fsize);// 读取文件全部内容到body字符串中// &body[0] 获取字符串底层字符数组的首地址// C++11及以上标准保证std::string内存是连续的ifs.read(&body[0], fsize);// 检查读取操作是否成功// good()检查流状态是否正常(无错误)if (ifs.good() == false) {// 记录读取失败日志ELOG("read %s file content failed!", filename.c_str());// 关闭文件流ifs.close();return false;}// 成功读取后关闭文件ifs.close();// 返回成功状态return true;}
    };#endif
    

4.2 数据管理模块实现

  • db.sql

    创建user表,用来记录用户信息 及积分信息

    用户信息:用来实现登录、注册、游戏对战数据管理等功能

    积分信息:用来实现匹配功能

    drop database if exists gobang;
    create database if not exists gobang;
    use gobang;
    create table if not exists user(id int primary key auto_increment,username varchar(32) unique key not null,password varchar(128) not null,score int,total_count int,win_count int 
    );
    

    验证数据库是否创建成功

    sudo mysql -u root -p
    MariaDB [(none)]> show databases;
    +--------------------+
    | Database |
    +--------------------+
    | information_schema |
    | mysql 			|
    | online_gobang 	 |
    | performance_schema |
    | sys 				|
    +--------------------+
    6 rows in set (0.000 sec)
    MariaDB [(none)]> use online_gobang;
    Database changed
    MariaDB [online_gobang]> show tables;
    +-------------------------+
    | Tables_in_online_gobang |
    +-------------------------+
    | user 					|
    +-------------------------+
    1 row in set (0.000 sec)
    MariaDB [online_gobang]> select * from user;
    +----+----------+----------------------------------+-------+-------------+-----
    ------+
    | id | username | password | score | total_count | 
    win_count |
    +----+----------+----------------------------------+-------+-------------+-----
    ------+
    | 1 | xiaobai | 202cb962ac59075b964b07152d234b70 | 1030 | 1 | 1 |
    | 2 | xiaohei | 202cb962ac59075b964b07152d234b70 | 970 | 1 | 0 |
    +----+----------+----------------------------------+-------+-------------+-----
    ------+
    2 rows in set (0.000 sec)
    MariaDB [online_gobang]>
    
  • db.hpp

    数据库中有可能存在很多张表,每张表中管理的数据又有不同,要进行的数据操作也各不相同,因此我们可以为每一张表中的数据操作都设计一个类,通过类实例化的对象来访问这张数据库表中的数据,这样的话当我们要访问哪张表的时候,使用哪个类实例化的对象即可。

    创建user_table类,该类的作用是负责通过MySQL接口管理用户数据,主要提供了四个方法:

    • select_by_name根据用户名查找用户信息,用于实现登录功能;
    • insert新增用户,用户实现注册功能;
    • login登录验证,并获取完整的用户信息;
    • win用于给获胜玩家修改分数;lose用户给失败玩家修改分数。
    #ifndef __M_DB_H__
    #define __M_DB_H__#include "util.hpp"
    #include <cassert>//为数据库中的user_table表创建类来管理
    class user_table{
    private:MYSQL *_mysql;//mysql操作句柄std::mutex _mutex; //互斥锁保护数据库的访问操作public://初始化mysql操作句柄user_table(const std::string &host,const std::string &username,const std::string &password,const std::string &dbname,uint16_t port = 3306){_mysql = mysql_util::mysql_create(host,username,password,dbname,port);assert(_mysql != NULL);}//销毁mysql句柄~user_table(){mysql_util::mysql_destroy(_mysql);_mysql = NULL;}//注册时新增用户bool insert(Json::Value &user){
    #define INSERT_USER "insert user values(null,'%s','%s',1000,0,0);"if(user["password"].isNull() || user["username"].isNull()){DLOG("INPUT PASSWORD OR USERNAME");return false;}char sql[4096] = {0};sprintf(sql,INSERT_USER,user["username"].asCString(),user["password"].asCString());bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false){DLOG("insert user info failed!!\n");return false;}return true;}//登录验证时,并返回详细的用户信息bool login(Json::Value &user){//如果用户信息不存在则返回falseif(user["password"].isNull() || user["username"].isNull()){DLOG("INPUT PASSWORD OR USERNAME");return false;}//以用户名和密码共同作为查询过滤条件,查询到数据则表示用户名密码一致,没有信息则用户名密码错误
    #define LOGIN_USER "select id,score,total_count,win_count from user where username='%s' and password= '%s';"char sql[4096] = {0};sprintf(sql,LOGIN_USER,user["username"].asCString(),user["password"].asCString());MYSQL_RES *res = NULL;//定义一个存储结果的变量{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false){DLOG("user login failed!!\n");return false;}//要么有数据,要么没有数据,就算有数据也只能有一条数据res = mysql_store_result(_mysql);if(res == NULL){DLOG("have no login user info!!");return false;}}//拿到结果之后进行结果条数的获取,和遍历结果集存储到json变量中int row_num = mysql_num_rows(res);if(row_num != 1){   //用户信息如果大于一条,说明用户不唯一了DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stol(row[0]);user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);//不要忘记释放resultreturn true;}//通过用户名获取用户信息bool select_by_name(const std::string &name,Json::Value &user){
    #define USER_BY_NAME "select id,score,total_count,win_count form user where username='%s';"char sql[4096] = {0};sprintf(sql,USER_BY_NAME,name.c_str());//把查询语句整合到sql中MYSQL_RES *res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false) {DLOG("get user by name failed!!\n");return false;}//按理说要么有数据,要么没有数据,就算有数据也只能有一条数据res = mysql_store_result(_mysql);if (res == NULL) {DLOG("have no user info!!");return false;}}int row_num = mysql_num_rows(res);if (row_num != 1) {DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)std::stol(row[0]);user["username"] = name;user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}//通过用户id获取用户信息bool select_by_id(uint64_t id, Json::Value &user) {
    #define USER_BY_ID "select username, score, total_count, win_count from user where id = %ld;"char sql[4096] = {0};sprintf(sql, USER_BY_ID, id);MYSQL_RES *res = NULL;{std::unique_lock<std::mutex> lock(_mutex);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false) {DLOG("get user by id failed!!\n");return false;}//按理说要么有数据,要么没有数据,就算有数据也只能有一条数据res = mysql_store_result(_mysql);if (res == NULL) {DLOG("have no user info!!");return false;}}int row_num = mysql_num_rows(res);if (row_num != 1) {DLOG("the user information queried is not unique!!");return false;}MYSQL_ROW row = mysql_fetch_row(res);user["id"] = (Json::UInt64)id;user["username"] = row[0];user["score"] = (Json::UInt64)std::stol(row[1]);user["total_count"] = std::stoi(row[2]);user["win_count"] = std::stoi(row[3]);mysql_free_result(res);return true;}//胜利时天梯分数增加30分,战斗场次增加1,胜利场次增加1bool win(uint64_t id){
    #define USER_WIN "update user set score=score+30,total_count=total_count+1,win_count=win_count+1 where id=%ld;"char sql[4096] = {0};sprintf(sql,USER_WIN,id);bool ret = mysql_util::mysql_exec(_mysql,sql);if(ret == false){DLOG("update win user info failed!!\n");return false;}return true;}//失败时天梯分数减少30,战斗场次增加1,其他不变bool lose(uint64_t id){
    #define USER_LOSE "update user set score=score-30, total_count=total_count+1 where id=%ld;"char sql[4096] = {0};sprintf(sql, USER_LOSE, id);bool ret = mysql_util::mysql_exec(_mysql, sql);if (ret == false) {DLOG("update lose user info failed!!\n");return false;}return true;    }};#endif
    

4.3 在线用户管理模块实现

  • online.hpp

    在线用户管理是对于当前游戏大厅和游戏房间中的用户进行管理,主要是建立起用户与Socket连接的映射关系,这个模块具有两个功能:

    1. 能够让程序中根据用户信息,进而找到能够与用户客户端进行通信的Socket连接,进而实现与客户端的通信;2. 判断一个用户是否在线,或者判断用户是否已经掉线。
    #ifndef __M_ONLINE_H__
    #define __M_ONLINE_H__
    #include"util.hpp"class online_manager{private:std::mutex _mutex;//用于建立游戏大厅用户的用户ID与通信连接的关系std::unordered_map< uint64_t, wsserver_t::connection_ptr > _hall_user;//用于建立游戏房间用户的用户ID与通信连接的关系std::unordered_map<uint64_t,wsserver_t::connection_ptr> _room_user;public://websocket连接建立的时候才会加入游戏大厅&游戏房间在线用户管理void enter_game_hall(uint64_t uid,   wsserver_t::connection_ptr &conn) {std::unique_lock<std::mutex> lock(_mutex);//用RALL包装类实例化lock对象来维护互斥锁_mutex_hall_user.insert(std::make_pair(uid, conn));//创建一个 std::pair 对象,将 uid 和 conn 组合成一个键值对。}void enter_game_room(uint64_t uid,   wsserver_t::connection_ptr &conn) {std::unique_lock<std::mutex> lock(_mutex);_room_user.insert(std::make_pair(uid, conn));}//websocket连接断开的时候,才会移除游戏大厅&游戏房间在线用户管理void exit_game_hall(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);_hall_user.erase(uid);}void exit_game_room(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);_room_user.erase(uid);}//判断当前指定用户是否在游戏大厅/游戏房间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;}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 wsserver_t::connection_ptr();}return it->second;//返回迭代器 it 指向的键值对中的 connection_ptr(即 uid 对应的WebSocket连接)。}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 wsserver_t::connection_ptr();}return it->second;}};#endif
    

4.4 游戏房间管理模块

  • room.hpp

    首先,需要设计一个房间类,能够实现房间的实例化,房间类主要是对匹配成对的玩家建立一个小范围的关联关系,一个房间中任意一个用户发生的任何动作,都会被广播给房间中的其他用户。而房间中的动作主要包含两类:1. 棋局对战;2. 实时聊天。然后再设计一个房间管理类实现对所有的游戏房间进行管理。

    #ifndef __M_ROOM_H__
    #define __M_ROOM_H__#include "util.hpp"
    #include "logger.hpp"
    #include "online.hpp"
    #include "db.hpp"// 前向声明 room 类,用于解决循环依赖问题
    class room;// 定义智能指针类型,方便管理room对象生命周期
    using room_ptr = std::shared_ptr<room>;// 定义棋盘的行数和列数(15x15的棋盘)
    #define BOARD_ROW 15
    #define BOARD_COL 15
    // 定义棋子颜色常量:白色棋子为1,黑色棋子为2
    #define CHESS_WHITE 1
    #define CHESS_BLACK 2
    // 定义房间状态枚举:游戏开始和游戏结束两种状态
    typedef enum { GAME_START, GAME_OVER } room_statu;/*** @brief 游戏房间类,管理五子棋对局的所有逻辑*/
    class room {
    private:uint64_t _room_id;                // 房间唯一IDroom_statu _statu;                // 房间当前状态int _player_count;                // 房间内玩家数量uint64_t _white_id;               // 白棋玩家IDuint64_t _black_id;               // 黑棋玩家IDuser_table* _tb_user;             // 用户数据表操作指针online_manager* _online_user;     // 在线用户管理指针std::vector<std::vector<int>> _board;  // 15x15的棋盘,0表示空,1表示白棋,2表示黑棋private:/*** @brief 检查是否五子连珠* @param row 当前落子的行坐标* @param col 当前落子的列坐标* @param row_off 行方向偏移量(-1,0,1)* @param col_off 列方向偏移量(-1,0,1)* @param color 棋子颜色* @return 如果五子连珠返回true,否则false*/bool five(int row, int col, int row_off, int col_off, int color) {int count = 1;  // 当前棋子本身计数为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;}// 如果同色棋子数大于等于5,则返回truereturn (count >= 5);}/*** @brief 检查当前落子是否获胜* @param row 行坐标* @param col 列坐标* @param color 棋子颜色* @return 如果获胜返回获胜玩家ID,否则返回0*/uint64_t check_win(int row, int col, int color) {// 检查四个方向是否有五子连珠if (five(row, col, 0, 1, color) ||   // 水平方向five(row, col, 1, 0, color) ||    // 垂直方向five(row, col, -1, 1, color) ||  // 左上到右下five(row, col, -1, -1, color)) {  // 右上到左下return color == CHESS_WHITE ? _white_id : _black_id;}return 0;}public:/*** @brief 构造函数* @param room_id 房间ID* @param tb_user 用户数据表指针* @param online_user 在线用户管理指针*/room(uint64_t room_id, user_table* tb_user, online_manager* online_user):_room_id(room_id), _statu(GAME_START), _player_count(0), _tb_user(tb_user), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0)) {DLOG("%lu 房间创建成功!!", _room_id);}/*** @brief 析构函数*/~room() {DLOG("%lu 房间销毁成功!!", _room_id);}// 以下是各种getter和setter方法uint64_t id() { return _room_id; }room_statu statu() { return _statu; }int player_count() { return _player_count; }void add_white_user(uint64_t uid) { _white_id = uid; _player_count++; }void add_black_user(uint64_t uid) { _black_id = uid; _player_count++; }uint64_t get_white_user() { return _white_id; }uint64_t get_black_user() { return _black_id; }/*** @brief 处理下棋请求* @param req 请求的JSON数据* @return 响应的JSON数据*/Json::Value handle_chess(Json::Value& req) {Json::Value json_resp = req;// 检查白棋玩家是否在线if (!_online_user->is_in_game_room(_white_id)) {json_resp["result"] = true;json_resp["reason"] = "对方掉线,不战而胜!";json_resp["winner"] = (Json::UInt64)_black_id;return json_resp;}// 检查黑棋玩家是否在线if (!_online_user->is_in_game_room(_black_id)) {json_resp["result"] = true;json_resp["reason"] = "对方掉线,不战而胜!";json_resp["winner"] = (Json::UInt64)_white_id;return json_resp;}// 获取落子位置和当前玩家IDint chess_row = req["row"].asInt();int chess_col = req["col"].asInt();uint64_t cur_uid = req["uid"].asUInt64();// 检查该位置是否已有棋子if (_board[chess_row][chess_col] != 0) {json_resp["result"] = false;json_resp["reason"] = "当前位置已有棋子!";return json_resp;}// 根据玩家ID确定棋子颜色int cur_color = (cur_uid == _white_id) ? CHESS_WHITE : CHESS_BLACK;_board[chess_row][chess_col] = cur_color;// 检查是否获胜uint64_t winner_id = check_win(chess_row, chess_col, cur_color);if (winner_id != 0) {json_resp["reason"] = "五星连珠,战无敌!";}json_resp["result"] = true;json_resp["winner"] = (Json::UInt64)winner_id;return json_resp;}/*** @brief 处理聊天请求* @param req 请求的JSON数据* @return 响应的JSON数据*/Json::Value handle_chat(Json::Value& req) {Json::Value json_resp = req;std::string msg = req["message"].asString();uint64_t sender_uid = req["uid"].asUInt64();// 敏感词检测(示例)if (msg.find("垃圾") != std::string::npos || msg.find("sb") != std::string::npos) {json_resp["result"] = false;json_resp["reason"] = "消息包含敏感词!";send_to_user(sender_uid, json_resp);  // 只发给发送者return json_resp;}// 正常消息广播给房间内所有玩家json_resp["result"] = true;broadcast(json_resp);return json_resp;}/*** @brief 处理玩家退出房间* @param uid 退出玩家的ID* @return 响应的JSON数据*/Json::Value handle_exit(uint64_t uid) {Json::Value json_resp;if (_statu == GAME_START) {// 确定获胜者uint64_t winner_id = (uid == _white_id) ? _black_id : _white_id;json_resp["optype"] = "put_chess";json_resp["result"] = true;json_resp["reason"] = "对方掉线,不战而胜!";json_resp["room_id"] = (Json::UInt64)_room_id;json_resp["uid"] = (Json::UInt64)uid;json_resp["row"] = -1;json_resp["col"] = -1;json_resp["winner"] = (Json::UInt64)winner_id;// 更新数据库中的胜负记录uint64_t loser_id = (winner_id == _white_id) ? _black_id : _white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_statu = GAME_OVER;broadcast(json_resp);}_player_count--;return json_resp;}/*** @brief 向指定用户发送消息* @param uid 目标用户ID* @param rsp 要发送的JSON响应*/void send_to_user(uint64_t uid, Json::Value& rsp) {std::string body;json_util::serialize(rsp, body);wsserver_t::connection_ptr conn = _online_user->get_conn_from_room(uid);if (conn) {conn->send(body);} else {DLOG("玩家 %lu 连接不存在", uid);}}/*** @brief 广播消息给房间内所有玩家* @param rsp 要广播的JSON响应* @param target_uid 如果指定则只发给该用户,否则广播给所有人*/void broadcast(Json::Value& rsp, uint64_t target_uid = 0) {if (target_uid != 0) {send_to_user(target_uid, rsp);return;}std::string body;json_util::serialize(rsp, body);// 发送给白棋玩家if (_white_id != 0) {send_to_user(_white_id, rsp);}// 发送给黑棋玩家if (_black_id != 0) {send_to_user(_black_id, rsp);}}/*** @brief 请求处理入口* @param req 请求的JSON数据*/void handle_request(Json::Value& req) {Json::Value json_resp;uint64_t room_id = req["room_id"].asUInt64();// 检查房间ID是否匹配if (room_id != _room_id) {json_resp["optype"] = req["optype"].asString();json_resp["result"] = false;json_resp["reason"] = "房间号不匹配!";broadcast(json_resp);return;}// 根据操作类型分发处理std::string optype = req["optype"].asString();if (optype == "put_chess") {json_resp = handle_chess(req);if (json_resp["winner"].asUInt64() != 0) {// 如果有玩家获胜,更新数据库uint64_t winner_id = json_resp["winner"].asUInt64();uint64_t loser_id = (winner_id == _white_id) ? _black_id : _white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_statu = GAME_OVER;}broadcast(json_resp);}else if (optype == "chat") {handle_chat(req);  // 内部已处理发送}else {json_resp["optype"] = optype;json_resp["result"] = false;json_resp["reason"] = "未知请求类型";broadcast(json_resp);}}
    };/*** @brief 房间管理类,负责创建、查找和删除房间*/
    class room_manager {
    private:uint64_t _next_rid;                             // 下一个房间IDstd::mutex _mutex;                              // 互斥锁,保证线程安全user_table* _tb_user;                           // 用户数据表指针online_manager* _online_user;                   // 在线用户管理指针std::unordered_map<uint64_t, room_ptr> _rooms;  // 房间ID到房间对象的映射std::unordered_map<uint64_t, uint64_t> _users;  // 用户ID到房间ID的映射public:/*** @brief 构造函数* @param ut 用户数据表指针* @param om 在线用户管理指针*/room_manager(user_table* ut, online_manager* om):_next_rid(1), _tb_user(ut), _online_user(om) {DLOG("房间管理器初始化完成");}/*** @brief 析构函数*/~room_manager() {DLOG("房间管理器销毁");}/*** @brief 创建新房间* @param uid1 玩家1的ID* @param uid2 玩家2的ID* @return 创建的房间指针,失败返回nullptr*/room_ptr create_room(uint64_t uid1, uint64_t uid2) {// 检查玩家是否在大厅if (!_online_user->is_in_game_hall(uid1)) {DLOG("玩家 %lu 不在大厅", uid1);return nullptr;}if (!_online_user->is_in_game_hall(uid2)) {DLOG("玩家 %lu 不在大厅", uid2);return nullptr;}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);// 更新映射关系_rooms[_next_rid] = rp;_users[uid1] = _next_rid;_users[uid2] = _next_rid;_next_rid++;return rp;}/*** @brief 通过房间ID获取房间* @param rid 房间ID* @return 房间指针,不存在返回nullptr*/room_ptr get_room_by_rid(uint64_t rid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _rooms.find(rid);return (it != _rooms.end()) ? it->second : nullptr;}/*** @brief 通过用户ID获取房间* @param uid 用户ID* @return 房间指针,不存在返回nullptr*/room_ptr get_room_by_uid(uint64_t uid) {std::unique_lock<std::mutex> lock(_mutex);auto uit = _users.find(uid);if (uit == _users.end()) return nullptr;auto rit = _rooms.find(uit->second);return (rit != _rooms.end()) ? rit->second : nullptr;}/*** @brief 移除指定房间* @param rid 要移除的房间ID*/void remove_room(uint64_t rid) {std::unique_lock<std::mutex> lock(_mutex);auto it = _rooms.find(rid);if (it == _rooms.end()) return;room_ptr rp = it->second;// 移除用户映射_users.erase(rp->get_white_user());_users.erase(rp->get_black_user());// 移除房间_rooms.erase(rid);}/*** @brief 移除房间中的用户* @param uid 要移除的用户ID*/void remove_room_user(uint64_t uid) {room_ptr rp = get_room_by_uid(uid);if (!rp) return;// 处理玩家退出逻辑rp->handle_exit(uid);// 如果房间没有玩家了,则移除房间if (rp->player_count() == 0) {remove_room(rp->id());}}
    };#endif
    

4.5 会话管理模块实现

4.5.1 session介绍
  • 什么是session

    在WEB开发中,HTTP协议是一种无状态短链接的协议,这就导致一个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提供所有服务是不合理的。因此,服务器为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。

  • session工作原理

    在这里插入图片描述


4.5.2 session实现
  • session.hpp

    • 这里我们简单地设计一个session类,但是session对象不能一直存在,这样是一种资源泄漏,因此需要使用定时器对每个创建的session对象进行定时销毁(一个客户端连接断开后,一段时间内都没有重新连接则销毁session)。
    • _ssid使用时间戳填充。实际上,我们通常使用唯一id生成器生成一个唯一的id。
    • _user保存当前用户的信息。
    • timer_ptr _tp保存当前session对应的定时销毁任务。
    • 再创建一个session管理类,session的管理主要包含以下几个点:1. 创建一个新的session;2. 通过ssid获取session;3. 通过ssid判断session是否存在;4. 销毁session;5. 为session设置过期时间,过期后session被销毁。
    #ifndef __M_SS_H__
    #define __M_SS_H__
    #include "util.hpp"typedef enum{UNLOGIN , LOGIN} ss_statu;
    //session类用于保存客户端的用户状态信息
    class session{private:uint64_t _ssid; //sessionIDuint64_t _uid; //session所对应的用户IDss_statu _statu; //用户状态:未登录,已登录wsserver_t::timer_ptr _tp;//session相关的定时器(通过判断session对象是否包含定时器,确定其是否添加定时销毁任务)public://初始化ssid和析构session(uint64_t ssid):_ssid(ssid){DLOG("SESSION %p 被创建!!", this);//打印session对象的地址}~session(){DLOG("SESSION %p 被释放!!", this); }//设置用户状态,用户id,session相关定时器void set_statu(ss_statu statu){_statu = statu;}void set_user(uint64_t uid){_uid = uid;}void set_timer(const wsserver_t::timer_ptr &tp){_tp = tp;}//获取session相关信息uint64_t ssid() { return _ssid; }uint64_t get_user() { return _uid; }wsserver_t::timer_ptr& get_timer() { return _tp; }//判断用户状态bool is_login() { return (_statu == LOGIN); }
    };#define SESSION_TIMEOUT 30000   //session销毁时间
    #define SESSION_FOREVER -1      //session永久存在的标识
    using session_ptr = std::shared_ptr<session>;//通过智能指针来维护session类
    //session_manager类用来管理创建的session类
    class session_manager{private:uint64_t _next_ssid;std::mutex _mutex;std::unordered_map<uint64_t,session_ptr> _session;//通过哈希表来维护ssid和session对象的映射wsserver_t *_server;//定义一个指向WebSocket服务器的指针public:session_manager(wsserver_t *srv):_next_ssid(1),_server(srv){DLOG("session管理器初始化完毕!");}~session_manager(){ DLOG("session管理器即将销毁!");}//创建sessionsession_ptr create_session(uint64_t uid,ss_statu statu){std::unique_lock<std::mutex> lock(_mutex);session_ptr ssp(new session(_next_ssid));//创建一个session对象ssp用智能指针来维护ssp->set_statu(statu);ssp->set_user(uid);_session.insert(std::make_pair(_next_ssid,ssp));_next_ssid++;return ssp;}//添加一个已经存在的sessionvoid append_session(const session_ptr &ssp){std::unique_lock<std::mutex> lock(_mutex);_session.insert(std::make_pair(ssp->ssid(),ssp));}//通过ssid获取session信息session_ptr get_session_by_ssid(uint64_t ssid){std::unique_lock<std::mutex> lock(_mutex);auto it = _session.find(ssid);if (it == _session.end()) {return session_ptr();}return it->second;}//移除sessionvoid remove_session(uint64_t ssid){std::unique_lock<std::mutex> lock(_mutex);_session.erase(ssid);}//设置session定时器void set_session_expire_time(uint64_t ssid,int ms){//依赖于websocketpp的定时器来完成session生命周期的管理。// 登录之后,创建session,session需要在指定时间无通信后删除// 但是进入游戏大厅,或者游戏房间,这个session就应该永久存在// 等到退出游戏大厅,或者游戏房间,这个session应该被重新设置为临时,在长时间无通信后被删除//先获取sessionsession_ptr ssp = get_session_by_ssid(ssid);if(ssp.get() == nullptr){return;}wsserver_t::timer_ptr tp = ssp->get_timer();if(tp.get()==nullptr && ms == SESSION_FOREVER){//1. 在session永久存在的情况下,设置永久存在return;}else if(tp.get()==nullptr && ms != SESSION_FOREVER){//2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务wsserver_t::timer_ptr tmp_tp =_server->set_timer(ms,std::bind(&session_manager::remove_session,this,ssid));ssp->set_timer(tmp_tp);}else if(tp.get()!=nullptr && ms == SESSION_FOREVER){//3. 在session设置了定时删除的情况下,将session设置为永久存在// 删除定时任务--- stready_timer删除定时任务会导致任务(删除一个session)直接被执行tp->cancel();//由于这个取消定时任务并不是立即取消的//因此重新给session管理器中,添加一个session信息, 且添加的时候需要使用定时器,而不是立即添加,这样才能保证代码的顺序执行ssp->set_timer(wsserver_t::timer_ptr());//将session关联的定时器设置为空_server->set_timer(0,std::bind(&session_manager::append_session, this, ssp));}else if(tp.get()!=nullptr && ms != SESSION_FOREVER){//4. 在session设置了定时删除的情况下,将session重置删除时间。tp->cancel();//因为这个取消定时任务并不是立即取消的ssp->set_timer(wsserver_t::timer_ptr());_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));//重新给session添加定时销毁任务wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssp->ssid()));//重新设置session关联的定时器ssp->set_timer(tmp_tp);    }}
    };#endif
    

4.6 用户匹配模块实现

  • match.hpp

    匹配队列实现:

    • 五子棋对战的玩家匹配是根据自己的天梯分数进行匹配的,而服务器中将玩家天梯分数分为三个档次:1. 青铜:天梯分数小于2000分;2. 白银:天梯分数介于2000~3000分之间;3. 黄金:天梯分数大于3000分。
    • 而实现玩家匹配的思想非常简单,为不同的档次设计各自的匹配队列,当一个队列中的玩家数量大于等于2的时候,则意味着同一档次中,有2个及以上的人要进行实战匹配,则出队队列中的前两个用户,相当于队首2个玩家匹配成功,这时候为其创建房间,并将两个用户信息加入房间中。
    #ifndef __M_MATCHER_H__
    #define __M_MATCHER_H__#include "util.hpp"
    #include "online.hpp"
    #include "db.hpp"
    #include "room.hpp"
    #include <list>
    #include <condition_variable>
    template <class T>//双向链表作为一个队列来管理匹配玩家
    class match_queue {private:/*用链表而不直接使用queue是因为我们有中间删除数据的需要*/std::list<T> _list;//列表中存放不同的元素,更改模版的类型/*实现线程安全*/std::mutex _mutex;/*这个条件变量主要为了阻塞消费者,后边使用的时候:队列中元素个数<2则阻塞*/std::condition_variable _cond;//多线程编程中实现线程同步的重要工具public:/*获取元素个数*/int size() {  std::unique_lock<std::mutex> lock(_mutex);return _list.size(); }/*判断是否为空*/bool empty() {std::unique_lock<std::mutex> lock(_mutex);return _list.empty();}/*阻塞线程*/void wait() {std::unique_lock<std::mutex> lock(_mutex);//获取锁_cond.wait(lock);//wait() 调用会自动释放 _mutex,允许其他线程获取该锁,线程状态变为阻塞(blocked)}/*入队数据,并唤醒线程*/void push(const T &data) {std::unique_lock<std::mutex> lock(_mutex);_list.push_back(data);_cond.notify_all();//唤醒一个线程,操作系统将线程状态从阻塞变为就绪,线程被调度器选中后,重新自动获取 _mutex 锁}/*出队数据*/bool pop(T &data) {std::unique_lock<std::mutex> lock(_mutex);if (_list.empty() == true) {return false;}data = _list.front();_list.pop_front();return true;}/*移除指定的数据*/void remove(T &data) {std::unique_lock<std::mutex> lock(_mutex);_list.remove(data);}
    };//匹配处理
    class matcher{private://根据不同的段位分成三个匹配队列match_queue<uint64_t> _q_normal;match_queue<uint64_t> _q_high;match_queue<uint64_t> _q_super;//为三个匹配队列定义三个处理线程std::thread _th_normal;std::thread _th_high;std::thread _th_super;room_manager *_rm;user_table *_ut;online_manager *_om;private://匹配处理函数void handle_match(match_queue<uint64_t> &mq){while(1){//判断队列人数是否大于2,<2则阻塞等待while(mq.size() < 2){mq.wait();}//走到这代表人数够了,出队两个玩家uint64_t uid1,uid2;bool ret = mq.pop(uid1);if(ret == false){continue;}ret = mq.pop(uid2);if(ret == false){//由于队列中没有玩家,导致队列出玩家失败//所以另一个玩家要重新回队列进行匹配this->add(uid1);continue;}//校验两个玩家是否在线,如果有人掉线,则要把另一个人重新添加到匹配队列中wsserver_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);if (conn1.get() == nullptr) {this->add(uid2); continue;}wsserver_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);if (conn2.get() == nullptr) {this->add(uid1); continue;}//为两个玩家创建房间,并将玩家加入房间中room_ptr rp = _rm->create_room(uid1,uid2);if(rp.get() == nullptr){this->add(uid1);this->add(uid2);continue;}//对两个玩家进行相应Json::Value resp;resp["optype"] = "match_success";resp["result"] = true;std::string body;json_util::serialize(resp,body);conn1->send(body);conn2->send(body);}}   //线程入口函数void _th_normal_entry(){return handle_match(_q_normal);}void _th_high_entry(){return handle_match(_q_high);}void _th_super_entry(){return handle_match(_q_super);}public:matcher(room_manager *rm,user_table *ut,online_manager *om):_rm(rm),_ut(ut),_om(om),_th_normal(std::thread(&matcher::_th_normal_entry,this)),_th_high(std::thread(&matcher::_th_high_entry,this)),_th_super(std::thread(&matcher::_th_super_entry,this)){DLOG("游戏匹配模块初始化完毕!");}bool add(uint64_t uid){//根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列//根据用户ID,获取玩家信息Json::Value user;bool ret = _ut->select_by_id(uid,user);if(ret == false){DLOG("获取玩家:%ld 信息失败!!",uid);return false;}int score = user["score"].asInt();//添加到指定队列if (score < 2000) {_q_normal.push(uid);}else if (score >= 2000 && score < 3000) {_q_high.push(uid);}else {_q_super.push(uid);}return true;}bool del(uint64_t uid){Json::Value user;bool ret = _ut->select_by_id(uid, user);if (ret == false) {DLOG("获取玩家:%ld 信息失败!!", uid);return false;}int score = user["score"].asInt();// 2. 添加到指定的队列中if (score < 2000) {_q_normal.remove(uid);}else if (score >= 2000 && score < 3000) {_q_high.remove(uid);}else {_q_super.remove(uid);}return true;}};#endif
    

4.7 网络通信模块实现

  • server.hpp

    服务器模块是对当前所实现的所有模块的一个整合,并进行服务器搭建的一个模块,最终封装实现出一个gobang_server的服务器模块类,向外提供搭建五子棋对战服务器的接口,通过实例化的对象可以简便地完成服务器的搭建。

    #ifndef __M_SRV_H__
    #define __M_SRV_H__
    #include "db.hpp"
    #include "matcher.hpp"
    #include "online.hpp"
    #include "room.hpp"
    #include "session.hpp"
    #include "util.hpp"#define WWWROOT "./wwwroot/"
    class gobang_server{private:std::string _web_root;//静态资源根目录 ./wwwroot/      /register.html ->  ./wwwroot/register.htmlwsserver_t _wssrv;user_table _ut;online_manager _om;room_manager _rm;matcher _mm;session_manager _sm;private://静态资源请求的处理void file_handler(wsserver_t::connection_ptr &conn) {//静态资源请求的处理//1. 获取到请求uri-资源路径,了解客户端请求的页面文件名称websocketpp::http::parser::request req = conn->get_request();std::string uri = req.get_uri();//2. 组合出文件的实际路径   相对根目录 + uristd::string realpath = _web_root + uri;//3. 如果请求的是个目录,增加一个后缀  login.html,    /  ->  /login.htmlif (realpath.back() == '/') {realpath += "login.html";}//4. 读取文件内容Json::Value resp_json;std::string body;bool ret = file_util::read(realpath, body);//  1. 文件不存在,读取文件内容失败,返回404if (ret == false) {body += "<html>";body += "<head>";body += "<meta charset='UTF-8'/>";body += "</head>";body += "<body>";body += "<h1> Not Found </h1>";body += "</body>";conn->set_status(websocketpp::http::status_code::not_found);conn->set_body(body);return;}//5. 设置响应正文conn->set_body(body);conn->set_status(websocketpp::http::status_code::ok);}//针对客户端请求的http回复void http_resp(wsserver_t::connection_ptr &conn, bool result, websocketpp::http::status_code::value code, const std::string &reason) {Json::Value resp_json;resp_json["result"] = result;resp_json["reason"] = reason;std::string resp_body;json_util::serialize(resp_json, resp_body);conn->set_status(code);conn->set_body(resp_body);conn->append_header("Content-Type", "application/json");return;}//注册函数void reg(wsserver_t::connection_ptr &conn) {//用户注册功能请求的处理websocketpp::http::parser::request req = conn->get_request();//1. 获取到请求正文std::string req_body = conn->get_request_body();//2. 对正文进行json反序列化,得到用户名和密码Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false) {DLOG("反序列化注册信息失败");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");}//3. 进行数据库的用户新增操作if (login_info["username"].isNull() || login_info["password"].isNull()) {DLOG("用户名密码不完整");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");}ret = _ut.insert(login_info);if (ret == false) {DLOG("向数据库插入数据失败");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名已经被占用!");}//  如果成功了,则返回200return http_resp(conn, true, websocketpp::http::status_code::ok, "注册用户成功");}//登录函数void login(wsserver_t::connection_ptr &conn) {//用户登录功能请求的处理//1. 获取请求正文,并进行json反序列化,得到用户名和密码std::string req_body = conn->get_request_body();Json::Value login_info;bool ret = json_util::unserialize(req_body, login_info);if (ret == false) {DLOG("反序列化登录信息失败");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");}//2. 校验正文完整性,进行数据库的用户信息验证if (login_info["username"].isNull() || login_info["password"].isNull()) {DLOG("用户名密码不完整");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");}ret = _ut.login(login_info);if (ret == false) {//  1. 如果验证失败,则返回400DLOG("用户名密码错误");return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名密码错误");}//3. 如果验证成功,给客户端创建sessionuint64_t uid = login_info["id"].asUInt64();session_ptr ssp = _sm.create_session(uid, LOGIN);if (ssp.get() == nullptr) {DLOG("创建会话失败");return http_resp(conn, false, websocketpp::http::status_code::internal_server_error , "创建会话失败");}_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);//4. 设置响应头部:Set-Cookie,将sessionid通过cookie返回std::string cookie_ssid = "SSID=" + std::to_string(ssp->ssid());conn->append_header("Set-Cookie", cookie_ssid);return http_resp(conn, true, websocketpp::http::status_code::ok , "登录成功");}//获得Cookie值函数bool get_cookie_val(const std::string &cookie_str, const std::string &key,  std::string &val) {// Cookie: SSID=XXX; path=/; //1. 以 ; 作为间隔,对字符串进行分割,得到各个单个的cookie信息std::string sep = "; ";std::vector<std::string> cookie_arr;string_util::split(cookie_str, sep, cookie_arr);for (auto str : cookie_arr) {//2. 对单个cookie字符串,以 = 为间隔进行分割,得到key和valstd::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2) { continue; }if (tmp_arr[0] == key) {val = tmp_arr[1];return true;}}return false;}//获取用户信息函数void info(wsserver_t::connection_ptr &conn) {//用户信息获取功能请求的处理Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()) {//如果没有cookie,返回错误:没有cookie信息,让客户端重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到cookie信息,请重新登录");}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false) {//cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到ssid信息,请重新登录");}// 2. 在session管理中查找对应的会话信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() == nullptr) {//没有找到session,则认为登录已经过期,需要重新登录return http_resp(conn, true, websocketpp::http::status_code::bad_request, "登录过期,请重新登录");}// 3. 从数据库中取出用户信息,进行序列化发送给客户端uint64_t uid = ssp->get_user();Json::Value user_info;ret = _ut.select_by_id(uid, user_info);if (ret == false) {//获取用户信息失败,返回错误:找不到用户信息return http_resp(conn, true, websocketpp::http::status_code::bad_request, "找不到用户信息,请重新登录");}std::string body;json_util::serialize(user_info, body);conn->set_body(body);conn->append_header("Content-Type", "application/json");conn->set_status(websocketpp::http::status_code::ok);// 4. 刷新session的过期时间_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}//http请求处理函数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();if (method == "POST" && uri == "/reg") {return reg(conn);}else if (method == "POST" && uri == "/login") {return login(conn);}else if (method == "GET" && uri == "/info") {return info(conn);}else {return file_handler(conn);}}//websocket回复函数 void ws_resp(wsserver_t::connection_ptr conn, Json::Value &resp) {std::string body;json_util::serialize(resp, body);conn->send(body);}//通过Cookie获取session会话函数session_ptr get_session_by_cookie(wsserver_t::connection_ptr conn) {Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()) {//如果没有cookie,返回错误:没有cookie信息,让客户端重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到cookie信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false) {//cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到SSID信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}// 2. 在session管理中查找对应的会话信息session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));if (ssp.get() == nullptr) {//没有找到session,则认为登录已经过期,需要重新登录err_resp["optype"] = "hall_ready";err_resp["reason"] = "没有找到session信息,需要重新登录";err_resp["result"] = false;ws_resp(conn, err_resp);return session_ptr();}return ssp;}void wsopen_game_hall(wsserver_t::connection_ptr conn) {//游戏大厅长连接建立成功Json::Value resp_json;//1. 登录验证--判断当前客户端是否已经成功登录session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr) {return;}//2. 判断当前客户端是否是重复登录if (_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())) {resp_json["optype"] = "hall_ready";resp_json["reason"] = "玩家重复登录!";resp_json["result"] = false;return ws_resp(conn, resp_json);}//3. 将当前客户端以及连接加入到游戏大厅_om.enter_game_hall(ssp->get_user(), conn);//4. 给客户端响应游戏大厅连接建立成功resp_json["optype"] = "hall_ready";resp_json["result"] = true;ws_resp(conn, resp_json);//5. 记得将session设置为永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);}void wsopen_game_room(wsserver_t::connection_ptr conn) {Json::Value resp_json;//1. 获取当前客户端的sessionsession_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr) {return;}//2. 当前用户是否已经在在线用户管理的游戏房间或者游戏大厅中---在线用户管理if (_om.is_in_game_hall(ssp->get_user()) || _om.is_in_game_room(ssp->get_user())) {resp_json["optype"] = "room_ready";resp_json["reason"] = "玩家重复登录!";resp_json["result"] = false;return ws_resp(conn, resp_json);}//3. 判断当前用户是否已经创建好了房间 --- 房间管理room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr) {resp_json["optype"] = "room_ready";resp_json["reason"] = "没有找到玩家的房间信息";resp_json["result"] = false;return ws_resp(conn, resp_json);}//4. 将当前用户添加到在线用户管理的游戏房间中_om.enter_game_room(ssp->get_user(), conn);//5. 将session重新设置为永久存在_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);//6. 回复房间准备完毕resp_json["optype"] = "room_ready";resp_json["result"] = true;resp_json["room_id"] = (Json::UInt64)rp->id();resp_json["uid"] = (Json::UInt64)ssp->get_user();resp_json["white_id"] = (Json::UInt64)rp->get_white_user();resp_json["black_id"] = (Json::UInt64)rp->get_black_user();return ws_resp(conn, resp_json);}void wsopen_callback(websocketpp::connection_hdl hdl) {//websocket长连接建立成功之后的处理函数wsserver_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") {//建立了游戏大厅的长连接return wsopen_game_hall(conn);}else if (uri == "/room") {//建立了游戏房间的长连接return wsopen_game_room(conn);}}void wsclose_game_hall(wsserver_t::connection_ptr conn) {//游戏大厅长连接断开的处理//1. 登录验证--判断当前客户端是否已经成功登录session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr) {return;}//1. 将玩家从游戏大厅中移除_om.exit_game_hall(ssp->get_user());//2. 将session恢复生命周期的管理,设置定时销毁_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);}void wsclose_game_room(wsserver_t::connection_ptr conn) {//获取会话信息,识别客户端session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr) {return;}//1. 将玩家从在线用户管理中移除_om.exit_game_room(ssp->get_user());//2. 将session回复生命周期的管理,设置定时销毁_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);//3. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间_rm.remove_room_user(ssp->get_user());}void wsclose_callback(websocketpp::connection_hdl hdl) {//websocket连接断开前的处理wsserver_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") {//建立了游戏大厅的长连接return wsclose_game_hall(conn);}else if (uri == "/room") {//建立了游戏房间的长连接return wsclose_game_room(conn);}}void wsmsg_game_hall(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {Json::Value resp_json;std::string resp_body;//1. 身份验证,当前客户端到底是哪个玩家session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr) {return;}//2. 获取请求信息std::string req_body = msg->get_payload();Json::Value req_json;bool ret = json_util::unserialize(req_body, req_json);if (ret == false) {resp_json["result"] = false;resp_json["reason"] = "请求信息解析失败";return ws_resp(conn, resp_json);}//3. 对于请求进行处理:if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start"){//  开始对战匹配:通过匹配模块,将用户添加到匹配队列中_mm.add(ssp->get_user());resp_json["optype"] = "match_start";resp_json["result"] = true;return ws_resp(conn, resp_json);}else if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_stop") {//  停止对战匹配:通过匹配模块,将用户从匹配队列中移除_mm.del(ssp->get_user());resp_json["optype"] = "match_stop";resp_json["result"] = true;return ws_resp(conn, resp_json);}resp_json["optype"] = "unknow";resp_json["reason"] = "请求类型未知";resp_json["result"] = false;return ws_resp(conn, resp_json);}void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {Json::Value resp_json;//1. 获取客户端session,识别客户端身份session_ptr ssp = get_session_by_cookie(conn);if (ssp.get() == nullptr) {DLOG("房间-没有找到会话信息");return;}//2. 获取客户端房间信息room_ptr rp = _rm.get_room_by_uid(ssp->get_user());if (rp.get() == nullptr) {resp_json["optype"] = "unknow";resp_json["reason"] = "没有找到玩家的房间信息";resp_json["result"] = false;DLOG("房间-没有找到玩家房间信息");return ws_resp(conn, resp_json);}//3. 对消息进行反序列化Json::Value req_json;std::string req_body = msg->get_payload();bool ret = json_util::unserialize(req_body, req_json);if (ret == false) {resp_json["optype"] = "unknow";resp_json["reason"] = "请求解析失败";resp_json["result"] = false;DLOG("房间-反序列化请求失败");return ws_resp(conn, resp_json);}DLOG("房间:收到房间请求,开始处理....");//4. 通过房间模块进行消息请求的处理return rp->handle_request(req_json);}void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg) {//websocket长连接通信处理wsserver_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") {//建立了游戏大厅的长连接return wsmsg_game_hall(conn, msg);}else if (uri == "/room") {//建立了游戏房间的长连接return wsmsg_game_room(conn, msg);}}public:/*进行成员初始化,以及服务器回调函数的设置*/gobang_server(const std::string &host,const std::string &user,const std::string &pass,const std::string &dbname,uint16_t port = 3306,const std::string &wwwroot = WWWROOT):_web_root(wwwroot), _ut(host, user, pass, dbname, port),_rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om) {_wssrv.set_access_channels(websocketpp::log::alevel::none);_wssrv.init_asio();_wssrv.set_reuse_addr(true);_wssrv.set_http_handler(std::bind(&gobang_server::http_callback, this, std::placeholders::_1));_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback, this, std::placeholders::_1));_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this, std::placeholders::_1));_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));}/*启动服务器*/void start(int port) {_wssrv.listen(port);_wssrv.start_accept();_wssrv.run();}
    };
    #endif
    

4.8 前端交互界面实现

4.8.1 登录⻚⾯: 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">网络五子棋对战游戏</div><div class="login-container"><!-- 登录界面的对话框 --><div class="login-dialog"><!-- 提示信息 --><h3>登录</h3><!-- 这个表示一行 --><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"><button id="submit" onclick="login()">提交</button></div><!--如果没有账户,则点击注册跳转到注册页面-->><div class="row"><span><a href="http://121.37.46.196:8888/register.html">注册</a></span></div></div></div><script src="./js/jquery.min.js"></script><script>//1. 给按钮添加点击事件,调用登录请求函数//2. 封装登录请求函数function login() {//  1. 获取输入框中的用户名和密码,并组织json对象var login_info = {username: document.getElementById("user_name").value,password: document.getElementById("password").value};//  2. 通过ajax向后台发送登录验证请求$.ajax({url: "/login",type: "post",data: JSON.stringify(login_info),success: function(result) {//  3. 如果验证通过,则跳转游戏大厅页面alert("登录成功");window.location.assign("/game_hall.html");},error: function(xhr) {//  4. 如果验证失败,则提示错误信息,并清空输入框alert(JSON.stringify(xhr));document.getElementById("user_name").value = "";document.getElementById("password").value = "";}})}</script>
</body>
</html>

4.8.2 注册页面: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>注册</title><link rel="stylesheet" href="./css/common.css"><link rel="stylesheet" href="./css/login.css">
</head>
<body><div class="nav">网络五子棋对战游戏</div><div class="login-container"><!-- 注册界面的对话框 --><div class="login-dialog"><!-- 提示信息 --><h3>注册</h3><!-- 这个表示一行 --><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"><button id="submit" onclick="reg()">提交</button></div><!--如果已有账户,则点击登录跳转到登录页面-->><div class="row"><span><a href="http://121.37.46.196:8888/login.html">登录</a></span></div></div></div> <script src="js/jquery.min.js"></script><script>//1. 给按钮添加点击事件,调用注册函数//2. 封装实现注册函数function reg() {//  1. 获取两个输入框空间中的数据,组织成为一个json串var reg_info = {username: document.getElementById("user_name").value,password: document.getElementById("password").value};console.log(JSON.stringify(reg_info));//  2. 通过ajax向后台发送用户注册请求$.ajax({url : "/reg",type : "post",data : JSON.stringify(reg_info),success : function(res) {if (res.result == false) {//  4. 如果请求失败,则清空两个输入框内容,并提示错误原因document.getElementById("user_name").value = "";document.getElementById("password").value = "";alert(res.reason);}else {//  3. 如果请求成功,则跳转的登录页面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>

4.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">网络五子棋对战游戏</div><!-- 整个页面的容器元素 --><div class="container"><!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 --><div><!-- 展示用户信息 --><div id="screen"></div><!-- 匹配按钮 --><div id="match-button">开始匹配</div></div></div><script src="./js/jquery.min.js"></script><script>var ws_url = "ws://" + location.host + "/hall";var ws_hdl = null;window.onbeforeunload = function() {ws_hdl.close();}//按钮有两个状态:没有进行匹配的状态,正在匹配中的状态var button_flag = "stop";//点击按钮的事件处理:var be = document.getElementById("match-button");be.onclick = function() {if (button_flag == "stop") {//1. 没有进行匹配的状态下点击按钮,发送对战匹配请求var req_json = {optype: "match_start"}ws_hdl.send(JSON.stringify(req_json));}else {//2. 正在匹配中的状态下点击按钮,发送停止对战匹配请求var req_json = {optype: "match_stop"}ws_hdl.send(JSON.stringify(req_json));}}function get_user_info() {$.ajax({url: "/info",type: "get",success: function(res) {var info_html = "<p>" + "用户:" + res.username + " 积分:" + res.score + "</br>" + "比赛场次:" + res.total_count + " 获胜场次:" + res.win_count + "</p>";var screen_div = document.getElementById("screen");screen_div.innerHTML = info_html;ws_hdl = new WebSocket(ws_url);ws_hdl.onopen = ws_onopen;ws_hdl.onclose = ws_onclose;ws_hdl.onerror = ws_onerror;ws_hdl.onmessage = ws_onmessage;},error: function(xhr) {alert(JSON.stringify(xhr));location.replace("/login.html");}})}function ws_onopen() {console.log("websocket onopen");}function ws_onclose() {console.log("websocket onopen");}function ws_onerror() {console.log("websocket onopen");}function ws_onmessage(evt) {var rsp_json = JSON.parse(evt.data);if (rsp_json.result == false) {alert(evt.data);location.replace("/login.html");return;}if (rsp_json["optype"] == "hall_ready") {alert("游戏大厅连接建立成功!");}else if (rsp_json["optype"] == "match_success") {//对战匹配成功alert("对战匹配成功,进入游戏房间!");location.replace("/game_room.html");}else if (rsp_json["optype"] == "match_start") {console.log("玩家已经加入匹配队列");button_flag = "start";be.innerHTML = "匹配中....点击按钮停止匹配!";return;}else if (rsp_json["optype"] == "match_stop"){console.log("玩家已经移除匹配队列");button_flag = "stop";be.innerHTML = "开始匹配";return;}else {alert(evt.data);location.replace("/login.html");return;}}get_user_info();</script>
</body>
</html>

4.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">网络五子棋对战游戏</div><div class="container"><div id="chess_area"><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvas id="chess" width="450px" height="450px"></canvas><!-- 显示当前回合状态 --><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><p id="peer_msg">leihoua~</p></br></div><div id="msg_show"><input type="text" id="chat_input"><button id="chat_button">发送</button></div></div></div><script>let chessBoard = []; // 15x15的二维数组,记录棋盘状态let BOARD_ROW_AND_COL = 15; // 棋盘行列数let chess = document.getElementById('chess');let context = chess.getContext('2d'); // 获取canvas的2D绘图上下文var ws_url = "ws://" + location.host + "/room"; // WebSocket连接地址var ws_hdl = new WebSocket(ws_url); // WebSocket连接对象var room_info = null; // 保存房间信息(房间ID、玩家ID等)var is_me; // 标记当前是否轮到自己走棋/*** 初始化游戏:绘制棋盘和背景*/function initGame() {initBoard(); // 初始化棋盘数组context.strokeStyle = "#BFBFBF"; // 设置棋盘线颜色// 加载背景图片let logo = new Image();logo.src = "image/sky.jpeg";logo.onload = function () {context.drawImage(logo, 0, 0, 450, 450); // 绘制背景drawChessBoard(); // 绘制棋盘网格}}/*** 初始化棋盘数组(全部置0表示空位)*/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; // 0表示空,1表示黑棋,2表示白棋(根据实际需求调整)}}}/*** 绘制15x15的棋盘网格线*/function drawChessBoard() {for (let i = 0; i < BOARD_ROW_AND_COL; i++) {// 画横线context.moveTo(15 + i * 30, 15);context.lineTo(15 + i * 30, 435);context.stroke();// 画竖线context.moveTo(15, 15 + i * 30);context.lineTo(435, 15 + i * 30);context.stroke();}}/*** 在指定位置绘制棋子* @param {number} i - 列坐标(0-14)* @param {number} j - 行坐标(0-14)* @param {boolean} isWhite - 是否为白棋*/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();// 创建渐变效果(增强棋子立体感)var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13,15 + i * 30 + 2, 15 + j * 30 - 2, 0);// 根据棋子颜色设置渐变if (!isWhite) {gradient.addColorStop(0, "#0A0A0A"); // 黑棋gradient.addColorStop(1, "#636766");} else {gradient.addColorStop(0, "#D1D1D1"); // 白棋gradient.addColorStop(1, "#F9F9F9");}context.fillStyle = gradient;context.fill();}// 棋盘点击事件:处理玩家走棋chess.onclick = function (e) {if (!is_me) {alert("等待对方走棋....");return;}// 计算点击位置对应的棋盘坐标let x = e.offsetX;let y = e.offsetY;let col = Math.floor(x / 30); // 列let row = Math.floor(y / 30); // 行if (chessBoard[row][col] != 0) {alert("当前位置已有棋子!");return;}send_chess(row, col); // 发送走棋请求};/*** 发送走棋请求到服务器* @param {number} r - 行坐标* @param {number} c - 列坐标*/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));}// 页面关闭前关闭WebSocket连接window.onbeforeunload = function() {ws_hdl.close();};// WebSocket事件处理ws_hdl.onopen = function() {console.log("房间长连接建立成功");};ws_hdl.onclose = function() {console.log("房间长连接断开");};ws_hdl.onerror = function() {console.log("房间长连接出错");};/*** 更新界面提示信息(轮到谁走棋)* @param {boolean} me - 是否轮到自己*/function set_screen(me) {var screen_div = document.getElementById("screen");screen_div.innerHTML = me ? "轮到己方走棋..." : "轮到对方走棋...";}// 处理服务器消息ws_hdl.onmessage = function(evt) {var info = JSON.parse(evt.data);console.log("收到消息:", info);// 1. 房间准备就绪if (info.optype == "room_ready") {room_info = info;// 修正问题1:黑棋(black_id)先手is_me = (room_info.uid == room_info.black_id); // 自己是黑棋则先手set_screen(is_me);initGame();} // 2. 处理走棋消息else if (info.optype == "put_chess") {if (info.result == false) {alert(info.reason);return;}// 修正问题2:更新回合提示// 如果当前下棋的是对方,则下一手轮到自己is_me = (info.uid != room_info.uid);set_screen(is_me);// 绘制棋子(根据下棋者身份决定颜色)var isWhite = (info.uid == room_info.white_id);if (info.row != -1 && info.col != -1) {oneStep(info.col, info.row, isWhite);chessBoard[info.row][info.col] = isWhite ? 2 : 1; // 更新棋盘状态}// 处理游戏结束if (info.winner != 0) {var screen_div = document.getElementById("screen");screen_div.innerHTML = (room_info.uid == info.winner) ? "你赢了!" : "你输了";// 添加返回大厅按钮var button_div = document.createElement("div");button_div.innerHTML = "返回大厅";button_div.onclick = function() {ws_hdl.close();location.replace("/game_hall.html");};document.getElementById("chess_area").appendChild(button_div);}} // 3. 处理聊天消息else if (info.optype == "chat") {if (info.result == false) {alert(info.reason);return;}var msg_div = document.createElement("p");msg_div.innerHTML = info.message;msg_div.setAttribute("id", info.uid == room_info.uid ? "self_msg" : "peer_msg");var chat_show_div = document.getElementById("chat_show");chat_show_div.appendChild(msg_div);chat_show_div.appendChild(document.createElement("br"));document.getElementById("chat_input").value = ""; // 清空输入框}};// 聊天发送按钮事件document.getElementById("chat_button").onclick = function() {var send_msg = {optype: "chat",room_id: room_info.room_id,uid: room_info.uid,message: document.getElementById("chat_input").value};ws_hdl.send(JSON.stringify(send_msg));};</script>
</body>
</html>

4.9 main函数和Makefile文件

  • gobang.cc

    #include "util.hpp"
    #include "server.hpp"#define HOST "127.0.0.1"
    #define PORT 3306
    #define USER "root"
    #define PASS "123456"
    #define DBNAME "gobang"int main()
    {gobang_server _server(HOST, USER, PASS, DBNAME, PORT);_server.start(8888);return 0;
    }
    
  • makefile

    .PHONY: gobang
    gobang:gobang.cc logger.hpp util.hpp db.hpp online.hpp room.hppg++ -g -std=c++11 $^ -o $@ -L/usr/lib64/mysql -lmysqlclient -ljsoncpp -lpthread
    
http://www.dtcms.com/a/262928.html

相关文章:

  • 1.1_2 计算机网络的组成和功能
  • python+uniapp基于微信小程序的食堂菜品查询系统
  • Deepoc 大模型:无人机行业的智能变革引擎
  • vue-33(实践练习:使用 Nuxt.js 和 SSR 构建一个简单的博客)
  • SpringCloud Gateway
  • C++ 第四阶段 STL 容器 - 第五讲:详解 std::set 与 std::unordered_set
  • 蓝牙耳机开发--探讨AI蓝牙耳机功能、瓶颈及未来展望
  • 链表题解——两两交换链表中的节点【LeetCode】
  • AWS 开源 Strands Agents SDK,简化 AI 代理开发流程
  • Objective-c把字符解析成字典
  • 【微服务】.Net中使用Consul实现服务高可用
  • 链表重排序问题
  • java JNDI高版本绕过 工具介绍 自动化bypass
  • Python训练营打卡Day58(2025.6.30)
  • 晨控CK-FR03与和利时LX系列PLC配置EtherNetIP通讯连接操作手册
  • linux下fabric环境搭建
  • [免费]微信小程序停车场预约管理系统(Springboot后端+Vue3管理端)【论文+源码+SQL脚本】
  • Spring Security 鉴权与授权详解(前后端分离项目)
  • 系统自带激活管理脚本 ospp.vbs 文件
  • Python 的内置函数 object
  • Spring Boot属性配置方式
  • Linux 系统管理:自动化运维与容器化部署
  • 淘宝API接口在数据分析中的应用
  • 【Day 7-N17】Python函数(1)——函数定义、位置参数调用函数、关键字参数调用函数、函数的默认值
  • JMeter常用断言方式
  • python crawling data
  • HTML5 实现的圣诞主题网站源码,使用了 HTML5 和 CSS3 技术,界面美观、节日氛围浓厚。
  • VR协作香港:虚拟现实技术重塑商业协作新模式
  • Jenkins Pipeline 实战指南
  • VMware vSphere 9与ESXi 9正式发布:云原生与AI驱动的虚拟化平台革新