C++基于websocketpp的多用户网页五子棋项目

项目结构设计

模块划分

大模块划分,各自的功能,如何进行配合协同完成项目

分为三个大模块:业务处理(接收客户端的一些请求并且做出响应)、数据管理(当用户请求注册或者登录的时候,业务处理模块收到请求,去数据管理模块进行相关操作)、前端模块(响应yoghurt的页面请求)

项目流程图

用户玩家角度

服务器角度

每次会话验证验证的是这个用户的登录信息存不存在或者匹配与否,以及过期了没有(过期了也会删除此用户的会话)

实用工具类代码实现

负责提前实现项目中边缘功能模块代码

日志宏的实现

代码实现

##__VA_ARGS__:处理可变参数,兼容无参数时的空输入

行末尾反斜杠是宏定义中行与行之间的续行符

cpp 复制代码
#pragma once

#include <time.h>
#include <stdio.h>

#define INFO 0
#define DEBUG 1
#define ERROR 2
#define DefaultLevel DEBUG
#define LOG(level, format, ...) do\
{\
    if (level > DefaultLevel) break;\
    time_t t = time(NULL);\
    struct tm *lt = localtime(&t);\
    char timebuf[32] = {0};\
    strftime(timebuf, 31, "%H:%M:%S", lt);\
    fprintf(stdout, "[%s %s:%d] " format "\n", timebuf, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)

#define ILOG(format, ...) LOG(INFO, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DEBUG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERROR, format, ##__VA_ARGS__)

mysql工具类的实现

前四个封装在一个函数中,mysql_create, 执行语句在mysql_exec中,释放句柄在mysql_destroy中;6,7不需封装,在操作的时候查看

代码实现

cpp 复制代码
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)
    {
        // 初始化句柄
        MYSQL* mysql = mysql_init(NULL);
        if (mysql == NULL)
        {
            ELOG("mysql init failed");
            return NULL;
        }
        // 连接mysql服务器
        if (mysql_real_connect(mysql,
                    host.c_str(),
                    username.c_str(),
                    password.c_str(),  
                    dbname.c_str(),
                    port, NULL, 0) == NULL)
        {
            ELOG("mysql connect server failed: %s", mysql_error(mysql));
            mysql_close(mysql);
            return NULL;
        }
        // 设置字符集
        if (mysql_set_character_set(mysql, "utf8") != 0)
        {
            ELOG("mysql set character failed: %s", mysql_error(mysql));
            mysql_close(mysql);
            return NULL;
        }
        return  mysql;
    }
    static bool mysql_exec(MYSQL* mysql, const std::string &sql)
    {
        int n = mysql_query(mysql, sql.c_str());
        if (n != 0)
        {
            ELOG("%s", sql.c_str());
            ELOG("mysql query failed: %s", mysql_error(mysql));
            return false;
        }
        return true;
    }
    static void mysql_destory(MYSQL *mysql)
    {
        if (mysql != NULL)
        {
            mysql_close(mysql);
        }
        return;
    }
};

json工具类

cpp 复制代码
class json_util
{
public:
    static bool serialize(const Json::Value &root, std::string &str)
    {
        Json::StreamWriterBuilder swb;
        std::unique_ptr<Json::StreamWriter>sw(swb.newStreamWriter());
        std::stringstream ss;
        int ret = sw->write(root, &ss);
        if (ret != 0) {
            ELOG("json serialize failed!!");
            return false;
        }
        str = ss.str();
        return true;
    }
    static bool unserialize(const std::string &str, Json::Value &root)
    {
        Json::CharReaderBuilder crb;
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
        std::string err;
        bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);
        if (ret == false) {
            ELOG("json unserialize failed: %s", err.c_str());
            return false;
        }
        return true;
    }
};

string工具类

实现字符串分割

cpp 复制代码
class string_util
{
public:
    // 将字符串src按照sep分割,结果存入ret中
    static int string_split(const std::string &src, const std::string &sep, std::vector<std::string> &ret)
    {
        // 123,123,123,,,222
        int pos = 0, idx = 0;
        while (pos < src.size())
        {
            idx = src.find(sep, pos);
            if (idx == std::string::npos)
            {
                ret.push_back(src.substr(pos));
                break;
            }
            if (idx == pos) // 当前位置就是分隔符,跳过
            {
                pos += sep.size();
                continue;
            }
            ret.push_back(src.substr(pos, idx - pos));
            pos = idx + sep.size();
        }
        return ret.size();
    }
};

file工具类

实现读取文件的功能, 将结果保留在一个字符串内

包含头文件<fstream>

整体思路:首先需要传入文件路径和结果保留字符串;之后使用ifstream打开文件,并且以二进制std::ios::binary的形式打开,用is_open()判断打开是否成功,失败则输出日志消息,告诉哪一个路径下的文件打开失败,并且返回false。

然后获取文件大小,使用seekg()将文件指针指向结尾std::ios::end,再使用tellg()获取指针相对于起始位置的偏移量,这就是文件大小,然后seekg(0, std::ios::beg)重置文件指针到开头位置。之后将保留字符串resize成为文件大小,这样字符串就可以保存文件内容,大小一样。然后使用文件接口read读取文件,第一个参数是结果字符串的地址,第二个参数是读取的内容大小,使用good()==false判断读取是否失败,失败输出日志消息并且close关闭文件。最后关闭文件

cpp 复制代码
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) {
                ELOG("%s file open failed!!", filename.c_str());
                return false;
            }
            //获取文件大小
            size_t fsize = 0;
            ifs.seekg(0, std::ios::end);
            fsize = ifs.tellg();
            ifs.seekg(0, std::ios::beg);
            body.resize(fsize);
            //将文件所有数据读取出来
            ifs.read(&body[0], fsize);
            if (ifs.good() == false) {
                ELOG("read %s file content failed!", filename.c_str());
                ifs.close();
                return false;
            }
            //关闭文件
            ifs.close();
            return true;
        }
};

数据管理模块

这个模块的作用是操作管理用户的数据。在比赛中用户可能进行一系列操作造成用户数据的改变,这个改变通过数据管理模块来同步到数据库

首先创建一个数据库gobang,并且在这个库下面创建一张表user,表的属性包含:int类型的id为主键并且设置为自增,vachar(32)类型的username并且添加唯一键约束且不为空,varchar(128)类型的password不为空,int类型的score,int类型的total_count, win_count分别表示分数、比赛总场次,胜利的场次

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

mysql> source /home/user1/MyProject/project/MyGoBang/source/db.sql; 导入db.sql文件

现在封装一个user_table类,每一个用户都可以实例化通过此类调用类中的方法来对数据库中的user表进行数据操作

数据管理类

这个类里面成员属性为MYSQL *_mysql 和 std::mutex _mutex 。通过操作句柄和util.hpp中mysql_util类中的方法对数据库进行操作。而锁则是处理多个用户同时操作数据库造成的数据不一致问题

构造:调用mysql_util::mysql_create初始化,获取句柄;

析构:调用mysql_util::mysql_destroy释放句柄,并且将mysql置为空

注册:insert,新增用户,函数参数为Json::Value &user,传递过来的用户注册信息用value序列化了,现在只知道用户的用户名和密码。#define INSERT_USER "insert user value(null, '%s', '%s', 1000, 0, 0);" 定义一个这个东西,通过sprintf 将user中的用户名和密码格式化进去,之后保存在一个字符串中,然后调用mysql_util::mysql_exec执行语句。在最开始要判断用户名和密码是不是都有,使用Json库中的isNull(),判断,例如user"username".isNull()

C语言中的宏定义成为字符串,若字符串内部有%s信息,用单引号,有%d,不加引号

cpp 复制代码
    bool insert(Json::Value &user)
    {
#define INSERT_USER "insert user value(null, '%s', '%s', 1000, 0, 0);"
        if (user["username"].isNull() || user["password"].isNull())
        {
            DLOG("username or password missing");
            return false;
        }
        char sql[1024];
        sprintf(sql, INSERT_USER, user["username"].asCString(), user["password"].asCString());
        bool n = mysql_util::mysql_exec(_mysql, sql);
        if (n == false)
        {
            DLOG("user insert failed");
            return false;
        }
        return true;
    }

登录:登录的函数是通过判断用户名和密码执行select语句看信息存在与否;找到的结果要保存在传入的参数user中。首先判断传递的用户名和密码是不是都有,然后#define 一条查询语句和上面一样格式化到缓存sql中,然后执行sql查询语句。通过MYSQL_RES *res = mysql_store_result(_mysql)保存查询之后的存储信息,按理说只有一条,要先判断res有没有,没有说明没有这个用户DLOG输出用户登陆失败,返回false,若有则继续判断mysql_num_rows(res)获取的行数是不是等于1,若不是说明用户信息不唯一,那么DLOG输出此信息返回false。之后才可以通过MYSQL_ROW row = mysql_fetch_row(res);先拿到行然后通过下标的方式将信息填入user。最后释放res mysql_free_result(res);

其中要加锁,加锁内容是mysql_exex以及mysql_store_result,因为多用户同时执行时这两个虽然各自是原子的,但是写在一起就不行了,有的用户可能执行mysql_store_result的时候,另一个用户先执行了mysql_exex,并且也是查询,那么就会出错

cpp 复制代码
    bool login(Json::Value &user)
    {
#define LOGIN_USER "select id, score, total_count, win_count from user where username='%s' and password='%s';"
        if (user["username"].isNull() || user["password"].isNull())
        {
            DLOG("username or password missing");
            return false;
        }  
        char sql[1024];
        sprintf(sql, LOGIN_USER, user["username"].asCString(), user["password"].asCString()); 
        MYSQL_RES *res = NULL;   
        {
            std::unique_lock<std::mutex> lock(_mutex);// 中途退出时不用手动释放锁
            {
                bool n = mysql_util::mysql_exec(_mysql, sql);
                if (n == false)
                {
                    DLOG("user login failed");
                    return false;
                }          
                res = mysql_store_result(_mysql);
                if (res == NULL)
                {
                    DLOG("there is no user info");
                    return false;
                }
            }
        }
        int rowcnt = mysql_num_rows(res);
        if (rowcnt != 1)
        {
            DLOG("user info is not unique");
            return false;
        }
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = std::stoi(row[0]);
        user["score"] = std::stoi(row[1]);
        user["total_count"] = std::stoi(row[2]);
        user["win_count"] = std::stoi(row[3]);
        mysql_free_result(res);
        return true;
    }

通过用户名获取用户信息:参数为Value &user 以及一个name,通过此name进行select查询,找到用户信息,存储在res中,这部分逻辑和login中一样。最后将数据保留在user中

cpp 复制代码
    bool user_by_name(Json::Value &user, const std::string &name)
    {
#define USER_BY_NAME "select id, score, total_count, win_count from user where username='%s';"
        if (user["username"].isNull())
        {
            DLOG("username missing");
            return false;
        }  
        char sql[1024];
        sprintf(sql, USER_BY_NAME, name.c_str()); 
        MYSQL_RES *res = NULL;   
        {
            std::unique_lock<std::mutex> lock(_mutex);
            {
                bool n = mysql_util::mysql_exec(_mysql, sql);
                if (n == false)
                {
                    DLOG("get user info by name failed");
                    return false;
                }          
                res = mysql_store_result(_mysql);
                if (res == NULL)
                {
                    DLOG("there is no user info");
                    return false;
                }
            }
        }
        int rowcnt = mysql_num_rows(res);
        if (rowcnt != 1)
        {
            DLOG("user info is not unique");
            return false;
        }
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = std::stoi(row[0]);
        user["username"] = name;
        user["score"] = std::stoi(row[1]);
        user["total_count"] = std::stoi(row[2]);
        user["win_count"] = std::stoi(row[3]);
        mysql_free_result(res);
        return true;        
    }

通过用户id获取用户信息

cpp 复制代码
    bool user_by_id(Json::Value &user, uint64_t id)
    {
#define USER_BY_ID "select username, score, total_count, win_count from user where id=%ld;" 
        char sql[1024];
        sprintf(sql, USER_BY_ID, id); 
        MYSQL_RES *res = NULL;   
        {
            std::unique_lock<std::mutex> lock(_mutex);
            {
                bool n = mysql_util::mysql_exec(_mysql, sql);
                if (n == false)
                {
                    DLOG("get user info by id failed");
                    return false;
                }          
                res = mysql_store_result(_mysql);
                if (res == NULL)
                {
                    DLOG("there is no user info");
                    return false;
                }
            }
        }
        int rowcnt = mysql_num_rows(res);
        if (rowcnt != 1)
        {
            DLOG("user info is not unique");
            return false;
        }
        MYSQL_ROW row = mysql_fetch_row(res);
        user["id"] = id;
        user["username"] = row[0];
        user["score"] = std::stoi(row[1]);
        user["total_count"] = std::stoi(row[2]);
        user["win_count"] = std::stoi(row[3]);
        mysql_free_result(res);
        return true;        
    }

胜利时分数加30,总场次加1,胜利场次加1

失败时分数减30,总场次加1,胜利场次加1

cpp 复制代码
    bool win(uint64_t id)
    {
#define WIN "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id=%ld;" 
        char sql[1024];
        sprintf(sql, WIN, id);
        bool n = mysql_util::mysql_exec(_mysql, sql);
        if (n == false)
        {
            DLOG("update user info when win failed");
            return false;
        }
        return true;
    }
    bool lose(uint64_t id)
    {
#define LOSE "update user set score=score-30, total_count=total_count+1 where id=%ld;" 
        char sql[1024];
        sprintf(sql, LOSE, id);
        bool n = mysql_util::mysql_exec(_mysql, sql);
        if (n == false)
        {
            DLOG("update user info when lose failed");
            return false;
        }
        return true;
    }

在线用户管理模块

管理的是进入游戏大厅和游戏房间的用户,这些用户会建立websocket长连接,通过此连接可以向其他在线用户发送自己的消息(例如聊天或者下棋消息);在线说明建立了websocket,反之用户要退出那么就要关闭连接,所以通过此连接可以判断用户是否在线(在线的体现就是进入了房间或者大厅)

具体地,通过用户的id和连接建立对应关系,用哈希表来建立映射;通过此id可以对通信连接进行一系列操作,例如建立连接、断开连接、判断用户是否在线以及获取连接从而通信

代码实现

将头文件包进util.hpp工具类,进入了游戏大厅或者房间就是要建立长连接websocket 将会使用到其中的一些接口

#include <websocketpp/server.hpp>

#include <websocketpp/config/asio_no_tls.hpp>

typedef websocketpp::server<websocketpp::config::asio> wsserver_t; 方便后序使用

还需要包含锁<mutex>和哈哈希表<unordered_map>,包含锁是因为建立映射关系的哈希表是一个临界资源,会被共享,应该互斥访问占有

online_manager中包含属性_mutex,用于建立游戏大厅用户的用户ID与通信连接的关系 _hall_user,用于建立游戏房间用户的用户ID与通信连接的关系 _room_user 两者的类型为unordered_map<uint64_t, wsserver_t::connection_ptr>,分别表示进入大厅和进入房间的用户id与其对应的通信连接

包含成员属性 enter_game_hal以及enter_game_room(websocket连接建立的时候才会加入游戏大厅&游戏房间在线用户管理)、exit_game_hall以及exit_game_room(websocket连接断开的时候,才会移除游戏大厅&游戏房间在线用户管理)、is_in_game_hall以及is_in_game_room(判断当前指定用户是否在游戏大厅/游戏房间)、get_conn_from_hall以及get_conn_from_room(通过用户ID在游戏大厅/游戏房间用户管理中获取对应的通信连接)

cpp 复制代码
#ifndef __ONLINE_H__
#define __ONLINE_H__

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

class online_manager
{
private:
    std::mutex _mutex;
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _hall_user;
    std::unordered_map<uint64_t, wsserver_t::connection_ptr> _room_user;
public:
    void enter_game_hall(uint64_t uid, wsserver_t::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, wsserver_t::connection_ptr &conn)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _room_user.insert(std::make_pair(uid, conn));
    }    
    void exit_game_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if (_hall_user.find(uid) != _hall_user.end())
            _hall_user.erase(uid);
    }
    void exit_game_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if (_room_user.find(uid) != _room_user.end())
            _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;
    }
    wsserver_t::connection_ptr get_conn_from_hall(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _hall_user.find(uid);
        if (it == _hall_user.end())
            return wsserver_t::connection_ptr();
        return it->second;
    }
    wsserver_t::connection_ptr get_conn_from_room(uint64_t uid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto it = _room_user.find(uid);
        if (it == _room_user.end())
            return wsserver_t::connection_ptr();
        return it->second;
    }    
};

#endif

游戏房间管理模块

分为游戏房间(小范围用户的关联关系)的设计和管理房间的设计(房间可能存在很多,需要统一管理)

游戏房间的设计

思想

补充:8.在线用户的管理句柄(在广播时需要知道房间内其他用户的句柄,拿着这个句柄将数据广播到其他用户)

在房间操作之前,了解通信的结构数据格式


websocket⻓连接协议切换请求(进入游戏房间)

/* ws://localhost:9000/room */
GET /room HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
......
HTTP/1.1 101 Switching
......
WebSocket握手成功后的回复:表示游戏房间已经进入成功
/*协议切换成功, 房间已经建⽴*/
{
"optype": "room_ready",
"room_id": 222, //房间ID
"self_id": 1, //⾃⾝ID
"white_id": 1, *//
⽩棋
*ID

"black_id": 2, //⿊棋ID
}
走棋
{
"optype": "put_chess", // put_chess**表⽰当前请求是下棋操作
"room_id": 222, // room_id**表⽰当前动作属于哪个房间
"uid": 1, //当前的下棋操作是哪个⽤⼾发起的
"row": 3, //当前下棋位置的⾏号
"col": 2 *//当前下棋位置的列号
}
{
"optype": "put_chess",
"result": false
"reason": "⾛棋失败具体原因...."
}
{
"optype": "put_chess",
"room_id": 222,
"uid": 1,
"row": 3,
"col": 2,
"result": true,
"reason": "对⽅掉线,不战⽽胜!" / "对⽅/⼰⽅五星连珠,战⽆敌/虽败犹荣!",
"winner": 0 *// 0-未分胜负,!0-已分胜负(uid
是谁,谁就赢了
)

}

聊天

{
"optype": "chat",
"room_id": 222,
"uid": 1,
"message": "赶紧点"
}
{
"optype": "chat",
"result": false
"reason": "聊天失败具体原因....⽐如有敏感词..."
}
{
"optype": "chat",
"result": true,
"room_id": 222,
"uid": 1,
"message": "赶紧点"
}

游戏房间类的代码

总体思路:分为这个几个方法:下棋或者聊天,退出房间,广播。将下棋和聊天的调用写在一个总的请求函数里面,因为这个两个操作有很多重复的。

函数的参数以及返回值大多都是请求信息,也就是Json::Value类型格式的信息,要么通过信息来确定进行哪个操作,要么是将操作的结果保留在Value中方便进行广播

构造函数里面需要传入房间id,用户表操作的句柄,在线用户管理的句柄,在函数初始化列表初始化成员属性,函数内部打印DLOG房间初始化成功

提供一批接口获取房间信息,比如房间id,房间状态,房间人数,添加白棋用户黑棋用户同时设置白棋id黑棋id此时要将人数加加,白棋id,黑棋id

总的请求处理函数:先定义一个Value类型的json_resp,因为无论如何请求处理结果都将广播。通过当前房间号与req中的房间号相等于否判断判断房间号是否匹配,不匹配则设置json_resp中的optype为req中的optype、result为false、reason为房间号不匹配,匹配那么则根据req中的操作类型调用下棋或者聊天函数,返回值保存在json_value中,两者都不是那么就将json_resp中的optype为req中的optype、result为false、reason为未知请求类型,当是下棋请求,处理完之后先判断winner字段是不是为0,不为0才可以进行下一步,也就是要用用户表句柄更新用户表,通过白棋黑棋id,当然要先判断哪一个赢了,通过返回的json_value中的winner字段判断,最后更新房间状态。最终将json_value序列化到一个字符串body中并且打印DLOG("房间-广播:%s", body.c_str()),返回的时候调用broadcast(json_resp)

cpp 复制代码
    // 总的请求处理函数,通过请求值调用不同函数,将得到的结果进行广播
    void handle_request(Json::Value &req)
    {
        Json::Value json_resq;
        // 判断房间号是否匹配
        uint64_t room_id = req["room_id"].asUInt64();
        if (room_id != _rid)
        {
            json_resq["optype"] = req["optype"].asString();
            json_resq["result"] = false;
            json_resq["reason"] = "房间号不匹配";
            return broadcast(json_resq);
        }
        std::string op = req["optype"].asString();
        if (op == "put_chess")
        {
            json_resq = handle_chess(req);
            if (json_resq["winner"].asUInt64() != 0)
            {
                uint64_t winner_id = json_resq["winner"].asUInt64();
                uint64_t loser_id = winner_id == _white_uid ? _white_uid : _black_uid;
                _ut->win(winner_id);
                _ut->lose(loser_id);
                _room_stat = GAME_OVER;
            }
        }
        else if (op == "chat")
        {
            json_resq = handle_chat(req);
        }
        else
        {
            json_resq["optype"] = req["optype"].asString();
            json_resq["result"] = false;
            json_resq["reason"] = "未知请求类型";
        }
        std::string body;
        json_util::serialize(json_resq, body);
        DLOG("房间-广播: %s", body.c_str());
        return broadcast(json_resq);
    }

下棋的函数:首先判断当前两个玩家是否有掉线的,有则对方胜利(拿到各自id通过在线用户管理句柄中的接口判断);之后判断当前下棋的位置是否有棋子,有则冲突修改json_resq之后返回即可;前两部都正确了开始正常下一步棋,改变棋盘这一个位置的颜色,并且之后设计一个接口check_win判断是否五星连珠了,若有则返回这个胜利者的id,应该不为0,然后将结果更新到json_resq中

check_win的设计思路:通过判断四个方向上是否有五个连续的和当前下棋棋子颜色相同的,通过传递偏移量之后循环加减此偏移量判断

cpp 复制代码
    // 下棋
    Json::Value handle_chess(Json::Value &req)
    {
        Json::Value json_resq = req;
        // 判断双方是否有掉线的
        if (_om->is_in_game_room(_white_uid) == false)
        {
            json_resq["result"] = true;
            json_resq["reason"] = "对方掉线,不战而胜";
            json_resq["winner"] =_black_uid;
            return json_resq;
        }
        if (_om->is_in_game_room(_black_uid) == false)
        {
            json_resq["result"] = true;
            json_resq["reason"] = "对方掉线,不战而胜";
            json_resq["winner"] =_white_uid;
            return json_resq;
        }
        // 判断当前位置有没有棋子,有则退出
        // 通过req中uid和白棋或者黑棋玩家是否一致判断当前req下棋颜色
        int cur_row = req["row"].asInt(), cur_col = req["col"].asInt();
        uint64_t cur_uid = req["uid"].asUInt64();
        int cur_color = cur_uid == _white_uid ? WHITE_COLOR : BLACK_COLOR;
        if (_board[cur_row][cur_col] != 0) // 冲突
        {
            json_resq["result"] = false;
            json_resq["reason"] = "当前位置已有棋子,下棋位置冲突";            
            json_resq["winner"] = 0;
        }
        // 开始下棋
        _board[cur_row][cur_col] = cur_color;
        // 判断是否五星连珠
        uint64_t winner_id = check_win(cur_row, cur_col, cur_color);
        if (winner_id != 0)
            json_resq["reason"] = "五星连珠,胜利";
        json_resq["winner"] = winner_id;
        json_resq["result"] = true; // 不管如何下棋操作成功
        return json_resq;
    }

    // 每一个方向上的判断
    bool five(int cur_row, int cur_col, int cur_color, int row_off, int col_off)
    {
        int count = 1; // 当前位置
        // 此偏移量正方向
        int row = cur_row + row_off;
        int col = cur_col + col_off;
        while (row >= 0 && row < BOARD_ROW && col >= 0 && col < BOARD_COL && 
                        cur_color == _board[row][col])
        {
            count++;
            row += row_off;
            col += col_off;
        }
        // 此偏移量反方向
        row = cur_row - row_off;
        col = cur_col - col_off;
        while (row >= 0 && row < BOARD_ROW && col >= 0 && col < BOARD_COL && 
                        cur_color == _board[row][col])
        {
            count++;
            row -= row_off;
            col -= col_off;
        }        
        return (count >= 5);
    }
    // 判断是否五星连珠
    uint64_t check_win(int cur_row, int cur_col, int cur_color)
    {
        // 判断四个方向上是否五星连珠
        if (five(cur_row, cur_col, cur_color, 0, 1) || 
            five(cur_row, cur_col, cur_color, 1, 0) ||
            five(cur_row, cur_col, cur_color, 1, 1) ||
            five(cur_row, cur_col, cur_color, -1, 1))
        {
            return cur_color == WHITE_COLOR ? _white_uid : _black_uid;
        }
        return 0;
    }

聊天:过滤敏感词,有则不能发送这个消息,通过查找message中有无敏感词判断

cpp 复制代码
    // 聊天
    Json::Value handle_chat(Json::Value &req)
    {
        Json::Value json_resq = req;
        std::string msg = req["message"].asString();
        int pos = msg.find("垃圾");
        if (pos != std::string::npos) // 说明包含敏感词不能发送
        {
            json_resq["result"] = false;
            json_resq["reason"] = "消息包含敏感词不能发送";
            return json_resq;
        }
        json_resq["result"] = true;
        return json_resq;
    }

退出处理:这个函数不属于请求的一种;而是独立的一种退出游戏的操作。那么这个操作可能发生在游戏中的任何时候,所以应该判断退出游戏的时候房间状态是不是GAME_SATRT,如果是的,那么同样地,用户掉线,对方不战而胜,此时更行json_resq中的各个字段并且广播。最后将_player_count--

cpp 复制代码
    // 退出
    void handle_exit(uint64_t uid) 
    {
        //如果是下棋中退出,则对方胜利,否则下棋结束了退出,则是正常退出
        Json::Value json_resp;
        if (_room_stat == GAME_START) {
            uint64_t winner_id = uid == _white_uid ? _black_uid : _white_uid;
            json_resp["optype"] = "put_chess";
            json_resp["result"] = true;
            json_resp["reason"] = "对方掉线,不战而胜!";
            json_resp["room_id"] = _rid;
            json_resp["uid"] = uid;
            json_resp["row"] = -1;
            json_resp["col"] = -1;
            json_resp["winner"] = winner_id;
            uint64_t loser_id = winner_id == _white_uid ? _black_uid : _white_uid;
            _ut->win(winner_id);
            _ut->lose(loser_id);
            _room_stat = GAME_OVER;
            broadcast(json_resp);
        }
        //房间中玩家数量--
        _player_count--;
        return;
    }

广播:先将响应消息序列化成为字符串,之后获取白棋和黑棋玩家的通信连接,如果有(get()判断),那么拿着这个通信连接通过接口send发送消息

cpp 复制代码
    void broadcast(Json::Value &req)
    {
        std::string body;
        json_util::serialize(req, body);
        wsserver_t::connection_ptr wconn = _om->get_conn_from_room(_white_uid);
        if (wconn.get())
            wconn->send(body);
        else
            DLOG("获取白棋玩家通信接口失败");

        wsserver_t::connection_ptr bconn = _om->get_conn_from_room(_black_uid);
        if (bconn.get())
            bconn->send(body);
        else
            DLOG("获取黑棋玩家通信接口失败");
        return;
    }

游戏房间管理的设计

属性含义:房间id应该一直递增,所以要设置一个计数器;房间对象使用智能指针对象管理,并且建立房间id到管理这个房间的智能指针对象的映射关系,这样一来操作房间的时候使用指针即可,销毁的时候直接销毁指针因为只有一个智能指针指向这个房间对象,引用计数器为1,指针没了对象自动销毁;建立一个用户id到房间id的映射,这样三者就可以贯通,方便操作;

创建房间:两个用户在游戏大厅中进行对战匹配,匹配成功后创建房间。1. 校验两个用户是否都还在游戏大厅中,只有都在才需要创建房间。2. 创建房间,将用户信息添加到房间中。3. 将房间信息管理起来。4. 返回房间信息

通过房间id销毁房间:因为房间信息,是通过shared_ptr在_rooms中进行管理,因此只要将shared_ptr从_rid_to_room中移除则shared_ptr计数器==0,外界没有对房间信息进行操作保存的情况下就会释放。1. 通过房间ID,获取房间信息;2. 通过房间信息,获取房间中所有用户的ID;3. 移除房间管理中的用户信息;4. 移除房间管理信息

cpp 复制代码
// 房间管理类
using room_ptr = std::shared_ptr<room>;
class room_manager
{
private:
    int _next_rid; // 房间id分配计数器
    user_table *_ut;
    online_manager *_om;
    std::mutex _mutex;
    std::unordered_map<uint64_t, room_ptr> _rid_to_room;
    std::unordered_map<uint64_t, uint64_t> _uid_to_rid;
public:
    room_manager(user_table *ut, online_manager *om):
        _next_rid(1), _ut(ut), _om(om)
    {
            DLOG("初始化房间管理模块成功");
    }
    ~room_manager() { DLOG("销毁房间管理模块成功"); }

    //为两个用户创建房间,并返回房间的智能指针管理对象
    room_ptr create_room(uint64_t uid1, uint64_t uid2)
    {
        // 先看在不在大厅中
        if (!_om->is_in_game_hall(uid1))
        {
            DLOG("%lu 用户不在游戏大厅中,无法为其创建房间", uid1);
            return room_ptr();
        }
        if (!_om->is_in_game_hall(uid2))
        {
            DLOG("%lu 用户不在游戏大厅中,无法为其创建房间", uid2);
            return room_ptr();
        }
        // 创建房间,添加用户信息进房间
        room_ptr rp(new room(_next_rid, _ut, _om));
        rp->add_white_user(uid1);
        rp->add_black_user(uid2);
        // 管理房间信息
        std::unique_lock<std::mutex> lock(_mutex); // 映射哈希表是一个临界资源
        _rid_to_room.insert(std::make_pair(_next_rid, rp));
        _uid_to_rid.insert(std::make_pair(uid1, _next_rid));
        _uid_to_rid.insert(std::make_pair(uid2, _next_rid));
        _next_rid++;// 这也是临界资源,计数器是共享的
        // 返回房间信息
        return rp;

    }
    /*通过房间ID获取房间信息*/
    room_ptr get_room_by_rid(uint64_t rid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto rit = _rid_to_room.find(rid);
        if (rit == _rid_to_room.end())
        {
            DLOG("不存在房间id %lu 对应的房间", rid);
            return room_ptr();
        }
        return rit->second;
    }
    /*通过用户ID获取房间信息*/
    room_ptr get_room_by_uid(uint64_t uid)
    {
        // 先通过uid找到rid,再通过rid找uid
        std::unique_lock<std::mutex> lock(_mutex);
        auto uit = _uid_to_rid.find(uid);
        if (uit == _uid_to_rid.end())
        {
            DLOG("不存在用户id %lu 对应的房间id", uid);
            return room_ptr();
        }
        uint64_t rid = uit->second;
        auto rit = _rid_to_room.find(rid);
        if (rit == _rid_to_room.end())
        {
            DLOG("不存在房间id %lu 对应的房间", rid);
            return room_ptr();
        }
        return rit->second;
        // ps:不能直接调用get_room_by_rid 会出现死锁
    }
    /*通过房间ID销毁房间*/
    void remove_room_by_rid(uint64_t rid)
    {
        room_ptr rp = get_room_by_rid(rid);
        if (rp.get() == nullptr)
        {
            DLOG("不存在房间id %lu 对应的房间", rid);
            return;
        }
        // 移除管理关系中的用户信息
        std::unique_lock<std::mutex> lock(_mutex);
        uint64_t uid1 = rp->white_user();
        uint64_t uid2 = rp->black_user();
        _uid_to_rid.erase(uid1);
        _uid_to_rid.erase(uid2);
        // 销毁房间
        _rid_to_room.erase(rid);
    }
    /*删除房间中指定用户,如果房间中没有用户了,则销毁房间,用户连接断开时被调用*/
    void remove_room_user(uint64_t uid)
    {
        room_ptr rp = get_room_by_uid(uid);
        if (rp.get() == nullptr)
        {
            DLOG("用户 %lu 不在此房间中", uid);
            return;
        }
        rp->handle_exit(uid);
        // 没有用户了销毁房间
        if (rp->player_count() == 0)
        {
            remove_room_by_rid(rp->room_id());
        }
        return;
    }
};

session管理模块

了解session

Cookie 和 Session 都是用于在 Web 应用中跟踪用户状态的技术

Cookie

  • 定义:Cookie 是服务器发送到用户浏览器并保存在本地的一小段文本数据。
  • 工作流程:当用户首次访问网站时,服务器会生成 Cookie 并通过响应头发送给浏览器,浏览器将其存储在本地。之后用户再次访问该网站时,浏览器会自动将 Cookie 通过请求头携带到服务器,服务器以此来识别用户。

Session

  • 定义:Session 是在服务器端创建的,用于保存用户会话信息的对象。
  • 工作流程:用户首次访问服务器时,服务器会创建一个 Session,并生成一个唯一的 Session ID。然后服务器将这个 Session ID 通过 Cookie(或 URL 重写等方式)发送给浏览器。后续用户请求时,浏览器携带 Session ID 到服务器,服务器根据 Session ID 找到对应的 Session,从而获取用户的会话信息。

HTTP 是无状态协议,意味着每次客户端与服务器的请求 - 响应过程都是相互独立的 ,**服务器不会自动记住之前客户端的请求信息;同时 HTTP 短连接是指每次请求建立连接,请求处理完就关闭连接,不会长期保持连接状态。**在这种情况下,Session 有着关键作用:

维持用户状态

由于 HTTP 无状态,服务器本身无法区分多次请求是否来自同一用户。Session 可以为每个用户创建一个专属的会话对象,当用户首次访问时,服务器生成 Session 并分配唯一的 Session ID,通过 Cookie(或 URL 重写等方式)将 Session ID 传递给客户端。后续该用户的每次请求都会携带这个 Session ID,服务器就能依据 Session ID 找到对应的 Session,从而识别出是同一用户,进而维持用户的登录状态、购物车内容等状态信息。比如用户登录网站后,在短连接且无状态的 HTTP 通信中,通过 Session 能一直保持登录状态,无需每次请求都重新登录。

存储用户相关数据

HTTP 无状态且短连接的特性,使得服务器难以在多次请求间共享用户的个性化数据。Session 存储在服务器端,可用于保存用户的各种个性化设置、操作记录等复杂信息。例如电商网站中,用户浏览商品的记录可以存储在 Session 里,这样在用户后续的短连接请求中,服务器能通过 Session 快速获取这些记录,为用户提供更个性化的服务,而不需要每次都重新获取或生成这些数据。

保障数据安全性

HTTP 无状态短连接下,若直接在客户端存储敏感信息(如用户身份认证信息),存在较大安全风险。Session 将敏感数据存储在服务器端,客户端仅持有 Session ID。即使 Session ID 被窃取,没有服务器端的 Session 数据,也难以获取到真正的敏感信息,从而在一定程度上保障了用户数据的安全。比如用户的身份凭证等信息存储在 Session 中,相比直接用 Cookie 存储,安全性更高。

相比于cookie安全性更高的体现

在 HTTP 通信中,Cookie 会随着每次请求和响应在客户端与服务器之间传输。如果通信过程没有采用加密协议(如 HTTPS),Cookie 中的数据就可能被网络上的中间人窃取。而 Session 只有 Session ID 会在客户端和服务器之间传输,实际的 Session 数据始终在服务器端,即使 Session ID 被窃取,没有服务器的配合,攻击者也难以利用该 Session ID 获取到 Session 中的有效数据。

总结:Cookie 能辅助实现业务层面的 "有状态会话跟踪"(让服务器知道多次请求来自同一用户(标识用户)),但无法改变 HTTP 无状态的特性,Cookie 常被作为传递 Session ID 的载体,辅助 Session 实现用户状态的跟踪,而 Session 借助服务器端存储的优势,提供了比单纯依赖 Cookie 更安全的状态管理能力

session类的实现

成员函数是成员属性的设置和获取接口,目的是为session提供管理接口

cpp 复制代码
typedef enum {LOGIN, UNLOGIN} session_status; // 保存用户的状态信息 
class session
{
private:
    uint64_t _ssid;
    uint64_t _uid; // session对应的用户信息
    session_status _ss_stat;
    wsserver_t::timer_ptr _tp; //session关联的定时器
public:
    session(uint64_t ssid):_ssid(ssid) // 要传入ssid这是session管理类给定的 
    { 
        DLOG("会话 %p 创建成功", this);
    }
    ~session() { DLOG("会话 %p 销毁", this); }
    uint64_t get_ssid() { return _ssid; }
    void set_uid(uint64_t) { _uid = uid; }
    uint64_t get_uid() { return _uid; }
    void set_status(session_status status) { _ss_stat = status; }
    bool is_login() { return _ss_stat == LOGIN; }
    // 设置和获取定时器
    void set_timer(wsserver_t::timer_ptr &tp) { _tp = tp; }
    wsserver_t::timer_ptr& get_timer() { return _tp; }    
}

用智能指针管理一个session对象,在session_manager里面设置一个哈希表,建立ssid和session+pre的映射,需要包含互斥锁,因为管理器哈希表是一个临界资源。还有一个server对象用来添加定时任务

设置会话过期时间这个接口:首先依赖于sebsocketpp中的定时器来完成session生命周期的管理;一个session应该有这样的几种转换形式:通过http建立连接比如登录或者注册的时候,会话应该是在指定时间内无通信之后删除(这是短连接)、进入游戏大厅或者房间之后会话应该永久存在;当游戏结束退出房间或者大厅的时候会话又要在无通信的一定时间内被删除。那么基于此种变换过程,应该在这个函数内部设计这样的几种操作:会话永久存在设置永久存在、会话永久存在设置指定时间内删除的定时任务、会话会执行指定时间内删除的任务设置其永久存在、回哈会执行指定时间内删除的任务重新设定删除任务的指定时间

具体思路:首先要获取ssid对应的会话,因为后序的操作都是针对于定时器的,而要通过会话才能拿到其内部的任务定时器,那么若是会话不存在直接退出即可。会话存在获取内部定时器,现在开始处理第一种情况 :内部定时器为空说明会话永久存在(因为会有指定之间内删除的任务)并且ms==SESSION_FOREVER 直接退出即可,不需要改变;第二种情况 :会话永久存在但是ms!=SESSION_FOREVER,那么就要重新设定任务 wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms, std::bind(&session_manager::remove_session, this, ssid));tmp_ptr是通过_server重新设定的任务计时器,之后通过会话的设定计时器的接口改变会话计时器,remove_session就是这个类内部删除哈希表中ssid对应的映射关系,也就是删除这个会话,ms就是指定的时间之后执行这个任务,使用bind所以要绑定参数;第三种情况:会话会指定删除任务,但是要修改其成为永久存在,那么要先使用计时器的接口cancel(),这个接口就是取消会话定时任务,之后将会话的定时任务设置为空就代表永久存在set_timer(wsserver_t::timer_ptr()); 但是cancel这个取消指定是异步的也就是延迟生效,那么它有可能将新设定的定时任务取消了,所以添加的时候也要使用定时器而不是立即添加,这样避免被取消,这个接口的执行也是异步的

_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp)); append_session是类内部的函数,作用是添加会话进入管理器

第四种情况:更改时间,先和第三种情况一样,cancel之后定时器置空,之后延迟添加。完成这一步之后重新给session设定销毁任务,时间改为ms,定义出tmp_tp之后调用ssp的set_timer接口即可

cpp 复制代码
#define SESSION_TIMEOUT 30000
#define SESSION_FOREVER -1

using session_ptr = std::shares_ptr<session>;
class session_manager
{
private:
    uint64_t _next_ssid;
    std::mutex _mutex;
    std::unordered_map<uint64_t,session_ptr> _ssid_to_ssp;
    wsserver_t *_server;
public:
    session_manager(wsserver_t * server):_next_ssid(1), _server(server)
    { DLOG("session管理器初始化成功"); }
    ~session_manager() { DLOG("session管理器销毁"); }
    session_ptr session_create(uint64_t uid, session_status ss_stat) // 为用户创建一个session会话
    {
        std::unique_lock<std::mutex> lock(_mutex);
        session_ptr ssp(new session(uid));
        _ssid_to_ssp.insert(make_pair(_next_ssid, ssp));
        ssp->set_uid(uid);
        ssp->set_status(ss_stat);
        _next_ssid++;
        return ssp;
    }
    session_ptr get_session_by_ssid(uint64_t ssid) // 通过会话id获取会话
    {
        std::unique_lock<std::mutex> lock(_mutex);
        auto ssit = _ssid_to_ssp.find(ssid);
        if (ssit == _ssid_to_ssp.end())
        {
            DLOG("没有ssid %lu 对应的会话", ssid);
            return session_ptr();
        }
        return ssit->second;
    }
    void remove_session(uint64_t ssid)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _ssid_to_ssp.erase(ssid);
    }
    void append_session(uint64_t ssid, session_ptr &ssp)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _ssid_to_ssp.insert(make_pair(ssid, ssp));
    }
    void set_session_expire_time(uint64_t ssid, int ms) // 设置会话过期时间
    {
        session_ptr ssp = get_session_by_ssid(ssid);
        if (ssp.get() == nullptr) return;
        timer_ptr tp = ssp->get_timer(); // 获取会话定时器
        // 开始判断之后设定
        if (tp.get() == nullptr && ms == SESSION_FOREVER)
            return;
        else if (tp.get() == nullptr && ms != SESSION_FOREVER)
        {
            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)
        {
            tp->cancel();
            ssp->set_timer(timer_ptr());
            _server->set_timer(0, 
                std::bind(&session_manager::append_session, this, ssid));
        }
        else if (tp.get() != nullptr && ms != SESSION_FOREVER)
        {
            tp->cancel();
            ssp->set_timer(timer_ptr());
            _server->set_timer(0, 
                std::bind(&session_manager::append_session, this, ssid));
            
            wsserver_t::timer_ptr tmp_tp = _server->set_timer(ms, 
                std::bind(&session_manager::remove_session, this, ssid));
            ssp->set_timer(tmp_tp);
        }
    }
};

游戏对战匹配模块

将来我们启动线程,三个线程分别到自己的阻塞队列里面拿数据进行创建房间匹配,当队列中元素小于2的时候会阻塞,新插入数据的时候会唤醒阻塞的线程继续消费

cpp 复制代码
#include "util.hpp"
#include "online.hpp"
#include "db.hpp"
#include "room.hpp"
#include <list>
#include <mutex>
#include <condition_variable>

template<typename T>
class match_queue
{
private:
    std::mutex _mutex;
    std::condition_variable _cond;
    std::list<T> _list;
public:
    size_t size() { return _list.size(); }
    bool is_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(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.size() == 0)
            return false;
        data = _list.front();
        _list.pop_front();
        return true;
    }
    void remove(T &data) // list支持删除指定元素,无论在哪个位置
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _list.remove(data);
    }
}
cpp 复制代码
#ifndef __M_MATCH_H__
#define __M_MATCH_H__

#include "util.hpp"
#include "online.hpp"
#include "db.hpp"
#include "room.hpp"
#include <list>
#include <mutex>
#include <condition_variable>

template<typename T>
class match_queue
{
private:
    std::mutex _mutex;
    std::condition_variable _cond;
    std::list<T> _list;
public:
    size_t size() { return _list.size(); }
    bool is_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(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.size() == 0)
            return false;
        data = _list.front();
        _list.pop_front();
        return true;
    }
    void remove(T &data) // list支持删除指定元素,无论在哪个位置
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _list.remove(data);
    }
};

class matcher
{
private:
    match_queue<uint64_t> _low;
    match_queue<uint64_t> _medium;
    match_queue<uint64_t> _high;

    std::thread _th_low;
    std::thread _th_medium;
    std::thread _th_high;

    room_manager *_rm;
    online_manager *_om;
    user_table *_ut;
private:
    // 线程入口函数
    // 由于三个线程处理方式一样只是处理对象即队列改变,因此统一设计接口,复用
    void handle_entry(match_queue<uint64_t> &mq)
    {
        // 线程处理的目的是拿出两个用户进行匹配并且为之创建房间,将两个玩家匹配成功的消息响应
        // 并且此操作应该死循环一直访问队列拿去数据
        while (1)
        {
            // 首先判断这两个队列是不是有两个及以上的用户,不是则在_cond上等待
            // 只有当mq.size() >= 2 了循环才会结束,而结束此循环的动作是push,push里面刚好唤醒了线程
            while (mq.size() < 2)
                mq.wait();
            // 获取用户uid
            uint64_t uid1, uid2;
            bool ret = mq.pop(uid1);
            if (ret == false)
            {
                continue;
            }
            ret = mq.pop(uid2);
            if (ret == false)
            {
                this->add(uid1);
                continue;
            }
            // 获取用户的网络通信连接,判断其在不在大厅,只有在大厅才能为其创建房间
            wsserver_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);
            if (conn1.get() == nullptr)
            {
                this->add(uid2);
                continue;
            }
            wsserver_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);
            if (conn2.get() == nullptr)
            {
                this->add(uid1);
                continue;
            }
            // 创建房间
            room_ptr rp = _rm->create_room(uid1, uid2);
            if (rp.get() == nullptr)
            {
                DLOG("创建房间失败");
                this->add(uid1);
                this->add(uid2);
                continue;
            }
            Json::Value resp;
            resp["optype"] = "match_success";
            resp["result"] = true;
            std::string body;
            json_util::serialize(resp, body);
            conn1->send(body);
            conn2->send(body);
        }
    }

    void _th_low_entry() {}
    void _th_medium_entry() {}
    void _th_high_entry() {}
public:
    matcher(room_manager *rm, online_manager *om, user_table *ut)
    : _rm(rm), _om(om), _ut(ut),
    _th_low(std::thread(&matcher::_th_low_entry, this)),
    _th_medium(std::thread(&matcher::_th_medium_entry, this)),
    _th_high(std::thread(&matcher::_th_high_entry, this))
    {
        DLOG("匹配模块初始化完毕");
    }
    bool add(uint64_t uid)
    {
        Json::Value user;
        bool ret = _ut->user_by_id(user, uid);
        if (ret == false)
        {
            DLOG("获取用户信息失败");
            return false;
        }
        // 根据分数选择加入的队列
        int score = user["score"].asInt();
        if (score < 2000)
            _low.push(uid);
        else if (2000 <= score && score < 3000)
            _medium.push(uid);
        else
            _high.push(uid);
        return true;
    }
    bool del(uint64_t uid)
    {
        Json::Value user;
        bool ret = _ut->user_by_id(user, uid);
        if (ret == false)
        {
            DLOG("获取用户信息失败");
            return false;
        }
        // 根据分数选择队列
        int score = user["score"].asInt();
        if (score < 2000)
            _low.remove(uid);
        else if (2000 <= score && score < 3000)
            _medium.remove(uid);
        else
            _high.remove(uid);
        return true;
    }
};


#endif

服务器

服务器业务请求流程

服务器类的设计

第一步完成整体框架的搭建

server_gobang类中有一个成员属性std::string _web_root 是静态资源根目录,当客户端发送静态资源请求的时候,比如注册登录,就会去这个目录下面找到对应的html代码返回

cpp 复制代码
#ifndef __M_SERVER_H__
#define __M_SERVER_H__

#include "db.hpp"
#include "log.hpp"
#include "matcher.hpp"
#include "online.hpp"
#include "room.hpp"
#include "session.hpp"


class gobang_server
{
private:
    std::string _web_root; // 静态资源根目录
    wwserver_t _wwsrv; // websocket服务器
    user_table _ut;
    session_manager _sm;
    online_manager _om;
    room_manager _rm;
    matcher _mather;
private:
    // 收到请求之后调用的回调函数
    void http_callback(websocketpp::connection_hdl hdl) 
    {}
    void wsopen_callback(websocketpp::connection_hdl hdl)
    {}
    void wsclose_callback(websocketpp::connection_hdl hdl)
    {}
    void wsmsg_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg)
    {}

public:
    gobang_server(const std::string &host,
            const std::string &username,
            const std::string &password,
            const std::string &dbname,
            uint16_t port = 3306, 
            std::string &web_root = defaultpath)
            :_web_root(web_root)
            , _ut(host, username, password, dbname, port)
            , _sm(&_web_root)
            , _om()
            , _rm(&_ut, &_om)
            , _mather(&_rm, &_om, &_ut)
    {
        // 成员的初始化和回调函数的设置
        // 设置日志等级
        _wwsrv.set_access_channels(websocketpp::log::alevel::none);
        // 初始化asio调度器
        _wwsrv.init_asio();
        _wwsrv.set_reuse_addr(true);
        // 设置回调函数
        _wwsrv.set_http_handler(std::bind(http_callback, this, std::placeholders::_1));
        _wwsrv.set_open_handler(std::bind(wsopen_callback, this, std::placeholders::_1));
        _wwsrv.set_close_handler(std::bind(wsclose_callback, this, std::placeholders::_1));
        _wwsrv.set_message_handler(std::bind(wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));

    }
    void Start(int port)
    {
        _wwsrv.listen(port);
        _wwsrv.start_accept();
        _wwsrv.run();
    }
};

#endif

静态资源导入

http回调函数的设计

http类的请求有静态资源请求包括注册页面和登录页面、注册、登录、信息发送。那么首先要判断这个请求的什么,然后根据请求的类型去调用函数

wsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);

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

std::string method = req.get_method();

std::string uri = req.get_uri();

这几行代码在http_callback中获取请求的方法以及类型

代码:

cpp 复制代码
    // 收到请求之后调用的回调函数
    void http_callback(websocketpp::connection_hdl hdl) 
    {
        wsserver_t::connection_ptr conn = _wwsrv.get_con_from_hdl(hdl);
        websocketpp::http::parser::request req = conn->get_request();
        std::string method = req.get_method();
        std::string uri = req.get_uri();
        if (method == "POST" && uri == "/rgt") // 注册
        {
            return rgt(conn);
        }
        else if (method == "POST" && uri == "/login") // 登录
        {
            return login(conn);
        }
        else if (method == "GET" && uri == "/info") // 获取信息
        {
            return info(conn);
        }
        else // 静态资源请求
        {
            return file_handler(conn);
        }
    }

收到静态资源请求之后响应的函数

file_handler;思路:首先通过uri组合出这个静态资源的路径;若是这个路径是一个目录那么就直接在路径后面增加登陆页面;反之则是正确路径,开始从此路径下读取路径下文件内容,读取文件正确将结果返回给客户端,不正确说明文件不存在返回404以及一个简单notfound页面。最后都要通过conn设置想响应正文和响应状态码,也就是读取的文件内容和conn内部封装的状态码,正确读取则设置为ok,错误设置为not_found

测试注册、登录、页面请求没问题

代码

cpp 复制代码
private:
    // 静态资源请求的响应函数
    void file_handler(wsserver_t::connection_ptr &conn)
    {
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        std::string realpath = defaultpath + uri;
        if (realpath.back() == '/') //说明是目录
        {
            realpath += "login.html";
        }
        // 读取文件内容
        std::string body;
        bool ret = file_util::file_read(realpath, body);
        if (ret == false)
        {
            DLOG("读取文件失败");
            body += "<html>";
            body += "<head>";
            body += "<meta charset='UTF-8'/>";
            body += "</head>";
            body += "<body>";
            body += "<h1> Not Found </h1>";
            body += "</body>";
            conn->set_status(websocketpp::http::status_code::not_found);
            conn->set_body(body);
            return;
        }
        conn->set_body(body);
        conn->set_status(websocketpp::http::status_code::ok);
    }

注册请求响应

后端代码:通过conn获取前端请求正文之后反序列化,检验Value中密码和用户名知否都有,然后向数据库插入,没有冲突则插入成功,然后mysql表中就有一行用户信息,并且返回成功结果给前端页面;http_resp是封装了每一步的处理过程,这些过程有很多重复的,失败了就直接返回

cpp 复制代码
    void http_resp(wsserver_t::connection_ptr &conn, bool result, 
        websocketpp::http::status_code::value code, const std::string &reason) {
        Json::Value resp_json;
        resp_json["result"] = result;
        resp_json["reason"] = reason;
        std::string resp_body;
        json_util::serialize(resp_json, resp_body);
        conn->set_status(code);
        conn->set_body(resp_body);
        conn->append_header("Content-Type", "application/json");
        return;
    }
    // 注册请求响应
    void reg(wsserver_t::connection_ptr &conn)
    {
        //用户注册功能请求的处理
        websocketpp::http::parser::request req = conn->get_request();
        //1. 获取到请求正文
        std::string req_body = conn->get_request_body();
        //2. 对正文进行json反序列化,得到用户名和密码
        Json::Value login_info;
        bool ret = json_util::unserialize(req_body, login_info);
        if (ret == false) {
            DLOG("反序列化注册信息失败");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");
        }
        //3. 进行数据库的用户新增操作
        if (login_info["username"].isNull() || login_info["password"].isNull()) {
            DLOG("用户名密码不完整");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");
        }
        ret = _ut.insert(login_info);
        if (ret == false) {
            DLOG("向数据库插入数据失败");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名已经被占用!");
        }
        //  如果成功了,则返回200
        return http_resp(conn, true, websocketpp::http::status_code::ok, "注册用户成功");
    }

前端register.html:显示注册页面,之后输入用户名密码,然后添加了一个点击事件,也就是点击注册,此时向后端发送了注册请求,并且获取响应结果,若是注册失败则情况用户名密码的输入并且拿到响应结果可以打印错误原因,成功则跳转到登陆页面

cpp 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>注册</title>
    <link rel="stylesheet" href="./css/common.css">
    <link rel="stylesheet" href="./css/login.css">
</head>
<body>
    <div class="nav">
        网络五子棋对战游戏
    </div>
    <div class="login-container">
        <!-- 登录界面的对话框 -->
        <div class="login-dialog">
            <!-- 提示信息 -->
            <h3>注册</h3>
            <!-- 这个表示一行 -->
            <div class="row">
                <span>用户名</span>
                <input type="text" id="user_name" name="username">
            </div>
            <!-- 这是另一行 -->
            <div class="row">
                <span>密码</span>
                <input type="password" id="password" name="password">
            </div>
            <!-- 提交按钮 -->
            <div class="row">
                <button id="submit" onclick="reg()">提交</button>
            </div>
        </div>
    </div> 

    <script src="js/jquery.min.js"></script>
    <script>
        //1. 给按钮添加点击事件,调用注册函数
        //2. 封装实现注册函数
        function reg() {
            //  1. 获取两个输入框空间中的数据,组织成为一个json串
            var reg_info = {
                username: document.getElementById("user_name").value,
                password: document.getElementById("password").value
            };
            console.log(JSON.stringify(reg_info));
            //  2. 通过ajax向后台发送用户注册请求
            $.ajax({
                url : "/reg",
                type : "post",
                data : JSON.stringify(reg_info),
                success : function(res) {
                    if (res.result == false) {
                        //  4. 如果请求失败,则清空两个输入框内容,并提示错误原因
                        document.getElementById("user_name").value = "";
                        document.getElementById("password").value = "";
                        alert(res.reason);
                    }else {
                        //  3. 如果请求成功,则跳转的登录页面
                        alert(res.reason);
                        window.location.assign("/login.html");
                    }
                },
                error : function(xhr) {
                    document.getElementById("user_name").value = "";
                    document.getElementById("password").value = "";
                    alert(JSON.stringify(xhr));
                }
            })
        }
    </script>
</body>
</html>

登录请求响应

后端:前几部和注册一样,获取请求正文反序列化,然后检查用户名以及密码是否完整,之后登录,登录看密码是否冲突;这些做完了之后为用户创建session会话,首先要获取uid,之后通过uid创建,创建成功之后设置会话过期时间;最后设置响应头部并且返回响应

cpp 复制代码
    // 登录请求响应
    void login(wsserver_t::connection_ptr &conn)
    {
        //用户登录功能请求的处理
        //1. 获取请求正文,并进行json反序列化,得到用户名和密码
        std::string req_body = conn->get_request_body();
        Json::Value login_info;
        bool ret = json_util::unserialize(req_body, login_info);
        if (ret == false) {
            DLOG("反序列化登录信息失败");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请求的正文格式错误");
        }
        //2. 校验正文完整性,进行数据库的用户信息验证
        if (login_info["username"].isNull() || login_info["password"].isNull()) {
            DLOG("用户名密码不完整");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "请输入用户名/密码");
        }
        ret = _ut.login(login_info);
        if (ret == false) {
            //  1. 如果验证失败,则返回400
            DLOG("用户名密码错误");
            return http_resp(conn, false, websocketpp::http::status_code::bad_request, "用户名密码错误");
        }
        //3. 如果验证成功,给客户端创建session
        uint64_t uid = login_info["id"].asUInt64();
        session_ptr ssp = _sm.session_create(uid, LOGIN);
        if (ssp.get() == nullptr) {
            DLOG("创建会话失败");
            return http_resp(conn, false, websocketpp::http::status_code::internal_server_error , "创建会话失败");
        }
        _sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT);
        //4. 设置响应头部:Set-Cookie,将sessionid通过cookie返回
        std::string cookie_ssid = "SSID=" + std::to_string(ssp->get_ssid());
        conn->append_header("Set-Cookie", cookie_ssid);
        return http_resp(conn, true, websocketpp::http::status_code::ok , "登录成功");        
    }

前端login.html:同样设置点击事件,输入用户名密码,点击注册,向后端发送请求正文,获取响应,失败则清空用户名密码输入行并且打印错误消息,成功则调转至游戏大厅页面

cpp 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录</title>

    <link rel="stylesheet" href="./css/common.css">
    <link rel="stylesheet" href="./css/login.css">
</head>
<body>
    <div class="nav">
        网络五子棋对战游戏
    </div>
    <div class="login-container">
        <!-- 登录界面的对话框 -->
        <div class="login-dialog">
            <!-- 提示信息 -->
            <h3>登录</h3>
            <!-- 这个表示一行 -->
            <div class="row">
                <span>用户名</span>
                <input type="text" id="user_name">
            </div>
            <!-- 这是另一行 -->
            <div class="row">
                <span>密码</span>
                <input type="password" id="password">
            </div>
            <!-- 提交按钮 -->
            <div class="row">
                <button id="submit" onclick="login()">提交</button>
            </div>
        </div>

    </div>

    <script src="./js/jquery.min.js"></script>
    <script>
        //1. 给按钮添加点击事件,调用登录请求函数
        //2. 封装登录请求函数
        function login() {
            //  1. 获取输入框中的用户名和密码,并组织json对象
            var login_info = {
                username: document.getElementById("user_name").value,
                password: document.getElementById("password").value
            };
            //  2. 通过ajax向后台发送登录验证请求
            $.ajax({
                url: "/login",
                type: "post",
                data: JSON.stringify(login_info),
                success: function(result) {
                    //  3. 如果验证通过,则跳转游戏大厅页面
                    alert("登录成功");
                    window.location.assign("/game_hall.html");
                },
                error: function(xhr) {
                    //  4. 如果验证失败,则提示错误信息,并清空输入框
                    alert(JSON.stringify(xhr));
                    document.getElementById("user_name").value = "";
                    document.getElementById("password").value = "";
                }
            })
        }

    </script>
</body>
</html>

获取信息请求响应

后端:首先要看这个用户的Cookie存不存在,存在则通过Cookie获取ssid,然后通过ssid获取会话,通过会话得到用户的uid,在表中通过uid查找用户信息,然后序列化成为字符串,之后设置进响应正文,设置响应头部以及状态,醉胡刷新会话过期时间,每一次获取信息相当于是一次访问

cpp 复制代码
        bool get_cookie_val(const std::string &cookie_str, const std::string &key,  std::string &val) {
            // Cookie: SSID=XXX; path=/; 
            //1. 以 ; 作为间隔,对字符串进行分割,得到各个单个的cookie信息
            std::string sep = "; ";
            std::vector<std::string> cookie_arr;
            string_util::split(cookie_str, sep, cookie_arr);
            for (auto str : cookie_arr) {
                //2. 对单个cookie字符串,以 = 为间隔进行分割,得到key和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;
        }
        void info(wsserver_t::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, "找不到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);
        }

前端game_hall.html:进入游戏大厅则调用get_user_info函数,向后端发送请求,成功之后将用户信息显示在屏幕上。失败则重新调转回登录页面

cpp 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏大厅</title>
    <link rel="stylesheet" href="./css/common.css">
    <link rel="stylesheet" href="./css/game_hall.css">
</head>
<body>
    <div class="nav">网络五子棋对战游戏</div>
    <!-- 整个页面的容器元素 -->
    <div class="container">
        <!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 -->
        <div>
            <!-- 展示用户信息 -->
            <div id="screen"></div>
            <!-- 匹配按钮 -->
            <div id="match-button">开始匹配</div>
        </div>
    </div>

    <script src="./js/jquery.min.js"></script>
    <script>
        var ws_url = "ws://" + location.host + "/hall";
        var ws_hdl = null;

        window.onbeforeunload = function() {
            ws_hdl.close();
        }
        //按钮有两个状态:没有进行匹配的状态,正在匹配中的状态
        var button_flag = "stop";
        //点击按钮的事件处理:
        var be = document.getElementById("match-button");
        be.onclick = function() {
            if (button_flag == "stop") {
                //1. 没有进行匹配的状态下点击按钮,发送对战匹配请求
                var req_json = {
                    optype: "match_start"
                }
                ws_hdl.send(JSON.stringify(req_json));
            }else {
                //2. 正在匹配中的状态下点击按钮,发送停止对战匹配请求
                var req_json = {
                    optype: "match_stop"
                }
                ws_hdl.send(JSON.stringify(req_json));
            }
        }
        function get_user_info() {
            $.ajax({
                url: "/info",
                type: "get",
                success: function(res) {
                    var info_html = "<p>" + "用户:" + res.username + " 积分:" + res.score + 
                        "</br>" + "比赛场次:" + res.total_count + " 获胜场次:" + res.win_count + "</p>";
                    var screen_div = document.getElementById("screen");
                    screen_div.innerHTML = info_html;

                    ws_hdl = new WebSocket(ws_url);
                    ws_hdl.onopen = ws_onopen;
                    ws_hdl.onclose = ws_onclose;
                    ws_hdl.onerror = ws_onerror;
                    ws_hdl.onmessage = ws_onmessage;
                },
                error: function(xhr) {
                    alert(JSON.stringify(xhr));
                    location.replace("/login.html");
                }
            })
        }
        function ws_onopen() {
            console.log("websocket onopen");
        }
        function ws_onclose() {
            console.log("websocket onopen");
        }
        function ws_onerror() {
            console.log("websocket onopen");
        }
        function ws_onmessage(evt) {
            var rsp_json = JSON.parse(evt.data);
            if (rsp_json.result == false) {
                alert(evt.data);
                location.replace("/login.html");
                return;
            }
            if (rsp_json["optype"] == "hall_ready") {
                alert("游戏大厅连接建立成功!");
            }else if (rsp_json["optype"] == "match_success") {
                //对战匹配成功
                alert("对战匹配成功,进入游戏房间!");
                location.replace("/game_room.html");
            }else if (rsp_json["optype"] == "match_start") {
                console.log("玩家已经加入匹配队列");
                button_flag = "start";
                be.innerHTML = "匹配中....点击按钮停止匹配!";
                return;
            }else if (rsp_json["optype"] == "match_stop"){
                console.log("玩家已经移除匹配队列");
                button_flag = "stop";
                be.innerHTML = "开始匹配";
                return;
            }else {
                alert(evt.data);
                location.replace("/login.html");
                return;
            }
        }
        get_user_info();
    </script>
</body>
</html>

测试正确

wsbsokcet长连接部分

有大厅的长连接和房间的长连接,再wsopen_callback函数内部根据请求报文类型调用

游戏大厅长连接的建立

首先是游戏大厅长连接的建立,当登录之后会会跳转至游戏大厅的页面,游戏大厅前端代码先显示用户信息,然后直接建立游戏大厅长连接(向后台发送大厅长连接请求,失败返回至登陆界面,成功则获取响应显示游戏大厅长连接建立成功)

后台的处理是先检查这个用户有没有登录,有没有重复登陆,没有的话将当前客户端连接到游戏大厅,并且设置响应报文,最后将过期时间设置为永久

cpp 复制代码
    void ws_resp(wsserver_t::connection_ptr conn, Json::Value &resp) {
        std::string body;
        json_util::serialize(resp, body);
        conn->send(body);
    }
    session_ptr get_session_by_cookie(wsserver_t::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信息,让客户端重新登录
            err_resp["optype"] = "hall_ready";
            err_resp["reason"] = "没有找到cookie信息,需要重新登录";
            err_resp["result"] = false;
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        // 1.5. 从cookie中取出ssid
        std::string ssid_str;
        bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);
        if (ret == false) {
            //cookie中没有ssid,返回错误:没有ssid信息,让客户端重新登录
            err_resp["optype"] = "hall_ready";
            err_resp["reason"] = "没有找到SSID信息,需要重新登录";
            err_resp["result"] = false;
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        // 2. 在session管理中查找对应的会话信息
        session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));
        if (ssp.get() == nullptr) {
            //没有找到session,则认为登录已经过期,需要重新登录
            err_resp["optype"] = "hall_ready";
            err_resp["reason"] = "没有找到session信息,需要重新登录";
            err_resp["result"] = false;
            ws_resp(conn, err_resp);
            return session_ptr();
        }
        return ssp;
    }
    void wsopen_game_hall(wsserver_t::connection_ptr conn) {
        //游戏大厅长连接建立成功
        Json::Value resp_json;
        //1. 登录验证--判断当前客户端是否已经成功登录
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr) {
            return;
        }
        //2. 判断当前客户端是否是重复登录
        if (_om.is_in_game_hall(ssp->get_uid()) || _om.is_in_game_room(ssp->get_uid())) {
            resp_json["optype"] = "hall_ready";
            resp_json["reason"] = "玩家重复登录!";
            resp_json["result"] = false;
            return ws_resp(conn, resp_json);
        }
        //3. 将当前客户端以及连接加入到游戏大厅
        _om.enter_game_hall(ssp->get_uid(), conn);
        //4. 给客户端响应游戏大厅连接建立成功
        resp_json["optype"] = "hall_ready";
        resp_json["result"] = true;
        ws_resp(conn, resp_json);
        //5. 记得将session设置为永久存在
        _sm.set_session_expire_time(ssp->get_ssid(), SESSION_FOREVER);
    }
    void wsopen_callback(websocketpp::connection_hdl hdl)
    {
        //websocket长连接建立成功之后的处理函数
        wsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        if (uri == "/hall") {
            //建立了游戏大厅的长连接
            return wsopen_game_hall(conn);
        }else if (uri == "/room") {
            //建立了游戏房间的长连接
            return wsopen_game_room(conn);
        }
    }

游戏大厅长连接关闭

同样地,根据是大厅还是房间前端页面的关闭调用wsclose_callback中大厅的关闭还是房间连接的关闭

大厅关闭的处理:先判断用户是否登录,没有直接返回,不需要关闭;登录则先从大厅移除,然后恢复其session过期时间为定时销毁

cpp 复制代码
    void wsclose_game_hall(wsserver_t::connection_ptr conn) {
        //游戏大厅长连接断开的处理
        //1. 登录验证--判断当前客户端是否已经成功登录
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr) {
            return;
        }
        //1. 将玩家从游戏大厅中移除
        _om.exit_game_hall(ssp->get_uid());
        //2. 将session恢复生命周期的管理,设置定时销毁
        _sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT);
    }
    void wsclose_callback(websocketpp::connection_hdl hdl)
    {
        //websocket连接断开前的处理
        wsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        if (uri == "/hall") {
            //关闭了游戏大厅的长连接
            return wsclose_game_hall(conn);
        }else if (uri == "/room") {
            //关闭游戏房间的长连接
            return wsclose_game_room(conn);
        }
    }

游戏大厅消息的处理

在大厅内部,客户端发送而来的消息只有两种,一是开始匹配,二是停止匹配;前端设置按钮事件,点击开始匹配,向后台发送开始匹配的请求match_start,后台进行身份验证并且成功解析请求之后,将用户添加至匹配队列,并且响应报文match_start至客户端,前端检测到请求成功之后则将按钮信息改为"匹配中....点击按钮停止匹配!";此时可以点击按钮,发送match_stop停止匹配请求,后台做同样操作,唯一不同的是将用户从匹配队列移除,最终响应match_stop报文,前端检测到此报文,将按钮信息改为"开始匹配",此时点击重新开始匹配请求

当然在此过程种匹配队列也在工作,那么当匹配成功之后就向大厅前端发送match_success的响应,进入游戏房间了,前端页面跳转至房间页面

cpp 复制代码
    void wsmsg_game_hall(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg) {
        Json::Value resp_json;
        std::string resp_body;
        //1. 身份验证,当前客户端到底是哪个玩家
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr) {
            return;
        }
        //2. 获取请求信息
        std::string req_body = msg->get_payload();
        Json::Value req_json;
        bool ret = json_util::unserialize(req_body, req_json);
        if (ret == false) {
            resp_json["result"] = false;
            resp_json["reason"] = "请求信息解析失败";
            return ws_resp(conn, resp_json);
        }
        //3. 对于请求进行处理:
        if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start"){
            //  开始对战匹配:通过匹配模块,将用户添加到匹配队列中
            _mm.add(ssp->get_uid());
            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_uid());
            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_callback(websocketpp::connection_hdl hdl, wsserver_t::message_ptr msg)
    {
        //websocket长连接通信处理
        wsserver_t::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
        websocketpp::http::parser::request req = conn->get_request();
        std::string uri = req.get_uri();
        if (uri == "/hall") {
            //建立了游戏大厅的长连接
            return wsmsg_game_hall(conn, msg);
        }else if (uri == "/room") {
            //建立了游戏房间的长连接
            return wsmsg_game_room(conn, msg);
        }
    }

游戏大厅前端代码

cpp 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏大厅</title>
    <link rel="stylesheet" href="./css/common.css">
    <link rel="stylesheet" href="./css/game_hall.css">
</head>
<body>
    <div class="nav">网络五子棋对战游戏</div>
    <!-- 整个页面的容器元素 -->
    <div class="container">
        <!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 -->
        <div>
            <!-- 展示用户信息 -->
            <div id="screen"></div>
            <!-- 匹配按钮 -->
            <div id="match-button">开始匹配</div>
        </div>
    </div>

    <script src="./js/jquery.min.js"></script>
    <script>
        var ws_url = "ws://" + location.host + "/hall";
        var ws_hdl = null;

        window.onbeforeunload = function() {
            ws_hdl.close();
        }
        //按钮有两个状态:没有进行匹配的状态,正在匹配中的状态
        var button_flag = "stop";
        //点击按钮的事件处理:
        var be = document.getElementById("match-button");
        be.onclick = function() {
            if (button_flag == "stop") {
                //1. 没有进行匹配的状态下点击按钮,发送对战匹配请求
                var req_json = {
                    optype: "match_start"
                }
                ws_hdl.send(JSON.stringify(req_json));
            }else {
                //2. 正在匹配中的状态下点击按钮,发送停止对战匹配请求
                var req_json = {
                    optype: "match_stop"
                }
                ws_hdl.send(JSON.stringify(req_json));
            }
        }
        function get_user_info() {
            $.ajax({
                url: "/info",
                type: "get",
                success: function(res) {
                    var info_html = "<p>" + "用户:" + res.username + " 积分:" + res.score + 
                        "</br>" + "比赛场次:" + res.total_count + " 获胜场次:" + res.win_count + "</p>";
                    var screen_div = document.getElementById("screen");
                    screen_div.innerHTML = info_html;

                    ws_hdl = new WebSocket(ws_url);
                    ws_hdl.onopen = ws_onopen;
                    ws_hdl.onclose = ws_onclose;
                    ws_hdl.onerror = ws_onerror;
                    ws_hdl.onmessage = ws_onmessage;
                },
                error: function(xhr) {
                    alert(JSON.stringify(xhr));
                    location.replace("/login.html");
                }
            })
        }
        function ws_onopen() {
            console.log("websocket onopen");
        }
        function ws_onclose() {
            console.log("websocket onopen");
        }
        function ws_onerror() {
            console.log("websocket onopen");
        }
        function ws_onmessage(evt) {
            var rsp_json = JSON.parse(evt.data);
            if (rsp_json.result == false) {
                alert(evt.data);
                location.replace("/login.html");
                return;
            }
            if (rsp_json["optype"] == "hall_ready") {
                alert("游戏大厅连接建立成功!");
            }else if (rsp_json["optype"] == "match_success") {
                //对战匹配成功
                alert("对战匹配成功,进入游戏房间!");
                location.replace("/game_room.html");
            }else if (rsp_json["optype"] == "match_start") {
                console.log("玩家已经加入匹配队列");
                button_flag = "start";
                be.innerHTML = "匹配中....点击按钮停止匹配!";
                return;
            }else if (rsp_json["optype"] == "match_stop"){
                console.log("玩家已经移除匹配队列");
                button_flag = "stop";
                be.innerHTML = "开始匹配";
                return;
            }else {
                alert(evt.data);
                location.replace("/login.html");
                return;
            }
        }
        get_user_info();
    </script>
</body>
</html>

游戏房间长连接建立

进入游戏房间是匹配队列中两个用户匹配成功了;一旦进入房间,客户端就向后台发送房间游戏房间长连接的建立,后台先判断类型,发现是房间长连接的建立,调用函数wsopen_game_room,在此内部先获取session,再判断这个用户是否已经在大厅或者房间种,是的话设置响应报文为room_ready,原因为用户重复登陆;没有则判断房间有没有创建,没有创建设置响应报文为room_ready,原因为没有找到玩家的房间信息;之后将用户添加至在线管理模块并且将session、设置为永久存在,并且将响应报文类型设置为room_ready,房间的信息也保存在json串中,方便前端对后序房间中的操作进行处理;前端接收到成功响应报文会将房间信息保存起来

cpp 复制代码
    void wsopen_game_room(wsserver_t::connection_ptr conn) 
    {
        Json::Value resp_json;
        //1. 获取当前客户端的session
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr) 
        {
            return;
        }
        //2. 当前用户是否已经在在线用户管理的游戏房间或者游戏大厅中---在线用户管理
        if (_om.is_in_game_hall(ssp->get_uid()) || _om.is_in_game_room(ssp->get_uid())) {
            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_uid());
        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_uid(), conn);
        //5. 将session重新设置为永久存在
        _sm.set_session_expire_time(ssp->get_ssid(), SESSION_FOREVER);
        //6. 回复房间准备完毕
        resp_json["optype"] = "room_ready";
        resp_json["result"] = true;
        resp_json["room_id"] = (Json::UInt64)rp->room_id();
        resp_json["uid"] = (Json::UInt64)ssp->get_uid();
        resp_json["white_id"] = (Json::UInt64)rp->white_user();
        resp_json["black_id"] = (Json::UInt64)rp->black_user();
        return ws_resp(conn, resp_json);
    }

游戏房间长连接关闭

先判断是否成功登录,没有则不需要关闭房间,有则将玩家从在线用户管理中移除,将session回复生命周期的管理,设置定时销毁并且将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间

cpp 复制代码
    void wsclose_game_room(wsserver_t::connection_ptr conn) {
        //获取会话信息,识别客户端
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr) {
            return;
        }
        //1. 将玩家从在线用户管理中移除
        _om.exit_game_room(ssp->get_uid());
        //2. 将session回复生命周期的管理,设置定时销毁
        _sm.set_session_expire_time(ssp->get_ssid(), SESSION_TIMEOUT);
        //3. 将玩家从游戏房间中移除,房间中所有用户退出了就会销毁房间
        _rm.remove_room_user(ssp->get_uid());
    }

游戏房间消息的处理

cpp 复制代码
 void wsmsg_game_room(wsserver_t::connection_ptr conn, wsserver_t::message_ptr msg)
    {
        Json::Value resp_json;
        //1. 获取客户端session,识别客户端身份
        session_ptr ssp = get_session_by_cookie(conn);
        if (ssp.get() == nullptr) 
        {
            DLOG("房间-没有找到会话信息");
            return;
        }
        //2. 获取客户端房间信息
        room_ptr rp = _rm.get_room_by_uid(ssp->get_uid());
        if (rp.get() == nullptr) 
        {
            resp_json["optype"] = "unknow";
            resp_json["reason"] = "没有找到玩家的房间信息";
            resp_json["result"] = false;
            DLOG("房间-没有找到玩家房间信息");
            return ws_resp(conn, resp_json);
        }
        //3. 对消息进行反序列化
        Json::Value req_json;
        std::string req_body = msg->get_payload();
        bool ret = json_util::unserialize(req_body, req_json);
        if (ret == false)
        {
          resp_json["optype"] = "unknow";
            resp_json["reason"] = "请求解析失败";
            resp_json["result"] = false;
            DLOG("房间-反序列化请求失败");
            return ws_resp(conn, resp_json);
        }
        DLOG("房间:收到房间请求,开始处理....");
        //4. 通过房间模块进行消息请求的处理
        return rp->handle_request(req_json);
    }

游戏过程

返回大厅之后的界面

相关推荐
程序猿阿伟1 小时前
《从TCP到WebSocket:Discord静默断流的七层排查指南》
websocket·网络协议·tcp/ip
10WTW011 小时前
计网实验 模拟器的配置与使用
网络·智能路由器
酉鬼女又兒1 小时前
零基础入门计算机网络物理层:核心概念、传输媒体、传输方式、编码调制与信道极限容量完整知识点总结
开发语言·网络·计算机网络·考研·职场和发展·php·信息与通信
kong@react1 小时前
milvus(向量数据库)docker容器(升级1.0)
数据库·docker·milvus
逆境不可逃1 小时前
【WebSocket 01】 入门原理剖析,手写群发消息、私聊会话功能
网络·websocket·网络协议
流烟默1 小时前
国产数据库CERDB 数据库实战:核心概念与备份恢复全攻略
数据库·数据库备份·cerdb
曾几何时`1 小时前
Go(四)Channel
开发语言·后端·golang
天一生水water1 小时前
agent教程S01-Agent 最小循环教程整理
java·服务器·网络·agent
网络与设备以及操作系统学习使用者1 小时前
多路由设备静态路由配置详解
运维·网络·学习·华为·智能路由器