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

基于websocketpp实现的五子棋项目

该博客对于学完C++和linux操作系统,但不知道如何用C++开发项目,已经不知道C++如何使用第三方库的人来说一定很有帮助,请耐心看完!

先看一下游戏会显示的前端界面,对理解这个游戏的前后端交互过程会有帮助

1. 开发环境

1.1 使用的操作系统:Ubuntu-22.04

我们可以先在虚拟机或者服务器上选择或者安装这个Ubuntu-22.04操作系统。最后我们这个网页对战五子棋的服务器是要部署在服务器上的。博主选用的是腾讯云的服务器。

1.2 安装gcc/g++编译器

我们程序使用g++进行编译。我们在命令行输入下面指令即可安装

sudo apt-get install gcc g++

1.3 安装gdb调试器

我们程序使用gdb进行调试。我们在命令行输入下面指令即可安装

sudo apt-get install gdb

1.4 安装git⼯具。

我的代码已经上传到码云之后:https://gitee.com/xwyg/Cpp_project.git

sudo apt-get install git

1.5 安装cmake项⽬构建⼯具

我们项目的自动化构建和编译只用到了make,而cmake用来执行websocket库的构建和编译。

sudo apt-get install cmake

1.6 安装第三方库:

安装jsoncpp库和 安装jsoncpp库

sudo apt-get install libjsoncpp-dev
sudo apt-get install libboost-all-dev

Jsoncpp库用来处理JSON格式数据,我们在进行客户端和服务器通信过程中正文部分传递的是字符串,单纯字符串的提取处理要复杂一些,我们将其转换成Json格式的数据,便于我们在请求和响应中提取对于信息。

我们采取的是Restful风格的网络通信接口,即为使用GET/POST/PUT/DELETE代表不同请求类型,并且正文格式都是使用JSON格式序列化后的字符串

安装websocketpp库

Websocketpp没有官方维护的预编译包直接供apt-get使用,因此它的安装比较复杂,需要我们自己下载源代码进行编译,大家可以在deepseek或者chatgpt中之间搜安装教程。用这两个生成的安装方法还是很准确的。

Websocketpp库是我们服务器使用的主要库,它依赖于boost库,处理WebSocket通信.

Websocket介绍: 

WebSocket协议是从HTML5开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制。

• 传统的web程序都是属于"⼀问⼀答"的形式,即客⼾端给服务器发送了⼀个HTTP请求,服务器给客⼾端返回⼀个HTTP响应。这种情况下服务器是属于被动的⼀⽅,如果客⼾端不主动发起请求服务器就无法主动给客户端响应

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

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

WebSocket原理解析

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

WebSocket也有自己的报文格式,但是其实与本项目关系没有那么大,我们之间WebSocketpp调用对应的接口即可,在后面项目代码中会有具体的介绍。

WebSocketpp同时⽀持HTTP和Websocket两种⽹络协议,⽐较适⽤于我们本次的项⽬,所以我们选⽤该库作为项⽬的依赖库⽤来搭建HTTP和WebSocket服务器。

1.7 mysql安装

博主安装的是mysql 5.7版本的数据库,各位安装8.0版本的数据库也是一样的,各位从deepseek或者chatgpt搜mysql的安装教程比较靠谱。

2.项目流程

这个流程图非常重要,关系到我们整个游戏的运行流程,已经前后端交互流程,还有服务器的各个模块之间的交互和联系。

2.1 客户端流程

客户端流程: 进入用户注册页面--->完成用户注册--->跳转到用户登录页面-->完成用户登录
---->跳转到游戏大厅页面--->点击按钮进入游戏匹配-->匹配成功跳转到游戏房价页面
---->游戏房间可以进行下棋或者聊天等操作--->游戏结束,加分或者扣分
--->该房间内无法下棋,弹出"回到大厅"按钮--->用户又可以点击按钮进入游戏匹配

2. 2 服务器流程

首先玩家想要访问我们的服务器得通过访问101.35.46.142:7080/register.html 注册页面或者101.35.46.142:7080/login.html登录页面,访问这两个页面会向我们的服务器发送http请求。

对于注册页面发送的http请求,服务器从请求正文中获取此时输入的用户名和密码,然后进行数据库中数据的插入; 对于登录页面发送的htpp请求,服务器从请求正文中获取此时输入的用户名和密码,然后服务器会建立用户的session并保存( 此后每次用户发送http或者websocket请求过来都会进行session的验证), 此时客户端跳转到游戏大厅页面,发送 大厅websocket长连接建立请求,服务器此时建立同客户端的长连接,当客户端点击游戏大厅页面的 "进入匹配" 按钮之后,会向客户端发送Websocket消息,服务器会按照该用户的等级分数 把他加入对应的匹配队列之中,等匹配成功之后会向两个进行匹配的客户端发送匹配成功的消息,客户端收到该请求进入游戏房间,此时参与匹配的两个客户端(玩家)都会向服务器发送 房间webhasocket长连接建立请求,服务器同客户端建立两个房间长连接,此时用户进行下棋或者聊天动作,都会向服务器发送对应请求,服务器也会给出对应的响应。

3.websocketpp库的介绍

3.1  websocketpp常用接口

namespace websocketpp {
typedef lib::weak_ptr<void> connection_hdl;
template <typename config>
class endpoint : public config::socket_type {
    typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;
    typedef typename connection_type::ptr connection_ptr;
    typedef typename connection_type::message_ptr message_ptr;
    typedef lib::function<void(connection_hdl)> open_handler;
    typedef lib::function<void(connection_hdl)> close_handler;
    typedef lib::function<void(connection_hdl)> http_handler;
    typedef lib::function<void(connection_hdl,message_ptr)> message_handler;
    * websocketpp::log::alevel::none 禁⽌打印所有⽇志*/
    void set_access_channels(log::level channels);/*设置⽇志打印等级*/
    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 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();
}

看到这些代码可能会一脸懵,我们只需要知道websocketpp命名空间下定义了 server类(继承自endpoint),它就是我们要启动的服务器,调用它的方法,我们就可以对服务器进行各种设置,同时它有一些回调函数:

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 (connection_hdl hdl)类型,请看使用实例:

#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
using namespace std;
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;

// 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客⼾端
    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);
    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());  //设置http响应正文
    con->set_status(websocketpp::http::status_code::ok);  //设置http响应状态码
}

这是main(),告诉我们server如何初始化,格式都是一样的,同时还需要绑定(注册)对应请求来时的处理动作。

int main()
{
    // 使⽤websocketpp库创建服务器
    websocketsvr server;
    // 设置websocketpp库的⽇志级别 all表⽰打印全部级别⽇志  none表⽰什么⽇志都不打印
    server.set_access_channels(websocketpp::log::alevel::none);
    /*初始化asio*/
    server.init_asio();
    // 注册http请求的处理函数
    server.set_http_handler(bind(&OnHttp, &server, ::_1));
    // 注册websocket请求的处理函数
    server.set_open_handler(bind(&OnOpen, &server, ::_1));
    server.set_close_handler(bind(&OnClose, &server, _1));
    server.set_message_handler(bind(&OnMessage, &server, _1, _2));
    // 监听8888端⼝
    server.listen(8888);
    // 开始接收tcp连接
    server.start_accept();
    // 开始运⾏服务器
    server.run();
    return 0;
}

Http客⼾端,使⽤浏览器作为http客⼾端即可,访问服务器的8888端⼝。

任意浏览器输入即可请求我们服务器:

前端如何发送websocket请求,这个不是我们的重点,但是也大致了解一下前端代码:

<html>
<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://192.168.51.100:8888");
    // 处理连接打开的回调函数
    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>

服务器启动,我们将上面的代码复制到 abc.html文件中,打开并在输入框输入hello,即可得到以下内容:

 new WebSocket("ws://192.168.51.100:8888");然后将他绑定在一个按钮中,点击按钮即可向后端发送websocket请求,此时服务器收到请求,会同客户端建立websocket长连接

4.项目实现

4.1 日志宏的实现

我们不采用websocketpp库中的日志类,而是自己编写一个日志类,定义一个日志宏,传入日志等级和可变参数,然后将线程ID,文件名,行号,时间与传入的可变字符串拼接起来,一起向终端(或文件)打印。

1. strftime函数

其中 strftime函数:将时间格式化为字符串。我们可以用snprintf(time_buffer, sizeof(time_buffer), "%d",format_time->tm_year + 1900)代替。

2.宏参数允许 进行 字符串拼接

 fprintf中第二个参数,要求传入一个const char*字符串,我们可以直接拼接我们需要的格式化字符串和传入的格式化字符串,这在函数无法实现   "[%p %s %s:%d] " format " \n"。

3.宏中可变参数

C++98允许宏传入可变参数,由##__VA_ARGS__代替

代码如下:

#pragma once

#include <stdio.h>
#include <time.h>
#include<pthread.h>
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG  //超过这个日志等级才会被输出
// 宏里面有多条语句,使用do while(0),
#define LOG(level, format, ...)                                               \
    do                                                                        \
    {                                                                         \
        if (level < LOG_LEVEL)                                                \
            break;                                                            \
        time_t t = time(NULL);                                                \
        struct tm *ltm = localtime(&t);                                       \
        char tmp[32] = {0};                                                   \
        strftime(tmp, 31, "%H:%M:%S", ltm);                                   \
        fprintf(stdout, "[%p %s %s:%d] " format " \n", (void *)pthread_self(), \
                tmp, __FILE__, __LINE__, ##__VA_ARGS__);                      \
    }while (0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)

4.2 工具类的实现

我们实现四个工具类,工具类中封装有静态方法,全局都可以使用该方法

首先是  json_util,提供序列化和反序列化的方法。

序列化通过Json::StreamWriter对象指针将Json::Value对象写入 str字符串中,反序列化通过Json::CharReader将str字符串写入Json::Value对象中

class json_util
{
public:
    // 将jsonvalue对象写入 str字符串中
    static bool serialize(const Json::Value &value, std::string &str)
    {
        Json::StreamWriterBuilder swb;
        std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
        std::stringstream ss;
        int ret = sw->write(value, &ss);
        if (ret != 0)
        {
            std::cout << "json serialize failed!" << std::endl;
            return false;
        }
        str = ss.str();
        return true;
    }
    // 将json字符串写回到value对象中,由用户自己做[""].asInt等处理
    static bool unserialize(const std::string &str, Json::Value &value)
    {
        Json::CharReaderBuilder crb;
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
        bool ret = cr->parse(str.c_str(), str.c_str() + str.size(),
                             &value, nullptr);
        if (!ret)
        {
            ERR_LOG("json unserialize failed!");
            return false;
        }
        return true;
    }
};

第二个是mysql_util,提供数据库的创建,销毁,执行方法

class mysql_util
{
public:
    static MYSQL *mysql_create(const std::string &host,
                               const std::string &user,
                               const std::string &pass,
                               const std::string &db,
                               int port)
    {
        MYSQL *mysql = mysql_init(NULL);
        if (mysql == NULL)
        {
            ERR_LOG("mysql init failed!");
            return NULL;
        }
        if (mysql_real_connect(mysql, host.c_str(), user.c_str(),
                               pass.c_str(), db.c_str(), port, NULL, 0) == NULL)
        {
            ERR_LOG("mysql connect server failed! %s", mysql_error(mysql));
            mysql_close(mysql);
            return NULL;
        }
        if (mysql_set_character_set(mysql, "utf8") != 0)
        {
            ERR_LOG("mysql set character failed!");
            mysql_close(mysql);
            return NULL;
        }
        DBG_LOG("mysql connect success!");
        return mysql;
    }
    static void mysql_destroy(MYSQL *mysql)
    {
        if (mysql == NULL)
        {
            return;
        }
        mysql_close(mysql);
    }
    static bool mysql_exec(MYSQL *mysql, const std::string &sql)
    {
        if (mysql_query(mysql, sql.c_str()) != 0)
        {
            ERR_LOG("SQL: %s", sql.c_str());
            ERR_LOG("ERR: %s", mysql_error(mysql));
            return false;
        }
        return true;
    }
};

第三个是string_util,提供字符串分割方法,因为我们涉及http 某一个请求头的提取已经某一个cookie的提取,提取不会改变原有的字符串和分割字符串,提取到一个vector<string>中

class string_util
{
public:
    // 字符串 子串分割功能, 将分割的子串保存到一个字符串数组之中
    static int split(const std::string &in, const std::string &sep,
                     std::vector<std::string> &arry)
    {
        arry.clear();
        size_t pos, idx = 0; // pos保存为查找结果,如果pos和idx相等,该位置就是sep,则不保存
        while (idx < in.size())
        {
            pos = in.find(sep, idx);
            if (pos == std::string::npos)
            {
                arry.push_back(in.substr(idx));
                break;
            }
            if (pos != idx)
            {
                arry.push_back(in.substr(idx, pos - idx)); // 当前位置,长度
            }
            idx = pos + sep.size();
        }
        return arry.size();
    }
};

第四个是file_util,对静态请求处理时将html文件返回给客户端

class file_util
{
public:
    static bool read(const std::string &filename, std::string &body)
    {
        std::ifstream file;
        // 打开⽂件
        file.open(filename.c_str(), std::ios::in | std::ios::binary);
        if (!file)
        {
            std::cout << filename << " Open failed!" << std::endl;
            return false;
        }
        // 计算⽂件⼤⼩
        file.seekg(0, std::ios::end);
        body.resize(file.tellg());
        file.seekg(0, std::ios::beg);
        file.read(&body[0], body.size());
        if (file.good() == false)
        {
            std::cout << filename << " Read failed!" << std::endl;
            file.close();
            return false;
        }
        file.close();
        return true;
    }
};

4.3 数据库操作模块实现

我们先定义数据库中的表,我们这个项目比较简单,只有一张user表,提供用户id,username,password,score,对战总场次和获胜场次这些字段

我们可以将每个表都封装到一个类之中,提供一个对外的MYSQL句柄,执行对应的数据库操作。

由于字符串中拼接比较麻烦,我们选择#define 定义格式化字符串,再有sprintf()函数去进行写入。

我们所需要进行数据库操作的地方有 用户插入,用户登录,通过用户名查询用户,通过用户id查询用户,用户获胜和用户失败这些情况,分别实现这些函数

#pragma once
#include "util.hpp"
#include <mutex>
#include <assert.h>
//  user_table类,将所要执行数据库操作的地方全部封装到该类之中,包含一个MYSQL指针和mutex互斥量
//  调用了 mysql_util类中的 创建销毁和执行方法
//  提供了 insert(user),login(user),win,lose,select_by_uid等等函数


// 每个函数所要执行的sql由宏定义给出,sql的字符串都要以;结尾,同时varchar类型都要在''里面
// mysql_query是线程安全的,但是它和mysql_store_result(_mysql)保存一起就不是线程安全的了
class user_table
{
private:
    MYSQL *_mysql;
    std::mutex _mutex;

public:
    user_table(const std::string &host,
               const std::string &user,
               const std::string &pass,
               const std::string &db,
               int port = 3306)
    {
        _mysql = mysql_util::mysql_create(host, user, pass, db, port);
        assert(_mysql != NULL);
    }
    // 网络中传输的是字符串,需要讲它们序列化到一个个的request对象中,再调用_cal计算并将结果,反序列化成字符串返回
    bool insert(Json::Value &user)
    {
#define INSERT_USER "insert user values(null, '%s', password('%s'), 1000, 0,0);"
        if (user["password"].isNull() || user["username"].isNull())
        {
            DBG_LOG("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)
        {
            DBG_LOG("insert user info failed!!\n");
            return false;
        }
        return true;
    }
        bool login(Json::Value & user) // 用户登录,并返回完整的用户信息
        {
            if (user["password"].isNull() || user["username"].isNull())
            {
                DBG_LOG("INPUT PASSWORD OR USERNAME");
                return false;
            }
            // 以用户名和密码共同查询,查询到数据则表⽰⽤⼾名密码⼀致,没有信息则用户名密码错误
#define LOGIN_USER "select id, score, total_count,win_count from user where username='%s' and password=password('%s');"
            char sql[4096] = {0};
            sprintf(sql, LOGIN_USER, user["username"].asCString(),
                    user["password"].asCString());
            MYSQL_RES *res = NULL;
            {
                // std::lock_guard<std::mutex> lock(_mutex);
                std::unique_lock<std::mutex> lock(_mutex);
                bool ret = mysql_util::mysql_exec(_mysql, sql);
                if (ret == false)
                {
                    DBG_LOG("user login failed!!\n");
                    return false;
                }
                // 将查询结果保存到本地
                res = mysql_store_result(_mysql);
                if (res == NULL)
                {
                    DBG_LOG("mysql_store_result exec error!!");
                    return false;
                }
            }
            std::cout << res << std::endl;

            // 根据结果集获取条目输了
            int num_row = mysql_num_rows(res);
            if (num_row == 0)
            {
                DBG_LOG("have no login user info!!");
                return false;
            }
            MYSQL_ROW row = mysql_fetch_row(res);
            // 查询结果集的四行数据设置进 原有user中
            user["id"] = std::stoi(row[0]);    // 如果数据范围小,默认int够用则无需转换
            user["score"] = std::stoi(row[1]); // (Json::UInt64)
            user["total_count"] = std::stoi(row[2]);
            user["win_count"] = std::stoi(row[3]);
            std::cout << "jjjjj" << std::endl;
            mysql_free_result(res);
            return true;
        }
        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)
                {
                    DBG_LOG("get user by name failed!!\n");
                    return false;
                }
                // 按理说要么有数据,要么没有数据,就算有数据也只能有⼀条数据
                res = mysql_store_result(_mysql);
                if (res == NULL)
                {
                    DBG_LOG("hmysql_store_result!!");
                    return false;
                }
            }
            int row_num = mysql_num_rows(res);
            if (row_num == 0)
            {
                DBG_LOG("have no login user info!!");
                return false;
            }
            MYSQL_ROW row = mysql_fetch_row(res);
            user["id"] = (Json::UInt64)std::stoi(row[0]);
            user["username"] = name;
            user["score"] = (Json::UInt64)std::stoi(row[1]);
            user["total_count"] = std::stoi(row[2]);
            user["win_count"] = std::stoi(row[3]);
            mysql_free_result(res);
            return true;
        }
        bool select_by_id(int id, Json::Value &user) // 通过id查询用户
        {
#define USER_BY_ID "select username,score,total_count,win_count from user where id = %d;"
            MYSQL_RES *res = NULL;
            char sql[4096] = {0};
            sprintf(sql, USER_BY_ID, id);
            {
                std::lock_guard<std::mutex> lock(_mutex);
                bool ret = mysql_util::mysql_exec(_mysql, sql);
                if (ret == false)
                {
                    DBG_LOG("select_by_id mysql_exec error");
                    return false;
                }
                res = mysql_store_result(_mysql);
                if (res == NULL)
                {
                    DBG_LOG("mysql_store_result error!!");
                    return false;
                }
            }
            int row_num = mysql_num_rows(res);
            if (row_num == 0)
            {
                DBG_LOG("have no login user info!!");
                return false;
            }
            MYSQL_ROW row = mysql_fetch_row(res);
            user["username"] = row[0];
            user["score"] = std::stoi(row[1]);
            user["total_count"] = std::stoi(row[2]);
            user["win_count"] = std::stoi(row[3]);
            mysql_free_result(res);
            return true;
        }
        bool win(int id) // 用户胜利时,总场次和胜利场次都加1
        {
#define USER_WIN "update user set score=score+30,total_count=total_count+1,  \
        win_count=win_count+1 where id=%d;"
            char sql[1024] = {0};
            sprintf(sql, USER_WIN, id);
            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if (ret == false)
            {
                DBG_LOG("update win user info failed!!\n");
                return false;
            }
            return true;
        }
        bool lose(int id) // 用户失败时,总场次加1,分数不变
        {
#define USER_LOSE "update user set score=score-30,total_count=total_count+1 where id=%d;"
            char sql[1024] = {0};
            sprintf(sql, USER_LOSE, id);
            bool ret = mysql_util::mysql_exec(_mysql, sql);
            if (ret == false)
            {
                DBG_LOG("update win user info failed!!\n");
                return false;
            }
            return true;
        }
        ~user_table()
        {
            mysql_util::mysql_destroy(_mysql);
            _mysql = NULL;
        }
    };

4.4 用户在线管理模块实现

用户在线管理模块记录了用户进入我们服务器之后所处的存在状态,是否离线,是在大厅还是房间

用户首先发送http请求注册页面,输入用户名密码完成数据库插入,然后进入登录页面,登录成功之后我们就需要将这个用户管理起来,因为存在好多的客户端,我们需要根据用户id找到这些客户端,因此选用 std::unordered_map<int, websocket_server::connection_ptr> _game_hall建立游戏大厅中用户的管理和std::unordered_map<int, websocket_server::connection_ptr> _game_room;游戏房间中用户的管理。

#pragma once
#include "util.hpp"
#include <mutex>
#include <unordered_map>

// 在线用户的管理类,在线用户要么在游戏大厅,要么在游戏房间
// 维护用户id到服务器连接的 游戏大厅map和用户id到服务器连接的 游戏房间map,以及一个互斥量mutex
// 提供进入(退出)大厅,进入(退出)房间,获取这个用户的连接 等操作

class online_manager
{
    // 使用map维护 从id到connection的关系
private:
    /*游戏⼤厅的客⼾端连接管理*/
    std::unordered_map<int, websocket_server::connection_ptr> _game_hall;
    /*游戏房间的客⼾端连接管理*/
    std::unordered_map<int, websocket_server::connection_ptr> _game_room;
    std::mutex _mutex;

public:
    /*进⼊游戏⼤厅--游戏⼤厅连接建⽴成功后调⽤*/
    void enter_game_hall(int uid, const websocket_server::connection_ptr &conn)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _game_hall.insert(std::make_pair(uid, conn));
    }
    /*退出游戏⼤厅--游戏⼤厅连接断开后调⽤*/
    void exit_game_hall(int uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _game_hall.erase(uid);
    }
    /*进⼊游戏房间--游戏房间连接建⽴成功后调⽤*/
    void enter_game_room(int uid, const websocket_server::connection_ptr &conn)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _game_room.insert(std::make_pair(uid, conn));
    }
    /*退出游戏房间--游戏房间连接断开后调⽤*/
    void exit_game_room(int uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _game_room.erase(uid);
    }
    /*判断用户是否在游戏⼤厅*/
    bool in_game_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _game_hall.find(uid);
        if (it == _game_hall.end())
        {
            return false;
        }
        return true;
    }
    /*判断用户是否在游戏房间*/
    bool in_game_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _game_room.find(uid);
        if (it == _game_room.end())
        {
            return false;
        }
        return true;
    }
    /*从游戏⼤厅中获取指定⽤⼾关联的Socket连接*/
    websocket_server::connection_ptr get_conn_from_game_hall(uint64_t uid)
                                         
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _game_hall.find(uid);
        if (it == _game_hall.end())
        {
            return nullptr;
        }
        return it->second;
    }
    /*从游戏房间中获取指定⽤⼾关联的Socket连接*/
    websocket_server::connection_ptr get_conn_from_game_room(int uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _game_room.find(uid);
        if (it == _game_room.end())
        {
            return nullptr;
        }
        return it->second;
    }

    online_manager()
    {
    }
    ~online_manager()
    {
    }
};

4.5 会话管理模块实现

现在基本所有网络通信都要实现一个会话管理模块,当用户登录成功之后,服务器使用一个SeeionId需要标记这个用户,这样后续用户每次操作都会发送sessionid给服务器,服务器也可以做用户验证,同时识别这是哪个客户端。

我们在类的设计上需要实现两个类,一个会话类,一个会话管理类,

会话类中包含会话id,用户id,会话状态,定时器

这个定时器主要是看这个session下是否设置了定时器过期任务,如果websocket_server::timer_ptr为空,则为永久存在。

#pragma once
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include "util.hpp"

typedef enum
{
    LOGIN,
    UNLOGIN
} ss_statu;

// 一个会话类和一个会话管理类。
// 会话类中包含      会话id,用户id,会话状态,定时器(不过期和何时过期)
// 会话管理类中:  互斥锁,websocket服务器(给它设置定时任务) 和
// 分配的下一个sessionid和sessionid到整个会话的map。 提供创建session 和设置过期时间的函数

class session
{
private:
    uint64_t _ssid; // 标识符
    int _uid;
    ss_statu _statu;
    websocket_server::timer_ptr _tp; // 该session相关定时器
public:
    session(uint64_t ssid) : _ssid(ssid)
    {
        DBG_LOG("SESSION %p 被创建!!", this);
    }
    ~session() { DBG_LOG("SESSION %p 被释放!!", this); }
    uint64_t ssid() { return _ssid; };
    void set_statu(ss_statu statu) { _statu = statu; }
    void set_user(int uid) { _uid = uid; }
    uint64_t get_user() { return _uid; }
    bool is_login() { return (_statu == LOGIN); }
    void set_timer(const websocket_server::timer_ptr &tp) { _tp = tp; }
    websocket_server::timer_ptr &get_timer() { return _tp; }
};

会话管理类中包含,需要分配的下一个会话id,websocket服务器(用于设置定时任务),一个会话id到整个会话的映射map. 

注意,websocket定时器取消时,它取消绑定函数会执行一次(不一定马上执行),所以需要重新添加。

#define SESSION_TIMEOUT 3000
#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;
    websocket_server *_server;

public:
    session_manager(websocket_server *srv) : _next_ssid(1), _server(srv)
    {
        DBG_LOG("session管理器初始化完毕!");
    }
    ~session_manager() { DBG_LOG("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);
        ssp->set_user(uid);  //创建会话时需要将用户id 和用户状态都设置进去
        _session.insert(std::make_pair(_next_ssid, ssp));
        _next_ssid++;
        return ssp;
    }
    void append_session(const session_ptr &ssp)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _session.insert(std::make_pair(ssp->ssid(), ssp));
    }
    session_ptr get_session_by_ssid(uint64_t ssid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _session.find(ssid);
        // 不存在这个ssid就返回空指针
        if (it == _session.end())
        {
            return session_ptr();
        }
        return it->second;
    }
    void remove_session(uint64_t ssid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _session.erase(ssid);
    }
    // 定时器tp->cancel 不是立即执行的,所以在_server->set_timer执行插入
    // session的定时任务重置需要 先取消再重新添加。  
    void set_session_expire_time(uint64_t ssid, int ms)
    {
        session_ptr ssp = get_session_by_ssid(ssid);
        if (ssp.get() == nullptr)
        {
            return;
        }
        websocket_server::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永久存在的情况下,设置指定时间之后被删除的定时任务
            websocket_server::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设置为永久存在
            // 取消定时任务
            tp->cancel();
            ssp->set_timer(websocket_server::timer_ptr());
            _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
        }
        else
        {
            // 4. 在session设置了定时删除的情况下,将session重置删除时间。
            // 先取消定时任务,再把该session对象添加到管理队列中
            tp->cancel();
            ssp->set_timer(websocket_server::timer_ptr()); 
            _server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
            // 重新绑定新的定时任务
            websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,
                                                                    std::bind(&session_manager::remove_session, this, ssid));
            ssp->set_timer(tmp_tp);
        }
    }
};

4.6 房间管理模块

用户进入游戏大厅后,存在于在线管理模块的map之中,然后用户选择进入匹配,此时为用户创建匹配队列,匹配成功创建房间,按照逻辑下来是先有匹配队列再有房间,但是匹配队列中必须调用创建房间的接口,去帮用户进入房间之中。因此先介绍房间管理模块。

第一个房间类,里面有成员房间id,房间状态,棋盘,黑棋白棋用户id,玩家数量,以及在线用户和数据库的管理句柄。它需要提供处理用户请求(聊天或者下棋)的函数,以及判断输赢,将响应返回给所有房间用户。

第二个房间管理类,分配房间的roomid,维护两个map,即为房间id到整个房间的映射map和用户id到房间id的映射map。

代码如下:

#pragma once

#include <memory>
#include "db.hpp"
#include "online.hpp"

// 房间类和房间管理类
// 房间类中有房间id,房间状态,棋盘,黑棋白棋用户id,玩家数量,以及在线用户和数据库 管理句柄
//  提供handle_request识别请求,进行下棋或者聊天,同时有 用户退出,广播等等动作
// 房间管理类 分配的roomid,两个map,提供房间的 create remove selectbyroomid等接口

typedef enum
{
    GAME_START,
    GAME_OVER
} room_status;

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

class room
{
private:
    uint64_t _room_id;
    room_status _status;
    int _player_count;
    int _white_id;
    int _black_id;
    user_table *_tb_user;
    std::vector<std::vector<int>> _board;
    online_manager *_online_user;

public:
    room()
    {
    }
    room(uint64_t room_id, user_table *tb, online_manager *online_user)
        : _room_id(room_id), _status(GAME_START), _player_count(0),
          _tb_user(tb), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
    {
        DBG_LOG("%lu 房间创建成功!!", _room_id);
    }
    ~room()
    {
        DBG_LOG("%lu 房间销毁成功!!", _room_id);
    }
    /*添加白棋黑棋用户,获取房间id等接口*/
    uint64_t id() { return _room_id; }
    room_status statu() { return _status; }
    int player_count() { return _player_count; }
    void add_white_user(int uid)
    {
        _white_id = uid;
        _player_count++;
    }
    void add_black_user(int uid)
    {
        _black_id = uid;
        _player_count++;
    }
    int get_white_user() { return _white_id; }
    int get_black_user() { return _black_id; }
    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_row < BOARD_ROW &&
               search_col >= 0 && search_col < BOARD_COL &&
               _board[search_row][search_col] == color)
        {
            // 同⾊棋⼦数量++
            count++;
            // 检索位置继续向后偏移
            search_row += row_off;
            search_col += col_off;
        }
        search_row = row - row_off;
        search_col = col - col_off;
        while (search_row >= 0 && search_row < BOARD_ROW &&
               search_col >= 0 && search_col < BOARD_COL &&
               _board[search_row][search_col] == color)
        {
            // 同⾊棋⼦数量++
            count++;
            // 检索位置继续向后偏移
            search_row -= row_off;
            search_col -= col_off;
        }
        return (count >= 5);
    }
    int check_win(int row, int col, int color)
    {
        // 从下棋位置的四个不同⽅向上检测是否出现了5个及以上相同颜⾊的棋⼦(横⾏,纵 列,正斜,反斜)
        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))
        {
            // 任意⼀个⽅向上出现了true也就是五星连珠,则设置返回值
            return color == CHESS_WHITE ? _white_id : _black_id;
        }
        return 0;
    }

    /*处理下棋动作*/
    Json::Value handle_chess(Json::Value &req)
    {
        Json::Value json_resp = req;
        // 2. 判断房间中两个玩家是否都在线,任意⼀个不在线,就是另⼀⽅胜利。
        int chess_row = req["row"].asInt();
        int chess_col = req["col"].asInt();
        uint64_t cur_uid = req["uid"].asUInt64();
        if (_online_user->in_game_room(_white_id) == false)
        {
            json_resp["result"] = true;
            json_resp["reason"] = "运⽓真好!对⽅掉线,不战⽽胜!";
            json_resp["winner"] = (Json::UInt64)_black_id;
            return json_resp;
        }
        if (_online_user->in_game_room(_black_id) == false)
        {
            json_resp["result"] = true;
            json_resp["reason"] = "运⽓真好!对⽅掉线,不战⽽胜!";
            json_resp["winner"] = (Json::UInt64)_white_id;
            return json_resp;
        }
        // 3. 获取⾛棋位置,判断当前⾛棋是否合理(位置是否已经被占⽤)
        if (_board[chess_row][chess_col] != 0)
        {
            json_resp["result"] = false;
            json_resp["reason"] = "当前位置已经有了其他棋⼦!";
            return json_resp;
        }
        int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;
        _board[chess_row][chess_col] = cur_color;
        // 4. 判断是否有玩家胜利(从当前⾛棋位置开始判断是否存在五星连珠)
        int 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;
    }
    /*处理聊天动作*/
    Json::Value handle_chat(const Json::Value &req)
    {
        Json::Value json_resp = req;
        std::string chat_message = req["message"].asString();
        if (chat_message.find("垃圾") != std::string::npos || chat_message.find("你干嘛") != std::string::npos)
        {
            json_resp["result"] = false;
            json_resp["reason"] = "嘻嘻,请说喜欢你";
            return json_resp;
        }
        json_resp["result"] = true;
        return json_resp;
    }
    /*处理退出动作*/
    void handle_exit(int uid)
    {
        Json::Value json_resp;
        // 如果是下棋状态中退出,一方胜利
        if (_status == GAME_START)
        {
            int 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"] = uid;
            json_resp["row"] = -1;
            json_resp["col"] = -1;
            json_resp["winner"] = winner_id;
            int loser_id = winner_id == _white_id ? _black_id : _white_id;
            _tb_user->win(winner_id);
            _tb_user->lose(loser_id);
            _status = GAME_OVER;
            broadcast(json_resp);
        }
        // 房间中玩家数量--
        _player_count--;
    }
    /*总的请求处理函数,区分不同请求类型,调用不同函数执行对应响应,得到响应进行广播*/
    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["reason"] = "房间号不匹配!";
            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"].asUInt64();
                uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
                _tb_user->win(winner_id);
                _tb_user->lose(loser_id);
                _status = GAME_OVER;
            }
        }
        else if (req["optype"].asString() == "chat")
        {
            json_resp = handle_chat(req);
        }
        else
        {
            json_resp["optype"] = req["optype"].asString();
            json_resp["result"] = false;
            json_resp["reason"] = "未知请求类型";
        }
        std::string body;
        json_util::serialize(json_resp, body);
        DBG_LOG("房间-⼴播动作: %s", body.c_str());
        return broadcast(json_resp);
    }
    /*将指定的信息广播给房间中的所有用户,即返回响应给所有用户*/
    void broadcast(const Json::Value &resp)
    {
        // 1. 对要响应的信息进⾏序列化,将Json::Value中的数据序列化成为json格式字符串
        std::string body;
        json_util::serialize(resp, body);
        // 2. 获取房间中所有⽤⼾的通信连接
        // 3. 发送响应信息
        websocket_server::connection_ptr white_conn =
            _online_user->get_conn_from_game_room(_white_id);
        if (white_conn.get() != nullptr)
        {
            white_conn->send(body);
        }
        else
        {
            DBG_LOG("房间-⽩棋玩家连接获取失败");
        }
        websocket_server::connection_ptr bconn = _online_user->get_conn_from_game_room(_black_id);
        if (bconn.get() != nullptr)
        {
            bconn->send(body);
        }
        else
        {
            DBG_LOG("房间-⿊棋玩家连接获取失败");
        }
        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; // 房间id到整个房间的映射
    std::unordered_map<int, uint64_t> _users;      // 用户id到房间id的映射

public:
    room_manager(user_table *ut, online_manager *om)
        : _next_rid(1000),
          _tb_user(ut),
          _online_user(om)
    {
        DBG_LOG("房间管理模块初始化完毕!");
    }
    ~room_manager()
    {
        DBG_LOG("房间管理模块即将销毁!");
    }
    /*两个用户匹配成功的用户创建房间*/
    room_ptr create_room(int uid1, int uid2)
    {
        // 1. 校验两个⽤⼾是否都还在游戏⼤厅中,只有都在才需要创建房间
        if (_online_user->in_game_hall(uid1) == false)
        {

            DBG_LOG("⽤⼾:%d 不在⼤厅中,创建房间失败!", uid1);
            return room_ptr();
        }
        if (_online_user->in_game_hall(uid2) == false)
        {
            DBG_LOG("⽤⼾:%d 不在⼤厅中,创建房间失败!", uid2);
            return room_ptr();
        }
        // 2. 创建房间,将⽤⼾信息添加到房间中
        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 room)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto rit = _rooms.find(room);
        if (rit == _rooms.end())
        {
            return room_ptr();
        }
        return rit->second; // 等价_rooms[room];
    }
    /*通过用户id获取房间*/
    room_ptr get_room_by_uid(int uid)
    {
        std::unique_lock<std::mutex> lock(_mutex); // 加锁??
        // 1. 通过⽤⼾ID获取房间ID
        auto uit = _users.find(uid);
        if (uit == _users.end())
        {
            return room_ptr();
        }
        uint64_t rid = uit->second;
        // 2. 通过房间ID获取房间信息
        auto rit = _rooms.find(rid);
        if (rit == _rooms.end())
        {
            return room_ptr();
        }
        return rit->second;
    }
    /*通过房间id删除房间*/
    void remove_room(uint64_t rid)
    {
        // 因为房间信息,是通过shared_ptr在_rooms中进⾏管理,因此只要将shared_ptr从_rooms中移除
        // 则shared_ptr计数器==0,外界没有对房间信息进⾏操作保存的情况下就会释放
        // 1. 通过房间ID,获取房间信息
        room_ptr rp = get_room_by_rid(rid);
        if (rp.get() == nullptr)
        {
            return;
        }
        // 2. 通过房间信息,获取房间中所有⽤⼾的ID
        uint64_t uid1 = rp->get_white_user();
        uint64_t uid2 = rp->get_black_user();
        // 3. 移除房间管理中的⽤⼾信息
        std::unique_lock<std::mutex> lock(_mutex);
        _users.erase(uid1);
        _users.erase(uid2);
        // 4. 移除房间管理信息
        _rooms.erase(rid);

        // auto it = _rooms.find(room);
        // if (it == _rooms.end())
        // {
        //     return;
        // }
        // std::unique_lock<std::mutex> lock(_mutex);
        // _users.erase(_rooms[room]->get_black_user());
        // _users.erase(_rooms[room]->get_white_user());

        // _rooms.erase(room);
    }
    /*删除房间中指定⽤⼾,如果房间中没有⽤⼾了,则销毁房间,⽤⼾连接断开时被调⽤*/
    void remove_room_by_user(int user)
    {
        auto it = get_room_by_uid(user);
        if (it.get() == nullptr)
        {
            return;
        }
        it->handle_exit(user);
        if (it->player_count() == 0)
        {
            remove_room(it->id());
        }
    }
};

4.7 匹配管理模块

我们将根据用户得分维护三个匹配队列,每次用户匹配请求都在各自所属的段位里面进行匹配。

匹配队列类:包含好多用户id,mutex和条件变量cond, 还有push,wait,pop,remove等接口。

匹配管理类: 包含三个匹配队列,同时初始化三个匹配队列的 线程入口函数,线程入口队列函数不断检测队列的大小是否超过2,超过则出队列创建房间,为两个玩家进行对战操作.

#pragma once
#include "room.hpp"
#include <list>
#include <condition_variable>

// 提供匹配队列和 匹配队列的管理类
// 匹配队列中包含好多用户id,mutex和条件变量cond,  还有push,wait,pop,remove等接口

// 匹配队列的管理类 ,包含三个匹配队列,同时初始化三个匹配队列的 线程入口函数
// 线程入口队列函数不断检测队列的大小是否超过2,超过则出队列创建房间,为两个玩家进行对战操作
// 其提供add(uid)和del(uid)两个函数

// T就是int类型就是每个队列中一个个的用户id
template <class T>
class match_queue
{
private:
    std::list<T> _list; // 我们使用list是因为我们需要 remove某些用户id
    std::mutex _mutex;
    std::condition_variable _cond; //条件变量,在该条件变量下 进行wait阻塞等待

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 wait()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _cond.wait(lock);
    }
    /*入队数据,并唤醒线程*/
    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())
        {
            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<int> _q_normal;
    /*⾼⼿匹配队列*/
    match_queue<int> _q_high;
    /*⼤神匹配队列*/
    match_queue<int> _q_super;
    /*对应三个匹配队列的处理线程*/
    std::thread _th_normal;
    std::thread _th_high;
    std::thread _th_super;
    room_manager *_rm;
    user_table *_ut;
    online_manager *_om;

    void handle_match(match_queue<int> &mq)
    {
        while(1)
        {
            // 队列人数小于2,则阻塞
            while(mq.size()<2)
            {
                mq.wait();
            }
            // 出队两个玩家,
            int id1,id2;
            bool ret= mq.pop(id1);
            if(ret==false)
            {
                continue;
            }
            ret= mq.pop(id2);
            if(ret==false)
            {
                mq.push(id2);
                continue;
            }
            //检测两个玩家是否在线
            websocket_server::connection_ptr conn1=_om->get_conn_from_game_hall(id1);
            if(conn1.get()==nullptr)
            {
                mq.push(id2);
                continue;
            }
            websocket_server::connection_ptr conn2=_om->get_conn_from_game_hall(id2);
            if(conn2.get()==nullptr)
            {
                mq.push(id1);
                continue;
            }
            //为两个玩家创建房间
            room_ptr rp= _rm->create_room(id1,id2);
            if(rp.get()==nullptr)
            {
                mq.push(id1);
                mq.push(id2);
                continue;
            }
            //给两个玩家返回响应
            Json::Value resp;
            resp["result"]=true;
            resp["optype"]="match_success";
            std::string body;
            json_util::serialize(resp,body);
            conn1->send(body);
            conn2->send(body);
        }
    }
    // 三个线程的入口函数
    void th_normal_entry()
    {
        handle_match(_q_normal);
    }
    void th_high_entry()
    {
        handle_match(_q_high);
    }
    void th_super_entry()
    {
        handle_match(_q_super);
    }

public:
    matcher(room_manager *rm, user_table *ut, online_manager *om)
        : _rm(rm), _ut(ut), _om(om),_th_normal(&matcher::th_normal_entry,this),
        _th_high(&matcher::th_high_entry,this),
        _th_super(&matcher::th_super_entry,this)
    {
        DBG_LOG("游戏匹配模块初始化完毕....");
    }
    bool add(int id)
    {
        Json::Value user;
        bool ret = _ut->select_by_id(id, user);
        if (ret == false)
        {
            DBG_LOG("获取玩家:%d 信息失败!!", id);
            return false;
        }
        int score = user["score"].asInt();
        if (score < 2000)
        {
            _q_normal.push(id);
        }
        else if (score >= 2000 && score <= 3000)
        {
            _q_high.push(id);
        }
        else
        {
            _q_super.push(id);
        }
        return true;
    }
    bool del(int id)
    {
        Json::Value user;
        bool ret = _ut->select_by_id(id, user);
        if (ret == false)
        {
            DBG_LOG("获取玩家:%d 信息失败!!", id);
            return false;
        }
        int score = user["score"].asInt();
        if (score < 2000)
        {
            _q_normal.remove(id);
        }
        else if (score >= 2000 && score <= 3000)
        {
            _q_high.remove(id);
        }
        else
        {
            _q_super.remove(id);
        }
        return true;
    }
    ~matcher()
    {
    }
};

4.7 服务器模块

最后服务器模块应该包含前面所有的模块,服务器类为第三方库websocketpp的 websocket服务器,因此其成员应该有这些:

    std::string _web_root;   // 静态资源根⽬录

    websocket_server _wssrv; // websocket_server对象

    user_table _ut;

    online_manager _om;

    room_manager _rm;

    matcher _mm;

    session_manager _sm;

接收到http或websocket连接请求,websocket请求是会调用相关的回调函数,我们只需要注册这些回调函数即可,同上面所写的websocket服务器框架相同

http请求,客户端发送http请求只有 刚开始访问服务器时,请求注册或者登陆页面或者根目录(静态资源请求),还有就是点击注册或登录时(功能请求),还有刚进入游戏大厅时,请求用户信息(功能请求).我们对这些请求进行判断,执行对应的函数。

注意req获得的uri是我们再http服务器所发送的/ 后面的资源路径,它是不带/的

    void http_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        std::string method = req.get_method();
        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 wsopen_callback(websocketpp::connection_hdl hdl)
    {
        // websocket长连接 建立成功之后 根据uri分辨是上面的哪一种
        websocket_server::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_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::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);
        }
    }

用户长连接消息请求有两种,一种是大厅发出的开始匹配请求,第二种是房间发出的下棋或者聊天请求。我们可以按照如下方式得到请求消息的Json::Value对象,注意这个与http请求获取正文内容的方式有所不同。

std::string req_body = msg->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg)
    {
        // websocket长连接通信处理回调函数
        // 1.判断是哪里的请求
        websocket_server::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);
        }
    }

总的服务器server.hpp

#pragma once

#include "room.hpp"
#include "online.hpp"
#include "session.hpp"
#include "matcher.hpp"
#include "db.hpp"
#include <string>

#define WWWROOT "./wwwroot/"

// 用户先进行注册(ajax请求),然后(跳转)登录(ajax请求),然后(跳转)匹配大厅(ajax请求)
// 点击开始匹配 进入匹配队列,客户端需要隔一段时间就问一下是否匹配成功

// websocket服务器可以返回http响应,con->setStatus,也可以返回websocket响应(直接send)
class gobang_server
{
private:
    std::string _web_root;   // 静态资源根⽬录 ./wwwroot/  ->./wwwroot/register.html
    websocket_server _wssrv; // websocket_server对象
    user_table _ut;
    online_manager _om;
    room_manager _rm;
    matcher _mm;
    session_manager _sm;
    // 静态网页的返回
    void file_handler(websocket_server::connection_ptr &conn)
    {
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        std::string file_path = WWWROOT + uri;
        // 如果请求路径是一个目录,则在路径后面加上login
        if (file_path.back() == '/')
        {
            file_path += "login.html";
        }
        std::string body;
        bool ret = file_util::read(file_path, body);
        if (ret == false)
        {
            std::string No_path = WWWROOT;
            No_path += "404.html";
            file_util::read(No_path, 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);
    }
    void http_resp(websocket_server::connection_ptr &conn, bool result,
                   websocketpp::http::status_code::value code, const std::string &reason)
    {
        Json::Value resp;
        resp["result"] = result;
        resp["reason"] = reason;
        std::string body;
        json_util::serialize(resp, body);
        conn->set_status(code);
        conn->append_header("Content-Type", "application/json");
        conn->set_body(body);
        return;
    }
    void reg(websocket_server::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)
        {
            DBG_LOG("反序列化注册信息失败");
            return http_resp(conn, false,
                             websocketpp::http::status_code::bad_request, "请求的正⽂格式错误");
        }
        // 3. 进⾏数据库的⽤⼾新增操作
        if (login_info["username"].isNull() ||
            login_info["password"].isNull())
        {
            DBG_LOG("⽤⼾名密码不完整");
            return http_resp(conn, false,
                             websocketpp::http::status_code::bad_request, "请输⼊⽤⼾名/密码");
        }
        ret = _ut.insert(login_info);
        if (ret == false)
        {
            DBG_LOG("向数据库插⼊数据失败");
            return http_resp(conn, false,
                             websocketpp::http::status_code::bad_request, "⽤⼾名已经被占⽤!");
        }
        // 如果成功了,则返回200
        return http_resp(conn, true, websocketpp::http::status_code::ok, "注册⽤⼾成功");
    }
    /*用户登录请求处理*/
    void login(websocket_server::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)
        {
            DBG_LOG("反序列化登录信息失败");
            return http_resp(conn, false,
                             websocketpp::http::status_code::bad_request, "请求的正⽂格式错误");
        }
        // 2. 校验正⽂完整性,进⾏数据库的⽤⼾信息验证
        if (login_info["username"].isNull() ||
            login_info["password"].isNull())
        {
            DBG_LOG("⽤⼾名密码不完整");
            return http_resp(conn, false,
                             websocketpp::http::status_code::bad_request, "请输⼊⽤⼾名/密码");
        }
        ret = _ut.login(login_info);
        if (ret == false)
        {
            // 1. 如果验证失败,则返回400
            DBG_LOG("⽤⼾名密码错误");
            return http_resp(conn, false,
                             websocketpp::http::status_code::bad_request, "⽤⼾名密码错误");
        }
        // 如果创建成功,则创建一个会话,并通过set-cookie返回会话
        int uid = login_info["id"].asInt();
        session_ptr ssp = _sm.create_session(uid, LOGIN);
        if (ssp.get() == nullptr)
        {
            DBG_LOG("创建会话失败");
            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_session_id = "SSID=" + std::to_string(ssp->ssid());
        conn->append_header("Set-Cookie", cookie_session_id);
        return http_resp(conn, true, websocketpp::http::status_code::ok,
                         "登录成功");
    }
    bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val)
    {
        // Cookie: SSID=XXX; path=/;   Cookie之间以;作为间隔,
        // 1. 我们对字符串进⾏分割,得到各个单个的cookie信息
        std::string sep = ";";
        std::vector<std::string> arr;
        string_util::split(cookie_str, sep, arr);
        for (auto str : arr)
        {
            // 2. 对单个cookie字符串,以 = 为间隔进⾏分割,得到key和val
            std::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;
    }
    // 用户会将 Cookie=abc 返回  先找cookie,再找cookie对应的SSID,再找SSID对应的会话,再找用户信息返回,然后设置会话过期时间
    void info(websocket_server::connection_ptr &conn)
    {
        // ⽤⼾信息获取功能请求的处理
        Json::Value err_resp;
        // 1. 获取请求信息中的Cookie,从Cookie中获取ssid
        std::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中取出ssid
        std::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, "找不到cookie的对应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);
    }

    // 一般的回调函数传入一个websocket服务器和连接管理句柄(必须传),我们有this可以访问服务器
    // 通过  服务器和连接处理句柄  我们可以获取这个连接,这个连接被我们传入各个功能函数
    void http_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        std::string method = req.get_method();
        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);
        }
    }
    // 用户建立长连接之后,服务器使用send发送信息给客户端
    void ws_resp(websocket_server::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(websocket_server::connection_ptr conn)
    {
        Json::Value err_resp;
        // 1. 获取请求信息中的Cookie,从Cookie中获取ssid
        std::string cookie_str = conn->get_request_header("Cookie");
        if (cookie_str.empty())
        {
            err_resp["optype"] = "hall_ready";
            err_resp["result"] = false;
            err_resp["reason"] = "没有cookie信息,请重新登录";
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        std::string value;
        bool ret = get_cookie_val(cookie_str, "SSID", value);
        if (ret == false)
        {
            err_resp["optype"] = "hall_ready";
            err_resp["result"] = false;
            err_resp["reason"] = "cookie中没有用户会话信息,请重新登录";
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        session_ptr ssp = _sm.get_session_by_ssid(std::stol(value));
        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(websocket_server::connection_ptr conn)
    {
        // 游戏⼤厅⻓连接建⽴成功
        Json::Value resp_json;
        // 1. 登录验证--判断当前客⼾端是否已经成功登录
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 2. 判断当前客⼾端是否是重复登录
        if (_om.in_game_hall(ssp->get_user()) ||
            _om.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. 将当前客⼾端以及连接加⼊到游戏⼤厅,游戏大厅维护了用户id到连接的map
        _om.enter_game_hall(ssp->get_user(), conn);
        // 4. 给客⼾端响应游戏⼤厅连接建⽴成功
        resp_json["optype"] = "hall_ready";
        resp_json["reason"] = "游戏大厅进入成功!";
        resp_json["result"] = true;
        ws_resp(conn, resp_json);
        // 5. 记得将session设置为永久存在
        _sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);
    }

    // 逻辑:大厅中加入匹配队列,线程创建房间并返回前端match_success, 前端离开在线用户管理模块
    void wsopen_game_room(websocket_server::connection_ptr conn)
    {
        // 1. 获取当前客户端的session
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return;
        }
        // 2.判断该用户是否在其它房间或者大厅中,如果是则出错
        Json::Value resp_json;
        if (_om.in_game_hall(ssp->get_user()) || _om.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"] = ssp->get_user();
        resp_json["white_id"] = rp->get_white_user();
        resp_json["black_id"] = rp->get_black_user();
        return ws_resp(conn, resp_json);
    }
    // 长连接建立有两种,第一种是进入匹配队列,第二种是进入游戏房间的时候
    void wsopen_callback(websocketpp::connection_hdl hdl)
    {
        // websocket长连接 建立成功之后 根据uri分辨是上面的哪一种
        websocket_server::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(websocket_server::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(websocket_server::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_by_user(ssp->get_user());
    }

    void wsclose_callback(websocketpp::connection_hdl hdl)
    {
        websocket_server::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(websocket_server::connection_ptr conn, websocket_server::message_ptr msg)
    {
        Json::Value resp_json;
        // 1. ⾝份验证,当前客⼾端到底是哪个玩家
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            return; // get_session_by_cookie内部已经返回了错误响应
        }
        // 2. 获取请求信息
        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["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(websocket_server::connection_ptr conn, websocket_server::message_ptr msg)
    {
        // 进入房间页面,建立房间的长连接
        Json::Value resp_json;
        // 1. 获取当前客⼾端的session
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr)
        {
            DBG_LOG("房间-没有找到会话信息");
            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;
            DBG_LOG("房间-没有找到玩家房间信息");
            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;
            DBG_LOG("房间-反序列化请求失败");
            return ws_resp(conn, resp_json);
        }
        DBG_LOG("房间:收到房间请求,开始处理....");
        // 4. 通过房间模块进⾏消息请求的处理
        return rp->handle_request(req_json);
    }
    void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg)
    {
        // websocket长连接通信处理回调函数
        // 1.判断是哪里的请求
        websocket_server::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();
    }
};

主函数gobang.cc

#include "room.hpp"
#include"session.hpp"

#define HOST "127.0.0.1"
#define PORT 3306
#define USER "root"
#define PASSWD "123456"
#define DBNAME "gobang"
#include"matcher.hpp"
#include"server.hpp"

int main()
{
    // user_table ut(HOST, USER, PASSWD, DBNAME, PORT);
    // match_queue<int> mq;
    // online_manager om;
    // room_manager rm(&ut,&om);
    // matcher mt(&rm,&ut,&om);
    gobang_server s(HOST, USER, PASSWD, DBNAME, PORT);
    s.start(7080);
    return 0;
}


http://www.kler.cn/a/600406.html

相关文章:

  • MIPI 详解:XAPP894 D-PHY Solutions
  • 北京交通大学第三届C语言积分赛
  • 新手如何使用 Milvus
  • 大数据学习(83)-数仓建模理论
  • 新版 eslintrc 文件弃用 .eslintignore已弃用 替代方案
  • x-cmd install | Wuzz - Web 开发与安全测试利器,交互式 HTTP 工具
  • 基于javaweb的SpringBoot公司财务管理设计与实现(源码+文档+部署讲解)
  • Linux上位机开发实战(编写API库)
  • VitePress由 Vite 和 Vue 驱动的静态站点生成器
  • Python:单例模式魔法方法
  • 【机器学习】--二分类
  • flink广播算子Broadcast
  • matlab近似计算联合密度分布
  • 当汉堡遇上便当:TypeScript命名空间 vs JavaScript模块化
  • 销售易CRM:技术革新助力客户关系管理智能化
  • DHCPv6 Stateless Vs Stateful Vs Stateless Stateful
  • 关于网络的一点知识(持续更新)
  • 【第二月_day7】Pandas 简介与数据结构_Pandas_ day1
  • 数据结构——链式队列
  • Spring Security核心源码和功能实现