基于websocket的多用户五子棋网页游戏

环境搭建

Ubuntu 22.04更换为国内镜像源(确保基础库可获取)

备份并清空当前源配置

bash 复制代码
sudo cp /etc/apt/sources.list /etc/apt/sources.list.old

清空文件

bash 复制代码
sudo > /etc/apt/sources.list  
bash 复制代码
echo "deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list

安装lrzsz传输工具

复制代码
 sudo apt-get install lrzsz

安装gcc/g++编译器

复制代码
sudo apt-get install gcc g++

安装gdb调试器

cpp 复制代码
sudo apt-get install gdb

安装git工具

cpp 复制代码
sudo apt-get install git

安装cmake项目构建工具

cpp 复制代码
sudo apt-get install cmake

安装boost库

cpp 复制代码
sudo apt-get install libboost-all-dev

安装jsoncpp库

cpp 复制代码
sudo apt-get install libjsoncpp-dev

安装mysql数据库

获取mysql-5.7安装源

bash 复制代码
wget http://repo.mysql.com/mysql-apt-config_0.8.12-1_all.deb

装mysql官方源:

cpp 复制代码
sudo dpkg -i mysql-apt-config_0.8.12-1_all.deb
  1. 过程中出现apt源的安装选择,则选择 bionic 然后 OK

  2. 过程中出现mysql源版本选择时,选择mysql-5.7,然后 OK

更新apt源

cpp 复制代码
sudo apt-get update

出现gpg-key过期的情况解决方案:

bash 复制代码
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 467B942D3A79BD29

出现更新警告:Key is stored in legacy trusted.gpg keyring (/etc/apt/trusted.gpg)这个警告的意思是,需要在 trusted.gpg.d/ ⽬录下查找GPG密钥,因此将密钥文件拷贝过去即可。

bash 复制代码
sudo cp /etc/apt/trusted.gpg /etc/apt/trusted.gpg.d/

改完记得更新

bash 复制代码
sudo apt-get update

查看当前可安装mysql版本:

cpp 复制代码
sudo apt-cache policy mysql-server

安装5.7版本mysql服务及开发包:

bash 复制代码
sudo apt install -f mysql-client=5.7* mysql-community-server=5.7* mysql-server=5.7* libmysqlclient-dev=5.7*

修改配置文件:/etc/mysql/my.cnf

在配置文件增加这几行,设置存取数据时的字符集,只有自已本主机能够使用root登录mysql,任何用户都可以使用mysql

bash 复制代码
[client]
default-character-set = utf8
[mysql]
default-character-set = utf8
[mysqld]
character-set-server = utf8
bind-address = 0.0.0.0

密码不是直接存储到数据库,而是使用password关键字加密存储,要进行强度校验

设置/修改mysql密码,以及设置密码强度等级:

bash 复制代码
sudo mysql_secure_installation

安装websocketpp库

bash 复制代码
git clone https://github.com/zaphoyd/websocketpp.git

ls /usr/include/查看有没有安装好

知识预备

Websocketpp

Websocket介绍

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

传统的 web 程序都是属于 "一问一答" 的形式,即客户端给服务器发送了⼀个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应**。**如下A要给B发消息,是通过A,B两个主机链接服务器,通过服务器转发,但是服务器不会主动向主机发消息,A的消息能到服务器,但是服务器转发不了给B

像网页即时聊天或者我们做的五子棋游戏这样的程序都是非常依赖 "消息推送" 的, 即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 "轮询" 的方式实现, 而轮询的成本比较高并且也不能及时的获取到消息的响应。

基于上述两个问题, 就产生了WebSocket协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。

基于上述两个问题, 就产生了WebSocket协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。

原理解析

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

首先建立TCP连接,三次握手后用户层发送HTTP报文,用户登录啥的在这一步完成,然后要进行服务器主动发起请求,就要切换到websocket,在http报文里面携带切换成websocket的请求,客户端收到后回复101的响应代码,互相收到后,两方切换成websocket进行通信

具体协议升级的过程如下:

报文格式

报文字段比较多,我们重点关注这几个字段:

  • FIN: WebSocket传输数据以消息为概念单位,一个消息有可能由一个或多个帧组成,FIN字段为1表示末尾帧。
  • RSV1~3:保留字段,只在扩展时使用,若未启用扩展则应置1,若收到不全为0的数据帧,且未协商扩展则立即终止连接。
  • **opcode:**标志当前数据帧的类型
  1. 0x0: 表示这是个延续帧,当 opcode 为 0 表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片
  2. 0x1: 表示这是文本帧
  3. 0x2: 表示这是二进制帧
  4. 0x3-0x7: 保留,暂未使用
  5. 0x8: 表示连接断开
  6. 0x9: 表示 ping 帧
  7. 0xa: 表示 pong 帧
  8. 0xb-0xf: 保留,暂未使用
  • mask:表示Payload数据是否被编码,若为1则必有Mask-Key,用于解码Payload数据。仅客户端发送给服务端的消息需要设置。
  • Payload length:数据载荷的长度,单位是字节, 有可能为7位、7+16位、7+64位。假设Payload length = x
  1. x为0~126:数据的长度为x字节
  2. x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度
  3. x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度
  • **Mask-Key:**当mask为1时存在,长度为4字节,解码规则: DECODEDi = ENCODEDi ^ MASKi% 4
  • Payload data: 报文携带的载荷数据

Websocketpp使用

websocketpp

cpp 复制代码
namespace websocketpp
{
  typedef lib::weak_ptr<void> connection_hdl;

  template <typename config>
  class endpoint : public config::socket_type 
 {
    typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;
    typedef typename connection_type::ptr connection_ptr;
    typedef typename connection_type::message_ptr message_ptr;
    typedef lib::function<void(connection_hdl)> open_handler;
    typedef lib::function<void(connection_hdl)> close_handler;
    typedef lib::function<void(connection_hdl)> http_handler;
    typedef lib::function<void(connection_hdl,message_ptr)> message_handler;

     /* websocketpp::log::alevel::none 禁⽌打印所有⽇志*/
     void set_access_channels(log::level channels);/*设置⽇志打印等级*/
     void clear_access_channels(log::level channels);/*清除指定等级的⽇志*/

     /*设置指定事件的回调函数*/
     void set_open_handler(open_handler h);/*websocket握⼿成功回调处理函数*/
     void set_close_handler(close_handler h);/*websocket连接关闭回调处理函数*/
     void set_message_handler(message_handler h);/*websocket消息回调处理函数*/
     void set_http_handler(http_handler h);/*http请求回调处理函数*/

     /*发送数据接⼝*/
     void 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();

     /*设置是否启⽤地址重⽤*/
     void set_reuse_addr(bool value);

     /*设置endpoint的绑定监听端⼝*/
     void listen(uint16_t port);

     /*对io_service对象的run接⼝封装,⽤于启动服务器*/
     std::size_t run();

     /*websocketpp提供的定时器,以毫秒为单位*/
     timer_ptr set_timer(long duration, timer_handler callback);
 };


template <typename config>
class server : public endpoint<connection<config>,config>
 {
    /*初始化并启动服务端监听连接的accept事件处理*/
    void start_accept();
 };


template <typename config>
 class connection
: public config::transport_type::transport_con_type, public config::connection_base
 {
    /*发送数据接⼝*/
    error_code send(std::string&payload, frame::opcode::value op=frame::opcode::text);

    /*获取http请求头部*/
    std::string const & get_request_header(std::string const & key)

    /*获取请求正⽂*/
    std::string const & get_request_body();

    /*设置响应状态码*/
    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 
   {
    class parser 
     {
        std::string const & get_header(std::string const & key)
     }
    class request : public parser 
      {
        /*获取请求⽅法*/
        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;
     static level const fail = 0x2000;
     static level const access_core = 0x00003003;
     static level const all = 0xffffffff;
   };
 }


  namespace http 
 {
     namespace status_code 
      {
        enum value 
        {
             uninitialized = 0,
             continue_code = 100,
             switching_protocols = 101,
             ok = 200,
             created = 201,
             accepted = 202,
             non_authoritative_information = 203,
             no_content = 204,
             reset_content = 205,
             partial_content = 206,
             multiple_choices = 300,
             moved_permanently = 301,
             found = 302,
             see_other = 303,
             not_modified = 304,
             use_proxy = 305,
             temporary_redirect = 307,
             bad_request = 400,
             unauthorized = 401,
             payment_required = 402,
             forbidden = 403,
             not_found = 404,
             method_not_allowed = 405,
             not_acceptable = 406,
             proxy_authentication_required = 407,
             request_timeout = 408,
             conflict = 409,
             gone = 410,
             length_required = 411,
             precondition_failed = 412,
             request_entity_too_large = 413,
             request_uri_too_long = 414,
             unsupported_media_type = 415,
             request_range_not_satisfiable = 416,
             expectation_failed = 417,
             im_a_teapot = 418,
             upgrade_required = 426,
             precondition_required = 428,
             too_many_requests = 429,
             request_header_fields_too_large = 431,
             internal_server_error = 500,
             not_implemented = 501,
             bad_gateway = 502,
             service_unavailable = 503,
             gateway_timeout = 504,
             http_version_not_supported = 505,
             not_extended = 510,
             network_authentication_required = 511
         };
      }
 }


namespace frame 
{
  namespace opcode 
  {
    enum value 
     {
         continuation = 0x0,
         text = 0x1,
         binary = 0x2,
         rsv3 = 0x3,
         rsv4 = 0x4,
         rsv5 = 0x5,
         rsv6 = 0x6,
         rsv7 = 0x7,
         close = 0x8,
         ping = 0x9,
         pong = 0xA,
         control_rsvb = 0xB,
         control_rsvc = 0xC,
         control_rsvd = 0xD,
         control_rsve = 0xE,
         control_rsvf = 0xF,
     };
   }
 }
}

websocketpp常用接口介绍:

日志接口

cpp 复制代码
/* websocketpp::log::alevel::none 禁⽌打印所有⽇志*/
void set_access_channels(log::level channels);/*设置⽇志打印等级*/
void clear_access_channels(log::level channels);/*清除指定等级的⽇志*/

回调接口

websocketpp搭建了服务器之后,给不同事件设置了不同的处理函数指针,这些指针可以指向指定的函数,当服务器收到了指定的数据触发指定的事件后就会通过函数指针取调用这些函数,可以给这些事件设置对应的业务处理函数

cpp 复制代码
/*设置指定事件的回调函数*/
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请求回调处理函数*/

通讯连接相关接口

cpp 复制代码
/*发送数据接⼝*/
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);

其他服务器搭建的接口

cpp 复制代码
/*websocketpp基于asio框架实现,init_asio⽤于初始化asio框架中的io_service调度
器*/
void init_asio();
/*设置是否启⽤地址重⽤*/
void set_reuse_addr(bool value);
/*设置endpoint的绑定监听端⼝*/
void listen(uint16_t port);
/*对io_service对象的run接⼝封装,⽤于启动服务器*/
std::size_t run();
/*websocketpp提供的定时器,以毫秒为单位*/
timer_ptr set_timer(long duration, timer_handler callback);

Simple http/websocket服务器

使用websocketpp搭建服务器的流程:

  1. 实例化server对象
  2. 设置日志输出等级
  3. 初始化asio框架种的调度
  4. 设置业务处理回调函数
  5. 设置服务器监听端口
  6. 开始获取新建连接
  7. 启动服务器
cpp 复制代码
#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>

typedef websocketpp::server<websocketpp::config::asio> wsserver_t;

void http_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl)
{
    // 给客户端返回hello word!的页面

    // 1.获取对应连接的connection指针对象
    wsserver_t::connection_ptr conn = wssev->get_con_from_hdl(hdl);

    // 请求正文
    std::cout << "body:" << conn->get_request_body() << std::endl;

    websocketpp::http::parser::request req = conn->get_request();

    // 请求方法
    std::cout << "method: " << req.get_method() << std::endl;
    // 请求资源路径
    std::cout << "uri: " << req.get_uri() << std::endl;

    std::string body = "<html><body><h1>hello word!</h1></body></html>";

    // 封装报文
    conn->set_body(body);
    conn->append_header("Content-Type", "text/html");
    conn->set_status(websocketpp::http::status_code::ok);
}
void ws_open_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl)
{
    std::cout << "websocket 握手成功!" << std::endl;
}
void ws_close_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl)
{
    std::cout << "websocket 连接断开!" << std::endl;
}
void ws_message_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl, wsserver_t::message_ptr ptr)
{
    wsserver_t::connection_ptr conn = wssev->get_con_from_hdl(hdl);

    std::cout << "wsmesg: " << ptr->get_payload() << std::endl;
    std::string rsp = "client say:" + ptr->get_payload();
    conn->send(rsp, websocketpp::frame::opcode::text);
}
int main()
{
    // 1.实例化server对象
    wsserver_t wsser;

    // 2.设置日志输出等级
    wsser.set_access_channels(websocketpp::log::alevel::none);

    // 3.初始化asio框架种的调度
    wsser.init_asio();
    wsser.set_reuse_addr(true);

    // 4.设置业务处理回调函数
    wsser.set_http_handler(std::bind(http_callback, &wsser, std::placeholders::_1));
    wsser.set_open_handler(std::bind(ws_open_callback, &wsser, std::placeholders::_1));
    wsser.set_close_handler(std::bind(ws_close_callback, &wsser, std::placeholders::_1));
    wsser.set_message_handler(std::bind(ws_message_callback, &wsser, std::placeholders::_1, std::placeholders::_2));

    // 5.设置服务器监听端口
    wsser.listen(8080);

    // 6.开始获取新建连接
    wsser.start_accept();

    // 7.启动服务器
    wsser.run();
    return 0;
}

游览器访问

客户端

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>Test Websocket</title>
</head>
<body>
<input type="text" id="message">
<button id="submit">提交</button>
<script>
// 创建 websocket 实例
// ws://192.168.51.100:8888
// 类⽐http
// ws表⽰websocket协议
// 192.168.51.100 表⽰服务器地址
// 8888表⽰服务器绑定的端⼝
let websocket = new WebSocket("ws://82.157.11.196:8080/");
// 处理连接打开的回调函数
websocket.onopen = function() {
console.log("连接建⽴");
}
// 处理收到消息的回调函数
// 控制台打印消息
websocket.onmessage = function(e) {
console.log("收到消息: " + e.data);
}
// 处理连接异常的回调函数
websocket.onerror = function() {
console.log("连接异常");
}
// 处理连接关闭的回调函数
websocket.onclose = function() {
console.log("连接关闭");
}
// 实现点击按钮后, 通过 websocket实例 向服务器发送请求
let input = document.querySelector('#message');
let button = document.querySelector('#submit');
button.onclick = function() {
console.log("发送消息: " + input.value);
websocket.send(input.value);
}
</script>
</body>
</html>

JsonCpp

Json数据格式

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

例如: 我们想表示一个同学的学生信息

  • C 代码表示
cpp 复制代码
char *name = "xx";
int age = 18;
float score[3] = {88.5, 99, 58};
  • json代码表示
cpp 复制代码
{
  "姓名" : "xx",
  "年龄" : 18,
  "成绩" : [88.5, 99, 58]
}
[
  {"姓名":"⼩明", "年龄":18, "成绩":[23, 65, 78]},
  {"姓名":"⼩红", "年龄":19, "成绩":[88, 95, 78]}
]

Json 的数据类型包括对象,数组,字符串,数字等。

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

JsonCpp介绍

Jsoncpp 库主要是⽤于实现 Json 格式数据的序列化和反序列化,它实现了将多个数据对象组织成

为 json 格式字符串,以及将 Json 格式字符串解析得到多个数据对象的功能。先看一下 Json 数据对象类的表示

cpp 复制代码
class Json::Value
{
   // Value重载了[]和=,因此所有的赋值和获取数据都可以通过
    Value &operator=(const Value &other); 
    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 库主要借助三个类以及其对应的少量成员函数完成序列化及反序列化

  • 序列化接口
cpp 复制代码
class JSON_API StreamWriter
{
//将json串反序列化再IO流输出出去
    virtual int write(Value const &root, std::ostream *sout) = 0;
} 

class JSON_API StreamWriterBuilder : public StreamWriter::Factory
{
    virtual StreamWriter *newStreamWriter() const;
}
  • 反序列化接口
cpp 复制代码
class JSON_API CharReader
{
//将起始地址和结束地址内的Json串解析,root是解析内容,errs是返回错误信息
    virtual bool parse(char const *beginDoc, char const *endDoc,Value *root, std::string *errs) = 0;
} class JSON_API CharReaderBuilder : public CharReader::Factory
{
    virtual CharReader *newCharReader() const;
}
cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h>

std::string serialize(const Json::Value root)
{
    // 1.实例化StreamWriterBuilder工厂对象
    Json::StreamWriterBuilder swb;

    // 2.通过StreamWriterBuilder生产基类StreamWriter

    Json::StreamWriter *str = swb.newStreamWriter();

    // 3.使用StreamWriter的writer进行序列化
    std::stringstream ss;
    int ret = str->write(root, &ss);

    if (ret != 0)
    {
        std::cout << "json serialize error! " << std::endl;
        exit(-1);
    }
    delete str;
    return ss.str();
}

Json::Value unserialize(std::string str)
{
    // 1.实例化一个CharReaderBuilder工厂类
    Json::CharReaderBuilder crb;

    // 2.使用CharReaderBuilder工厂类生产一个CharReader对象
    Json::CharReader *cr = crb.newCharReader();

    // 3.CharReader对象调用parse反序列化
    Json::Value value;
    bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &value, nullptr);
    if (ret == false)
    {
        std::cout << "json unserialize error!" << std::endl;
        exit(-2);
    }
    delete cr;
    return value;
}
int main()
{

    // 将需要序列化的数据存储再json::value对象中
    Json::Value root;
    root["姓名"] = "小明";
    root["年龄"] = "18";
    root["成绩"].append(100);
    root["成绩"].append(99);
    root["成绩"].append(98);

    std::cout << root << std::endl;

    // 使用jsoncpp库进行多个数据对象的序列化
    std::string sing = serialize(root);
    std::cout << sing<<std::endl;

    // 使用json反序列化
    Json::Value value = unserialize(sing);
    std::cout << value<<std::endl;

    return 0;
}

MySQL API

MySQL API介绍

MySQL 是 C/S 模式, C API 其实就是一个 MySQL 客户端,提供一种用 C 语言代码操作数据库的流程

nysql数据库的访问操作流程:

  1. 初始化mysql操作句柄
  2. 连接mysql服务器
  3. 设置客户端的字符集
  4. 选择想要操作的数据库
  5. 执行sql语句
  6. 将查询结果保存到本地
  7. 获取查询结果集中的结果条数
  8. 遍历访问结果集的每一条数据
  9. 释放结果集
  • 新增,修改,删除(sql语句操作成功了数据库中的数据就会发送改变,没有反馈结果)
  • 查询有返回结果,执行成功后要把查询结果拉去到本地
cpp 复制代码
// Mysql操作句柄初始化
// 参数说明:
//     mysql为空则动态申请句柄空间进⾏初始化
// 返回值: 成功返回句柄指针, 失败返回NULL
MYSQL *mysql_init(MYSQL *mysql);

    // 连接mysql服务器
    // 参数说明:
    //     mysql--初始化完成的句柄
    //     host---连接的mysql服务器的地址
    //     user---连接的服务器的⽤⼾名
    //     passwd-连接的服务器的密码
    //     db ----默认选择的数据库名称
    //     port---连接的服务器的端⼝: 默认0是3306端⼝
    //     unix_socket---通信管道⽂件或者socket⽂件,通常置NULL
    //     client_flag---客⼾端标志位,通常置0
    // 返回值:成功返回句柄指针,失败返回NULL
    MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user,
    const char *passwd, const char *db, unsigned intport, 
    const char *unix_socket, unsigned longclient_flag);

    // 设置当前客⼾端的字符集
    // 参数说明:
    //    mysql--初始化完成的句柄
    //    csname--字符集名称,通常:"utf8"
    // 返回值:成功返回0, 失败返回⾮0
    int mysql_set_character_set(MYSQL *mysql, const char *csname)

    // 选择操作的数据库
    // 参数说明:
    //    mysql--初始化完成的句柄
    //    db-----要切换选择的数据库名称
    // 返回值:成功返回0, 失败返回⾮0
    int mysql_select_db(MYSQL *mysql, const char *db)


    // 执⾏sql语句
    // 参数说明:
    //    mysql--初始化完成的句柄
    //    stmt_str--要执⾏的sql语句
    // 返回值:成功返回0, 失败返回⾮0
    int mysql_query(MYSQL *mysql, const char *stmt_str)

    // 保存查询结果到本地
    // 参数说明:
    //    mysql--初始化完成的句柄
    // 返回值:成功返回结果集的指针, 失败返回NULL
    MYSQL_RES *mysql_store_result(MYSQL *mysql)


    // 获取结果集中的⾏数
    // 参数说明:
    //    result--保存到本地的结果集地址
    // 返回值:结果集中数据的条数
    uint64_t mysql_num_rows(MYSQL_RES *result);


    // 获取结果集中的列数
    // 参数说明:
    //    result--保存到本地的结果集地址
    // 返回值:结果集中每⼀条数据的列数
    unsigned int mysql_num_fields(MYSQL_RES *result)

    // 遍历结果集, 并且这个接⼝会保存当前读取结果位置,每次获取的都是下⼀条数据
    // 参数说明:
    //    result--保存到本地的结果集地址
    // 返回值:实际上是⼀个char **的指针,将每⼀条数据做成了字符串指针数组
    //        row[0]-第0列 row[1]-第1列 ...
    MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)


    // 释放结果集
    // 参数说明:
    //    result--保存到本地的结果集地址
    void mysql_free_result(MYSQL_RES *result)

    // 关闭数据库客⼾端连接,销毁句柄
    // 参数说明:
    //    mysql--初始化完成的句柄
    void mysql_close(MYSQL *mysql)


    // 获取mysql接⼝执⾏错误原因
    // 参数说明:
    //    mysql--初始化完成的句柄
    const char *mysql_error(MYSQL *mysql)

MySQL API使用

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

• 创建测试数据库

sql 复制代码
create database if not exists gobang;
use gobang;
sql 复制代码
 create table if not exists stu( 
sn int primary key auto_increment, 
name varchar(32), 
age int, 
ch decimal(5,2), 
en decimal(5,2), 
math decimal(5,3)
);

增删查改

增删改不需要拉取到本地,再mysql里面就做完没有响应

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<mysql/mysql.h>


#define HOST "127.0.0.1"
#define USER "root"
#define PASS "xm123456"
#define DBNAME "gobang"
#define PORT 3306


int main()
{

    //1.初始化mysql语句
    MYSQL *mysql = mysql_init(NULL); 
    // MYSQL *mysql = mysql_init(MYSQL *mysql) ,传空的话会在堆上申请MYSQL对象在返回首地址传给该函数
    if(mysql == NULL)
    {
        printf("MYSQL init error!");
        return -1;
    }

    //2.连接服务器
    //MYSQL* mysql_real_connect(mysql,host,user,pass,dbname,port,unix_socket,flag) 
     if(mysql_real_connect(mysql,HOST,USER,PASS,DBNAME,PORT,NULL,0) == NULL)
     {
        printf("mysql_real_connect error: %s\n!",mysql_error(mysql));
        mysql_close(mysql);
        return -1;
     }

    //3.设置客户端字符集
    //int mysql_set_character_set(mysql,"utf8")
    if(mysql_set_character_set(mysql,"utf8") != 0)
    {
        printf("mysql_set_character_set error %s\n!",mysql_error(mysql));
        mysql_close(mysql);
        return -1;       
    }

    //4.选择操作的数据库
    //int mysql_select_db(mysql,dbname)
    //mysql_select_db(mysql,DBNAME);

    //5.执行sql语句
    //int mysql_query(MYSQL*mysql,char * sql)
    char* sql ="insert stu values(null,'吴建',18,56,68,98);";
    int ret = mysql_query(mysql,sql);
    if( ret != 0 )
    {
        printf("sql: %s\n",sql);
        printf("mysql_query error %s\n!",mysql_error(mysql));
        mysql_close(mysql);
        return -1;       
    }

    //6.若果是查询语句则需要保存查询结果到本地
    

    //7.获取结果集中的结果条数

    //8.遍历保存到本地的结果集

    //9.释放结果集

    //10.关闭连接,释放句柄
    mysql_close(mysql);
    return 0;
}

查的话需要拉取到本地,所以更麻烦

cpp 复制代码
#include<stdio.h>
#include<string.h>
#include<mysql/mysql.h>


#define HOST "127.0.0.1"
#define USER "root"
#define PASS "xm123456"
#define DBNAME "gobang"
#define PORT 3306


int main()
{

    //1.初始化mysql语句
    MYSQL *mysql = mysql_init(NULL); 
    // MYSQL *mysql = mysql_init(MYSQL *mysql) ,传空的话会在堆上申请MYSQL对象在返回首地址传给该函数
    if(mysql == NULL)
    {
        printf("MYSQL init error!");
        return -1;
    }

    //2.连接服务器
    //MYSQL* mysql_real_connect(mysql,host,user,pass,dbname,port,unix_socket,flag) 
     if(mysql_real_connect(mysql,HOST,USER,PASS,DBNAME,PORT,NULL,0) == NULL)
     {
        printf("mysql_real_connect error: %s\n!",mysql_error(mysql));
        mysql_close(mysql);
        return -1;
     }

    //3.设置客户端字符集
    //int mysql_set_character_set(mysql,"utf8")
    if(mysql_set_character_set(mysql,"utf8") != 0)
    {
        printf("mysql_set_character_set error %s\n!",mysql_error(mysql));
        mysql_close(mysql);
        return -1;       
    }

    //4.选择操作的数据库
    //int mysql_select_db(mysql,dbname)
    //mysql_select_db(mysql,DBNAME);

    //5.执行sql语句
    //int mysql_query(MYSQL*mysql,char * sql)
    // char* sql ="insert stu values(null,'吴建',18,56,68,98);";   增
    // char* sql ="update stu set ch=ch+30 where sn = 1;";         改
    // char * sql = "delete from stu where sn= 1";                 删
    char * sql = " select * from stu;";
    int ret = mysql_query(mysql,sql);
    if( ret != 0 )
    {
        printf("sql: %s\n",sql);
        printf("mysql_query error %s\n!",mysql_error(mysql));
        mysql_close(mysql);
        return -1;       
    }

    //6.若果是查询语句则需要保存查询结果到本地
    //MYSQL_RES * mysql_store_result(MYSQL *mysql)
    MYSQL_RES * res = mysql_store_result(mysql);
    if(res == NULL)
    {
        mysql_close(mysql);
        return -1;          
    }

    //7.获取结果集中的结果条数
    //int mysql_num_row(MYSQL * res)      行
    //int mysql_num_fields(MYSQL * res)   列
    int num_row = mysql_num_rows(res);
    int num_col = mysql_num_fields(res);

    //8.遍历保存到本地的结果集
    for(int i = 0;i < num_row ; i++)
    {
        //MYSQL_ROW mysql_fetch_row(MYSQL_RES *res)
        MYSQL_ROW row = mysql_fetch_row(res);
        for(int i = 0;i < num_col; i++)
        {
            printf("%s\t",row[i]);
        }
        printf("\n");
    }

    //9.释放结果集
    mysql_free_result(res);
    //10.关闭连接,释放句柄
    mysql_close(mysql);
    return 0;
}
cpp 复制代码
  char* sql ="insert stu values(null,'吴建',18,56,68,98);";
cpp 复制代码
  char * sql = "delete from stu where sn= 1";
cpp 复制代码
char * sql = " select * from stu;";
cpp 复制代码
 char* sql ="update stu set ch=ch+30 where sn = 1;";

前端

  • HTML:标签语言,渲染前端网页的语言
  • CSS:层叠样式表,对html标签进行样式修饰,让页面控件更加好看
  • JS:脚本语言,在前端web这里控制页面的渲染
  • AJAX:异步的http客户端,向服务器发送http请求
  • WebSocket: 创建一个websocket请求,请求服务器建立websocket长连接进行持久通信

AJAX的使用是使用jequery的ajax,使用起来要比js原生的ajax要好用

AJAX的安装

html 复制代码
<script src="jquery-1.10.2.min.js"></script>

百度CDN

html 复制代码
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js">
</script>

AJAX的使用

将数据以AJAX形式发送给服务器

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>Test Websocket</title>
</head>

<body>

    <form action="'http://82.157.11.196:8080/login" method="post">
        <input type='text' id='username' name='username'>
        </br>
        <input type='password' id='password' name='password'>
        <input type='submit' name='submit' value='提交'>
    </form>
    <button onclick="text()">点击提交</button>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>

    <script>
        function text() {
            var login_info = {
                username: document.getElementById("username").value,
                password: document.getElementById("password").value

            }
            $.ajax({
                type: "post",
                url: "http://82.157.11.196:8080/login",
                data: JSON.stringify(login_info),
                success: function (res, statu, xhr) {
                    alert(res);
                },
                error: function (xhr) {
                    alert(JSON.stringify(xhr));

                }
            });

        }
    </script>


</body>

</html>

在learn.html中创建一个websocket通信,从输入框中获取数据发送给服务器,得到响应后展示在页面中

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>Test Websocket</title>
</head>

<body>
<h1 id = "h1">hello word!</h1>
    <form action="'http://82.157.11.196:8080/login" method="post">
        <input type='text' id='username' name='username'>
        </br>
        <input type='password' id='password' name='password'>
        <input type='submit' name='submit' value='提交'>
    </form>
    <button onclick="text()">点击提交</button>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>


    <script>
        var ws = new WebSocket("ws://82.157.11.196:8080/ws");
        ws.onopen = function(){alert("握手成功!");}
        ws.onerror = function(){alert("通讯出错!");}
        ws.onclose = function(){alert("连接断开!");}
        ws.onmessage = function(evt)
        {
            var h1 = document.getElementById("h1");
            h1.innerHTML = evt.data;
        }

        function text() {
          ws.send(document.getElementById("username").value);
          document.getElementById("username").value = "";
        }
    </script>


</body>

</html>

项目结构设计

项目模块划分说明

项目的实现,咱们将其划分为三个大模块来进行:

  • 数据管理模块:基于Mysql数据库进行用户数据的管理
  • 前端界面模块:基于JS实现前端页面(注册,登录,游戏大厅,游戏房间)的动态控制以及与服务器的通信。
  • 业务处理模块:搭建WebSocket服务器与客户端进行通信,接收请求并进行业务处理。

在这里回顾一下我们要实现的项目功能,我们要实现的是一个在线五子棋对战服务器,提供用户通过浏览器进行用户注册,登录,以及实时匹配,对战,聊天等功能。而如果要实现这些功能,那么就需要对业务处理模块再次进行细分为多个模块来实现各个功能。

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

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

项目流程图

玩家用户角度流程图:

服务器流程结构图:

实用工具类模块代码实现

实用工具类模块主要是负责提前实现一些项目中会用到的边缘功能代码,提前实现好了就可以在项目中用到的时候直接使用了。

实用工具模块:

  1. 日志宏:实现程序日志打印
  2. mysql_util:数据库的连接和初始化,句柄的销毁,语句的执行
  3. json_util:封装实现json的序列化和反序列化
  4. string_util:主要实现字符串分割功能(可以boost库)
  5. file_util:主要封装了文件按的读取功能(读取html文件数据)

日志宏封装

日志宏的实现:

8:29:12 main.cc:120 文件打开失败

  • time_t time(NULL) 获取系统时间戳
  • struct tm* localtime(time_t *t)将该时间戳转换成具体的时间
  • char*strftime(char * buf,int max,char * format, struct* tm) 将时间结构的各项属性按照指定的格式放到指定的空间中
  • int fprintf(FILE *fp, char* format,...)对多个格式进行组织写入到指定文件
cpp 复制代码
#ifndef LOG_HPP
#define LOG_HPP

#include <stdio.h>
#include <time.h>
#include <string.h>
#define LOG_LEVEL_DEBUG 0
#define LOG_LEVEL_INFO 1
#define LOG_LEVEL_WARN 2
#define LOG_LEVEL_ERROR 3

#define DEFAULT_LOG_LEVEL LOG_LEVEL_DEBUG

#define LOG(level, format, ...) do { \
    if (DEFAULT_LOG_LEVEL > level) break; \
    time_t t = time(NULL); \
    struct tm* lt = localtime(&t); \
    char buffer[32] = {0}; \
    strftime(buffer, sizeof(buffer)-1, "%Y-%m-%d %H:%M:%S", lt); \
    fprintf(stdout, "[%s %s:%d] " format"\n", buffer, __FILE__, __LINE__, ##__VA_ARGS__); \
} while(0)

// 简化不同级别日志的使用
#define LOG_DEBUG(format, ...) LOG(LOG_LEVEL_DEBUG, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...)  LOG(LOG_LEVEL_INFO,  format, ##__VA_ARGS__)
#define LOG_WARN(format, ...)  LOG(LOG_LEVEL_WARN,  format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) LOG(LOG_LEVEL_ERROR, format, ##__VA_ARGS__)

#endif // LOG_HPP
    

Mysql-API封装

通用的就封装出来,剩下的两个是查询的时候用的,查询的时候再实现出来

  1. 初始化句柄
  2. 连接服务器
  3. 设置字符集
  4. 选择数据库
  5. 执行语句
  6. 保存结果到本地
  7. 获取结果条数遍历处理结果
  8. 释放句柄

log.hpp

cpp 复制代码
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
#include <iostream>
#include "log.hpp"
#include <mysql/mysql.h>
#include <string>

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)
        {
            LOG_ERROR("MYSQL init error!");
            return nullptr;
        }

        // 2.连接服务器
        // MYSQL* mysql_real_connect(mysql,host,user,pass,dbname,port,unix_socket,flag)
        if (mysql_real_connect(mysql, host.c_str(), username.c_str(), 
        password.c_str(), dbname.c_str(), port, NULL, 0) == NULL)
        {
            LOG_ERROR("mysql_real_connect error: %s!", mysql_error(mysql));
            mysql_close(mysql);
            return nullptr;
        }
        return mysql;
    }
    static bool mysql_exec(MYSQL *mysql, const std::string &sql)
    {
        int ret = mysql_query(mysql, sql.c_str());
        if (ret != 0)
        {
            printf("sql: %s\n", sql.c_str());
            LOG_ERROR("mysql_query error %s!", mysql_error(mysql));
            mysql_close(mysql);
            return false;
        }
        return true;
    }
    static void mysql_destory(MYSQL *mysql)
    {
        if (mysql != NULL)
        {
            mysql_close(mysql);
        }
    }
};

#endif

gobang.cc

cpp 复制代码
#include "log.hpp"
#include "mysql.hpp"

#define HOST "127.0.0.1"
#define USER "root"
#define PASS "xm123456"
#define DBNAME "gobang"
#define PORT 3306

int main()
{

    MYSQL *mysql = mysql_util::mysql_create(HOST, USER, PASS, DBNAME, PORT);
    const char *sql = "insert stu values(null,'小狗',18,61,71,71);";
    bool ret = mysql_util::mysql_exec(mysql, sql);
    if (ret == false)
    {
        return -1;
    }
    mysql_util::mysql_destory(mysql);
    return 0;
}

makefile

cpp 复制代码
.PHONY:gobang
gobang:gobang.cc
	g++ $^ -o $@ -L/usr/lib/x86_64-linux-gnu/mysql -lmysqlclient

Json封装

json_util封装实现json的序列化和反序列化

  • 序列化:将多个数据对象,进行序列化得到一个json格式的字符串
  • 反序列化:将json格式字符串,反序列化得到多个数据
cpp 复制代码
#include<jsoncpp/json/json.h>
#include<iostream>
#include"log.hpp"
#include<memory>

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());

    // 3.使用StreamWriter的writer进行序列化
    std::stringstream ss;
    int ret = sw->write(root, &ss);

    if (ret != 0)
    {
        LOG_ERROR("json serialize error");
       return false;
    }
    str = ss.str();
return true;
}

static bool unserialize(std::string &str, Json::Value &root)
{
     // 1.实例化一个CharReaderBuilder工厂类
    Json::CharReaderBuilder crb;

    // 2.使用CharReaderBuilder工厂类生产一个CharReader对象
    std::unique_ptr<Json::CharReader>cr ( crb.newCharReader());

    // 3.CharReader对象调用parse反序列化
    std::string err;
    bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);
    if (ret == false)
    {
        LOG_ERROR( "json unserialize error: %s",err.c_str() );
       return false;
    }
    return true;
}
};
cpp 复制代码
#include "log.hpp"
#include "json.hpp"



void json_test()
{

 Json::Value root;
 root["姓名"] = "小明";
 root["年龄"] = 18;
 root["成绩"].append(88);
 root["成绩"].append(61);
 root["成绩"].append(78);

 std::string str;
 json_util::serialize(root,str);
 LOG_DEBUG("%s",str.c_str());

 Json::Value val;
 json_util::unserialize(str,val);
 std::cout<<"姓名:"<<val["姓名"].asString()<<std::endl;
 std::cout<<"年龄:"<<val["年龄"].asString()<<std::endl;
 int sz = val["成绩"].size();
   for(int i = 0; i<sz;i++)
  {
    std::cout<<"成绩:"<<val["成绩"][i].asString()<<std::endl;
  }
}
int main()
{

json_test();
    return 0;
}

string封装

string_util主要实现字符串的分割功能

cpp 复制代码
#include<iostream>
#include<string>
#include<vector>

class string_util
{
public:

static int split(const std::string &src,const std::string &sep,std::vector<std::string> &res)
{
 //123 456,788   123,,,456
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;
}
}
return res.size();
}
};
cpp 复制代码
#include "log.hpp"
#include "mysql.hpp"
#include "json.hpp"
#include "string.hpp"



void str_test()
{
  std::string s ="123,456,789,,111";
 std::vector<std::string>arry;
  string_util::split(s,",",arry);
  for(auto s:arry)
  LOG_DEBUG("%s",s.c_str());
}
int main()
{

str_test();
    return 0;
}

file封装

主要封装了文件数据(html)的读取功能

cpp 复制代码
#include<iostream>
#include"log.hpp"
#include<fstream>

class file_util
{
    public:
    static bool read(const std::string &filename, std::string &body)
    {
        //打开文件
        std::ifstream ifs(filename,std::ios::binary);
        if(ifs.is_open() == false)
        {
            LOG_ERROR("%s file open error!",filename.c_str());
            return false;
        }
        //获取文件大小wsws
        size_t fsize = 0;
        ifs.seekg(0,std::ios::end);
        fsize = ifs.tellg();
        ifs.seekg(0,std::ios::beg);
        //读取数据
        body.resize(fsize);
        ifs.read(&body[0],fsize);
        if(ifs.good() == false)
        {
            LOG_ERROR("read %s error",filename.c_str());
            ifs.close();
            return false;
        }
        //关闭文件
        ifs.close();
        return true;
    }
};
cpp 复制代码
#include "log.hpp"
#include "mysql.hpp"
#include "json.hpp"
#include "string.hpp"
#include "file.hpp"

void file_test()
{
  std::string filename = "./makefile";
  std::string body;
  file_util::read(filename, body);
  std::cout << body << std::endl;
}
int main()
{

  file_test();
  return 0 ;
}

数据管理模块实现

数据管理模块主要负责对于数据库中数据进行统一的增删改查管理,其他模块要对数据操作都必须通过数据管理模块完成。

数据库设计

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

  • 用户信息, 用来实现登录、注册、游戏对战数据管理等功能
  • 积分信息, 用来实现匹配功能

数据库管理模块的封装与实现

实现一个我们自己的mysql客户端来访问服务器进行数据操作,针对每一张表都设计一个类,通过类实例化的对象管理指定的数据库表

注册用户

cpp 复制代码
#include "log.hpp"
#include "mysql.hpp"
#include "json.hpp"
#include "string.hpp"
#include "file.hpp"

#define HOST "127.0.0.1"
#define USER "root"
#define PASS "xm123456"
#define DBNAME "gobang"
#define PORT 3306

void db_test()
{

  user_table ut(HOST,USER,PASS,DBNAME,PORT);
  Json::Value user;
  user["username"] = "xiaoming";
  user["password"] = "123123";
  ut.insert(user);
}
int main()
{

   db_test();
  return 0 ;
}

登陆验证

cpp 复制代码
  // 用户登录验证并返回详细的用户信息
    bool login(Json::Value &user)
    {
// 用户名和密码作为过滤条件,数据用户名和密码一致登录,不一致则用户名密码错误
// 1.执行sql
#define LOGIN_USER "select id ,score,total_count,win_count from user where username ='%s' and password = SHA2('%s', 256);"
        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)
            {
                LOG_ERROR("insert user info error!");
                return false;
            }

            // 2.获取结果集
            res = mysql_store_result(_mysql);
            if (res == NULL)
            {
                LOG_DEBUG("没有结果集,查询不到该用户");
                return false;
            }
        }

        // 2.查询结果集条数
        int row_num = mysql_num_rows(res);
        if (row_num != 1)
        {
            LOG_DEBUG("查询的用户信息不唯一");
            return false;
        }

        // 3.将结果集保存到本地
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = std::stol(row[0]);
        user["score"] = std::stol(row[1]);
        user["total_count"] = std::stol(row[2]);
        user["win_count"] = std::stol(row[3]);

        // 4,释放结果集
        mysql_free_result(res);
        return true;
    }
cpp 复制代码
void db_test()
{

  user_table ut(HOST,USER,PASS,DBNAME,PORT);
  Json::Value user;
  user["username"] = "xiaoming";
  user["password"] = "123123";
  //ut.insert(user);
   bool ret = ut.login(user);
   if(ret == false)
   {
    LOG_DEBUG("LOGIN ERROR!");
   }
   else{
    LOG_DEBUG("LOGIN SCUESS!");
   }
}
int main()
{

   db_test();
  return 0 ;
}

通过名字查信息

cpp 复制代码
 // 通过用户名获取用户信息
    bool select_by_name(const std::string &name, Json::Value &user)
    {
#define USER_BY_NAME "select id ,score,total_count,win_count from user where username ='%s' ;"
        char sql[4096] = {0};
        sprintf(sql, USER_BY_NAME, name.c_str());
        MYSQL_RES *res = NULL;
        {
            std::unique_lock<std::mutex> lock(_mutex);
            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if (ret == false)
            {
                LOG_ERROR("get user info error!");
                return false;
            }

            // 2.获取结果集
            res = mysql_store_result(_mysql);
            if (res == NULL)
            {
                LOG_DEBUG("没有结果集,查询不到该用户");
                return false;
            }
        }
        // 2.查询结果集条数
        int row_num = mysql_num_rows(res);
        if (row_num != 1)
        {
            LOG_DEBUG("查询的用户信息不唯一");
            return false;
        }

        // 3.将结果集保存到本地
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = std::stol(row[0]);
        user["score"] = std::stol(row[1]);
        user["total_count"] = std::stol(row[2]);
        user["win_count"] = std::stol(row[3]);

        // 4,释放结果集
        mysql_free_result(res);

        return true;
    }
cpp 复制代码
void db_test()
{

  user_table ut(HOST,USER,PASS,DBNAME,PORT);
  Json::Value user;
  //user["username"] = "xiaoming";
 // user["password"] = "123123";
  //ut.insert(user);
   bool ret = ut.select_by_name("xiaoming",user);
   if(ret == false)
   {
    LOG_DEBUG("select_by_name ERROR!");
   }
   else{
    LOG_DEBUG("select_by_name SCUESS!");
   }

   std::string body;
   json_util::serialize(user,body);
   std::cout<<body<<std::endl;
}
int main()
{

   db_test();
  return 0 ;
}

通过id获取用户信息

cpp 复制代码
   // 通过用户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 ='%lu' ;"
        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)
            {
                LOG_ERROR("get user info error!");
                return false;
            }

            // 2.获取结果集
            res = mysql_store_result(_mysql);
            if (res == NULL)
            {
                LOG_DEBUG("没有结果集,查询不到该用户");
                return false;
            }
        }
        // 2.查询结果集条数
        int row_num = mysql_num_rows(res);
        if (row_num != 1)
        {
            LOG_DEBUG("查询的用户信息不唯一");
            return false;
        }

        // 3.将结果集保存到本地
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = id;
        user["username"] = row[0];
        user["score"] = std::stol(row[1]);
        user["total_count"] = std::stol(row[2]);
        user["win_count"] = std::stol(row[3]);

        // 4,释放结果集
        mysql_free_result(res);

        return true;
    }
cpp 复制代码
void db_test()
{

  user_table ut(HOST,USER,PASS,DBNAME,PORT);
  Json::Value user;
  //user["username"] = "xiaoming";
 // user["password"] = "123123";
  //ut.insert(user);
   bool ret = ut.select_by_id(1,user);
   if(ret == false)
   {
    LOG_DEBUG("select_by_id ERROR!");
   }
   else{
    LOG_DEBUG("select_by_id SCUESS!");
   }

   std::string body;
   json_util::serialize(user,body);
   std::cout<<body<<std::endl;
}
int main()
{

   db_test();
  return 0 ;
}

胜利加分

cpp 复制代码
   // 对于获胜方的数据操纵(天梯分数增加30分,战斗场次增加1,胜利场次增加1)
    bool 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 = %lu;"
        char sql[4096] = {0};
        sprintf(sql, USER_WIN, id);
        bool ret = mysql_util::mysql_exec(_mysql, sql);
        if (ret == false)
        {
            LOG_ERROR("updata  win user info error!");
            return false;
        }
        return true;
    }
cpp 复制代码
void db_test()
{

  user_table ut(HOST,USER,PASS,DBNAME,PORT);
  Json::Value user;
  //user["username"] = "xiaoming";
 // user["password"] = "123123";
  //ut.insert(user);
   bool ret = ut.win(1);
   if(ret == false)
   {
    LOG_DEBUG("win ERROR!");
   }
   else{
    LOG_DEBUG("win SCUESS!");
   }

  //  std::string body;
  //  json_util::serialize(user,body);
  //  std::cout<<body<<std::endl;
}
int main()
{

   db_test();
  return 0 ;
}

失败扣分

cpp 复制代码
    // 对于战败方的数据操纵(天梯分数减少30分,战斗场次增加1,其余不变)
    bool lose(uint64_t id)
    {
#define USER_LOSE "update user set score = score - 30,total_count = total_count + 1 where id = %lu;"
        char sql[4096] = {0};
        sprintf(sql, USER_LOSE, id);
        bool ret = mysql_util::mysql_exec(_mysql, sql);
        if (ret == false)
        {
            LOG_ERROR("update  lose user info error!");
            return false;
        }
        return true;
    }
cpp 复制代码
void db_test()
{

  user_table ut(HOST,USER,PASS,DBNAME,PORT);
  Json::Value user;
  //user["username"] = "xiaoming";
 // user["password"] = "123123";
  //ut.insert(user);
   bool ret = ut.lose(1);
   if(ret == false)
   {
    LOG_DEBUG("lose ERROR!");
   }
   else{
    LOG_DEBUG("lose SCUESS!");
   }

  //  std::string body;
  //  json_util::serialize(user,body);
  //  std::cout<<body<<std::endl;
}
int main()
{

   db_test();
  return 0 ;
}

在线用户管理模块

在线用户管理模块实现

在线用户管理,是对于当前游戏大厅游戏房间 中的用户进行管理,主要是建立起用户与Socket连接的映射关系(用用户id和客户端的websocket建立长连接),当一个用户发送消息或者下了一步棋,我们可以找到房间里的另一个用户,在用户管理模块中找到这个用户的websocket连接,然后同步消息,这个模块具有两个功能:

  1. 能够让程序中根据用户信息,进而找到能够与用户客户端进行通信的Socket连接,进而实现与客户端的通信。
  2. 判断一个用户是否在线,或者判断用户是否已经掉线。
cpp 复制代码
#ifndef __M_ONLINE_H__
#define __M_ONLINE_H__
#include<iostream>
#include<mutex>
#include<unordered_map>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#include"file.hpp"


typedef websocketpp::server<websocketpp::config::asio> websocket_server;
class online_manager
{
    private:
    std::mutex _mutex;
    //用来建立游戏大厅用户的用户ID与通信连接的关系
    std::unordered_map<uint64_t, websocket_server::connection_ptr> _hall_user;
     //用来建立游戏房间用户的用户ID与通信连接的关系
    std::unordered_map<uint64_t, websocket_server::connection_ptr> _room_user;

    public:
    online_manager(){}
    ~online_manager(){}
    //websocket连接建立时才会加入游戏大厅&游戏房间在线用户管理
    void enter_game_hall(uint64_t uid,websocket_server::connection_ptr &conn)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _hall_user.insert(std::make_pair(uid,conn));
    }

    void enter_game_room(uint64_t uid,websocket_server::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在游戏大厅/游戏房间用户管理中获取对应的通信连接
    websocket_server::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 websocket_server::connection_ptr();
        }
        return it->second;
    }
    websocket_server::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 websocket_server::connection_ptr();
        }
        return it->second;
    }

};

#endif // __M_ONLINE_H__

测试

cpp 复制代码
#include "log.hpp"
#include "mysql.hpp"
#include "json.hpp"
#include "string.hpp"
#include "file.hpp"
#include "online.hpp"

#define HOST "127.0.0.1"
#define USER "root"
#define PASS "xm123456"
#define DBNAME "gobang"
#define PORT 3306


void online_test ()
{

  online_manager om;
  websocket_server::connection_ptr conn;
  uint64_t uid = 2 ;
  om.enter_game_hall(uid,conn);
  if (om.is_in_game_hall(uid))
  {
    LOG_DEBUG("in game hall");
  }else{
    LOG_DEBUG("not in game hall");
  }
  om.exit_game_hall(uid);
   if (om.is_in_game_hall(uid))
  {
    LOG_DEBUG("in game hall");
  }else{
    LOG_DEBUG("not in game hall");
  }
}
int main()
{
online_test();
  
  return 0 ;
}

游戏房间管理模块

1.对匹配成功的玩家创建房间,建立起一个小范围的玩家之间的关联关系,房间里的一个玩家产生的动作会广播给其他用户

2.房间会创建很多,因此需要将这些房间管理起来,以便于对于房间生命周期的控制

实现两个部分:

  1. 房间的设计
  2. 房间管理的设计

房间的设计

管理房间的数据,处理房间的动作,包含房间的ID,房间的状态(决定了一个玩家退出房间时所做的动作),房间玩家的数量(决定了什么时候房间销毁),白棋玩家id,黑棋玩家id,用户信息表的句柄(胜利/失败时更新用户数据)棋盘的信息(二维数组),聊天,下棋这两个动作都要广播给房间的其他用户,在线用户的管理句柄

Restful风格的网络通信接口设计

Restful风格:依托于HTTP协议来实现

  1. GET - 获取
  2. POST - 新增
  3. PUT - 更新
  4. DELETE - 删除

正文采用xml/json格式进行正文数据的格式组织

检测是否胜利,从该位置为中心横竖撇捺依次判断,分别统计同一条线中,以该棋子为中心两边相邻同色棋子之和

横行:统计下棋位置的右边同色棋子数量,行不变,列++;统计下棋位置的左边同色棋子数量,行不变,列--;

竖列:统计下棋位置上方同色棋子数量,列不变行--;统计下棋位置下方同色棋子数量,列不变,行++;

撇列:统计右上行--,列++;左下,行++,列--;

捺列:统计右下,行++,列++;统计左上,行--,列--;

房间管理
  1. 创建房间(两个玩家对战匹配完成了,为他们创建一个房间,在传入两个玩家的用户ID)
  2. 查找房间(通过房间ID查找房间信息;通过用户ID查找所在房间信息)
  3. 销毁房间(根据房间ID来校回房间;所有用户都退出也得销毁房间)

需要管理的数据:

  1. 数据管理模块句柄
  2. 在线用户管理模块句柄
  3. 房间ID分配计数器
  4. 互斥锁
  5. unorderedmap<room_id,room_ptr> //using room_ptr = std::shared_ptr<room>; 房间信息管理
  6. unorderedmap<uid,rid> 房间与用户ID的映射关系(先通过用户ID找到所在的房间ID,再去查找房间信息)
session

什么是session?

在WEB开发中,HTTP协议是⼀种无状态短链接的协议,这就导致一个客户端连接到服务器上之后,服务器不知道当前的连接对应的是哪个用户,也不知道客户端是否登录成功,这时候为客户端提所有服务是不合理的。

因此,服务器为每个用户浏览器创建一个会话对象(session对象),注意:一个浏览器独占一个session对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户浏览器独占的session中,当用户使用浏览器访问其它程序时,其它程序可以从用户的session中取出该用户的数据,识别该连接对应的用户,并为用户提供服务。

原理:

使用cookie和session进行http在短链接通信的情况下进行用户状态管理,但是这个服务器上管理的session都会有过期时间,时间超了,就会删除对应的session,每次客户端与服务器的通信都要延长session的过期时间

session模块:保存客户端的用户状态信息

  1. 有自己的标识ssid
  2. 用户的状态信息
  3. 用户ID
  4. session相关的定时器(通过是否包含定时器,确定当前session是否已经添加了定时销毁任务)

session管理:

  1. 创建一个session
  2. 为session设计过期时间
  3. 获取session
  4. 销毁session(过期自动销毁)

管理的信息:

  1. ssid的计数器
  2. 互斥锁
  3. 管理器 unordered_map<ssid,session_ptr>
  4. websocket的server的对象--用于添加定时任务
websocket库中定时器的作用

boost::asio::steady_timer设置五秒后响应

wsserver_t::timer_ptr tp = wssev->set_timer(5000,std::bind(Print,"绑定成功"));

cpp 复制代码
#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>

typedef websocketpp::server<websocketpp::config::asio> wsserver_t;


void Print(const std::string &str)
{
    std::cout<<str<<std::endl;
}
void http_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl)
{
    // 给客户端返回hello word!的页面

    // 1.获取对应连接的connection指针对象
    wsserver_t::connection_ptr conn = wssev->get_con_from_hdl(hdl);

    // 请求正文
    std::cout << "body:" << conn->get_request_body() << std::endl;

    websocketpp::http::parser::request req = conn->get_request();

    // 请求方法 
    std::cout << "method: " << req.get_method() << std::endl;
    // 请求资源路径
    std::cout << "uri: " << req.get_uri() << std::endl;

    std::string body = "<html><body><h1>hello word!</h1></body></html>";

    // 封装报文
    // conn->set_body(conn->get_request_body());
    // conn->append_header("Content-Type", "text/html");
    conn->set_body(conn->get_request_body());
    conn->set_status(websocketpp::http::status_code::ok);
  wssev->set_timer(5000,std::bind(Print,"绑定成功"));
}
void ws_open_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl)
{
    std::cout << "websocket 握手成功!" << std::endl;
}
void ws_close_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl)
{
    std::cout << "websocket 连接断开!" << std::endl;
}
void ws_message_callback(wsserver_t *wssev, websocketpp::connection_hdl hdl, wsserver_t::message_ptr ptr)
{
    wsserver_t::connection_ptr conn = wssev->get_con_from_hdl(hdl);

    std::cout << "wsmesg: " << ptr->get_payload() << std::endl;
    std::string rsp = "client say:" + ptr->get_payload();
    conn->send(rsp, websocketpp::frame::opcode::text);
}
int main()
{
    // 1.实例化server对象
    wsserver_t wsser;

    // 2.设置日志输出等级
    wsser.set_access_channels(websocketpp::log::alevel::none);

    // 3.初始化asio框架种的调度
    wsser.init_asio();
    wsser.set_reuse_addr(true);

    // 4.设置业务处理回调函数
    wsser.set_http_handler(std::bind(http_callback, &wsser, std::placeholders::_1));
    wsser.set_open_handler(std::bind(ws_open_callback, &wsser, std::placeholders::_1));
    wsser.set_close_handler(std::bind(ws_close_callback, &wsser, std::placeholders::_1));
    wsser.set_message_handler(std::bind(ws_message_callback, &wsser, std::placeholders::_1, std::placeholders::_2));

    // 5.设置服务器监听端口
    wsser.listen(8080);

    // 6.开始获取新建连接
    wsser.start_accept();

    // 7.启动服务器
    wsser.run();
    return 0;
}

tp->cancel();

但是,定时任务的取消会导致任务立即执行

session代码实现

cpp 复制代码
#ifndef __M_SS_H__
#define __M_SS_H__
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
#include "log.hpp"
#include"mysql.hpp"
#include <unordered_map>

typedef enum{UNLOGIN,LOGIN} ss_statu;
typedef websocketpp::server<websocketpp::config::asio> wsserver_t;

class session
{

    private:
    uint64_t _ssid; //标识符
    uint64_t _uid;  //session对应的用户ID
    ss_statu _statu;    //用户状态:未登录,已登录
    wsserver_t::timer_ptr _tp; //session关联的定时器

    public:
    session(uint64_t ssid)
    :_ssid(ssid)
     { LOG_DEBUG("session %p 被创建!!",this); }
    ~session(){ LOG_DEBUG("session %p 被释放!!",this);}
uint64_t ssid(){return _ssid;}
void set_user(uint64_t uid){_uid = uid;}
void set_statu(ss_statu statu){_statu=statu;}
uint64_t get_user()  {return _uid;}

bool is_login() {return _statu==LOGIN;}
void set_timer(wsserver_t::timer_ptr tp)  {_tp = tp;}
wsserver_t::timer_ptr  get_timer()  {return _tp;}

};


#define SESSION_TIMEOUT 30000
#define SESSION_FOREVER -1

using session_ptr = std::shared_ptr<session>;
class session_manager
{

private:
uint64_t _next_ssid;
std::mutex _mutex;
std::unordered_map<uint64_t, session_ptr> _session;
wsserver_t *_server;

public:

session_manager(wsserver_t *srv)
:_next_ssid(1)
,_server(srv)
{
    LOG_DEBUG("session管理器初始化完毕!");
}
~session_manager(){
    LOG_DEBUG("session管理器初销毁完毕!");
}
 session_ptr create_session(uint64_t uid,ss_statu statu)
 {

    std::unique_lock<std::mutex> lock(_mutex);
    session_ptr ssp(new session(_next_ssid));
    ssp->set_statu(statu);
    _session.insert(std::make_pair(_next_ssid,ssp));
    _next_ssid++;
    return ssp;
 }
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;
}

void append_session(const session_ptr & ssp)
{
 std::unique_lock<std::mutex> lock(_mutex);
  _session.insert(std::make_pair(ssp->ssid(),ssp));
}
void remove_session(uint64_t ssid)
{
 std::unique_lock<std::mutex> lock(_mutex);
 _session.erase(ssid);

}
void set_session_expire_time(uint64_t ssid,int ms)
{
//依赖于websocketpp的定时器来完成session生命周期的管理

//在http通信的时候(登录,注册)session应该具备生命周期,指定时间无通信后删除
//在客户端建立weebsocket长连接之后,session应该永久存在

//登录之后,创建session,session需要在指定时间无通信后删除
//但是进入游戏后就得永久存在
//退出游戏大厅或者游戏房间,session就得设置为临时的,在长时间无通信后删除

session_ptr ssp = get_session_by_ssid(ssid);
if(ssp.get() == nullptr)
{
    return;
}

wsserver_t::timer_ptr tp = ssp->get_timer();
//1.在session永久存在的情况下,设置永久存在
if(tp.get() == nullptr && ms == SESSION_FOREVER){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设置为永久存在
 //删除定时任务--- stread_time 删除定时任务会导致任务直接被执行
 tp->cancel();//取消定时任务不是立即取消,防止取消在下面的添加操作之前,所以用定时器来加
 ssp->set_timer(wsserver_t::timer_ptr());
 //重新添加session,添加一个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());
 //重新添加session,添加一个session信息
  wsserver_t::timer_ptr tmp_tp =_server->set_timer(ms,std::bind(&session_manager::append_session,this,ssp));
  //重新设置session的关联器
   ssp->set_timer(tmp_tp);
}

}

};



#endif

房间代码实现

cpp 复制代码
#ifndef __M_ROOM_H__
#define __M_ROOM_H__

#define  BOARD_ROW 15
#define  BOARD_COL 15
#define  CHESS_WHITE 1
#define  CHESS_BLACK 2

#include "mysql.hpp"
#include "online.hpp"
#include "json.hpp"
#include "log.hpp"

typedef enum
{
    GAME_START,
    GAME_OVEER
} room_statu;

class room
{

private:
    uint64_t _room_id; //房间ID
    room_statu _statu; //房间状态
    int _player_count; //房间人数
    uint64_t _white_id; //白棋id
    uint64_t _black_id; //黑棋ID
    user_table *_tb_user; //修改数据句柄
    online_manager* _online_user; //房间用户管理
    std::vector<std::vector<int>> _board; //棋盘
private:
 bool five(int row,int col,int row_off,int col_off,int color)
 {
    //row和col是下棋位置 ,row_off col_off是偏移量,也是方向
    int count = 1;//自己的位置
    int search_row = row + row_off;
    int search_col = col + col_off;
    while(search_row >= 0&&search_col<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_col<BOARD_ROW&&search_col>=0&&search_col<BOARD_COL&&_board[search_row][search_col]==color)
{
    //同色相加
    count++;
    //继续偏移
    search_row -= row_off;
    search_col -= col_off;
}
    return(count >= 5);
 }
 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;
}
    //返回胜利玩家的id,否则没有胜利者就返回零
    return 0;
 }
    public:
    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))
    {
       LOG_DEBUG("房间被创建成功!房间号:%ld",_room_id);

    }
    ~room()
    {
        LOG_DEBUG("房间被销毁成功!房间号:%ld",_room_id);
    }

    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_use(){return _white_id;}
    uint64_t get_black_use(){return _black_id;}

    //下棋动作
    Json::Value handle_chess(Json::Value &req)
    { Json::Value json_resp = req;
        //1.当前请求的房间号是不是与当前的房间号匹配
       uint64_t room_id = req["room_id"].asUInt64();
        //2.房间中两个玩家是否都在线,其中一个不在线直接判另一方胜利
        int chess_row = req["row"].asInt();
        int chess_col = req["col"].asInt();
        uint64_t cur_uid = req["uid"].asUInt64();
        if(_online_user->is_in_game_room(_white_id) == false)
        {
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜!";
            json_resp["winner"] = _black_id;
            return json_resp;
        }
                if(_online_user->is_in_game_room(_black_id) == false)
        {
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜!";
            json_resp["winner"] = _white_id;
            return json_resp;
        }
        //3.获取走棋位置,判断当前走棋是否合理(位置是否被占用)
        if(_board[chess_row][chess_col] !=0)
        {
            json_resp["result"] = false;
            json_resp["rason"] = "当前位置被其他棋子占用,请选择其他空白位置!";
            return json_resp;
        }
        int cur_color = cur_uid == _white_id?CHESS_WHITE:CHESS_BLACK;
        _board[chess_row][chess_col] =cur_color;
        //4.判断是否有玩家胜利(当前走棋位置是否存在五星连珠,横竖撇捺四个位置)
        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"] = winner_id;
            return json_resp;
    }
    //处理聊天动作
    Json::Value handle_chat(Json::Value &req)
    {
        Json::Value json_resp = req;
        //检测消息中是否包含敏感词
        std::string msg = req["message"].asString() ;
        size_t pos = msg.find("垃圾");
        if (pos != std::string::npos)
        {
            json_resp["result"] = false;
            json_resp["rason"] = "消息中包含敏感词汇,不能发送";
            return json_resp;
        }
        //广播消息-->返回消息
        json_resp["result"] = true;
        return json_resp;
    }
    //处理玩家退出房间动作
    void handle_exit(uint64_t uid)
    {
          Json::Value json_resp ;
        //如果下棋中退出,则对方胜利,如果是下棋结束后退出,则是正常退出
        
        if(_statu == GAME_START)
        {
            json_resp["optype"] = "put_chess";
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜!";
            json_resp["room_id"] = _room_id;
            
            json_resp["row"] = -1;
            json_resp["col"] = -1;
            json_resp["winner"] = uid==_white_id?_black_id:_white_id;
            broadcast(json_resp);
        }
        
        _player_count--;
        return;
        //房间人数--;
    }
    //总的请求函数,处理后调用对于函数,得到响应
    void  handle_request(Json::Value &req)
    {
        
        //1.校验房间号的匹配
        
          Json::Value json_resp ;
          uint64_t room_id = req["room_id"].asUInt64();
        if(room_id !=_room_id)
        {
            json_resp["optype"] =req["optype"].asString();
            json_resp["result"] = false;
            json_resp["rason"] = "房间号不匹配";
            return broadcast(json_resp);
        }
        //2.根据不同的请求类型处理不同的函数
        if(req["optype"].asString()=="put_chess")
        {
            json_resp = handle_chess(req);
            if(json_resp["winner"].asUInt64()!=0)
            {
                          //有玩家胜出,更新数据库信息
            uint64_t winner_id = json_resp["winner"].asInt64();
            uint64_t loser_id = winner_id == _white_id ? _black_id:_white_id;
            _tb_user->win(winner_id);
            _tb_user->lose(loser_id);
            _statu = GAME_OVEER;
            }
        }else if(req["optype"].asString()=="chat")
        {
            json_resp = handle_chat(req);
        }else
        {
            json_resp["optype"] =req["optype"].asString();
            json_resp["result"] = false;
            json_resp["rason"] = "未知请求类型";
            
        }
        broadcast(json_resp);
        return;
    }
    //将指定信息广播给房间所有人
    void broadcast(Json::Value &rsp)
    {
        //1.对要响应的信息进行序列号,将Json::Value中的数据序列号成json格式字符串
        std::string body;
        json_util::serialize(rsp,body);
        //2.获取房间中所有用户的通信连接
        //3.发送响应信息
         websocket_server::connection_ptr wconn = _online_user->get_conn_from_room(_white_id);
         if(wconn.get() != nullptr)
         {
            wconn->send(body);
         }
        websocket_server::connection_ptr bconn = _online_user->get_conn_from_room(_black_id);
         if(bconn.get() != nullptr)
         {
            bconn->send(body);
         }
         return;
    }


};

using room_ptr  = std::shared_ptr<room>;

class room_manager{
    private:
    uint64_t _next_rid;
    std::mutex _mutex;
    user_table * _tb_user;
    online_manager * _online_user;
    std::unordered_map<uint64_t,room_ptr> _rooms;
    std::unordered_map<uint64_t,uint64_t> _users;

    public:
    room_manager(user_table *ut,online_manager *om)
    : _next_rid(1)
    ,_tb_user(ut)
    ,_online_user(om)
    {
       LOG_DEBUG("房间管理模块初始化完毕!");
    }

    ~room_manager(){
         LOG_DEBUG("房间管理模块即将销毁!");
    }
    //为两个用户创建房间,并返回房间的智能指针管理对象
    room_ptr create_room(uint64_t uid1,uint64_t uid2)
    {
        //两个用户在游戏大厅匹配成功后创建房间
        //1.校验两个用户在不在游戏大厅中,只有都在才需要创建房间
        if(_online_user->is_in_game_hall(uid1) == false)
        {
            LOG_DEBUG("用户:%lu不在大厅中,创建房间失败!",uid1);
            return room_ptr();
        }
                if(_online_user->is_in_game_hall(uid2) == false)
        {
            LOG_DEBUG("用户:%lu不在大厅中,创建房间失败!",uid2);
            return room_ptr();
        }
        //2.创建房间,将用户信息添加到房间
        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);
        //3.将房间信息管理起来
        _rooms.insert(std::make_pair(_next_rid,rp));
        _users.insert(std::make_pair(uid1,_next_rid));
        _users.insert(std::make_pair(uid2,_next_rid));
        _next_rid++;
        //4.返回房间信息
        return rp;
    }
    //通过房间ID获取房间信息
    room_ptr get_room_by_rid(uint64_t rid )
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _rooms.find(rid);
        if(it == _rooms.end())
        {
            return room_ptr();
        }
        return it->second;
    }
    //通过用户ID获取房间信息
    room_ptr get_room_by_uid(uint64_t uid)
    {
        
        std::unique_lock<std::mutex> lock(_mutex);
        //通过用户ID获取房间ID
        auto uit = _users.find(uid);
        if(uit == _users.end())
        {
            return room_ptr();
        }
        //通过房间ID获取房间信息
        uint64_t rid = uit->second;
        auto rit = _rooms.find(rid);
        if(rit == _rooms.end())
        {
            return room_ptr();
        }        
        return rit->second;
    }
    //通过房间ID销毁房间
    void remove_room(uint64_t rid)
    {
        //因为房间信息是通过shareed_ptr在_room中进行管理的,所有只要将shared_ptr从_rooms中移除
        //该外界没有对房间信息操作的额情况下指针的计数器为零,自动释放
        //1.通过房间id获取房间信息
        room_ptr rp = get_room_by_rid(rid);
        if(rp.get() == nullptr)
        {
            return;
        }

        //2.通过房间信息获得所有用户的id
        uint64_t uid1 = rp->get_white_use();
        uint64_t uid2 = rp->get_black_use();
        //3.移除所有房间管理中的用户信息
        _users.erase(uid1);
        _users.erase(uid2);
        //4.销毁房间信息
        _rooms.erase(rid);
    }
    //删除房间中的指定用户,房间没有用户,则销毁房间,用户断开连接时调用
    void remove_room_user(uint64_t uid)
    {
        //1.通过房间id获取房间信息
        room_ptr rp = get_room_by_rid(uid);
        if(rp.get() == nullptr)
        {
            return;
        }
         rp->handle_exit(uid);

        //2.房间中没有玩家,则销毁房间
        if(rp->player_count() == 0 )
        {
            remove_room(rp->id());
        }
        return;
    }
};

#endif

匹配对战管理设计实现

将所有玩家根据得分,划分为三个档次

  1. score < 2000;
  2. score >= 2000&&score <3000 ;
  3. score >= 3000;

为3个不同的档次创建3个不同的匹配队列,如果有玩家要进行对战匹配,则根据玩家分数,将玩家的id,加入到指定的队列中,当一个队列中玩家数量>=2个时,就表示有两个玩家要进行匹配,就匹配成功,出队列中的前两个元素,为这两个玩家创建房间,向匹配成功的玩家发送匹配响应,对战匹配成功

设计:

  1. 设计一个匹配队列---阻塞队列
  2. 匹配管理,匹配队列有3个,因此创建3个线程,阻塞等待指定队列中的玩家数量>=2

功能:

  1. 入队数据
  2. 出队数据
  3. 移除指定的数据
  4. 线程安全
  5. 获取队列元素个数
  6. 阻塞
  7. 判断队列是否为空

匹配管理:

  1. 三个不同的队列
  2. 三个线程分别对应三个队列中的玩家进行匹配
  3. 房间管理模块的句柄
  4. 在线用户管理模块的句柄
  5. 数据管理模块的句柄 --用户操作表操作句柄

功能:

  1. 添加用户到匹配队列
  2. 从匹配队列移除用户
  3. 线程入口函数
cpp 复制代码
#include "room.hpp"
#include <condition_variable>
#include <list>
#include "log.hpp"



template <class T>
class match_queue
{

    private:
    std::list<T> _list; //用链表而不是queue,因为我们中间有删除数据的需求
    std::mutex _mutex; //实现线程安全
    std::condition_variable _cond;  //主要为了阻塞消费者,后边使用的时候,队列元素<2则阻塞,超过2则唤醒

    public:
    match_queue(){}
    ~match_queue(){}

    //获取元素个数
    int size()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        return _list.size();
    }
    //判断是否为空
    bool empty()
    {
          std::unique_lock<std::mutex> lock(_mutex);
          return _list.empty();
    }
    //入队列
    void push(const T & data)
    {
         std::unique_lock<std::mutex> lock(_mutex);
        _list.push_back(data);
        _cond.notify_all();
       
    }
    //出队列
    bool pop(T&data)
    {
         std::unique_lock<std::mutex> lock(_mutex);
         if(_list.empty() == 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);
      
    }
    //阻塞线程
    void wait()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _cond.wait(lock);
    }
};

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;
    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))
    {
        LOG_DEBUG("游戏匹配初始化完毕...");
     }
    ~matcher(){}
    bool add(uint64_t uid)
    {
        //根据玩家的天梯分数,来判定玩家档次,添加到不同的匹配队列
        //1.根据uid获取玩家信息
        Json::Value user;
      bool ret =  _ut->select_by_id(uid,user);
      if(ret == false)
    {
        LOG_DEBUG("获取玩家:%ld信息失败",uid);
        return false;
    }
        //2.添加到指定的队列中
        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)
    {
        LOG_DEBUG("获取玩家:%ld信息失败",uid);
        return false;
    }
        //2.添加到指定的队列中
        int score = user["score"].asInt();
        //添加到指定的队列中
        if(score < 2000)
        {
            _q_normal.remove(uid);
        }else if(score >=2000&&score <3000)
        {
            _q_high.remove(uid);
        }
        else 
        {
            _q_super.remove(uid);
        }
        return true;
    }
    private:
    void handle_match(match_queue<uint64_t> &mq)
    {
        while(1)
        {
            //1.判断队列人数是否大于2,<2则阻塞等待
            while(mq.size()<2)
            {
                mq.wait();
            }
            //2.人数够了,出队两个玩家
            uint64_t uid1 ,uid2;
            bool ret = mq.pop(uid1);
            if(ret == false)
            {
                continue;
            }
            ret = mq.pop(uid2);
            if(ret == false)
            {
                this->add(uid1);
                continue;
            }
            //3.校验两个玩家是否在线,有人掉线的话就得把另一个人从新放回队列
            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(uid1);
            if(conn2.get()==nullptr) 
            {
                this->add(uid1);
                continue;
            }
            //4.没问题的话,为两个玩家创建房间,并将玩家加入到房间中
          room_ptr rp = _rm->create_room(uid1,uid2);
          if(rp.get() == nullptr)
          {
            this->add(uid1);
            this->add(uid2);
            continue;
          }
            //5.对两个玩家进行响应
            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);}
};

整合服务器业务模块

服务器的整合实现:

  1. 网络通信接口的设计(收到了什么格式的数据,代表了什么样的请求,因该给与什么样的业务处理以及响应)
  2. 开始搭建服务器,搭建websocket服务器,实现网络通信,针对各种不同的请求进行不同的业务处理

客户端存在的HTTP请求:

  1. 客户端从服务器获取的注册页面
  2. 客户端给服务器发送了一个注册请求(提交了用户名&密码)
  3. 客户端从服务器获取了一个登陆页面
  4. 客户端给服务器发送了一个登录请求(提交了用户名&密码)
  5. 客户端从服务器获取游戏大厅页面
  6. 客户端给服务器发送流量一个获取个人信息的请求(展示个人信息)

websocket的请求

  1. 客户端给服务器发送了一个切换websocket协议通信的请求(建立游戏大厅长连接)
  2. 客户端给服务器发送一个对战匹配请求
  3. 客户端给服务器发送了一个停止匹配的请求
  4. 对战匹配成功,客户端从服务器获取一个游戏房间页面
  5. 客户端给服务器发送了一个切换websocket协议通信请求(建立游戏房间长连接)
  6. 客户端给服务器发送下棋请求
  7. 客户端给服务器发送了聊天请求
  8. 游戏结束返回游戏大厅(获取客户端给服务器发送一个获取游戏大厅页面的请求)

通信接口设计(restful风格)

静态资源请求

静态资源页面,在后台服务器就是个html/css/js文件,静态资源的请求处理,其实就是将文件中的内容发送给客户端

  1. 注册页面请求 请求:GET /register.html HTTP/1.1 响应:HTTP/1.1 200 OK Content-Length:xxx register.html文件内容数据
  2. 登录页面请求 请求 :GET /login.html HTTP/1.1
  3. 大厅页面请求 请求 :GET /gamehall.html HTTP/1.1
  4. 房间页面请求 请求 :GET /gameroom.html HTTP/1.1
相关推荐
yangmu32032 小时前
【碧蓝幻想Relink】DPS伤害统计Mod安装教程 + XMOD物品面板功能推荐
游戏
fpcc3 小时前
Linux 7.1介绍
linux·运维
蝶恋舞者3 小时前
Wireshark 抓包工具
网络·测试工具·wireshark
Sirius Wu3 小时前
OpenClaw Observability 并发安全(Per-Request Hook)完整详解
服务器·网络·人工智能·安全·ai·架构·aigc
三8443 小时前
磁盘管理/fdisk
linux·运维·服务器
库玛西4 小时前
深入剖析 Linux 线程机制与分页式存储管理
linux·服务器·c++·笔记
Helen_cai4 小时前
OpenHarmony 路由封装 RouterUtil 页面跳转统一管理(API Version23+)
运维·服务器·nginx·华为·harmonyos
春卷同学4 小时前
HarmonyOS掌上记账APP开发实践第18篇:编译时优化与 ArkTS 语法限制 — 提升应用性能的编码规范
linux·运维·ubuntu
Dovis(誓平步青云)4 小时前
《 AI直连数据库落地实践:基于MCP协议打通开发工具与数据库全链路运维分析》
大数据·linux·运维·数据库·人工智能·架构