基于websocketpp实现的五子棋项目
该博客对于学完C++和linux操作系统,但不知道如何用C++开发项目,已经不知道C++如何使用第三方库的人来说一定很有帮助,请耐心看完!
先看一下游戏会显示的前端界面,对理解这个游戏的前后端交互过程会有帮助
1. 开发环境
1.1 使用的操作系统:Ubuntu-22.04
我们可以先在虚拟机或者服务器上选择或者安装这个Ubuntu-22.04操作系统。最后我们这个网页对战五子棋的服务器是要部署在服务器上的。博主选用的是腾讯云的服务器。
1.2 安装gcc/g++编译器
我们程序使用g++进行编译。我们在命令行输入下面指令即可安装
sudo apt-get install gcc g++
1.3 安装gdb调试器
我们程序使用gdb进行调试。我们在命令行输入下面指令即可安装
sudo apt-get install gdb
1.4 安装git⼯具。
我的代码已经上传到码云之后:https://gitee.com/xwyg/Cpp_project.git
sudo apt-get install git
1.5 安装cmake项⽬构建⼯具
我们项目的自动化构建和编译只用到了make,而cmake用来执行websocket库的构建和编译。
sudo apt-get install cmake
1.6 安装第三方库:
安装jsoncpp库和 安装jsoncpp库
sudo apt-get install libjsoncpp-dev
sudo apt-get install libboost-all-dev
Jsoncpp库用来处理JSON格式数据,我们在进行客户端和服务器通信过程中正文部分传递的是字符串,单纯字符串的提取处理要复杂一些,我们将其转换成Json格式的数据,便于我们在请求和响应中提取对于信息。
我们采取的是Restful风格的网络通信接口,即为使用GET/POST/PUT/DELETE代表不同请求类型,并且正文格式都是使用JSON格式序列化后的字符串
安装websocketpp库
Websocketpp没有官方维护的预编译包直接供apt-get使用,因此它的安装比较复杂,需要我们自己下载源代码进行编译,大家可以在deepseek或者chatgpt中之间搜安装教程。用这两个生成的安装方法还是很准确的。
Websocketpp库是我们服务器使用的主要库,它依赖于boost库,处理WebSocket通信.
Websocket介绍:
WebSocket协议是从HTML5开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的消息推送机制。
• 传统的web程序都是属于"⼀问⼀答"的形式,即客⼾端给服务器发送了⼀个HTTP请求,服务器给客⼾端返回⼀个HTTP响应。这种情况下服务器是属于被动的⼀⽅,如果客⼾端不主动发起请求服务器就无法主动给客户端响应
• 网页即时聊天 或者 我们做的五子棋游戏这样的程序都是⾮常依赖"消息推送"的,即需要服务器主动推动消息到客户端。如果只是使⽤原⽣的HTTP协议,要想实现消息推送⼀般需要通过客户端"轮询"的⽅式实现,而轮询的成本⽐较⾼并且也不能及时的获取到消息的响应。
基于上述两个问题,就产⽣了WebSocket协议。WebSocket更接近于TCP这种级别的通信⽅式,⼀旦连接建⽴完成客户端或者服务器都可以主动的向对⽅发送数据。
WebSocket原理解析
WebSocket协议本质上是⼀个基于TCP的协议。为了建⽴⼀个WebSocket连接,客⼾端浏览器⾸先要向服务器发起⼀个HTTP请求,这个请求和通常的HTTP请求不同,包含了⼀些附加头信息,通过这个附加头信息完成握⼿过程并升级协议的过程。
WebSocket也有自己的报文格式,但是其实与本项目关系没有那么大,我们之间WebSocketpp调用对应的接口即可,在后面项目代码中会有具体的介绍。
WebSocketpp同时⽀持HTTP和Websocket两种⽹络协议,⽐较适⽤于我们本次的项⽬,所以我们选⽤该库作为项⽬的依赖库⽤来搭建HTTP和WebSocket服务器。
1.7 mysql安装
博主安装的是mysql 5.7版本的数据库,各位安装8.0版本的数据库也是一样的,各位从deepseek或者chatgpt搜mysql的安装教程比较靠谱。
2.项目流程
这个流程图非常重要,关系到我们整个游戏的运行流程,已经前后端交互流程,还有服务器的各个模块之间的交互和联系。
2.1 客户端流程
客户端流程: 进入用户注册页面--->完成用户注册--->跳转到用户登录页面-->完成用户登录
---->跳转到游戏大厅页面--->点击按钮进入游戏匹配-->匹配成功跳转到游戏房价页面
---->游戏房间可以进行下棋或者聊天等操作--->游戏结束,加分或者扣分
--->该房间内无法下棋,弹出"回到大厅"按钮--->用户又可以点击按钮进入游戏匹配
2. 2 服务器流程
首先玩家想要访问我们的服务器得通过访问101.35.46.142:7080/register.html 注册页面或者101.35.46.142:7080/login.html登录页面,访问这两个页面会向我们的服务器发送http请求。
对于注册页面发送的http请求,服务器从请求正文中获取此时输入的用户名和密码,然后进行数据库中数据的插入; 对于登录页面发送的htpp请求,服务器从请求正文中获取此时输入的用户名和密码,然后服务器会建立用户的session并保存( 此后每次用户发送http或者websocket请求过来都会进行session的验证), 此时客户端跳转到游戏大厅页面,发送 大厅websocket长连接建立请求,服务器此时建立同客户端的长连接,当客户端点击游戏大厅页面的 "进入匹配" 按钮之后,会向客户端发送Websocket消息,服务器会按照该用户的等级分数 把他加入对应的匹配队列之中,等匹配成功之后会向两个进行匹配的客户端发送匹配成功的消息,客户端收到该请求进入游戏房间,此时参与匹配的两个客户端(玩家)都会向服务器发送 房间webhasocket长连接建立请求,服务器同客户端建立两个房间长连接,此时用户进行下棋或者聊天动作,都会向服务器发送对应请求,服务器也会给出对应的响应。
3.websocketpp库的介绍
3.1 websocketpp常用接口
namespace websocketpp {
typedef lib::weak_ptr<void> connection_hdl;
template <typename config>
class endpoint : public config::socket_type {
typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr;
typedef typename connection_type::ptr connection_ptr;
typedef typename connection_type::message_ptr message_ptr;
typedef lib::function<void(connection_hdl)> open_handler;
typedef lib::function<void(connection_hdl)> close_handler;
typedef lib::function<void(connection_hdl)> http_handler;
typedef lib::function<void(connection_hdl,message_ptr)> message_handler;
* websocketpp::log::alevel::none 禁⽌打印所有⽇志*/
void set_access_channels(log::level channels);/*设置⽇志打印等级*/
void clear_access_channels(log::level channels);/*清除指定等级的⽇志*/
/*设置指定事件的回调函数*/
void set_open_handler(open_handler h);/*websocket握⼿成功回调处理函数*/
void set_close_handler(close_handler h);/*websocket连接关闭回调处理函数*/
void set_message_handler(message_handler h);/*websocket消息回调处理函数*/
void set_http_handler(http_handler h);/*http请求回调处理函数*/
/*关闭连接接⼝*/
void close(connection_hdl hdl, close::status::value code, std::string& reason);
/*获取connection_hdl 对应连接的connection_ptr*/
connection_ptr get_con_from_hdl(connection_hdl hdl);
/*websocketpp基于asio框架实现,init_asio⽤于初始化asio框架中的io_service调度器*/
void init_asio();
/*设置是否启⽤地址重⽤*/
void set_reuse_addr(bool value);
/*设置endpoint的绑定监听端⼝*/
void listen(uint16_t port);
/*对io_service对象的run接⼝封装,⽤于启动服务器*/
std::size_t run();
/*websocketpp提供的定时器,以毫秒为单位*/
timer_ptr set_timer(long duration, timer_handler callback);
};
template <typename config>
class server : public endpoint<connection<config>,config> {
/*初始化并启动服务端监听连接的accept事件处理*/
void start_accept();
}
看到这些代码可能会一脸懵,我们只需要知道websocketpp命名空间下定义了 server类(继承自endpoint),它就是我们要启动的服务器,调用它的方法,我们就可以对服务器进行各种设置,同时它有一些回调函数:
set_open_handler(open_handler h);/*websocket握⼿成功回调处理函数*/
void set_close_handler(close_handler h);/*websocket连接关闭回调处理函数*/
void set_message_handler(message_handler h);/*websocket消息回调处理函数*/
void set_http_handler(http_handler h);/*http请求回调处理函数*/
这些函数需要我们传入一个函数对象,这个函数对象是一个 void (connection_hdl hdl)类型,请看使用实例:
#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
using namespace std;
typedef websocketpp::server<websocketpp::config::asio> websocketsvr;
typedef websocketsvr::message_ptr message_ptr;
// websocket连接成功的回调函数
void OnOpen(websocketsvr *server,websocketpp::connection_hdl hdl){
cout<<"连接成功"<<endl;
}
// websocket连接成功的回调函数
void OnClose(websocketsvr *server,websocketpp::connection_hdl hdl){
cout<<"连接关闭"<<endl;
}
// websocket连接收到消息的回调函数
void OnMessage(websocketsvr *server,websocketpp::connection_hdl hdl,message_ptr msg){
cout << "收到消息" << msg->get_payload() << endl;
// 收到消息将相同的消息发回给websocket客⼾端
server->send(hdl, msg->get_payload(), websocketpp::frame::opcode::text);
}
// websocket连接异常的回调函数
void OnFail(websocketsvr *server,websocketpp::connection_hdl hdl){
cout<<"连接异常"<<endl;
}
// 处理http请求的回调函数 返回⼀个html欢迎⻚⾯
void OnHttp(websocketsvr *server,websocketpp::connection_hdl hdl){
cout<<"处理http请求"<<endl;
websocketsvr::connection_ptr con = server->get_con_from_hdl(hdl);
std::stringstream ss;
ss << "<!doctype html><html><head>"
<< "<title>hello websocket</title><body>"
<< "<h1>hello websocketpp</h1>"
<< "</body></head></html>";
con->set_body(ss.str()); //设置http响应正文
con->set_status(websocketpp::http::status_code::ok); //设置http响应状态码
}
这是main(),告诉我们server如何初始化,格式都是一样的,同时还需要绑定(注册)对应请求来时的处理动作。
int main()
{
// 使⽤websocketpp库创建服务器
websocketsvr server;
// 设置websocketpp库的⽇志级别 all表⽰打印全部级别⽇志 none表⽰什么⽇志都不打印
server.set_access_channels(websocketpp::log::alevel::none);
/*初始化asio*/
server.init_asio();
// 注册http请求的处理函数
server.set_http_handler(bind(&OnHttp, &server, ::_1));
// 注册websocket请求的处理函数
server.set_open_handler(bind(&OnOpen, &server, ::_1));
server.set_close_handler(bind(&OnClose, &server, _1));
server.set_message_handler(bind(&OnMessage, &server, _1, _2));
// 监听8888端⼝
server.listen(8888);
// 开始接收tcp连接
server.start_accept();
// 开始运⾏服务器
server.run();
return 0;
}
Http客⼾端,使⽤浏览器作为http客⼾端即可,访问服务器的8888端⼝。
任意浏览器输入即可请求我们服务器:
前端如何发送websocket请求,这个不是我们的重点,但是也大致了解一下前端代码:
<html>
<body>
<input type="text" id="message">
<button id="submit">提交</button>
<script>
// 创建 websocket 实例
// ws://192.168.51.100:8888
// 类⽐http
// ws表⽰websocket协议
// 192.168.51.100 表⽰服务器地址
// 8888表⽰服务器绑定的端⼝
let websocket = new WebSocket("ws://192.168.51.100:8888");
// 处理连接打开的回调函数
websocket.onopen = function () {
console.log("连接建⽴");
}
// 处理收到消息的回调函数
// 控制台打印消息
websocket.onmessage = function (e) {
console.log("收到消息: " + e.data);
}
// 处理连接异常的回调函数
websocket.onerror = function () {
console.log("连接异常");
}
// 处理连接关闭的回调函数
websocket.onclose = function () {
console.log("连接关闭");
}
// 实现点击按钮后, 通过 websocket实例 向服务器发送请求
let input = document.querySelector('#message');
let button = document.querySelector('#submit');
button.onclick = function () {
console.log("发送消息: " + input.value);
websocket.send(input.value);
}
</script>
</body>
</html>
服务器启动,我们将上面的代码复制到 abc.html文件中,打开并在输入框输入hello,即可得到以下内容:
new WebSocket("ws://192.168.51.100:8888");然后将他绑定在一个按钮中,点击按钮即可向后端发送websocket请求,此时服务器收到请求,会同客户端建立websocket长连接
4.项目实现
4.1 日志宏的实现
我们不采用websocketpp库中的日志类,而是自己编写一个日志类,定义一个日志宏,传入日志等级和可变参数,然后将线程ID,文件名,行号,时间与传入的可变字符串拼接起来,一起向终端(或文件)打印。
1.
strftime函数
其中 strftime函数
:将时间格式化为字符串。我们可以用snprintf(time_buffer, sizeof(time_buffer), "%d",format_time->tm_year + 1900)代替。
2.宏参数允许 进行 字符串拼接
fprintf中第二个参数,要求传入一个const char*字符串,我们可以直接拼接我们需要的格式化字符串和传入的格式化字符串,这在函数无法实现 "[%p %s %s:%d] " format " \n"。
3.宏中可变参数
C++98允许宏传入可变参数,由##__VA_ARGS__代替
代码如下:
#pragma once
#include <stdio.h>
#include <time.h>
#include<pthread.h>
#define INF 0
#define DBG 1
#define ERR 2
#define LOG_LEVEL DBG //超过这个日志等级才会被输出
// 宏里面有多条语句,使用do while(0),
#define LOG(level, format, ...) \
do \
{ \
if (level < LOG_LEVEL) \
break; \
time_t t = time(NULL); \
struct tm *ltm = localtime(&t); \
char tmp[32] = {0}; \
strftime(tmp, 31, "%H:%M:%S", ltm); \
fprintf(stdout, "[%p %s %s:%d] " format " \n", (void *)pthread_self(), \
tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
}while (0)
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
4.2 工具类的实现
我们实现四个工具类,工具类中封装有静态方法,全局都可以使用该方法
首先是 json_util,提供序列化和反序列化的方法。
序列化通过Json::StreamWriter对象指针将Json::Value对象写入 str字符串中,反序列化通过Json::CharReader将str字符串写入Json::Value对象中
class json_util
{
public:
// 将jsonvalue对象写入 str字符串中
static bool serialize(const Json::Value &value, std::string &str)
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
int ret = sw->write(value, &ss);
if (ret != 0)
{
std::cout << "json serialize failed!" << std::endl;
return false;
}
str = ss.str();
return true;
}
// 将json字符串写回到value对象中,由用户自己做[""].asInt等处理
static bool unserialize(const std::string &str, Json::Value &value)
{
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
bool ret = cr->parse(str.c_str(), str.c_str() + str.size(),
&value, nullptr);
if (!ret)
{
ERR_LOG("json unserialize failed!");
return false;
}
return true;
}
};
第二个是mysql_util,提供数据库的创建,销毁,执行方法
class mysql_util
{
public:
static MYSQL *mysql_create(const std::string &host,
const std::string &user,
const std::string &pass,
const std::string &db,
int port)
{
MYSQL *mysql = mysql_init(NULL);
if (mysql == NULL)
{
ERR_LOG("mysql init failed!");
return NULL;
}
if (mysql_real_connect(mysql, host.c_str(), user.c_str(),
pass.c_str(), db.c_str(), port, NULL, 0) == NULL)
{
ERR_LOG("mysql connect server failed! %s", mysql_error(mysql));
mysql_close(mysql);
return NULL;
}
if (mysql_set_character_set(mysql, "utf8") != 0)
{
ERR_LOG("mysql set character failed!");
mysql_close(mysql);
return NULL;
}
DBG_LOG("mysql connect success!");
return mysql;
}
static void mysql_destroy(MYSQL *mysql)
{
if (mysql == NULL)
{
return;
}
mysql_close(mysql);
}
static bool mysql_exec(MYSQL *mysql, const std::string &sql)
{
if (mysql_query(mysql, sql.c_str()) != 0)
{
ERR_LOG("SQL: %s", sql.c_str());
ERR_LOG("ERR: %s", mysql_error(mysql));
return false;
}
return true;
}
};
第三个是string_util,提供字符串分割方法,因为我们涉及http 某一个请求头的提取已经某一个cookie的提取,提取不会改变原有的字符串和分割字符串,提取到一个vector<string>中
class string_util
{
public:
// 字符串 子串分割功能, 将分割的子串保存到一个字符串数组之中
static int split(const std::string &in, const std::string &sep,
std::vector<std::string> &arry)
{
arry.clear();
size_t pos, idx = 0; // pos保存为查找结果,如果pos和idx相等,该位置就是sep,则不保存
while (idx < in.size())
{
pos = in.find(sep, idx);
if (pos == std::string::npos)
{
arry.push_back(in.substr(idx));
break;
}
if (pos != idx)
{
arry.push_back(in.substr(idx, pos - idx)); // 当前位置,长度
}
idx = pos + sep.size();
}
return arry.size();
}
};
第四个是file_util,对静态请求处理时将html文件返回给客户端
class file_util
{
public:
static bool read(const std::string &filename, std::string &body)
{
std::ifstream file;
// 打开⽂件
file.open(filename.c_str(), std::ios::in | std::ios::binary);
if (!file)
{
std::cout << filename << " Open failed!" << std::endl;
return false;
}
// 计算⽂件⼤⼩
file.seekg(0, std::ios::end);
body.resize(file.tellg());
file.seekg(0, std::ios::beg);
file.read(&body[0], body.size());
if (file.good() == false)
{
std::cout << filename << " Read failed!" << std::endl;
file.close();
return false;
}
file.close();
return true;
}
};
4.3 数据库操作模块实现
我们先定义数据库中的表,我们这个项目比较简单,只有一张user表,提供用户id,username,password,score,对战总场次和获胜场次这些字段
我们可以将每个表都封装到一个类之中,提供一个对外的MYSQL句柄,执行对应的数据库操作。
由于字符串中拼接比较麻烦,我们选择#define 定义格式化字符串,再有sprintf()函数去进行写入。
我们所需要进行数据库操作的地方有 用户插入,用户登录,通过用户名查询用户,通过用户id查询用户,用户获胜和用户失败这些情况,分别实现这些函数
#pragma once
#include "util.hpp"
#include <mutex>
#include <assert.h>
// user_table类,将所要执行数据库操作的地方全部封装到该类之中,包含一个MYSQL指针和mutex互斥量
// 调用了 mysql_util类中的 创建销毁和执行方法
// 提供了 insert(user),login(user),win,lose,select_by_uid等等函数
// 每个函数所要执行的sql由宏定义给出,sql的字符串都要以;结尾,同时varchar类型都要在''里面
// mysql_query是线程安全的,但是它和mysql_store_result(_mysql)保存一起就不是线程安全的了
class user_table
{
private:
MYSQL *_mysql;
std::mutex _mutex;
public:
user_table(const std::string &host,
const std::string &user,
const std::string &pass,
const std::string &db,
int port = 3306)
{
_mysql = mysql_util::mysql_create(host, user, pass, db, port);
assert(_mysql != NULL);
}
// 网络中传输的是字符串,需要讲它们序列化到一个个的request对象中,再调用_cal计算并将结果,反序列化成字符串返回
bool insert(Json::Value &user)
{
#define INSERT_USER "insert user values(null, '%s', password('%s'), 1000, 0,0);"
if (user["password"].isNull() || user["username"].isNull())
{
DBG_LOG("INPUT PASSWORD OR USERNAME");
return false;
}
char sql[4096] = {0};
sprintf(sql, INSERT_USER, user["username"].asCString(),
user["password"].asCString());
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
DBG_LOG("insert user info failed!!\n");
return false;
}
return true;
}
bool login(Json::Value & user) // 用户登录,并返回完整的用户信息
{
if (user["password"].isNull() || user["username"].isNull())
{
DBG_LOG("INPUT PASSWORD OR USERNAME");
return false;
}
// 以用户名和密码共同查询,查询到数据则表⽰⽤⼾名密码⼀致,没有信息则用户名密码错误
#define LOGIN_USER "select id, score, total_count,win_count from user where username='%s' and password=password('%s');"
char sql[4096] = {0};
sprintf(sql, LOGIN_USER, user["username"].asCString(),
user["password"].asCString());
MYSQL_RES *res = NULL;
{
// std::lock_guard<std::mutex> lock(_mutex);
std::unique_lock<std::mutex> lock(_mutex);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
DBG_LOG("user login failed!!\n");
return false;
}
// 将查询结果保存到本地
res = mysql_store_result(_mysql);
if (res == NULL)
{
DBG_LOG("mysql_store_result exec error!!");
return false;
}
}
std::cout << res << std::endl;
// 根据结果集获取条目输了
int num_row = mysql_num_rows(res);
if (num_row == 0)
{
DBG_LOG("have no login user info!!");
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
// 查询结果集的四行数据设置进 原有user中
user["id"] = std::stoi(row[0]); // 如果数据范围小,默认int够用则无需转换
user["score"] = std::stoi(row[1]); // (Json::UInt64)
user["total_count"] = std::stoi(row[2]);
user["win_count"] = std::stoi(row[3]);
std::cout << "jjjjj" << std::endl;
mysql_free_result(res);
return true;
}
bool select_by_name(const std::string &name, Json::Value &user) // 通过用户名查询用户
{
#define USER_BY_NAME "select id, score, total_count, win_count from user where username = '%s';"
char sql[4096] = {0};
sprintf(sql, USER_BY_NAME, name.c_str());
MYSQL_RES *res = NULL;
{
std::unique_lock<std::mutex> lock(_mutex);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
DBG_LOG("get user by name failed!!\n");
return false;
}
// 按理说要么有数据,要么没有数据,就算有数据也只能有⼀条数据
res = mysql_store_result(_mysql);
if (res == NULL)
{
DBG_LOG("hmysql_store_result!!");
return false;
}
}
int row_num = mysql_num_rows(res);
if (row_num == 0)
{
DBG_LOG("have no login user info!!");
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
user["id"] = (Json::UInt64)std::stoi(row[0]);
user["username"] = name;
user["score"] = (Json::UInt64)std::stoi(row[1]);
user["total_count"] = std::stoi(row[2]);
user["win_count"] = std::stoi(row[3]);
mysql_free_result(res);
return true;
}
bool select_by_id(int id, Json::Value &user) // 通过id查询用户
{
#define USER_BY_ID "select username,score,total_count,win_count from user where id = %d;"
MYSQL_RES *res = NULL;
char sql[4096] = {0};
sprintf(sql, USER_BY_ID, id);
{
std::lock_guard<std::mutex> lock(_mutex);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
DBG_LOG("select_by_id mysql_exec error");
return false;
}
res = mysql_store_result(_mysql);
if (res == NULL)
{
DBG_LOG("mysql_store_result error!!");
return false;
}
}
int row_num = mysql_num_rows(res);
if (row_num == 0)
{
DBG_LOG("have no login user info!!");
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
user["username"] = row[0];
user["score"] = std::stoi(row[1]);
user["total_count"] = std::stoi(row[2]);
user["win_count"] = std::stoi(row[3]);
mysql_free_result(res);
return true;
}
bool win(int id) // 用户胜利时,总场次和胜利场次都加1
{
#define USER_WIN "update user set score=score+30,total_count=total_count+1, \
win_count=win_count+1 where id=%d;"
char sql[1024] = {0};
sprintf(sql, USER_WIN, id);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
DBG_LOG("update win user info failed!!\n");
return false;
}
return true;
}
bool lose(int id) // 用户失败时,总场次加1,分数不变
{
#define USER_LOSE "update user set score=score-30,total_count=total_count+1 where id=%d;"
char sql[1024] = {0};
sprintf(sql, USER_LOSE, id);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
DBG_LOG("update win user info failed!!\n");
return false;
}
return true;
}
~user_table()
{
mysql_util::mysql_destroy(_mysql);
_mysql = NULL;
}
};
4.4 用户在线管理模块实现
用户在线管理模块记录了用户进入我们服务器之后所处的存在状态,是否离线,是在大厅还是房间
用户首先发送http请求注册页面,输入用户名密码完成数据库插入,然后进入登录页面,登录成功之后我们就需要将这个用户管理起来,因为存在好多的客户端,我们需要根据用户id找到这些客户端,因此选用 std::unordered_map<int, websocket_server::connection_ptr> _game_hall建立游戏大厅中用户的管理和std::unordered_map<int, websocket_server::connection_ptr> _game_room;游戏房间中用户的管理。
#pragma once
#include "util.hpp"
#include <mutex>
#include <unordered_map>
// 在线用户的管理类,在线用户要么在游戏大厅,要么在游戏房间
// 维护用户id到服务器连接的 游戏大厅map和用户id到服务器连接的 游戏房间map,以及一个互斥量mutex
// 提供进入(退出)大厅,进入(退出)房间,获取这个用户的连接 等操作
class online_manager
{
// 使用map维护 从id到connection的关系
private:
/*游戏⼤厅的客⼾端连接管理*/
std::unordered_map<int, websocket_server::connection_ptr> _game_hall;
/*游戏房间的客⼾端连接管理*/
std::unordered_map<int, websocket_server::connection_ptr> _game_room;
std::mutex _mutex;
public:
/*进⼊游戏⼤厅--游戏⼤厅连接建⽴成功后调⽤*/
void enter_game_hall(int uid, const websocket_server::connection_ptr &conn)
{
std::unique_lock<std::mutex> lock(_mutex);
_game_hall.insert(std::make_pair(uid, conn));
}
/*退出游戏⼤厅--游戏⼤厅连接断开后调⽤*/
void exit_game_hall(int uid)
{
std::unique_lock<std::mutex> lock(_mutex);
_game_hall.erase(uid);
}
/*进⼊游戏房间--游戏房间连接建⽴成功后调⽤*/
void enter_game_room(int uid, const websocket_server::connection_ptr &conn)
{
std::unique_lock<std::mutex> lock(_mutex);
_game_room.insert(std::make_pair(uid, conn));
}
/*退出游戏房间--游戏房间连接断开后调⽤*/
void exit_game_room(int uid)
{
std::unique_lock<std::mutex> lock(_mutex);
_game_room.erase(uid);
}
/*判断用户是否在游戏⼤厅*/
bool in_game_hall(uint64_t uid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _game_hall.find(uid);
if (it == _game_hall.end())
{
return false;
}
return true;
}
/*判断用户是否在游戏房间*/
bool in_game_room(uint64_t uid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _game_room.find(uid);
if (it == _game_room.end())
{
return false;
}
return true;
}
/*从游戏⼤厅中获取指定⽤⼾关联的Socket连接*/
websocket_server::connection_ptr get_conn_from_game_hall(uint64_t uid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _game_hall.find(uid);
if (it == _game_hall.end())
{
return nullptr;
}
return it->second;
}
/*从游戏房间中获取指定⽤⼾关联的Socket连接*/
websocket_server::connection_ptr get_conn_from_game_room(int uid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _game_room.find(uid);
if (it == _game_room.end())
{
return nullptr;
}
return it->second;
}
online_manager()
{
}
~online_manager()
{
}
};
4.5 会话管理模块实现
现在基本所有网络通信都要实现一个会话管理模块,当用户登录成功之后,服务器使用一个SeeionId需要标记这个用户,这样后续用户每次操作都会发送sessionid给服务器,服务器也可以做用户验证,同时识别这是哪个客户端。
我们在类的设计上需要实现两个类,一个会话类,一个会话管理类,
会话类中包含会话id,用户id,会话状态,定时器
这个定时器主要是看这个session下是否设置了定时器过期任务,如果websocket_server::timer_ptr为空,则为永久存在。
#pragma once
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>
#include "util.hpp"
typedef enum
{
LOGIN,
UNLOGIN
} ss_statu;
// 一个会话类和一个会话管理类。
// 会话类中包含 会话id,用户id,会话状态,定时器(不过期和何时过期)
// 会话管理类中: 互斥锁,websocket服务器(给它设置定时任务) 和
// 分配的下一个sessionid和sessionid到整个会话的map。 提供创建session 和设置过期时间的函数
class session
{
private:
uint64_t _ssid; // 标识符
int _uid;
ss_statu _statu;
websocket_server::timer_ptr _tp; // 该session相关定时器
public:
session(uint64_t ssid) : _ssid(ssid)
{
DBG_LOG("SESSION %p 被创建!!", this);
}
~session() { DBG_LOG("SESSION %p 被释放!!", this); }
uint64_t ssid() { return _ssid; };
void set_statu(ss_statu statu) { _statu = statu; }
void set_user(int uid) { _uid = uid; }
uint64_t get_user() { return _uid; }
bool is_login() { return (_statu == LOGIN); }
void set_timer(const websocket_server::timer_ptr &tp) { _tp = tp; }
websocket_server::timer_ptr &get_timer() { return _tp; }
};
会话管理类中包含,需要分配的下一个会话id,websocket服务器(用于设置定时任务),一个会话id到整个会话的映射map.
注意,websocket定时器取消时,它取消绑定函数会执行一次(不一定马上执行),所以需要重新添加。
#define SESSION_TIMEOUT 3000
#define SESSION_FOREVER -1
using session_ptr = std::shared_ptr<session>;
class session_manager
{
private:
uint64_t _next_ssid;
std::mutex _mutex;
std::unordered_map<uint64_t, session_ptr> _session;
websocket_server *_server;
public:
session_manager(websocket_server *srv) : _next_ssid(1), _server(srv)
{
DBG_LOG("session管理器初始化完毕!");
}
~session_manager() { DBG_LOG("session管理器即将销毁!"); }
session_ptr create_session(uint64_t uid, ss_statu statu)
{
std::unique_lock<std::mutex> lock(_mutex);
session_ptr ssp(new session(_next_ssid));
ssp->set_statu(statu);
ssp->set_user(uid); //创建会话时需要将用户id 和用户状态都设置进去
_session.insert(std::make_pair(_next_ssid, ssp));
_next_ssid++;
return ssp;
}
void append_session(const session_ptr &ssp)
{
std::unique_lock<std::mutex> lock(_mutex);
_session.insert(std::make_pair(ssp->ssid(), ssp));
}
session_ptr get_session_by_ssid(uint64_t ssid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _session.find(ssid);
// 不存在这个ssid就返回空指针
if (it == _session.end())
{
return session_ptr();
}
return it->second;
}
void remove_session(uint64_t ssid)
{
std::unique_lock<std::mutex> lock(_mutex);
_session.erase(ssid);
}
// 定时器tp->cancel 不是立即执行的,所以在_server->set_timer执行插入
// session的定时任务重置需要 先取消再重新添加。
void set_session_expire_time(uint64_t ssid, int ms)
{
session_ptr ssp = get_session_by_ssid(ssid);
if (ssp.get() == nullptr)
{
return;
}
websocket_server::timer_ptr tp = ssp->get_timer();
if (tp.get() == nullptr && ms == SESSION_FOREVER)
{
// 1. 在session永久存在的情况下,设置永久存在
return;
}
else if (tp.get() == nullptr && ms != SESSION_FOREVER)
{
// 2. 在session永久存在的情况下,设置指定时间之后被删除的定时任务
websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,
std::bind(&session_manager::remove_session, this, ssid));
ssp->set_timer(tmp_tp);
}
else if (tp.get() != nullptr && ms == SESSION_FOREVER)
{
// 3. 在session设置了定时删除的情况下,将session设置为永久存在
// 取消定时任务
tp->cancel();
ssp->set_timer(websocket_server::timer_ptr());
_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
}
else
{
// 4. 在session设置了定时删除的情况下,将session重置删除时间。
// 先取消定时任务,再把该session对象添加到管理队列中
tp->cancel();
ssp->set_timer(websocket_server::timer_ptr());
_server->set_timer(0, std::bind(&session_manager::append_session, this, ssp));
// 重新绑定新的定时任务
websocket_server::timer_ptr tmp_tp = _server->set_timer(ms,
std::bind(&session_manager::remove_session, this, ssid));
ssp->set_timer(tmp_tp);
}
}
};
4.6 房间管理模块
用户进入游戏大厅后,存在于在线管理模块的map之中,然后用户选择进入匹配,此时为用户创建匹配队列,匹配成功创建房间,按照逻辑下来是先有匹配队列再有房间,但是匹配队列中必须调用创建房间的接口,去帮用户进入房间之中。因此先介绍房间管理模块。
第一个房间类,里面有成员房间id,房间状态,棋盘,黑棋白棋用户id,玩家数量,以及在线用户和数据库的管理句柄。它需要提供处理用户请求(聊天或者下棋)的函数,以及判断输赢,将响应返回给所有房间用户。
第二个房间管理类,分配房间的roomid,维护两个map,即为房间id到整个房间的映射map和用户id到房间id的映射map。
代码如下:
#pragma once
#include <memory>
#include "db.hpp"
#include "online.hpp"
// 房间类和房间管理类
// 房间类中有房间id,房间状态,棋盘,黑棋白棋用户id,玩家数量,以及在线用户和数据库 管理句柄
// 提供handle_request识别请求,进行下棋或者聊天,同时有 用户退出,广播等等动作
// 房间管理类 分配的roomid,两个map,提供房间的 create remove selectbyroomid等接口
typedef enum
{
GAME_START,
GAME_OVER
} room_status;
#define BOARD_ROW 15
#define BOARD_COL 15
#define CHESS_WHITE 1
#define CHESS_BLACK 2
class room
{
private:
uint64_t _room_id;
room_status _status;
int _player_count;
int _white_id;
int _black_id;
user_table *_tb_user;
std::vector<std::vector<int>> _board;
online_manager *_online_user;
public:
room()
{
}
room(uint64_t room_id, user_table *tb, online_manager *online_user)
: _room_id(room_id), _status(GAME_START), _player_count(0),
_tb_user(tb), _online_user(online_user), _board(BOARD_ROW, std::vector<int>(BOARD_COL, 0))
{
DBG_LOG("%lu 房间创建成功!!", _room_id);
}
~room()
{
DBG_LOG("%lu 房间销毁成功!!", _room_id);
}
/*添加白棋黑棋用户,获取房间id等接口*/
uint64_t id() { return _room_id; }
room_status statu() { return _status; }
int player_count() { return _player_count; }
void add_white_user(int uid)
{
_white_id = uid;
_player_count++;
}
void add_black_user(int uid)
{
_black_id = uid;
_player_count++;
}
int get_white_user() { return _white_id; }
int get_black_user() { return _black_id; }
bool five(int row, int col, int row_off, int col_off, int color)
{
// row和col是下棋位置, row_off和col_off是偏移量,也是⽅向
int count = 1;
int search_row = row + row_off;
int search_col = col + col_off;
while (search_row >= 0 && search_row < BOARD_ROW &&
search_col >= 0 && search_col < BOARD_COL &&
_board[search_row][search_col] == color)
{
// 同⾊棋⼦数量++
count++;
// 检索位置继续向后偏移
search_row += row_off;
search_col += col_off;
}
search_row = row - row_off;
search_col = col - col_off;
while (search_row >= 0 && search_row < BOARD_ROW &&
search_col >= 0 && search_col < BOARD_COL &&
_board[search_row][search_col] == color)
{
// 同⾊棋⼦数量++
count++;
// 检索位置继续向后偏移
search_row -= row_off;
search_col -= col_off;
}
return (count >= 5);
}
int check_win(int row, int col, int color)
{
// 从下棋位置的四个不同⽅向上检测是否出现了5个及以上相同颜⾊的棋⼦(横⾏,纵 列,正斜,反斜)
if (five(row, col, 0, 1, color) ||
five(row, col, 1, 0, color) ||
five(row, col, -1, 1, color) ||
five(row, col, -1, -1, color))
{
// 任意⼀个⽅向上出现了true也就是五星连珠,则设置返回值
return color == CHESS_WHITE ? _white_id : _black_id;
}
return 0;
}
/*处理下棋动作*/
Json::Value handle_chess(Json::Value &req)
{
Json::Value json_resp = req;
// 2. 判断房间中两个玩家是否都在线,任意⼀个不在线,就是另⼀⽅胜利。
int chess_row = req["row"].asInt();
int chess_col = req["col"].asInt();
uint64_t cur_uid = req["uid"].asUInt64();
if (_online_user->in_game_room(_white_id) == false)
{
json_resp["result"] = true;
json_resp["reason"] = "运⽓真好!对⽅掉线,不战⽽胜!";
json_resp["winner"] = (Json::UInt64)_black_id;
return json_resp;
}
if (_online_user->in_game_room(_black_id) == false)
{
json_resp["result"] = true;
json_resp["reason"] = "运⽓真好!对⽅掉线,不战⽽胜!";
json_resp["winner"] = (Json::UInt64)_white_id;
return json_resp;
}
// 3. 获取⾛棋位置,判断当前⾛棋是否合理(位置是否已经被占⽤)
if (_board[chess_row][chess_col] != 0)
{
json_resp["result"] = false;
json_resp["reason"] = "当前位置已经有了其他棋⼦!";
return json_resp;
}
int cur_color = cur_uid == _white_id ? CHESS_WHITE : CHESS_BLACK;
_board[chess_row][chess_col] = cur_color;
// 4. 判断是否有玩家胜利(从当前⾛棋位置开始判断是否存在五星连珠)
int winner_id = check_win(chess_row, chess_col, cur_color);
if (winner_id != 0)
{
json_resp["reason"] = "五星连珠,国服棋王,你无敌了!";
}
json_resp["result"] = true;
json_resp["winner"] = (Json::UInt64)winner_id;
return json_resp;
}
/*处理聊天动作*/
Json::Value handle_chat(const Json::Value &req)
{
Json::Value json_resp = req;
std::string chat_message = req["message"].asString();
if (chat_message.find("垃圾") != std::string::npos || chat_message.find("你干嘛") != std::string::npos)
{
json_resp["result"] = false;
json_resp["reason"] = "嘻嘻,请说喜欢你";
return json_resp;
}
json_resp["result"] = true;
return json_resp;
}
/*处理退出动作*/
void handle_exit(int uid)
{
Json::Value json_resp;
// 如果是下棋状态中退出,一方胜利
if (_status == GAME_START)
{
int winner_id = uid == _white_id ? _black_id : _white_id;
json_resp["optype"] = "put_chess";
json_resp["result"] = true;
json_resp["reason"] = "对⽅掉线,不战⽽胜!";
json_resp["room_id"] = (Json::UInt64)_room_id;
json_resp["uid"] = uid;
json_resp["row"] = -1;
json_resp["col"] = -1;
json_resp["winner"] = winner_id;
int loser_id = winner_id == _white_id ? _black_id : _white_id;
_tb_user->win(winner_id);
_tb_user->lose(loser_id);
_status = GAME_OVER;
broadcast(json_resp);
}
// 房间中玩家数量--
_player_count--;
}
/*总的请求处理函数,区分不同请求类型,调用不同函数执行对应响应,得到响应进行广播*/
void handle_request(Json::Value &req)
{
// 1. 校验房间号是否匹配
Json::Value json_resp;
uint64_t room_id = req["room_id"].asUInt64();
if (room_id != _room_id)
{
json_resp["optype"] = req["optype"].asString();
json_resp["result"] = false;
json_resp["reason"] = "房间号不匹配!";
return broadcast(json_resp);
}
// 2. 根据不同的请求类型调⽤不同的处理函数
if (req["optype"].asString() == "put_chess")
{
json_resp = handle_chess(req);
if (json_resp["winner"].asUInt64() != 0)
{
uint64_t winner_id = json_resp["winner"].asUInt64();
uint64_t loser_id = winner_id == _white_id ? _black_id : _white_id;
_tb_user->win(winner_id);
_tb_user->lose(loser_id);
_status = GAME_OVER;
}
}
else if (req["optype"].asString() == "chat")
{
json_resp = handle_chat(req);
}
else
{
json_resp["optype"] = req["optype"].asString();
json_resp["result"] = false;
json_resp["reason"] = "未知请求类型";
}
std::string body;
json_util::serialize(json_resp, body);
DBG_LOG("房间-⼴播动作: %s", body.c_str());
return broadcast(json_resp);
}
/*将指定的信息广播给房间中的所有用户,即返回响应给所有用户*/
void broadcast(const Json::Value &resp)
{
// 1. 对要响应的信息进⾏序列化,将Json::Value中的数据序列化成为json格式字符串
std::string body;
json_util::serialize(resp, body);
// 2. 获取房间中所有⽤⼾的通信连接
// 3. 发送响应信息
websocket_server::connection_ptr white_conn =
_online_user->get_conn_from_game_room(_white_id);
if (white_conn.get() != nullptr)
{
white_conn->send(body);
}
else
{
DBG_LOG("房间-⽩棋玩家连接获取失败");
}
websocket_server::connection_ptr bconn = _online_user->get_conn_from_game_room(_black_id);
if (bconn.get() != nullptr)
{
bconn->send(body);
}
else
{
DBG_LOG("房间-⿊棋玩家连接获取失败");
}
return;
}
};
using room_ptr = std::shared_ptr<room>;
class room_manager
{
private:
uint64_t _next_rid;
std::mutex _mutex;
user_table *_tb_user;
online_manager *_online_user;
std::unordered_map<uint64_t, room_ptr> _rooms; // 房间id到整个房间的映射
std::unordered_map<int, uint64_t> _users; // 用户id到房间id的映射
public:
room_manager(user_table *ut, online_manager *om)
: _next_rid(1000),
_tb_user(ut),
_online_user(om)
{
DBG_LOG("房间管理模块初始化完毕!");
}
~room_manager()
{
DBG_LOG("房间管理模块即将销毁!");
}
/*两个用户匹配成功的用户创建房间*/
room_ptr create_room(int uid1, int uid2)
{
// 1. 校验两个⽤⼾是否都还在游戏⼤厅中,只有都在才需要创建房间
if (_online_user->in_game_hall(uid1) == false)
{
DBG_LOG("⽤⼾:%d 不在⼤厅中,创建房间失败!", uid1);
return room_ptr();
}
if (_online_user->in_game_hall(uid2) == false)
{
DBG_LOG("⽤⼾:%d 不在⼤厅中,创建房间失败!", uid2);
return room_ptr();
}
// 2. 创建房间,将⽤⼾信息添加到房间中
room_ptr rp(new room(_next_rid, _tb_user, _online_user)); // 智能指针管理指针对象,传入指针进行构造
rp->add_white_user(uid1);
rp->add_black_user(uid2);
// 3. 将房间信息管理起来
_rooms.insert(std::make_pair(_next_rid, rp));
_users.insert(std::make_pair(uid1, _next_rid));
_users.insert(std::make_pair(uid2, _next_rid));
_next_rid++;
// 4. 返回房间信息
return rp;
}
/*通过房间id获取房间*/
room_ptr get_room_by_rid(uint64_t room)
{
std::unique_lock<std::mutex> lock(_mutex);
auto rit = _rooms.find(room);
if (rit == _rooms.end())
{
return room_ptr();
}
return rit->second; // 等价_rooms[room];
}
/*通过用户id获取房间*/
room_ptr get_room_by_uid(int uid)
{
std::unique_lock<std::mutex> lock(_mutex); // 加锁??
// 1. 通过⽤⼾ID获取房间ID
auto uit = _users.find(uid);
if (uit == _users.end())
{
return room_ptr();
}
uint64_t rid = uit->second;
// 2. 通过房间ID获取房间信息
auto rit = _rooms.find(rid);
if (rit == _rooms.end())
{
return room_ptr();
}
return rit->second;
}
/*通过房间id删除房间*/
void remove_room(uint64_t rid)
{
// 因为房间信息,是通过shared_ptr在_rooms中进⾏管理,因此只要将shared_ptr从_rooms中移除
// 则shared_ptr计数器==0,外界没有对房间信息进⾏操作保存的情况下就会释放
// 1. 通过房间ID,获取房间信息
room_ptr rp = get_room_by_rid(rid);
if (rp.get() == nullptr)
{
return;
}
// 2. 通过房间信息,获取房间中所有⽤⼾的ID
uint64_t uid1 = rp->get_white_user();
uint64_t uid2 = rp->get_black_user();
// 3. 移除房间管理中的⽤⼾信息
std::unique_lock<std::mutex> lock(_mutex);
_users.erase(uid1);
_users.erase(uid2);
// 4. 移除房间管理信息
_rooms.erase(rid);
// auto it = _rooms.find(room);
// if (it == _rooms.end())
// {
// return;
// }
// std::unique_lock<std::mutex> lock(_mutex);
// _users.erase(_rooms[room]->get_black_user());
// _users.erase(_rooms[room]->get_white_user());
// _rooms.erase(room);
}
/*删除房间中指定⽤⼾,如果房间中没有⽤⼾了,则销毁房间,⽤⼾连接断开时被调⽤*/
void remove_room_by_user(int user)
{
auto it = get_room_by_uid(user);
if (it.get() == nullptr)
{
return;
}
it->handle_exit(user);
if (it->player_count() == 0)
{
remove_room(it->id());
}
}
};
4.7 匹配管理模块
我们将根据用户得分维护三个匹配队列,每次用户匹配请求都在各自所属的段位里面进行匹配。
匹配队列类:包含好多用户id,mutex和条件变量cond, 还有push,wait,pop,remove等接口。
匹配管理类: 包含三个匹配队列,同时初始化三个匹配队列的 线程入口函数,线程入口队列函数不断检测队列的大小是否超过2,超过则出队列创建房间,为两个玩家进行对战操作.
#pragma once
#include "room.hpp"
#include <list>
#include <condition_variable>
// 提供匹配队列和 匹配队列的管理类
// 匹配队列中包含好多用户id,mutex和条件变量cond, 还有push,wait,pop,remove等接口
// 匹配队列的管理类 ,包含三个匹配队列,同时初始化三个匹配队列的 线程入口函数
// 线程入口队列函数不断检测队列的大小是否超过2,超过则出队列创建房间,为两个玩家进行对战操作
// 其提供add(uid)和del(uid)两个函数
// T就是int类型就是每个队列中一个个的用户id
template <class T>
class match_queue
{
private:
std::list<T> _list; // 我们使用list是因为我们需要 remove某些用户id
std::mutex _mutex;
std::condition_variable _cond; //条件变量,在该条件变量下 进行wait阻塞等待
public:
match_queue()
{
}
~match_queue()
{
}
int size()
{
std::unique_lock<std::mutex> lock(_mutex);
return _list.size();
}
bool empty()
{
std::unique_lock<std::mutex> lock(_mutex);
return _list.empty();
}
/*阻塞队列*/
void wait()
{
std::unique_lock<std::mutex> lock(_mutex);
_cond.wait(lock);
}
/*入队数据,并唤醒线程*/
void push(const T &data)
{
std::unique_lock<std::mutex> lock(_mutex);
_list.push_back(data);
_cond.notify_all();
}
/*出队数据*/
bool pop(T &data)
{
std::unique_lock<std::mutex> lock(_mutex);
if (_list.empty())
{
return false;
}
data = _list.front();
_list.pop_front();
return true;
}
void remove(T &data)
{
std::unique_lock<std::mutex> lock(_mutex);
_list.remove(data);
}
};
// 需要了解网络通信接口的格式,匹配成功时回复给两个用户什么信息
class matcher
{
private:
/*普通选⼿匹配队列*/
match_queue<int> _q_normal;
/*⾼⼿匹配队列*/
match_queue<int> _q_high;
/*⼤神匹配队列*/
match_queue<int> _q_super;
/*对应三个匹配队列的处理线程*/
std::thread _th_normal;
std::thread _th_high;
std::thread _th_super;
room_manager *_rm;
user_table *_ut;
online_manager *_om;
void handle_match(match_queue<int> &mq)
{
while(1)
{
// 队列人数小于2,则阻塞
while(mq.size()<2)
{
mq.wait();
}
// 出队两个玩家,
int id1,id2;
bool ret= mq.pop(id1);
if(ret==false)
{
continue;
}
ret= mq.pop(id2);
if(ret==false)
{
mq.push(id2);
continue;
}
//检测两个玩家是否在线
websocket_server::connection_ptr conn1=_om->get_conn_from_game_hall(id1);
if(conn1.get()==nullptr)
{
mq.push(id2);
continue;
}
websocket_server::connection_ptr conn2=_om->get_conn_from_game_hall(id2);
if(conn2.get()==nullptr)
{
mq.push(id1);
continue;
}
//为两个玩家创建房间
room_ptr rp= _rm->create_room(id1,id2);
if(rp.get()==nullptr)
{
mq.push(id1);
mq.push(id2);
continue;
}
//给两个玩家返回响应
Json::Value resp;
resp["result"]=true;
resp["optype"]="match_success";
std::string body;
json_util::serialize(resp,body);
conn1->send(body);
conn2->send(body);
}
}
// 三个线程的入口函数
void th_normal_entry()
{
handle_match(_q_normal);
}
void th_high_entry()
{
handle_match(_q_high);
}
void th_super_entry()
{
handle_match(_q_super);
}
public:
matcher(room_manager *rm, user_table *ut, online_manager *om)
: _rm(rm), _ut(ut), _om(om),_th_normal(&matcher::th_normal_entry,this),
_th_high(&matcher::th_high_entry,this),
_th_super(&matcher::th_super_entry,this)
{
DBG_LOG("游戏匹配模块初始化完毕....");
}
bool add(int id)
{
Json::Value user;
bool ret = _ut->select_by_id(id, user);
if (ret == false)
{
DBG_LOG("获取玩家:%d 信息失败!!", id);
return false;
}
int score = user["score"].asInt();
if (score < 2000)
{
_q_normal.push(id);
}
else if (score >= 2000 && score <= 3000)
{
_q_high.push(id);
}
else
{
_q_super.push(id);
}
return true;
}
bool del(int id)
{
Json::Value user;
bool ret = _ut->select_by_id(id, user);
if (ret == false)
{
DBG_LOG("获取玩家:%d 信息失败!!", id);
return false;
}
int score = user["score"].asInt();
if (score < 2000)
{
_q_normal.remove(id);
}
else if (score >= 2000 && score <= 3000)
{
_q_high.remove(id);
}
else
{
_q_super.remove(id);
}
return true;
}
~matcher()
{
}
};
4.7 服务器模块
最后服务器模块应该包含前面所有的模块,服务器类为第三方库websocketpp的 websocket服务器,因此其成员应该有这些:
std::string _web_root; // 静态资源根⽬录
websocket_server _wssrv; // websocket_server对象
user_table _ut;
online_manager _om;
room_manager _rm;
matcher _mm;
session_manager _sm;
接收到http或websocket连接请求,websocket请求是会调用相关的回调函数,我们只需要注册这些回调函数即可,同上面所写的websocket服务器框架相同
http请求,客户端发送http请求只有 刚开始访问服务器时,请求注册或者登陆页面或者根目录(静态资源请求),还有就是点击注册或登录时(功能请求),还有刚进入游戏大厅时,请求用户信息(功能请求).我们对这些请求进行判断,执行对应的函数。
注意req获得的uri是我们再http服务器所发送的/ 后面的资源路径,它是不带/的
void http_callback(websocketpp::connection_hdl hdl)
{
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
std::string method = req.get_method();
if (method == "POST" && uri == "/reg")
{
return reg(conn);
}
else if (method == "POST" && uri == "/login")
{
return login(conn);
}
else if (method == "GET" && uri == "/info")
{
return info(conn);
}
else
{
return file_handler(conn);
}
}
客户端向服务器发送websocket请求有两次,第一次是大厅获取用户信息成功之后,会发送建立大厅长连接请求,第二次是进入游戏房间页面之后,自动发送建立房间长连接请求,我们对此执行对应函数。
void wsopen_callback(websocketpp::connection_hdl hdl)
{
// websocket长连接 建立成功之后 根据uri分辨是上面的哪一种
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
if (uri == "/hall")
{
// 建⽴了游戏⼤厅的⻓连接
return wsopen_game_hall(conn);
}
else if (uri == "/room")
{
// 建⽴了游戏房间的⻓连接
return wsopen_game_room(conn);
}
}
长连接关闭逻辑也同上
void wsclose_callback(websocketpp::connection_hdl hdl)
{
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
if (uri == "/hall")
{
// 建⽴了游戏⼤厅的⻓连接
return wsclose_game_hall(conn);
}
else if (uri == "/room")
{
// 建⽴了游戏房间的⻓连接
return wsclose_game_room(conn);
}
}
用户长连接消息请求有两种,一种是大厅发出的开始匹配请求,第二种是房间发出的下棋或者聊天请求。我们可以按照如下方式得到请求消息的Json::Value对象,注意这个与http请求获取正文内容的方式有所不同。
std::string req_body = msg->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg)
{
// websocket长连接通信处理回调函数
// 1.判断是哪里的请求
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
if (uri == "/hall")
{
// 游戏⼤厅⻓连接的消息
return wsmsg_game_hall(conn, msg);
}
else if (uri == "/room")
{
return wsmsg_game_room(conn, msg);
}
}
总的服务器server.hpp
#pragma once
#include "room.hpp"
#include "online.hpp"
#include "session.hpp"
#include "matcher.hpp"
#include "db.hpp"
#include <string>
#define WWWROOT "./wwwroot/"
// 用户先进行注册(ajax请求),然后(跳转)登录(ajax请求),然后(跳转)匹配大厅(ajax请求)
// 点击开始匹配 进入匹配队列,客户端需要隔一段时间就问一下是否匹配成功
// websocket服务器可以返回http响应,con->setStatus,也可以返回websocket响应(直接send)
class gobang_server
{
private:
std::string _web_root; // 静态资源根⽬录 ./wwwroot/ ->./wwwroot/register.html
websocket_server _wssrv; // websocket_server对象
user_table _ut;
online_manager _om;
room_manager _rm;
matcher _mm;
session_manager _sm;
// 静态网页的返回
void file_handler(websocket_server::connection_ptr &conn)
{
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
std::string file_path = WWWROOT + uri;
// 如果请求路径是一个目录,则在路径后面加上login
if (file_path.back() == '/')
{
file_path += "login.html";
}
std::string body;
bool ret = file_util::read(file_path, body);
if (ret == false)
{
std::string No_path = WWWROOT;
No_path += "404.html";
file_util::read(No_path, body);
conn->set_status(websocketpp::http::status_code::not_found);
conn->set_body(body);
return;
}
// 5. 设置响应正⽂
conn->set_body(body);
conn->set_status(websocketpp::http::status_code::ok);
}
void http_resp(websocket_server::connection_ptr &conn, bool result,
websocketpp::http::status_code::value code, const std::string &reason)
{
Json::Value resp;
resp["result"] = result;
resp["reason"] = reason;
std::string body;
json_util::serialize(resp, body);
conn->set_status(code);
conn->append_header("Content-Type", "application/json");
conn->set_body(body);
return;
}
void reg(websocket_server::connection_ptr &conn)
{
// ⽤⼾注册功能请求的处理
websocketpp::http::parser::request req = conn->get_request();
// 1. 获取到请求正⽂
std::string req_body = conn->get_request_body();
// 2. 对正⽂进⾏json反序列化,得到⽤⼾名和密码
Json::Value login_info;
bool ret = json_util::unserialize(req_body, login_info);
if (ret == false)
{
DBG_LOG("反序列化注册信息失败");
return http_resp(conn, false,
websocketpp::http::status_code::bad_request, "请求的正⽂格式错误");
}
// 3. 进⾏数据库的⽤⼾新增操作
if (login_info["username"].isNull() ||
login_info["password"].isNull())
{
DBG_LOG("⽤⼾名密码不完整");
return http_resp(conn, false,
websocketpp::http::status_code::bad_request, "请输⼊⽤⼾名/密码");
}
ret = _ut.insert(login_info);
if (ret == false)
{
DBG_LOG("向数据库插⼊数据失败");
return http_resp(conn, false,
websocketpp::http::status_code::bad_request, "⽤⼾名已经被占⽤!");
}
// 如果成功了,则返回200
return http_resp(conn, true, websocketpp::http::status_code::ok, "注册⽤⼾成功");
}
/*用户登录请求处理*/
void login(websocket_server::connection_ptr &conn)
{
// 1. 获取请求正⽂,并进⾏json反序列化,得到⽤⼾名和密码
std::string req_body = conn->get_request_body();
Json::Value login_info;
bool ret = json_util::unserialize(req_body, login_info);
if (ret == false)
{
DBG_LOG("反序列化登录信息失败");
return http_resp(conn, false,
websocketpp::http::status_code::bad_request, "请求的正⽂格式错误");
}
// 2. 校验正⽂完整性,进⾏数据库的⽤⼾信息验证
if (login_info["username"].isNull() ||
login_info["password"].isNull())
{
DBG_LOG("⽤⼾名密码不完整");
return http_resp(conn, false,
websocketpp::http::status_code::bad_request, "请输⼊⽤⼾名/密码");
}
ret = _ut.login(login_info);
if (ret == false)
{
// 1. 如果验证失败,则返回400
DBG_LOG("⽤⼾名密码错误");
return http_resp(conn, false,
websocketpp::http::status_code::bad_request, "⽤⼾名密码错误");
}
// 如果创建成功,则创建一个会话,并通过set-cookie返回会话
int uid = login_info["id"].asInt();
session_ptr ssp = _sm.create_session(uid, LOGIN);
if (ssp.get() == nullptr)
{
DBG_LOG("创建会话失败");
return http_resp(conn, false,
websocketpp::http::status_code::internal_server_error, "创建会话失败");
}
_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);
// 4. 设置响应头部:Set-Cookie, 将sessionid通过cookie返回
std::string cookie_session_id = "SSID=" + std::to_string(ssp->ssid());
conn->append_header("Set-Cookie", cookie_session_id);
return http_resp(conn, true, websocketpp::http::status_code::ok,
"登录成功");
}
bool get_cookie_val(const std::string &cookie_str, const std::string &key, std::string &val)
{
// Cookie: SSID=XXX; path=/; Cookie之间以;作为间隔,
// 1. 我们对字符串进⾏分割,得到各个单个的cookie信息
std::string sep = ";";
std::vector<std::string> arr;
string_util::split(cookie_str, sep, arr);
for (auto str : arr)
{
// 2. 对单个cookie字符串,以 = 为间隔进⾏分割,得到key和val
std::vector<std::string> tmp_arr;
string_util::split(str, "=", tmp_arr);
if (tmp_arr.size() != 2)
{
continue;
}
if (tmp_arr[0] == key)
{
val = tmp_arr[1];
return true;
}
}
return false;
}
// 用户会将 Cookie=abc 返回 先找cookie,再找cookie对应的SSID,再找SSID对应的会话,再找用户信息返回,然后设置会话过期时间
void info(websocket_server::connection_ptr &conn)
{
// ⽤⼾信息获取功能请求的处理
Json::Value err_resp;
// 1. 获取请求信息中的Cookie,从Cookie中获取ssid
std::string cookie_str = conn->get_request_header("Cookie");
if (cookie_str.empty())
{
// 如果没有cookie,返回错误:没有cookie信息,让客⼾端重新登录
return http_resp(conn, true,
websocketpp::http::status_code::bad_request, "无cookie信息,请重新登录");
}
// 1.5. 从cookie中取出ssid
std::string ssid_str;
bool ret = get_cookie_val(cookie_str, "SSID", ssid_str);
if (ret == false)
{
// cookie中没有ssid,返回错误:没有ssid信息,让客⼾端重新登录
return http_resp(conn, true,
websocketpp::http::status_code::bad_request, "找不到cookie的对应ssid信息,请重新登录");
}
// 2. 在session管理中查找对应的会话信息
session_ptr ssp = _sm.get_session_by_ssid(std::stol(ssid_str));
if (ssp.get() == nullptr)
{
// 没有找到session,则认为登录已经过期,需要重新登录
return http_resp(conn, true,
websocketpp::http::status_code::bad_request, "登录过期,请重新登录");
}
// 3. 从数据库中取出⽤⼾信息,进⾏序列化发送给客⼾端
uint64_t uid = ssp->get_user();
Json::Value user_info;
ret = _ut.select_by_id(uid, user_info);
if (ret == false)
{
// 获取⽤⼾信息失败,返回错误:找不到⽤⼾信息
return http_resp(conn, true,
websocketpp::http::status_code::bad_request, "找不到⽤⼾信息,请重新登录");
}
std::string body;
json_util::serialize(user_info, body);
conn->set_body(body);
conn->append_header("Content-Type", "application/json");
conn->set_status(websocketpp::http::status_code::ok);
// 4. 刷新session的过期时间
_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);
}
// 一般的回调函数传入一个websocket服务器和连接管理句柄(必须传),我们有this可以访问服务器
// 通过 服务器和连接处理句柄 我们可以获取这个连接,这个连接被我们传入各个功能函数
void http_callback(websocketpp::connection_hdl hdl)
{
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
std::string method = req.get_method();
if (method == "POST" && uri == "/reg")
{
return reg(conn);
}
else if (method == "POST" && uri == "/login")
{
return login(conn);
}
else if (method == "GET" && uri == "/info")
{
return info(conn);
}
else
{
return file_handler(conn);
}
}
// 用户建立长连接之后,服务器使用send发送信息给客户端
void ws_resp(websocket_server::connection_ptr conn, Json::Value &resp)
{
std::string body;
json_util::serialize(resp, body);
conn->send(body);
}
// 封装从 客户端的cookie 获取session信息
session_ptr get_session_by_cookie(websocket_server::connection_ptr conn)
{
Json::Value err_resp;
// 1. 获取请求信息中的Cookie,从Cookie中获取ssid
std::string cookie_str = conn->get_request_header("Cookie");
if (cookie_str.empty())
{
err_resp["optype"] = "hall_ready";
err_resp["result"] = false;
err_resp["reason"] = "没有cookie信息,请重新登录";
ws_resp(conn, err_resp);
return session_ptr();
}
std::string value;
bool ret = get_cookie_val(cookie_str, "SSID", value);
if (ret == false)
{
err_resp["optype"] = "hall_ready";
err_resp["result"] = false;
err_resp["reason"] = "cookie中没有用户会话信息,请重新登录";
ws_resp(conn, err_resp);
return session_ptr();
}
session_ptr ssp = _sm.get_session_by_ssid(std::stol(value));
if (ssp.get() == nullptr)
{
// 没有找到session,则认为登录已经过期,需要重新登录
err_resp["optype"] = "hall_ready";
err_resp["reason"] = "没有找到session信息,需要重新登录";
err_resp["result"] = false;
ws_resp(conn, err_resp);
return session_ptr();
}
return ssp;
}
void wsopen_game_hall(websocket_server::connection_ptr conn)
{
// 游戏⼤厅⻓连接建⽴成功
Json::Value resp_json;
// 1. 登录验证--判断当前客⼾端是否已经成功登录
session_ptr ssp = get_session_by_cookie(conn);
if (ssp.get() == nullptr)
{
return;
}
// 2. 判断当前客⼾端是否是重复登录
if (_om.in_game_hall(ssp->get_user()) ||
_om.in_game_room(ssp->get_user()))
{
resp_json["optype"] = "hall_ready";
resp_json["reason"] = "玩家重复登录!";
resp_json["result"] = false;
return ws_resp(conn, resp_json);
}
// 3. 将当前客⼾端以及连接加⼊到游戏⼤厅,游戏大厅维护了用户id到连接的map
_om.enter_game_hall(ssp->get_user(), conn);
// 4. 给客⼾端响应游戏⼤厅连接建⽴成功
resp_json["optype"] = "hall_ready";
resp_json["reason"] = "游戏大厅进入成功!";
resp_json["result"] = true;
ws_resp(conn, resp_json);
// 5. 记得将session设置为永久存在
_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);
}
// 逻辑:大厅中加入匹配队列,线程创建房间并返回前端match_success, 前端离开在线用户管理模块
void wsopen_game_room(websocket_server::connection_ptr conn)
{
// 1. 获取当前客户端的session
session_ptr ssp = get_session_by_cookie(conn);
if (ssp.get() == nullptr)
{
return;
}
// 2.判断该用户是否在其它房间或者大厅中,如果是则出错
Json::Value resp_json;
if (_om.in_game_hall(ssp->get_user()) || _om.in_game_room(ssp->get_user()))
{
resp_json["optype"] = "room_ready";
resp_json["reason"] = "玩家重复登录!";
resp_json["result"] = false;
return ws_resp(conn, resp_json);
}
// 3.判断当前用户是否创建好房间
room_ptr rp = _rm.get_room_by_uid(ssp->get_user());
if (rp.get() == nullptr)
{
resp_json["optype"] = "room_ready";
resp_json["reason"] = "没有找到玩家的房间信息";
resp_json["result"] = false;
return ws_resp(conn, resp_json);
}
// 4. 将当前⽤⼾添加到在线⽤⼾管理的游戏房间中
_om.enter_game_room(ssp->get_user(), conn);
// 5. 将session重新设置为永久存在
_sm.set_session_expire_time(ssp->ssid(), SESSION_FOREVER);
// 6. 向前端回复房间准备完毕
resp_json["optype"] = "room_ready";
resp_json["result"] = true;
resp_json["room_id"] = (Json::UInt64)rp->id();
resp_json["uid"] = ssp->get_user();
resp_json["white_id"] = rp->get_white_user();
resp_json["black_id"] = rp->get_black_user();
return ws_resp(conn, resp_json);
}
// 长连接建立有两种,第一种是进入匹配队列,第二种是进入游戏房间的时候
void wsopen_callback(websocketpp::connection_hdl hdl)
{
// websocket长连接 建立成功之后 根据uri分辨是上面的哪一种
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
if (uri == "/hall")
{
// 建⽴了游戏⼤厅的⻓连接
return wsopen_game_hall(conn);
}
else if (uri == "/room")
{
// 建⽴了游戏房间的⻓连接
return wsopen_game_room(conn);
}
}
// 玩家离开掉网页之后,会发送一个关掉网页连接的请求,调用该函数
void wsclose_game_hall(websocket_server::connection_ptr conn)
{
// 游戏⼤厅⻓连接断开的处理
// 1. 登录验证--判断当前客⼾端是否已经成功登录
session_ptr ssp = get_session_by_cookie(conn);
if (ssp.get() == nullptr)
{
return;
}
// 1. 将玩家从游戏⼤厅中移除
_om.exit_game_hall(ssp->get_user());
// 2. 将session恢复⽣命周期的管理,设置定时销毁
_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);
}
void wsclose_game_room(websocket_server::connection_ptr conn)
{
// 获取会话信息,识别客⼾端
session_ptr ssp = get_session_by_cookie(conn);
if (ssp.get() == nullptr)
{
return;
}
// 1. 将玩家从在线⽤⼾管理中移除
_om.exit_game_room(ssp->get_user());
// 2. 将session回复⽣命周期的管理,设置定时销毁
_sm.set_session_expire_time(ssp->ssid(), SESSION_TIMEOUT);
// 3. 将玩家从游戏房间中移除,房间中所有⽤⼾退出了就会销毁房间
_rm.remove_room_by_user(ssp->get_user());
}
void wsclose_callback(websocketpp::connection_hdl hdl)
{
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
if (uri == "/hall")
{
// 建⽴了游戏⼤厅的⻓连接
return wsclose_game_hall(conn);
}
else if (uri == "/room")
{
// 建⽴了游戏房间的⻓连接
return wsclose_game_room(conn);
}
}
// 玩家进入大厅建立长连接,同时玩家开始/停止匹配请求时 调用该函数
void wsmsg_game_hall(websocket_server::connection_ptr conn, websocket_server::message_ptr msg)
{
Json::Value resp_json;
// 1. ⾝份验证,当前客⼾端到底是哪个玩家
session_ptr ssp = get_session_by_cookie(conn);
if (ssp.get() == nullptr)
{
return; // get_session_by_cookie内部已经返回了错误响应
}
// 2. 获取请求信息
Json::Value req_json;
std::string req_body = msg->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
if (ret == false)
{
resp_json["result"] = false;
resp_json["reason"] = "请求信息解析失败";
return ws_resp(conn, resp_json);
}
// 3.对请求进行处理
if (!req_json["optype"].isNull() && req_json["optype"].asString() == "match_start")
{
// 开始对战匹配:通过匹配模块,将⽤⼾添加到匹配队列中
_mm.add(ssp->get_user());
resp_json["optype"] = "match_start";
resp_json["result"] = true;
return ws_resp(conn, resp_json);
}
else if (!req_json["optype"].isNull() &&
req_json["optype"].asString() == "match_stop")
{
// 停⽌对战匹配:通过匹配模块,将⽤⼾从匹配队列中移除
_mm.del(ssp->get_user());
resp_json["optype"] = "match_stop";
resp_json["result"] = true;
return ws_resp(conn, resp_json);
}
resp_json["optype"] = "unknow";
resp_json["reason"] = "请求类型未知";
resp_json["result"] = false;
return ws_resp(conn, resp_json);
}
void wsmsg_game_room(websocket_server::connection_ptr conn, websocket_server::message_ptr msg)
{
// 进入房间页面,建立房间的长连接
Json::Value resp_json;
// 1. 获取当前客⼾端的session
session_ptr ssp = get_session_by_cookie(conn);
if (ssp.get() == nullptr)
{
DBG_LOG("房间-没有找到会话信息");
return;
}
// 2. 获取客⼾端房间信息
room_ptr rp = _rm.get_room_by_uid(ssp->get_user());
if (rp.get() == nullptr)
{
resp_json["optype"] = "unknow";
resp_json["reason"] = "没有找到玩家的房间信息";
resp_json["result"] = false;
DBG_LOG("房间-没有找到玩家房间信息");
return ws_resp(conn, resp_json);
}
// 3. 对消息进⾏反序列化
Json::Value req_json;
std::string req_body = msg->get_payload();
bool ret = json_util::unserialize(req_body, req_json);
if (ret == false)
{
resp_json["optype"] = "unknow";
resp_json["reason"] = "请求解析失败";
resp_json["result"] = false;
DBG_LOG("房间-反序列化请求失败");
return ws_resp(conn, resp_json);
}
DBG_LOG("房间:收到房间请求,开始处理....");
// 4. 通过房间模块进⾏消息请求的处理
return rp->handle_request(req_json);
}
void wsmsg_callback(websocketpp::connection_hdl hdl, websocket_server::message_ptr msg)
{
// websocket长连接通信处理回调函数
// 1.判断是哪里的请求
websocket_server::connection_ptr conn = _wssrv.get_con_from_hdl(hdl);
websocketpp::http::parser::request req = conn->get_request();
std::string uri = req.get_uri();
if (uri == "/hall")
{
// 游戏⼤厅⻓连接的消息
return wsmsg_game_hall(conn, msg);
}
else if (uri == "/room")
{
return wsmsg_game_room(conn, msg);
}
}
public:
/*进⾏成员初始化,以及服务器回调函数的设置*/
gobang_server(const std::string &host,
const std::string &user,
const std::string &pass,
const std::string &dbname,
uint16_t port = 3306,
const std::string &wwwroot = WWWROOT) : _web_root(wwwroot), _ut(host, user, pass, dbname, port),
_rm(&_ut, &_om), _sm(&_wssrv), _mm(&_rm, &_ut, &_om)
{
_wssrv.set_access_channels(websocketpp::log::alevel::none);
_wssrv.init_asio();
_wssrv.set_reuse_addr(true);
_wssrv.set_http_handler(std::bind(&gobang_server::http_callback,
this, std::placeholders::_1));
_wssrv.set_open_handler(std::bind(&gobang_server::wsopen_callback,
this, std::placeholders::_1));
_wssrv.set_close_handler(std::bind(&gobang_server::wsclose_callback, this,
std::placeholders::_1));
_wssrv.set_message_handler(std::bind(&gobang_server::wsmsg_callback, this,
std::placeholders::_1, std::placeholders::_2));
}
/*启动服务器*/
void start(int port)
{
_wssrv.listen(port);
_wssrv.start_accept();
_wssrv.run();
}
};
主函数gobang.cc
#include "room.hpp"
#include"session.hpp"
#define HOST "127.0.0.1"
#define PORT 3306
#define USER "root"
#define PASSWD "123456"
#define DBNAME "gobang"
#include"matcher.hpp"
#include"server.hpp"
int main()
{
// user_table ut(HOST, USER, PASSWD, DBNAME, PORT);
// match_queue<int> mq;
// online_manager om;
// room_manager rm(&ut,&om);
// matcher mt(&rm,&ut,&om);
gobang_server s(HOST, USER, PASSWD, DBNAME, PORT);
s.start(7080);
return 0;
}