该博客对于学完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 2class 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获取房间IDauto 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. 通过房间信息,获取房间中所有⽤⼾的IDuint64_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某些用户idstd::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.htmlwebsocket_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;// 如果请求路径是一个目录,则在路径后面加上loginif (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, "⽤⼾名已经被占⽤!");}// 如果成功了,则返回200return 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. 如果验证失败,则返回400DBG_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和valstd::vector<std::string> tmp_arr;string_util::split(str, "=", tmp_arr);if (tmp_arr.size() != 2){continue;}if (tmp_arr[0] == key){val = tmp_arr[1];return true;}}return false;}// 用户会将 Cookie=abc 返回 先找cookie,再找cookie对应的SSID,再找SSID对应的会话,再找用户信息返回,然后设置会话过期时间void info(websocket_server::connection_ptr &conn){// ⽤⼾信息获取功能请求的处理Json::Value err_resp;// 1. 获取请求信息中的Cookie,从Cookie中获取ssidstd::string cookie_str = conn->get_request_header("Cookie");if (cookie_str.empty()){// 如果没有cookie,返回错误:没有cookie信息,让客⼾端重新登录return http_resp(conn, true,websocketpp::http::status_code::bad_request, "无cookie信息,请重新登录");}// 1.5. 从cookie中取出ssidstd::string ssid_str;bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);if (ret == false){// cookie中没有ssid,返回错误:没有ssid信息,让客⼾端重新登录return http_resp(conn, true,websocketpp::http::status_code::bad_request, "找不到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中获取ssidstd::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. 获取当前客户端的sessionsession_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. 获取当前客⼾端的sessionsession_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;
}