如何做正规电影网站,个人网站如何搭建,做外贸要有英文网站吗,网站图片一般像素目录
项目介绍
开发环境
核心技术
项目前置知识点介绍
Websocketpp
1. WebSocket基本认识
2. WebSocket协议切换原理解析
3. WebSocket报文格式
4. Websocketpp介绍
5. 搭建一个简单WebSocket服务器 JsonCpp
1. Json格式的基本认识
2. JsonCpp介绍
3. 序列化与反序…目录
项目介绍
开发环境
核心技术
项目前置知识点介绍
Websocketpp
1. WebSocket基本认识
2. WebSocket协议切换原理解析
3. WebSocket报文格式
4. Websocketpp介绍
5. 搭建一个简单WebSocket服务器 JsonCpp
1. Json格式的基本认识
2. JsonCpp介绍
3. 序列化与反序列化接口调用demo
MySQL API
1. MySQL数据库的访问操作流程
2. API介绍
3. API接口调用demo
项目结构设计
项目模块划分
业务处理模块的子模块划分
项目流程图
用户流程图
服务器流程图
项目类实现
工具类
日志宏封装
MySQL_API封装
Json格式数据的序列化和反序列封装
字符串分割封装
文件读取封装
数据管理类
数据库设计
实现user_table类
在线用户管理类实现
游戏房间管理类
房间类实现
房间管理类实现
| 房间类和房间管理类整合 |
session管理类
session的基本认识
session类实现
session管理类实现
| session类和session管理类整合 |
玩家匹配管理类
匹配队列类实现
匹配管理类实现
| 匹配队列类和匹配管理类整合 | 服务器类
Restful风格的网络通信接口设计 静态资源请求与响应格式
动态资源请求与响应格式
客户端对服务器的请求
服务器类实现
搭建基本的服务器框架
HTTP请求处理函数
静态资源请求处理函数
用户注册请求处理函数
用户登录请求处理函数
获取用户信息请求处理函数
WebSocket长连接建立成功后的处理函数
用户登录验证函数登录成功则返回用户session
游戏大厅长连接建立成功的处理函数
游戏房间长连接建立成功的处理函数
WebSocket长连接断开前的处理函数
游戏大厅长连接断开的处理函数
游戏房间长连接断开的处理函数
WebSocket长连接通信处理函数
游戏大厅请求处理函数游戏匹配请求/停止匹配请求
游戏房间请求处理函数下棋请求/聊天请求
| 服务器类所有函数整合 |
守护进程化
项目源码 项目介绍
本项目主要实现一个网页版的五子棋对战游戏其当前版本支持以下核心功能
用户管理实现用户注册、用户登录、获取用户信息、用户游戏分数记录、用户比赛场次记录等。匹配对战实现玩家在浏览器网页端根据玩家的游戏分数进行匹配游戏对手并进行五子棋游戏对战的功能。实时聊天实现在游戏房间内对战的两个玩家可以进行实时的聊天功能。
后续还可追加以下功能
落子计时棋局房间内观战人机对战 开发环境
Linux(CentOS-7.6)Visual Studio Code/Vimg/gdbMakefile 核心技术
HTTP/WebSocketWebsocketppJsonCppMySQLC11BlockQueueHTML/CSS/JS/AJAX 项目前置知识点介绍
Websocketpp
1. WebSocket基本认识
WebSocket是从HTML5开始支持的一种网页端和服务端保持长连接的消息推送机制。
WebSocket协议相较于HTTP协议的最大不同点在于WebSocket协议支持服务端主动向客户端发送消息这是HTTP协议所不具备的
HTTP本质上就是一个“请求-响应”协议客户端和服务器的通信属于“一问一答”的形式。在HTTP协议下服务器是属于被动的一方如果客户端不给服务器发送请求服务器是无法主动给客户端发送响应的。 HTTP协议切换到WebSocket协议 短连接切换到长连接 本项目中的在线聊天功能以及实时显示落子功能都需要支持服务器主动给客户端发送响应信息所以引入Websocketpp这个库。 2. WebSocket协议切换原理解析
WebSocket协议本质上是一个基于TCP的协议。为了建立一个WebSocket连接客户端浏览器首先要向服务器发起一个HTTP请求这个请求和通常的HTTP请求不同其中包含了些附加头信息通过这些附加头信息完成握手过程并升级协议的过程。 3. WebSocket报文格式 重点关注以下字段
FINWebSocket传输数据以消息为概念单位一个消息有可能由一个或多个帧组成FIN字段为1表示末尾帧。RSV1~3保留字段只在扩展时使用若未启用扩展则应置1若收到不全为0的数据帧且未协商扩展则立即终止连接。opcode标志当前数据帧的类型。 mask表示Payload数据是否被编码若为1则必有Mask-Key用于解码Payload数据。仅客户端发送给服务端的消息需要设置。Payload length数据载荷的长度单位是字节有可能为7位、716位、764位。假设Payload length x。Mask-Key当mask为1时存在长度为4字节解码规则DECODED[i] ENCODED[i] ^ MASK[i % 4]。Payload data报文携带的载荷数据。 4. Websocketpp介绍
WebSocketpp是一个跨平台的开源BSD许可证头部专用C库它实现了RFC6455WebSocket 协议和RFC7692WebSocketCompression Extensions。它允许将WebSocket客户端和服务器功能集成到C程序中。在最常见的配置中全功能网络I/O由Asio网络库提供。 项目内常用Websocketpp常用接口
日志相关接口
void set_access_channels(log::level channels); //设置⽇志打印等级
回调函数相关接口 针对不同的事件设置不同的处理函数。 搭建完WebSocket服务器后给不同的事件设置不同的处理函数指针这些指针指向指定的函数。当服务器收到了指定数据触发了指定事件后就会通过函数指针去调用对应的事件处理函数。 此时程序员只需要编写对应的业务处理函数并设置好对应的函数指针的指向即可做到当对应事件触发时执行对应的业务函数。 void set_open_handler(open_handler h); //websocket握⼿成功回调处理函数
void set_close_handler(close_handler h); //websocket连接关闭回调处理函数
void set_message_handler(message_handler h); //websocket消息回调处理函数
void set_http_handler(http_handler h); //http请求回调处理函数通信连接相关接口
// 给客户端发送信息
void send(connection_hdl hdl, std::string payload, frame::opcode::value op);
void send(connection_hdl hdl, void* payload, size_t len, frame::opcode::value op);// 关闭连接
void close(connection_hdl hdl, close::status::value code, std::string reason);// 通过connection_hdl获取对应的connection_ptr
connection_ptr get_con_from_hdl(connection_hdl hdl);
其他服务器搭建的接口
// 初始化asio框架
void init_asio();// 是否启用地址
void set_reuse_addr(bool value);// 开始获取新连接
void start_accept();// 设置endpoint的绑定监听端⼝
void listen(uint16_t port);// 启动服务器
std::size_t run();// 设置定时任务
timer_ptr set_timer(long duration, timer_handler callback); 5. 搭建一个简单WebSocket服务器 step1实例化一个WebSocket的server对象。 step2设置日志输出等级。 step3初始化asio框架中的调度器 step4设置业务处理回调函数。 step5设置监听端口。 step6开始获取tcp连接。 step7启动服务器。 #include iostream
#include string
#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpptypedef websocketpp::serverwebsocketpp::config::asio websocketsvr_t;void wsopen_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl)
{std::cout websocket建立连接 std::endl;
}void wsclose_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl)
{std::cout websocket连接断开 std::endl;
}void wsmsg_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg)
{// 获取通信连接websocketsvr_t::connection_ptr conn wssvr-get_con_from_hdl(hdl);std::cout msg: msg-get_payload() std::endl;// 将客户端发送的信息作为响应std::string resp client say: msg-get_payload();// 将响应信息发送给客户端conn-send(resp);
}// 给客户端返回一个hello world页面
void http_callback(websocketsvr_t* wssvr, websocketpp::connection_hdl hdl)
{// 获取通信连接websocketsvr_t::connection_ptr conn wssvr-get_con_from_hdl(hdl);// 打印请求正文std::cout body: conn-get_request_body() std::endl;// 获取http请求websocketpp::http::parser::request req conn-get_request();// 打印请求方法和urlstd::cout method: req.get_method() std::endl;std::cout uri: req.get_uri() std::endl;// 设置响应正文std::string body htmlbodyh1Hello World/h1/body/html;conn-set_body(body);conn-append_header(Content-Type, text/html);conn-set_status(websocketpp::http::status_code::ok);
}int main()
{// 1. 实例化server对象// 2. 设置日志等级// 3. 初始化asio调度器设置地址重用// 4. 设置回调函数// 5. 设置监听端口// 6. 开始获取新连接// 7. 启动服务器// 1.websocketsvr_t wssvr;// 2.wssvr.set_access_channels(websocketpp::log::alevel::none); // 禁止打印所有日志// 3.wssvr.init_asio();wssvr.set_reuse_addr(true);// 4.wssvr.set_open_handler(std::bind(wsopen_callback, wssvr, std::placeholders::_1));wssvr.set_close_handler(std::bind(wsclose_callback, wssvr, std::placeholders::_1));wssvr.set_message_handler(std::bind(wsmsg_callback, wssvr, std::placeholders::_1, std::placeholders::_2));wssvr.set_http_handler(std::bind(http_callback, wssvr, std::placeholders::_1));// 5.wssvr.listen(8080);// 6.wssvr.start_accept();// 7.wssvr.run();return 0;
}
使用浏览器直接访问主机ip和port 下面写一个简单的前端客户端界面用于连接刚刚搭建的WebSocket服务器。
!DOCTYPE html
html langenheadmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0titleTest Websocket/title
/headbodyinput typetext idmessagebutton idsubmit提交/buttonscript// 创建 websocket 实例// ws://111.230.200.206:8080// 类⽐http// ws表示websocket协议// 111.230.200.206 表示服务器地址// 8080表示服务器绑定的端⼝let websocket new WebSocket(ws://111.230.200.206:8080);// 处理连接打开的回调函数websocket.onopen function () {alert(连接建⽴);}// 处理收到消息的回调函数// 控制台打印消息websocket.onmessage function (e) {alert(收到消息: e.data);}// 处理连接异常的回调函数websocket.onerror function () {alert(连接异常);}// 处理连接关闭的回调函数websocket.onclose function () {alert(连接关闭);}// 实现点击按钮后, 通过websocket实例向服务器发送请求let input document.querySelector(#message);let button document.querySelector(#submit);button.onclick function () {alert(发送消息: input.value);websocket.send(input.value);}/script
/body/html
建立连接 发送信息 关闭浏览器 实际上该项目五子棋对战游戏后续的实现就是围绕以上这四个函数来实现的 JsonCpp
1. Json格式的基本认识
Json是⼀种数据交换格式它采用完全独立于编程语言的文本格式来存储和表示数据。
例如: 我们想表示一个同学的学生信息
| C代码 |
string name nK;
int age 21;
float score[3] {88.5, 99, 58};
| Json |
{姓名: nK,年龄: 21,成绩: [88.5, 99, 58]
}Json的数据类型包括对象数组字符串数字等。
对象使用花括号 {} 括起来的表示一个对象。数组使用中括号 [] 括起来的表示一个数组。字符串使用常规双引号 括起来的表示一个字符串。数字包括整形和浮点型直接使用。 2. JsonCpp介绍
Jsoncpp库主要是用于实现Json格式数据的序列化和反序列化它实现了将Json数据对象组织成为Json格式字符串以及将Json格式字符串解析得到Json数据对象的功能。
| Json数据对象类的表示 |
class Json::Value
{Value operator(const Value other); // Value重载了[]和因此所有的赋值和获取数据都可以通过Value operator[](const std::string key); // 简单的⽅式完成 val[name] xx;Value operator[](const char* key);Value removeMember(const char* key); // 移除元素const Value operator[](ArrayIndex index) const; // val[score][0]Value append(const Value value); // 添加数组元素 val[score].append(88);ArrayIndex size() const; // 获取数组元素个数 val[score].size();bool isNull(); // ⽤于判断是否存在某个字段std::string asString() const; // 转string string name val[name].asString();const char* asCString() const; // 转char* char* name val[name].asCString();int asInt() const; // 转int int age val[age].asInt();float asFloat() const; // 转float float weight val[weight].asFloat();bool asBool() const; // 转bool bool ok val[ok].asBool();
}; | Json::Value对象特性 | Json::Value root;
root[v1] 1;std::cout root[v2] std::endl; 访问Json::Value对象中一个不存在的字段该字段以null返回。 JsonCpp库主要借助三个类以及其对应的少量成员函数完成序列化和反序列化的工作。
| 序列化接口 |
class JSON_API StreamWriter
{virtual int write(Value const root, std::ostream* sout) 0;
}class JSON_API StreamWriterBuilder : public StreamWriter::Factory
{virtual StreamWriter* newStreamWriter() const;
}
| 反序列化接口 |
class JSON_API CharReader
{virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, std::string* errs) 0;
}class JSON_API CharReaderBuilder : public CharReader::Factory
{virtual CharReader* newCharReader() const;
} 3. 序列化与反序列化接口调用demo
下面是一个简单的demo调用JsonCpp库中的序列化和反序列化接口对数据进行序列化和反序列化操作。
#include iostream
#include sstream
#include vector
#include string
#include jsoncpp/json/json.h// 序列化
std::string serialize(const Json::Value root)
{// 1. 实例化一个StreamWriterBuilder工厂类对象Json::StreamWriterBuilder swb;// 2. 通过StreamWriterBuilder工厂类对象实例化一个StreamWriter对象Json::StreamWriter* sw swb.newStreamWriter();// 3. 使用StreamWriter对象对Json::Value对象中存储的数据进行序列化std::stringstream ss;sw-write(root, ss);delete sw; // sw是new出来的记得释放return ss.str();
}// 反序列化
Json::Value unserialize(const std::string str)
{// 1. 实例化一个CharReaderBuilder工厂类对象Json::CharReaderBuilder crb;// 2. 通过CharReaderBuilder工厂类对象实例化一个CharReader对象Json::CharReader* cr crb.newCharReader();// 3. 创建一个Json::Value对象存储解析后的数据Json::Value root;// 4. 使用CharReader对象对str字符串进行Json格式的反序列化std::string err;cr-parse(str.c_str(), str.c_str() str.size(), root, err);delete cr;return root;
}// 使用JosnCpp库进行多个数据对象的序列化与反序列化
int main()
{// 将需要进行序列化的数据存储到Json::Value对象中Json::Value student;student[name] nK;student[age] 21;student[score].append(98);student[score].append(100);student[score].append(80);std::string str serialize(student);std::cout 序列化结果\n str std::endl;Json::Value studentTmp unserialize(str);// 输出Json格式的数据std::cout 反序列化结果 std::endl;std::cout name: studentTmp[name].asString() std::endl;std::cout age: studentTmp[age].asInt() std::endl;for (int i 0; i studentTmp[score].size(); i){std::cout score studentTmp[score][i].asFloat() std::endl;} return 0;
} MySQL API
1. MySQL数据库的访问操作流程 ① 客户端初始化过程 初始化MySQL操作句柄。连接MySQL服务器。设置客户端字符集选择想要操作的数据库 ② 客户端对数据库中数据的操作 执行SQL语句若SQL语句是查询语句则将查询结果保存到本地获取查询结果集中的结果条数遍历获取结果集中的每一条数据进行处理释放结果集释放MySQL操作句柄 2. API介绍
| MySQL操作句柄初始化 |
MYSQL* mysql_init(MYSQL* mysql); 参数说明 mysql为空则动态申请句柄空间进行初始化 返回值 成功返回句柄指针失败返回NULL | 连接MySQL服务器 |
MYSQL* mysql_real_connect(MYSQL* mysql, const char* host, const char* user,const char* passwd, const char* db, unsigned int port,const char* unix_socket, unsigned long client_flag); 参数说明 mysql ---- 初始化完成的句柄 host ---- 连接的MySQL服务器的地址 user ---- 连接的服务器的用户名 passwd ---- 连接的服务器的密码 db ---- 默认选择的数据库名称 port ---- 连接的服务器的端口默认是3306端口 unix_socket ---- 通信管道文件或者socket文件通常设置为NULL client_flag ---- 客户端标志位通常置为0 返回值 成功返回句柄指针失败返回NULL | 设置当前客户端的字符集 |
int mysql_set_character_set(MYSQL* mysql, const char* csname); 参数说明 mysql ---- 初始化完成的句柄 csname ---- 字符集名称通常为 utf8 返回值 成功返回0 失败返回非0 | 选择操作的数据库 |
int mysql_select_db(MYSQL* mysql, const char* db); 参数说明 mysql ---- 初始化完成的句柄 db ---- 要切换选择的数据库名称 返回值 成功返回0 失败返回非0 | 执行SQL语句 |
int mysql_query(MYSQL* mysql, const char* stmt_str); 参数说明 mysql ---- 初始化完成的句柄 stmt_str ---- 要执⾏的SQL语句 返回值 成功返回0 失败返回非0 | 保存查询结果到本地 |
MYSQL_RES* mysql_store_result(MYSQL* mysql); 参数说明 mysql ---- 初始化完成的句柄 返回值 成功返回结果集的指针失败返回NULL | 获取结果集中的行数 |
uint64_t mysql_num_rows(MYSQL_RES* result); 参数说明 result ---- 保存到本地的结果集地址 返回值 结果集中数据的条数 | 获取结果集中的列数 |
unsigned int mysql_num_fields(MYSQL_RES* result); 参数说明 result ---- 保存到本地的结果集地址 返回值 结果集中每一条数据的列数 | 遍历结果集 |
MYSQL_ROW mysql_fetch_row(MYSQL_RES* result);
这个接口会保存当前读取结果位置每次获取的都是下⼀条数据。 参数说明 result ---- 保存到本地的结果集地址 返回值 实际上是一个char**的指针将每一条数据做成了字符串指针数组 row[0] ---- 第0列 row[1] ---- 第1列...... | 释放结果集 |
void mysql_free_result(MYSQL_RES* result); 参数说明 result ---- 保存到本地的结果集地址 | 关闭数据库客户端连接销毁句柄 |
void mysql_close(MYSQL* mysql); 参数说明 mysql ---- 初始化完成的句柄 | 获取mysql接口执行错误原因 |
const char* mysql_error(MYSQL* mysql);参数说明 mysql ---- 初始化完成的句柄 返回值 返回出错原因 3. API接口调用demo
#include iostream
#include string
#include mysql/mysql.h#define HOST 127.0.0.1
#define PORT 3306
#define USER root
#define PASSWD
#define DBNAME MySQL_API_studyint main()
{// 1. 初始化MySQL句柄MYSQL* mysql mysql_init(NULL);if (mysql NULL){std::cout MySQL init failed! std::endl;return -1;}// 2. 连接服务器if (mysql_real_connect(mysql, HOST, USER, PASSWD, DBNAME, PORT, NULL, 0) NULL){std::cout connect MySQL server failed! mysql_error(mysql) std::endl;mysql_close(mysql); // 退出前断开连接释放mysql操作句柄return -1;}// 3. 设置客户端字符集if (mysql_set_character_set(mysql, utf8) ! 0){std::cout set client character failed! mysql_error(mysql) std::endl;mysql_close(mysql); // 退出前断开连接释放mysql操作句柄return -1;}// 4. 选择要操作的数据库这一步在连接MySQL服务器时函数参数中已经设置过了// 5. 执行SQL语句// const char* sql insert into student values(null, nK, 21, 99.3, 100, 89.5);;// const char* sql update student set chinesechinese 30 where sn1;;// const char* sql delete from student where sn1;;const char* sql select * from student;int ret mysql_query(mysql, sql);if (ret ! 0){std::cout mysql query failed! mysql_error(mysql) std::endl;mysql_close(mysql); // 退出前断开连接释放mysql操作句柄return -1;}// 6. 若SQL语句是查询语句则将查询结果保存到本地MYSQL_RES* res mysql_store_result(mysql);if (res NULL){mysql_close(mysql);return -1;}// 7. 获取结果集中的结果条数int row mysql_num_rows(res);int col mysql_num_fields(res);// 8. 遍历保存到本地的结果集for (int i 0; i row; i){MYSQL_ROW line mysql_fetch_row(res);for (int j 0; j col; j){std::cout line[j] \t;}std::cout std::endl;}// 9. 释放结果集mysql_free_result(res);// 10. 关闭连接释放句柄mysql_close(mysql);return 0;
} 项目结构设计
项目模块划分
该项目要实现一个网络版的在线五子棋匹配对战由以下三个大模块构成。
数据管理模块基于MySQL数据库进行用户数据的管理。前端界面模块基于JS实现前端页面注册登录游戏大厅游戏房间的动态控制以及与服务器的通信。业务处理模块搭建WebSocket服务器与客户端进行通信接收请求并进行业务处理。 业务处理模块的子模块划分
网络通信模块基于Websocketpp库实现HTTPWebSocket服务器的搭建提供网络通信功能。会话管理模块对客户端的连接进行CookieSession管理实现HTTP短连接时客户端身份识别功能。在线管理模块对进⼊游戏⼤厅与游戏房间中用户进行管理提供判断用户是否在线以及获取用户连接的功能。房间管理模块为匹配成功的用户创建对战房间提供实时的五子棋对战与聊天业务功能。用户匹配模块根据天梯分数不同进行不同层次的玩家匹配为匹配成功的玩家创建房间并加⼊房间。 项目流程图
用户流程图 服务器流程图 项目类实现
工具类
工具类主要是一些项目中会用到的边缘功能代码提前实现好了就可以在项目中用到的时候直接使用了。
工具类中的成员函数都用static修饰目的是为了不实例化具体对象也能调用到类中的成员函数 日志宏封装
主要是为了输出程序运行时的一些关键的日志信息方便程序运行出错时调试代码。
#pragma once#include iostream
#include ctime#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL INF#define LOG(level, format, ...) \do \{ \if (DEFAULT_LOG_LEVEL level) \break; \time_t t time(NULL); \struct tm *lt localtime(t); \char buf[32] {0}; \strftime(buf, 31, %H:%M:%S, lt); \fprintf(stdout, [%s %s:%d] format \n, buf, __FILE__, __LINE__, ##__VA_ARGS__); \} while (false)#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERR, format, ##__VA_ARGS__) MySQL_API封装
该模块封装了对数据库的三个操作
数据库的初始化连接SQL语句的执行MySQL操作句柄的销毁 class mysql_util
{
public:static MYSQL* mysql_create(const std::string host, const std::string user, const std::string passwd, const std::string dbname, uint32_t port 3306){// 1. 初始化MySQL句柄MYSQL *mysql mysql_init(NULL);if (mysql NULL){ELOG(MySQL init failed!);return nullptr;}// 2. 连接服务器if (mysql_real_connect(mysql, host.c_str(), user.c_str(), passwd.c_str(), dbname.c_str(), port, NULL, 0) NULL){ELOG(connect MySQL server failed! %s, mysql_error(mysql));mysql_close(mysql); // 退出前断开连接释放mysql操作句柄return nullptr;}// 3. 设置客户端字符集if (mysql_set_character_set(mysql, utf8) ! 0){ELOG(set client character failed! %s, mysql_error(mysql));mysql_close(mysql); // 退出前断开连接释放mysql操作句柄return nullptr;}return mysql;}static bool mysql_exec(MYSQL* mysql, const std::string sql){int ret mysql_query(mysql, sql.c_str());if (ret ! 0){ELOG(%s, sql.c_str());ELOG(mysql query failed! %s, mysql_error(mysql));mysql_close(mysql); // 退出前断开连接释放mysql操作句柄return false;}return true;}static void mysql_destroy(MYSQL* mysql){if (mysql ! nullptr) mysql_close(mysql);}
}; Json格式数据的序列化和反序列封装 Json序列化将Json::Value对象进行序列化得到一个Json格式的字符串。Json反序列化将Json格式的字符串反序列化得到一个Json::Value对象。 class json_util
{
public:static bool serialize(const Json::Value root, std::string str){// 1. 实例化一个StreamWriterBuilder工厂类对象Json::StreamWriterBuilder swb;// 2. 通过StreamWriterBuilder工厂类对象实例化一个StreamWriter对象std::unique_ptrJson::StreamWriter sw(swb.newStreamWriter());// 3. 使用StreamWriter对象对Json::Value对象中存储的数据进行序列化std::stringstream ss;int ret sw-write(root, ss);if (ret ! 0){ELOG(serialize failed!);return false;}str ss.str();return true;}static bool unserialize(const std::string str, Json::Value root){// 1. 实例化一个CharReaderBuilder工厂类对象Json::CharReaderBuilder crb;// 2. 通过CharReaderBuilder工厂类对象实例化一个CharReader对象std::unique_ptrJson::CharReader cr(crb.newCharReader());// 3. 使用CharReader对象对str字符串进行Json格式的反序列化std::string err;bool ret cr-parse(str.c_str(), str.c_str() str.size(), root, err);if (ret false){ELOG(unserialize failed! %s, err.c_str());return false;}return true;}
}; 字符串分割封装
该模块封装字符串分割的功能通过传入的字符串和分割字符将字符串分割为若干份放入一个输出数组中。
class string_util
{
public:static int split(const std::string src, const std::string sep, std::vectorstd::string res){size_t start 0, pos 0;while (start src.size()){pos src.find(sep, start);if (pos std::string::npos){res.push_back(src.substr(start));break;}if (pos start){start sep.size();continue;}res.push_back(src.substr(start, pos - start));start pos sep.size();}return res.size();}
}; 循环中的这个if语句就是为了处理右侧这种字符串中存在连续多个分割字符的情况。 文件读取封装
该模块对读取文件数据的操作进行封装主要对于HTML文件数据进行读取。
读取一个文件数据分为以下步骤
打开文件获取文件大小读取文件中所有数据关闭文件
class file_util
{
public:static bool read(const std::string filename, std::string body){// 1. 打开文件std::ifstream ifs(filename, std::ios::binary);if (ifs.is_open() false){ELOG(open %s failed!, filename.c_str());return false;}// 2. 获取文件大小size_t fsize 0;ifs.seekg(0, std::ios::end); // 将文件指针移动到文件末尾处fsize ifs.tellg(); // 返回当前文件指针相较于文件开头的偏移量即当前文件指针的位置相对于文件开头的字节数ifs.seekg(0, std::ios::beg); // 将文件指针恢复到文件开头处body.resize(fsize); // 将body扩容至文件中数据的大小否则后续的read()无法将文件数据存放进body中// 3. 读取文件中所有数据ifs.read(body[0], fsize);if (ifs.good() false){ELOG(read %s content failed!, filename.c_str());ifs.close();return false;}// 4. 关闭文件ifs.close();return true;}
};
注意不要忘记body.resize()这个操作若没有给body扩容则后续read()则无法将文件中的数据放入body中 数据管理类
数据管理类主要负责对于数据库中数据进行统一的增删查改操作其他模块要对数据操作都必须通过数据管理类完成。 数据库设计
设计一个用户信息表表中包括以下几个数据
用户id用户名用户登录密码游戏积分游戏总场次游戏胜场次 创建一个名为online_gobang的数据库。 在online_gobang中创建一个user表表中包含以下6个数据。 实现user_table类
#pragma once#include mutex
#include cassert#include util.hppclass user_table
{
public:user_table(const std::string host, const std::string user, const std::string password, const std::string dbname, uint32_t port 3306){_mysql mysql_util::mysql_create(host, user, password, dbname, port);assert(_mysql ! nullptr);}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql nullptr;}// 注册用户bool signup(Json::Value user){// 缺少用户名或密码注册失败if (user[username].isNull() || user[password].isNull()){ELOG(missing username or password!);return false;}#define ADD_USER insert into user values(null, %s, password(%s), 1000, 0, 0);char sql[4096] { 0 };sprintf(sql, ADD_USER, user[username].asCString(), user[password].asCString());bool ret mysql_util::mysql_exec(_mysql, sql);if (ret false){DLOG(add user failed!);return false;}return true;}// 登录验证并返回详细的用户信息bool login(Json::Value user){// 缺少用户名或密码登录失败if (user[username].isNull() || user[password].isNull()){ELOG(missing username or password!);return false;}#define VERIFY_USER select id, score, total_count, win_count from user where username%s and passwordpassword(%s);char sql[4096] { 0 };sprintf(sql, VERIFY_USER, user[username].asCString(), user[password].asCString());MYSQL_RES* res nullptr;{std::unique_lockstd::mutex lock(_mtx);bool ret mysql_util::mysql_exec(_mysql, sql);if (ret false){DLOG(select user information failed!);return false;}// 保存查询结果res mysql_store_result(_mysql);int row mysql_num_rows(res);if (res nullptr || row 0){DLOG(havent users information!);return false;}}MYSQL_ROW line mysql_fetch_row(res); // 获取一行查询结果// 将用户的详细信息保存到形参user中user[id] (Json::UInt64)std::stol(line[0]);user[score] (Json::UInt64)std::stol(line[1]);user[total_count] std::stoi(line[2]);user[win_count] std::stoi(line[3]);mysql_free_result(res);return true;}// 通过用户名获取详细的用户详细bool select_by_username(const std::string username, Json::Value user){
#define SELECT_BY_NAME select id, score, total_count, win_count from user where username%s;char sql[4096] { 0 };sprintf(sql, SELECT_BY_NAME, username.c_str());MYSQL_RES* res nullptr;{std::unique_lockstd::mutex lock(_mtx);bool ret mysql_util::mysql_exec(_mysql, sql);if (ret false){DLOG(select user information by username failed!);return false;}// 保存查询结果res mysql_store_result(_mysql);int row mysql_num_rows(res);if (res nullptr || row 0){DLOG(havent users information!);return false;}}MYSQL_ROW line mysql_fetch_row(res); // 获取一行查询结果// 将用户的详细信息保存到形参user中user[id] (Json::UInt64)std::stol(line[0]);user[username] username;user[score] (Json::UInt64)std::stol(line[1]);user[total_count] std::stoi(line[2]);user[win_count] std::stoi(line[3]);mysql_free_result(res);return true;}// 通过用户id获取详细的用户详细bool select_by_id(uint64_t id, Json::Value user){
#define SELECT_BY_ID select username, score, total_count, win_count from user where id%d;char sql[4096] { 0 };sprintf(sql, SELECT_BY_ID, id);MYSQL_RES* res nullptr;{std::unique_lockstd::mutex lock(_mtx);bool ret mysql_util::mysql_exec(_mysql, sql);if (ret false){DLOG(select user information by id failed!);return false;}// 保存查询结果res mysql_store_result(_mysql);int row mysql_num_rows(res);if (res nullptr || row 0){DLOG(havent users information!);return false;}}MYSQL_ROW line mysql_fetch_row(res); // 获取一行查询结果// 将用户的详细信息保存到形参user中user[id] (Json::UInt64)id;user[username] line[0];user[score] (Json::UInt64)std::stol(line[1]);user[total_count] std::stoi(line[2]);user[win_count] std::stoi(line[3]);mysql_free_result(res);return true;}// 玩家获胜分数30总场1胜场1bool victory(uint64_t id){// 根据id查询是否有该玩家Json::Value val;if (select_by_id(id, val) false){return false;}#define WIN_GAME update user set scorescore30, total_counttotal_count1, win_countwin_count1 where id%d;char sql[4096] { 0 };sprintf(sql, WIN_GAME, id);bool ret mysql_util::mysql_exec(_mysql, sql);if (ret false){DLOG(update winners info failed!);return false;}return true;}// 玩家失败分数-30总场1其他不变bool defeat(uint64_t id){// 根据id查询是否有该玩家Json::Value val;if (select_by_id(id, val) false){return false;}#define LOSE_GAME update user set scorescore-30, total_counttotal_count1 where id%d;char sql[4096] { 0 };sprintf(sql, LOSE_GAME, id);bool ret mysql_util::mysql_exec(_mysql, sql);if (ret false){DLOG(update losers info failed!);return false;}return true;}private:MYSQL* _mysql; // mysql操作句柄std::mutex _mtx; // 互斥锁保证数据库的访问操作的安全性
}; 该类可能在多线程中运行在数据库中执行查询语句可能出现线程安全问题。 MySQL提供的两个API接口mysql_query()和mysql_store_result()这两个接口单独使用都是线程安全的但是两个组合在一起使用就可能会出现线程安全问题
线程A在对user表进行查询操作调用完mysql_query()后还没来得及调用my_store_result()将查询结果保存到本地就被挂起切换执行线程B线程B对user表进行了其他操作增、删、改就会导致线程A的查询结果遗失再切换回线程A时继续往下执行调用mysql_store_result()就会失败。
为了解决上述可能出现的线程安全问题要给类中执行查询操作的区域加上互斥锁进行保护将上面代码改为。 在线用户管理类实现
在线用户管理类是对于当前游戏⼤厅和游戏房间中的用户进行管理主要是建立起用户与Socket连接的映射关系该类具有以下两个功能
能够让程序根据用户信息进而找到能够与用户客户端进行通信的Socket连接进而实现与客户端的通信。判断一个用户是否在线或判断用户是否已经掉线。 在线用户管理模块管理的是这两类用户a.进入游戏大厅的用户 b.进入游戏房间的用户。 当客户端建立WebSocket长连接时才能将用户添加到游戏大厅或游戏房间中。 该类管理在线用户的方法是将用户id和对应的客户端WebSocket长连接关联起来。 实现在线用户管理类的作用是
当用户A执行了一个业务操作发送实时聊天信息/下棋操作可以在在线用户管理类中找到用户A对应的WebSocket长连接将业务处理后的响应发送给游戏房间内的用户B。通过用户id找到用户的WebSocket长连接从而实现给指定用户的客户端推送信息。用户A的WebSocket长连接关闭时会自动将用户A的信息从在线用户管理类中移除即可以通过查找一个用户是否还在在线用户管理类中来判断该用户是否在线。
#pragma once#include unordered_map
#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpp
#include mutex#include logger.hpptypedef websocketpp::serverwebsocketpp::config::asio websocketsvr_t;class online_manager
{
public:// websocket连接建立的时候才会加入游戏大厅的在线用户管理void enter_game_hall(uint64_t userId, websocketsvr_t::connection_ptr conn){std::unique_lockstd::mutex lock(_mtx);_hall_users.insert({userId, conn});}// websocket连接建立的时候才会加入游戏房间的在线用户管理bool enter_game_room(uint64_t userId, websocketsvr_t::connection_ptr conn){std::unique_lockstd::mutex lock(_mtx);_room_users.insert({userId, conn});}// websocket连接断开的时候才会移除游戏大厅的在线用户管理bool exit_game_hall(uint64_t userId){std::unique_lockstd::mutex lock(_mtx);_hall_users.erase(userId);}// websocket连接断开的时候才会移除游戏房间的在线用户管理bool exit_game_room(uint64_t userId){std::unique_lockstd::mutex lock(_mtx);_room_users.erase(userId);}// 判断指定用户当前是否在游戏大厅中bool is_in_game_hall(uint64_t userId){std::unique_lockstd::mutex lock(_mtx);if (_hall_users.find(userId) _hall_users.end()) return false;return true;}// 判断指定用户当前是否在游戏房间中bool is_in_game_room(uint64_t userId){std::unique_lockstd::mutex lock(_mtx);if (_room_users.find(userId) _room_users.end()) return false;return true;}// 通过用户id在游戏大厅用户管理中获取对应用户的通信连接websocketsvr_t::connection_ptr get_conn_from_hall(uint64_t userId){std::unique_lockstd::mutex lock(_mtx);auto it _hall_users.find(userId);if (it _hall_users.end()) return websocketsvr_t::connection_ptr();return it-second;}// 通过用户id在游戏房间用户管理中获取对应用户的通信连接websocketsvr_t::connection_ptr get_conn_from_room(uint64_t userId){std::unique_lockstd::mutex lock(_mtx);auto it _room_users.find(userId);if (it _room_users.end()) return websocketsvr_t::connection_ptr();return it-second;}private:std::unordered_mapuint64_t, websocketsvr_t::connection_ptr _hall_users; // 用于建立在游戏大厅的用户的用户id与通信连接的关系std::unordered_mapuint64_t, websocketsvr_t::connection_ptr _room_users; // 用于建立在游戏房间的用户的用户id与通信连接的关系std::mutex _mtx;
};
在线用户管理类中的所有成员函数都要加锁保护因为该类可能在多线程中运行加锁保护以防止多个线程同时对成员变量_hall_users和_room_users进行操作导致线程安全问题。 游戏房间管理类
游戏房间管理类由以下两个类构成房间类和房间管理类。 房间类实现
首先要设计一个房间类该类能够实例化一个游戏房间对象游戏房间主要是对匹配成功的两个玩家建立一个小范围的关联关系。房间中任意一个用户做出的动作都会被广播给房间中的所有用户。
房间中用户可执行的动作包含以下两种
下棋聊天
#define BOARD_ROWS 15
#define BOARD_COLS 15
#define WHITE_CHESS 1
#define BLACK_CHESS 2enum room_statu
{GAME_START,GAME_OVER
};class room
{
public:room(uint64_t room_id, user_table* user_table, online_manager* user_online):_room_id(room_id), _statu(GAME_START), _player_count(0), _user_table(user_table), _user_online(user_online), _board(BOARD_ROWS, std::vectorint(BOARD_COLS, 0)){DLOG(%lu 房间创建成功, _room_id);}~room() { DLOG(%lu 房间销毁成功, _room_id); }// 获取游戏房间iduint64_t id() { return _room_id; }// 获取游戏房间状态room_statu statu() { return _statu; }// 获取游戏房间的玩家数量int player_count() { return _player_count; }// 添加白棋玩家void add_white_player(uint64_t user_id){_white_id user_id;_player_count;}// 添加黑棋玩家void add_black_player(uint64_t user_id){_black_id user_id;_player_count;}// 获取白棋玩家iduint64_t get_white_player() { return _white_id; }// 获取黑棋玩家iduint64_t get_black_player() { return _black_id; }// 处理下棋动作Json::Value handle_chess(const Json::Value req){Json::Value resp;uint64_t cur_user_id req[uid].asUInt64();int chess_row req[row].asInt();int chess_col req[col].asInt();// 1. 判断走棋位置是否合理是否越界是否被占用if (chess_row BOARD_ROWS || chess_col BOARD_COLS){resp[optype] put_chess;resp[result] false;resp[reason] 下棋位置越界;return resp;}else if (_board[chess_row][chess_col] ! 0){resp[optype] put_chess;resp[result] false;resp[reason] 下棋位置被占用;return resp;}resp req;// 2. 判断房间中两个玩家是否在线若有一个退出则判另一个获胜// 判断白棋玩家是否在线if (_user_online-is_in_game_room(_white_id) false) // 白棋玩家掉线{resp[result] true;resp[reason] 白棋玩家掉线黑棋玩家获胜;resp[winner] (Json::UInt64)_black_id;return resp;}// 判断黑棋玩家是否在线if (_user_online-is_in_game_room(_black_id) false) // 黑棋玩家掉线{resp[result] true;resp[reason] 黑棋玩家掉线白棋玩家获胜;resp[winner] (Json::UInt64)_white_id;return resp;}// 3. 下棋int cur_chess_color cur_user_id _white_id ? WHITE_CHESS : BLACK_CHESS;_board[chess_row][chess_col] cur_chess_color;// 4. 判断是否有玩家胜利(从当前走棋位置开始判断是否存在五星连珠)uint64_t winner_id check_win(chess_row, chess_col, cur_chess_color);if (winner_id ! 0) // winner_id 等于0表示没有玩家获胜{std::string reason winner_id _white_id ? 白棋五星连珠白棋获胜游戏结束 : 黑棋五星连珠黑棋获胜游戏结束;resp[result] true;resp[reason] reason;resp[winner] (Json::UInt64)winner_id;return resp;}// 没有玩家获胜正常走棋resp[result] true;resp[reason] 正常走棋游戏继续;resp[winner] (Json::UInt64)winner_id;return resp;}// 处理聊天动作Json::Value handle_chat(const Json::Value req){Json::Value resp;// 检测消息中是否包含敏感词std::string msg req[message].asString();if (have_sensitive_word(msg)){resp[optype] chat;resp[result] false;resp[reason] 消息中包含敏感词;return resp;}resp req;resp[result] true;return resp;}// 处理玩家退出房间动作void handle_exit(uint64_t user_id){Json::Value resp;// 判断玩家退出时房间状态是否处于GAME_STARTif (_statu GAME_START) // 游戏进行中玩家A退出则判断玩家B胜利{uint64_t winner_id user_id _white_id ? _black_id : _white_id;std::string reason user_id _white_id ? 白棋玩家退出游戏房间黑棋玩家获胜 : 黑棋玩家退出游戏房间白棋玩家获胜;resp[optype] put_chess;resp[result] true;resp[reason] reason;resp[room_id] (Json::UInt64)_room_id;resp[uid] (Json::UInt64)user_id;resp[row] -1; // -1 表示玩家掉线没有走棋resp[col] -1; // -1 表示玩家掉线没有走棋resp[winner] (Json::UInt64)winner_id;// 更新数据库中用户信息表的相关信息uint64_t loser_id winner_id _white_id ? _black_id : _white_id;_user_table-victory(winner_id);_user_table-defeat(loser_id);_statu GAME_OVER; // 更新游戏房间的状态broadcast(resp); // 将处理信息广播给房间的所有用户}--_player_count; // 游戏房间中的玩家数量减一}// 总的请求处理函数在函数内部区分请求类型根据不同的请求调用不同的处理函数将得到的响应进行广播void handle_request(const Json::Value req){Json::Value resp;// 判断req请求中的房间id与当前房间id是否匹配uint64_t room_id req[room_id].asUInt64();if (room_id ! _room_id){resp[optype] req[optype];resp[result] false;resp[reason] 游戏房间id不匹配;}else{// 根据req[optype]来调用不同的处理函数if (req[optype].asString() put_chess){resp handle_chess(req);if (resp[winner].asUInt64() ! 0) // 说明有玩家获胜{// 更新数据库中用户信息表的相关信息uint64_t winner_id resp[winner].asUInt64();uint64_t loser_id winner_id _white_id ? _black_id : _white_id;_user_table-victory(winner_id);_user_table-defeat(loser_id);// 更新游戏房间的状态_statu GAME_OVER;}}else if (req[optype].asString() chat){resp handle_chat(req);}else{resp[optype] req[optype];resp[result] false;resp[reason] 未知类型的请求;}}// 将处理信息广播给房间的所有用户broadcast(resp);}// 将指定的信息广播给房间中所有玩家void broadcast(const Json::Value resp){// 1. 对resp进行序列化将序列化结果保存到一个string中std::string resp_str;json_util::serialize(resp, resp_str);// 2. 获取房间中白棋玩家和黑棋玩家的通信连接并通过通信连接给玩家发送响应信息websocketsvr_t::connection_ptr white_conn _user_online-get_conn_from_room(_white_id);if (white_conn.get() ! nullptr) white_conn-send(resp_str);websocketsvr_t::connection_ptr black_conn _user_online-get_conn_from_room(_black_id);if (black_conn.get() ! nullptr) black_conn-send(resp_str);}private:bool five_chess(int row, int col, int row_offset, int col_offset, int chess_color){int count 1; // 将刚刚下的棋也包括在内// 判断方向1int serch_row row row_offset;int serch_col col col_offset;while (serch_row 0 serch_row BOARD_ROWS serch_col 0 serch_col BOARD_COLS _board[serch_row][serch_col] chess_color){count; // 同色棋子数量// 检索位置继续偏移serch_row row_offset;serch_col col_offset;}// 判断方向2serch_row row - row_offset;serch_col col - col_offset;while (serch_row 0 serch_row BOARD_ROWS serch_col 0 serch_col BOARD_COLS _board[serch_row][serch_col] chess_color){count; // 同色棋子数量// 检索位置继续偏移serch_row - row_offset;serch_col - col_offset;}return count 5;}// 返回胜利玩家的id没有则返回0uint64_t check_win(int row, int col, int chess_color){// 在下棋的位置检查四个方向是是否有五星连珠的情况(横行纵列正斜反斜)if ((five_chess(row, col, 0, 1, chess_color)) || (five_chess(row, col, 1, 0, chess_color)) || (five_chess(row, col, -1, -1, chess_color)) || (five_chess(row, col, -1, 1, chess_color))){return chess_color WHITE_CHESS ? _white_id : _black_id;}return 0;}// 敏感词检测bool have_sensitive_word(const std::string msg){for (const auto word : _sensitive_words){// 聊天消息中包含敏感词if (msg.find(word) ! std::string::npos) return true;}return false;}private:uint64_t _room_id; // 游戏房间idroom_statu _statu; // 游戏房间的状态int _player_count; // 游戏房间中玩家的数量uint64_t _white_id; // 白棋玩家的iduint64_t _black_id; // 黑棋玩家的iduser_table* _user_table; // 数据库用户信息表的操作句柄online_manager* _user_online; // 在线用户管理句柄std::vectorstd::vectorint _board; // 棋盘static std::vectorstd::string _sensitive_words; // 聊天敏感词后期可补充
};
std::vectorstd::string room::_sensitive_words {色情, 裸体, 性爱, 性交, 色情片,色情服务, 色情网站, 色情图片, 色情小说,操, 滚, 傻逼, 蠢货, 贱人, 混蛋,畜生, 白痴, 废物, 黑鬼, 黄种人, 白猪,异教徒, 邪教, 基佬, 拉拉, 同性恋, 暴力,杀人, 打架, 战斗, 殴打, 刺杀, 爆炸,恐怖袭击, 毒品, 赌博, 贩卖, 贿赂, 偷窃,抢劫};
房间类中的成员函数handle_chess()和handle_chat以及handle_request()的参数都是Json::Value对象以下列举下棋和聊天的请求格式。 | 下棋 | {optype: put_chess, // put_chess表示当前请求是下棋操作room_id: 222, // room_id 表示当前动作属于哪个房间uid: 1, // 当前的下棋操作是哪个用户发起的row: 3, // 当前下棋位置的行号col: 2 // 当前下棋位置的列号
} {optype: put_chess,result: false,reason: 走棋失败具体原因....
} {optype: put_chess,result: true,reason: 对放掉线不战而胜 / 对方/己方五星连珠战无敌/虽败犹荣,room_id: 222,uid: 1,row: 3,col: 2,winner: 0 // 0 -- 未分胜负 !0 -- 已分胜负uid是谁谁就赢了
} | 聊天 | {optype: chat,room_id: 222,uid: 1,message: 快点
} {optype: chat,result: false,reason: 发送消息失败的原因
}{optype: chat,result: true,room_id: 222,uid: 1,message: 快点
} 房间管理类实现
该类负责对所有的游戏房间进行管理类中包括以下几个对游戏房间的操作
创建游戏房间查找游戏房间通过房间id查找通过用户id查找移除房间中的玩家销毁游戏房间 typedef std::shared_ptrroom room_ptr; 由于项目中使用的room实例化对象是通过new出来的所以不希望直接对指针进行操作。为了避免对一个已经释放的room对象进行操作所以使用room_ptr来管理room对象的指针。只要shared_ptr计数器还没有减到0就不存在对空指针进行访问从而避免了内存访问错误的问题。 typedef std::shared_ptrroom room_ptr;
class room_manager
{
public:room_manager(user_table* user_table, online_manager* user_online):_next_room_id(1), _user_table(user_table), _user_online(user_online){srand(time(nullptr)); // 用于将玩家1和玩家2随机分配给白棋和黑棋DLOG(房间管理类初始化完毕);}~room_manager() { DLOG(房间管理类即将销毁); }room_ptr create_room(uint64_t user_id1, uint64_t user_id2){// 两个玩家都在游戏大厅中匹配成功后才能创建房间// 1. 判断玩家1和玩家2是否都在游戏大厅中if (_user_online-is_in_game_hall(user_id1) false) // 玩家1不在游戏大厅中{DLOG(创建游戏房间失败玩家%lu 不在游戏大厅中, user_id1);return room_ptr();}if (_user_online-is_in_game_hall(user_id2) false) // 玩家2不在游戏大厅中{DLOG(创建游戏房间失败玩家%lu 不在游戏大厅中, user_id2);return room_ptr();}std::unique_lockstd::mutex lock(_mtx); // 对下面的操作进行加锁保护// 2. 创建房间将用户信息添加到房间中room_ptr proom(new room(_next_room_id, _user_table, _user_online));// 玩家1和玩家2随机匹配白棋和黑棋uint64_t white_id rand() % 2 0 ? user_id1 : user_id2;uint64_t black_id white_id user_id1 ? user_id2 : user_id1;proom-add_white_player(white_id);proom-add_black_player(black_id);//-----------------------存疑这里要不要调用_user_online-enter_game_room()-----------------------// 3. 将房间信息管理起来_room_id_and_room.insert({proom-id(), proom});_user_id_and_room_id.insert({user_id1, proom-id()});_user_id_and_room_id.insert({user_id2, proom-id()});return proom;}room_ptr get_room_by_room_id(uint64_t room_id){std::unique_lockstd::mutex lock(_mtx); // 对下面的操作进行加锁保护auto it _room_id_and_room.find(room_id);if (it _room_id_and_room.end()) // 没找到房间号为id的房间{DLOG(不存在房间id为%d 的房间, room_id);return room_ptr();}return it-second;}room_ptr get_room_by_user_id(uint64_t user_id){std::unique_lockstd::mutex lock(_mtx); // 对下面的操作进行加锁保护auto it1 _user_id_and_room_id.find(user_id);if (it1 _user_id_and_room_id.end()){DLOG(不存在与id为%d 的玩家匹配的房间);return room_ptr();}uint64_t room_id it1-second;auto it2 _room_id_and_room.find(room_id);if (it2 _room_id_and_room.end()){DLOG(不存在房间id为%d 的房间, room_id);return room_ptr();}return it2-second;}void remove_player_in_room(uint64_t user_id){// 1. 通过玩家id获取游戏房间信息room_ptr proom get_room_by_user_id(user_id);if (proom.get() nullptr){DLOG(通过玩家id获取游戏房间信息失败);return;}// 2. 处理玩家退出房间动作proom-handle_exit(user_id);// 3. 判断游戏房间中是否还有玩家没有则销毁游戏房间if (proom-player_count() 0) destroy_room(proom-id());}void destroy_room(uint64_t room_id){// 1. 通过房间id获取游戏房间信息room_ptr proom get_room_by_room_id(room_id);if (proom.get() nullptr){DLOG(通过房间id获取游戏房间信息失败);return;}// 2. 通过游戏房间获取白棋玩家id和黑棋玩家iduint64_t white_id proom-get_white_player();uint64_t black_id proom-get_black_player();{std::unique_lockstd::mutex lock(_mtx); // 加锁保护该作用域中的操作// 3. 将白棋玩家和黑棋玩家在“玩家id和游戏房间id的关联关系”中移除_user_id_and_room_id.erase(white_id);_user_id_and_room_id.erase(black_id);// 4. 将游戏房间信息从房间管理中移除_room_id_and_room.erase(proom-id());}}private:uint64_t _next_room_id; // 房间id分配器std::mutex _mtx; // 互斥锁user_table* _user_table; // 数据库用户信息表的操作句柄online_manager* _user_online; // 在线用户管理句柄std::unordered_mapuint64_t, room_ptr _room_id_and_room; // 游戏房间id和游戏房间的关联关系std::unordered_mapuint64_t, uint64_t _user_id_and_room_id; // 玩家id和游戏房间id的关联关系
}; | get_room_by_user_id()中的死锁问题 | 在实现房间管理类中的get_room_by_user_id()时想着复用代码结果写出来bug。 以上是存在bug的代码。 由于_mtx是类的成员变量在get_room_by_user_id()中获取_mtx在get_room_by_user_id()中调用了get_room_by_room_id()而在get_room_by_room_id()中也要获取_mtx但问题是get_room_by_user_id()在调用get_room_by_room_id()时已经持有了_mtx因此在get_room_by_room_id()中再次尝试获取_mtx就会导致死锁。 上面代码块中的代码已经是解决死锁bug之后的正确代码 | 房间类和房间管理类整合 |
#pragma once#include vector
#include jsoncpp/json/json.h
#include string
#include memory
#include mutex
#include unordered_map
#include cstdlib
#include ctime#include logger.hpp
#include db.hpp
#include online.hpp
#include util.hpp#define BOARD_ROWS 15
#define BOARD_COLS 15
#define WHITE_CHESS 1
#define BLACK_CHESS 2enum room_statu
{GAME_START,GAME_OVER
};class room
{
public:room(uint64_t room_id, user_table* user_table, online_manager* user_online):_room_id(room_id), _statu(GAME_START), _player_count(0), _user_table(user_table), _user_online(user_online), _board(BOARD_ROWS, std::vectorint(BOARD_COLS, 0)){DLOG(%lu 房间创建成功, _room_id);}~room() { DLOG(%lu 房间销毁成功, _room_id); }// 获取游戏房间iduint64_t id() { return _room_id; }// 获取游戏房间状态room_statu statu() { return _statu; }// 获取游戏房间的玩家数量int player_count() { return _player_count; }// 添加白棋玩家void add_white_player(uint64_t user_id){_white_id user_id;_player_count;}// 添加黑棋玩家void add_black_player(uint64_t user_id){_black_id user_id;_player_count;}// 获取白棋玩家iduint64_t get_white_player() { return _white_id; }// 获取黑棋玩家iduint64_t get_black_player() { return _black_id; }// 处理下棋动作Json::Value handle_chess(const Json::Value req){Json::Value resp;uint64_t cur_user_id req[uid].asUInt64();int chess_row req[row].asInt();int chess_col req[col].asInt();// 1. 判断走棋位置是否合理是否越界是否被占用if (chess_row BOARD_ROWS || chess_col BOARD_COLS){resp[optype] put_chess;resp[result] false;resp[reason] 下棋位置越界;return resp;}else if (_board[chess_row][chess_col] ! 0){resp[optype] put_chess;resp[result] false;resp[reason] 下棋位置被占用;return resp;}resp req;// 2. 判断房间中两个玩家是否在线若有一个退出则判另一个获胜// 判断白棋玩家是否在线if (_user_online-is_in_game_room(_white_id) false) // 白棋玩家掉线{resp[result] true;resp[reason] 白棋玩家掉线黑棋玩家获胜;resp[winner] (Json::UInt64)_black_id;return resp;}// 判断黑棋玩家是否在线if (_user_online-is_in_game_room(_black_id) false) // 黑棋玩家掉线{resp[result] true;resp[reason] 黑棋玩家掉线白棋玩家获胜;resp[winner] (Json::UInt64)_white_id;return resp;}// 3. 下棋int cur_chess_color cur_user_id _white_id ? WHITE_CHESS : BLACK_CHESS;_board[chess_row][chess_col] cur_chess_color;// 4. 判断是否有玩家胜利(从当前走棋位置开始判断是否存在五星连珠)uint64_t winner_id check_win(chess_row, chess_col, cur_chess_color);if (winner_id ! 0) // winner_id 等于0表示没有玩家获胜{std::string reason winner_id _white_id ? 白棋五星连珠白棋获胜游戏结束 : 黑棋五星连珠黑棋获胜游戏结束;resp[result] true;resp[reason] reason;resp[winner] (Json::UInt64)winner_id;return resp;}// 没有玩家获胜正常走棋resp[result] true;resp[reason] 正常走棋游戏继续;resp[winner] (Json::UInt64)winner_id;return resp;}// 处理聊天动作Json::Value handle_chat(const Json::Value req){Json::Value resp;// 检测消息中是否包含敏感词std::string msg req[message].asString();if (have_sensitive_word(msg)){resp[optype] chat;resp[result] false;resp[reason] 消息中包含敏感词;return resp;}resp req;resp[result] true;return resp;}// 处理玩家退出房间动作void handle_exit(uint64_t user_id){Json::Value resp;// 判断玩家退出时房间状态是否处于GAME_STARTif (_statu GAME_START) // 游戏进行中玩家A退出则判断玩家B胜利{uint64_t winner_id user_id _white_id ? _black_id : _white_id;std::string reason user_id _white_id ? 白棋玩家退出游戏房间黑棋玩家获胜 : 黑棋玩家退出游戏房间白棋玩家获胜;resp[optype] put_chess;resp[result] true;resp[reason] reason;resp[room_id] (Json::UInt64)_room_id;resp[uid] (Json::UInt64)user_id;resp[row] -1; // -1 表示玩家掉线没有走棋resp[col] -1; // -1 表示玩家掉线没有走棋resp[winner] (Json::UInt64)winner_id;// 更新数据库中用户信息表的相关信息uint64_t loser_id winner_id _white_id ? _black_id : _white_id;_user_table-victory(winner_id);_user_table-defeat(loser_id);_statu GAME_OVER; // 更新游戏房间的状态broadcast(resp); // 将处理信息广播给房间的所有用户}--_player_count; // 游戏房间中的玩家数量减一}// 总的请求处理函数在函数内部区分请求类型根据不同的请求调用不同的处理函数将得到的响应进行广播void handle_request(const Json::Value req){Json::Value resp;// 判断req请求中的房间id与当前房间id是否匹配uint64_t room_id req[room_id].asUInt64();if (room_id ! _room_id){resp[optype] req[optype];resp[result] false;resp[reason] 游戏房间id不匹配;}else{// 根据req[optype]来调用不同的处理函数if (req[optype].asString() put_chess){resp handle_chess(req);if (resp[winner].asUInt64() ! 0) // 说明有玩家获胜{// 更新数据库中用户信息表的相关信息uint64_t winner_id resp[winner].asUInt64();uint64_t loser_id winner_id _white_id ? _black_id : _white_id;_user_table-victory(winner_id);_user_table-defeat(loser_id);// 更新游戏房间的状态_statu GAME_OVER;}}else if (req[optype].asString() chat){resp handle_chat(req);}else{resp[optype] req[optype];resp[result] false;resp[reason] 未知类型的请求;}}// 将处理信息广播给房间的所有用户broadcast(resp);}// 将指定的信息广播给房间中所有玩家void broadcast(const Json::Value resp){// 1. 对resp进行序列化将序列化结果保存到一个string中std::string resp_str;json_util::serialize(resp, resp_str);// 2. 获取房间中白棋玩家和黑棋玩家的通信连接并通过通信连接给玩家发送响应信息websocketsvr_t::connection_ptr white_conn _user_online-get_conn_from_room(_white_id);if (white_conn.get() ! nullptr) white_conn-send(resp_str);websocketsvr_t::connection_ptr black_conn _user_online-get_conn_from_room(_black_id);if (black_conn.get() ! nullptr) black_conn-send(resp_str);}private:bool five_chess(int row, int col, int row_offset, int col_offset, int chess_color){int count 1; // 将刚刚下的棋也包括在内// 判断方向1int serch_row row row_offset;int serch_col col col_offset;while (serch_row 0 serch_row BOARD_ROWS serch_col 0 serch_col BOARD_COLS _board[serch_row][serch_col] chess_color){count; // 同色棋子数量// 检索位置继续偏移serch_row row_offset;serch_col col_offset;}// 判断方向2serch_row row - row_offset;serch_col col - col_offset;while (serch_row 0 serch_row BOARD_ROWS serch_col 0 serch_col BOARD_COLS _board[serch_row][serch_col] chess_color){count; // 同色棋子数量// 检索位置继续偏移serch_row - row_offset;serch_col - col_offset;}return count 5;}// 返回胜利玩家的id没有则返回0uint64_t check_win(int row, int col, int chess_color){// 在下棋的位置检查四个方向是是否有五星连珠的情况(横行纵列正斜反斜)if ((five_chess(row, col, 0, 1, chess_color)) || (five_chess(row, col, 1, 0, chess_color)) || (five_chess(row, col, -1, -1, chess_color)) || (five_chess(row, col, -1, 1, chess_color))){return chess_color WHITE_CHESS ? _white_id : _black_id;}return 0;}// 敏感词检测bool have_sensitive_word(const std::string msg){for (const auto word : _sensitive_words){// 聊天消息中包含敏感词if (msg.find(word) ! std::string::npos) return true;}return false;}private:uint64_t _room_id; // 游戏房间idroom_statu _statu; // 游戏房间的状态int _player_count; // 游戏房间中玩家的数量uint64_t _white_id; // 白棋玩家的iduint64_t _black_id; // 黑棋玩家的iduser_table* _user_table; // 数据库用户信息表的操作句柄online_manager* _user_online; // 在线用户管理句柄std::vectorstd::vectorint _board; // 棋盘static std::vectorstd::string _sensitive_words; // 聊天敏感词后期可补充
};
std::vectorstd::string room::_sensitive_words {色情, 裸体, 性爱, 性交, 色情片,色情服务, 色情网站, 色情图片, 色情小说,操, 滚, 傻逼, 蠢货, 贱人, 混蛋,畜生, 白痴, 废物, 黑鬼, 黄种人, 白猪,异教徒, 邪教, 基佬, 拉拉, 同性恋, 暴力,杀人, 打架, 战斗, 殴打, 刺杀, 爆炸,恐怖袭击, 毒品, 赌博, 贩卖, 贿赂, 偷窃,抢劫};typedef std::shared_ptrroom room_ptr;
class room_manager
{
public:room_manager(user_table* user_table, online_manager* user_online):_next_room_id(1), _user_table(user_table), _user_online(user_online){srand(time(nullptr)); // 用于将玩家1和玩家2随机分配给白棋和黑棋DLOG(房间管理类初始化完毕);}~room_manager() { DLOG(房间管理类即将销毁); }room_ptr create_room(uint64_t user_id1, uint64_t user_id2){// 两个玩家都在游戏大厅中匹配成功后才能创建房间// 1. 判断玩家1和玩家2是否都在游戏大厅中if (_user_online-is_in_game_hall(user_id1) false) // 玩家1不在游戏大厅中{DLOG(创建游戏房间失败玩家%lu 不在游戏大厅中, user_id1);return room_ptr();}if (_user_online-is_in_game_hall(user_id2) false) // 玩家2不在游戏大厅中{DLOG(创建游戏房间失败玩家%lu 不在游戏大厅中, user_id2);return room_ptr();}std::unique_lockstd::mutex lock(_mtx); // 对下面的操作进行加锁保护// 2. 创建房间将用户信息添加到房间中room_ptr proom(new room(_next_room_id, _user_table, _user_online));// 玩家1和玩家2随机匹配白棋和黑棋uint64_t white_id rand() % 2 0 ? user_id1 : user_id2;uint64_t black_id white_id user_id1 ? user_id2 : user_id1;proom-add_white_player(white_id);proom-add_black_player(black_id);//-----------------------存疑这里要不要调用_user_online-enter_game_room()-----------------------// 3. 将房间信息管理起来_room_id_and_room.insert({proom-id(), proom});_user_id_and_room_id.insert({user_id1, proom-id()});_user_id_and_room_id.insert({user_id2, proom-id()});return proom;}room_ptr get_room_by_room_id(uint64_t room_id){std::unique_lockstd::mutex lock(_mtx); // 对下面的操作进行加锁保护auto it _room_id_and_room.find(room_id);if (it _room_id_and_room.end()) // 没找到房间号为id的房间{DLOG(不存在房间id为%d 的房间, room_id);return room_ptr();}return it-second;}room_ptr get_room_by_user_id(uint64_t user_id){std::unique_lockstd::mutex lock(_mtx); // 对下面的操作进行加锁保护auto it1 _user_id_and_room_id.find(user_id);if (it1 _user_id_and_room_id.end()){DLOG(不存在与id为%d 的玩家匹配的房间);return room_ptr();}uint64_t room_id it1-second;auto it2 _room_id_and_room.find(room_id);if (it2 _room_id_and_room.end()){DLOG(不存在房间id为%d 的房间, room_id);return room_ptr();}return it2-second;}void remove_player_in_room(uint64_t user_id){// 1. 通过玩家id获取游戏房间信息room_ptr proom get_room_by_user_id(user_id);if (proom.get() nullptr){DLOG(通过玩家id获取游戏房间信息失败);return;}// 2. 处理玩家退出房间动作proom-handle_exit(user_id);// 3. 判断游戏房间中是否还有玩家没有则销毁游戏房间if (proom-player_count() 0) destroy_room(proom-id());}void destroy_room(uint64_t room_id){// 1. 通过房间id获取游戏房间信息room_ptr proom get_room_by_room_id(room_id);if (proom.get() nullptr){DLOG(通过房间id获取游戏房间信息失败);return;}// 2. 通过游戏房间获取白棋玩家id和黑棋玩家iduint64_t white_id proom-get_white_player();uint64_t black_id proom-get_black_player();{std::unique_lockstd::mutex lock(_mtx); // 加锁保护该作用域中的操作// 3. 将白棋玩家和黑棋玩家在“玩家id和游戏房间id的关联关系”中移除_user_id_and_room_id.erase(white_id);_user_id_and_room_id.erase(black_id);// 4. 将游戏房间信息从房间管理中移除_room_id_and_room.erase(proom-id());}}private:uint64_t _next_room_id; // 房间id分配器std::mutex _mtx; // 互斥锁user_table* _user_table; // 数据库用户信息表的操作句柄online_manager* _user_online; // 在线用户管理句柄std::unordered_mapuint64_t, room_ptr _room_id_and_room; // 游戏房间id和游戏房间的关联关系std::unordered_mapuint64_t, uint64_t _user_id_and_room_id; // 玩家id和游戏房间id的关联关系
}; session管理类
session管理类由以下两个类构成session类和session管理类。 session的基本认识
在Web开发中HTTP协议是一种无状态短链接协议这就导致一个客户端连接到服务器上之后服务器不知道当前的连接对应的是哪个用户也不知道客户端是否登录成功这时候为客户端提供服务是不合理的。因此服务器为每个用户浏览器创建⼀个会话对象session对象注意一个浏览器独占一个session对象默认情况下。因此在需要保存用户数据时服务器程序可以把用户数据写到用户浏览器独占的session中当用户使用浏览器访问其它程序时其它程序可以从用户的session中取出该用户的数据识别该连接对应的用户并为用户提供服务。
| session工作原理 | session类实现
session类用于保存用户的状态信息。
服务器管理的每一个session都会有过期的时间超过了过期时间就会将对应的session销毁。每次客户端与服务器通信后都要刷新session的过期时间。
为了实现销毁服务器中的超时session引入Websocketpp中的定时器。 | Websocketpp中的定时器 |
Websocketpp中提供的定时器是基于boost::asio::steady_timer封装实现的。 timer_ptr set_timer(long duration, timer_handler callback); 参数说明 duration ---- 延迟多少毫秒后执行callback callback ---- 可调用对象定时器倒计时结束后调用该函数 返回值 返回一个句柄如果不再需要定时器可用该句柄取消定时器 void func(const std::string str) { std::cout str std::endl; }websocketpp::serverwebsocketpp::config::asio wssvr;
websocketpp::serverwebsocketpp::config::asio::timer_ptr tp wssvr-set_timer(5000, std::bind(func, hello nK!));
tp-cancel(); 接收set_timer()的返回值来取消定时任务会导致定时任务被立即执行。 #pragma once#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpp#include logger.hppenum session_statu
{LOGIN,NOTLOGIN
};typedef websocketpp::serverwebsocketpp::config::asio websocketsvr_t;
class session
{
public:session(uint64_t session_id):_session_id(session_id){DLOG(session%p 已创建, this);}~session() { DLOG(session%p 已销毁, this); }uint64_t get_session_id() { return _session_id; }void set_statu(session_statu statu) { _statu statu; }void set_user(uint64_t user_id) { _user_id user_id; }uint64_t get_user_id() { return _user_id; }void set_timer(const websocketsvr_t::timer_ptr tp) { _tp tp; }bool is_login() { return _statu LOGIN; }private:uint64_t _session_id; // session idsession_statu _statu; // 用户当前的状态信息登录/未登录uint64_t _user_id; // 与session对应的用户idwebsocketsvr_t::timer_ptr _tp; // 与session关联的定时器
}; session管理类实现
该类负责对所有的session进行管理类中包括以下几个对session的操作
创建session重新添加一个已存在的session通过sessin id获取session移除session 设置session过期时间 typedef std::shared_ptrsession session_ptr; 由于项目中使用的session实例化对象是通过new出来的所以不希望直接对指针进行操作。为了避免对一个已经释放的session对象进行操作所以使用session_ptr来管理session对象的指针。只要shared_ptr计数器还没有减到0就不存在对空指针进行访问从而避免了内存访问错误的问题。 对于session的生命周期需要注意以下三点
用户登录后创建session此时的session是临时的在指定时间内无通信就被删除。进入游戏大厅或游戏房间此时session的生命周期是永久的。退出游戏大厅或游戏房间该session又被重新设置为临时的在指定时间内无通信就被删除。 #define SESSION_TEMOPRARY 30000 // session的生命周期为30s
#define SESSION_PERMANENT -1 // session的生命周期为永久typedef std::shared_ptrsession session_ptr;
class session_manager
{
public:session_manager(websocketsvr_t* wssvr):_next_session_id(1), _wssvr(wssvr){DLOG(session管理类初始化完毕);}~session_manager() { DLOG(session管理类即将销毁); }// 创建sessionsession_ptr create_session(uint64_t user_id, session_statu statu){std::unique_lockstd::mutex lock(_mtx);session_ptr psession(new session(_next_session_id));psession-set_user(user_id);psession-set_statu(statu);_session_id_and_session.insert({psession-get_session_id(), psession});return psession;}// 向_session_id_and_session中重新添加一个已存在的sessionvoid append_session(const session_ptr psession){std::unique_lockstd::mutex lock(_mtx);_session_id_and_session.insert({psession-get_session_id(), psession});}// 通过sessin id获取sessionsession_ptr get_session_by_session_id(uint64_t session_id){std::unique_lockstd::mutex lock(_mtx);auto it _session_id_and_session.find(session_id);if (it _session_id_and_session.end()){DLOG(不存在session id为%d 的session, session_id);return session_ptr();}return it-second;}// 移除sessionvoid remove_session(uint64_t session_id){std::unique_lockstd::mutex lock(_mtx);_session_id_and_session.erase(session_id);}// 设置session过期时间void set_session_expiration_time(uint64_t session_id, int ms){// 依赖于websocketpp的定时器来实现对session生命周期的管理// 用户登录后创建session此时的session是临时的在指定时间内无通信就被删除// 进入游戏大厅或游戏房间这个session的生命周期是永久的// 退出游戏大厅或游戏房间该session又被重新设置为临时的在指定时间内无通信就被删除// 获取sessionsession_ptr psession get_session_by_session_id(session_id);if (psession.get() nullptr){DLOG(通过session id获取session失败);return;}websocketsvr_t::timer_ptr tp psession-get_timer();// 1. 在session的生命周期为永久的情况下将session的生命周期设置为永久if (tp.get() nullptr ms SESSION_PERMANENT){// 无需处理return;}// 2. 在session的生命周期为永久的情况下设置定时任务超过指定时间移除该sessionelse if (tp.get() nullptr ms ! SESSION_PERMANENT){websocketsvr_t::timer_ptr tmp_tp _wssvr-set_timer(ms, std::bind(session_manager::remove_session, this, session_id));psession-set_timer(tmp_tp);}// 3. 在session设置了定时移除任务的情况下将session的生命周期设置为永久else if (tp.get() ! nullptr ms SESSION_PERMANENT){// 使用cancel()提前结束当前的定时任务向session中添加一个空的定时器tp-cancel();psession-set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加该session_wssvr-set_timer(0, std::bind(session_manager::append_session, this, psession));}// 4. 在session设置了定时移除任务的情况下重置session的生命周期else if (tp.get() ! nullptr ms ! SESSION_PERMANENT){// 使用cancel()提前结束当前的定时任务向session中添加一个空的定时器tp-cancel();psession-set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加该session_wssvr-set_timer(0, std::bind(session_manager::append_session, this, psession));// 给session设置新的定时任务并将定时器更新到session中websocketsvr_t::timer_ptr tmp_tp _wssvr-set_timer(ms, std::bind(session_manager::remove_session, this, session_id));psession-set_timer(tmp_tp);}}private:uint64_t _next_session_id; // session id分配器std::mutex _mtx; // 互斥锁std::unordered_mapuint64_t, session_ptr _session_id_and_session; // session id和session的关联关系websocketsvr_t* _wssvr; // Websocket服务器对象
}; 下面讲解一下set_session_expiration_time()中的几个细节问题。 1. 一个session中与之关联的定时器如果是nullptr则说明没有给该session设置定时任务既该session的生命周期为永久。 2. 修改一个session的生命周期需要先调用cancel()取消该session原先的定时任务。 但是cancel()并不是立即执行的cancel()何时执行由操作系统决定这就导致了可能会出现先调用了_session_id_and_session.insert()然后操作系统才执行了cancel()就会出现意料之外的情况。 正确写法。 将重新添加session这个操作也设置一个定时器来执行延迟时间为0秒。 cancel()由操作系统执行定时器也由操作系统执行且cancel()在新设置的定时器前面所以可以保证canel()执行完毕后再向_session_id_and_session中重新添加session。 | session类和session管理类整合 |
#pragma once#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpp
#include mutex
#include unordered_map
#include memory#include logger.hppenum session_statu
{LOGIN,NOTLOGIN
};typedef websocketpp::serverwebsocketpp::config::asio websocketsvr_t;
class session
{
public:session(uint64_t session_id):_session_id(session_id){DLOG(session%p 已创建, this);}~session() { DLOG(session%p 已销毁, this); }uint64_t get_session_id() { return _session_id; }void set_statu(session_statu statu) { _statu statu; }void set_user(uint64_t user_id) { _user_id user_id; }uint64_t get_user_id() { return _user_id; }void set_timer(const websocketsvr_t::timer_ptr tp) { _tp tp; }websocketsvr_t::timer_ptr get_timer() { return _tp; }bool is_login() { return _statu LOGIN; }private:uint64_t _session_id; // session idsession_statu _statu; // 用户当前的状态信息登录/未登录uint64_t _user_id; // 与session对应的用户idwebsocketsvr_t::timer_ptr _tp; // 与session关联的定时器
};#define SESSION_TEMOPRARY 30000 // session的生命周期为30s
#define SESSION_PERMANENT -1 // session的生命周期为永久typedef std::shared_ptrsession session_ptr;
class session_manager
{
public:session_manager(websocketsvr_t* wssvr):_next_session_id(1), _wssvr(wssvr){DLOG(session管理类初始化完毕);}~session_manager() { DLOG(session管理类即将销毁); }// 创建sessionsession_ptr create_session(uint64_t user_id, session_statu statu){std::unique_lockstd::mutex lock(_mtx);session_ptr psession(new session(_next_session_id));psession-set_user(user_id);psession-set_statu(statu);_session_id_and_session.insert({psession-get_session_id(), psession});return psession;}// 向_session_id_and_session中重新添加一个已存在的sessionvoid append_session(const session_ptr psession){std::unique_lockstd::mutex lock(_mtx);_session_id_and_session.insert({psession-get_session_id(), psession});}// 通过sessin id获取sessionsession_ptr get_session_by_session_id(uint64_t session_id){std::unique_lockstd::mutex lock(_mtx);auto it _session_id_and_session.find(session_id);if (it _session_id_and_session.end()){DLOG(不存在session id为%d 的session, session_id);return session_ptr();}return it-second;}// 移除sessionvoid remove_session(uint64_t session_id){std::unique_lockstd::mutex lock(_mtx);_session_id_and_session.erase(session_id);}// 设置session过期时间void set_session_expiration_time(uint64_t session_id, int ms){// 依赖于websocketpp的定时器来实现对session生命周期的管理// 用户登录后创建session此时的session是临时的在指定时间内无通信就被删除// 进入游戏大厅或游戏房间这个session的生命周期是永久的// 退出游戏大厅或游戏房间该session又被重新设置为临时的在指定时间内无通信就被删除// 获取sessionsession_ptr psession get_session_by_session_id(session_id);if (psession.get() nullptr){DLOG(通过session id获取session失败);return;}websocketsvr_t::timer_ptr tp psession-get_timer();// 1. 在session的生命周期为永久的情况下将session的生命周期设置为永久if (tp.get() nullptr ms SESSION_PERMANENT){// 无需处理return;}// 2. 在session的生命周期为永久的情况下设置定时任务超过指定时间移除该sessionelse if (tp.get() nullptr ms ! SESSION_PERMANENT){websocketsvr_t::timer_ptr tmp_tp _wssvr-set_timer(ms, std::bind(session_manager::remove_session, this, session_id));psession-set_timer(tmp_tp);}// 3. 在session设置了定时移除任务的情况下将session的生命周期设置为永久else if (tp.get() ! nullptr ms SESSION_PERMANENT){// 使用cancel()提前结束当前的定时任务向session中添加一个空的定时器tp-cancel();psession-set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加该session_wssvr-set_timer(0, std::bind(session_manager::append_session, this, psession));}// 4. 在session设置了定时移除任务的情况下重置session的生命周期else if (tp.get() ! nullptr ms ! SESSION_PERMANENT){// 使用cancel()提前结束当前的定时任务向session中添加一个空的定时器tp-cancel();psession-set_timer(websocketsvr_t::timer_ptr());// 重新向_session_id_and_session添加该session_wssvr-set_timer(0, std::bind(session_manager::append_session, this, psession));// 给session设置新的定时任务并将定时器更新到session中websocketsvr_t::timer_ptr tmp_tp _wssvr-set_timer(ms, std::bind(session_manager::remove_session, this, session_id));psession-set_timer(tmp_tp);}}private:uint64_t _next_session_id; // session id分配器std::mutex _mtx; // 互斥锁std::unordered_mapuint64_t, session_ptr _session_id_and_session; // session id和session的关联关系websocketsvr_t* _wssvr; // Websocket服务器对象
}; 玩家匹配管理类
玩家匹配管理类由以下两个类构成匹配队列类和匹配管理类。
根据玩家的分数将所有的玩家分三个等级
青铜score 2000白银2000 score 3000黄金score 3000
分别为三个不同等级创建对应的匹配队列。 有玩家要进行匹配时根据玩家的分数将玩家的id加入到相应等级的匹配队列中。当匹配队列中的元素大于等于2时则说明此时该等级有两个及以上的玩家正在进行游戏匹配。从该匹配队列中出队两个玩家id为这两个玩家创建一个游戏房间将玩家加入该游戏房间后向匹配成功的玩家发送游戏匹配成功的响应至此玩家游戏匹配结束。 匹配队列类实现
使用list来模拟queue而不直接使用queue的原因是后续操作中需要删除匹配队列中的指定元素queue不支持遍历所以不使用原生的queue。
#pragma once#include list
#include mutex
#include condition_variabletemplateclass T
class match_queue
{
public:// 获取队列中元素个数int size(){std::unique_lockstd::mutex lock(_mtx);return _list.size();}// 判断队列是否为空bool empty(){std::unique_lockstd::mutex lock(_mtx);return _list.empty();}// 阻塞队列所在的线程void wait(){std::unique_lockstd::mutex lock(_mtx);_cond.wait(lock);}// 将数据入队并唤醒线程void push(const T data){std::unique_lockstd::mutex lock(_mtx);_list.push_back(data);_cond.notify_all();}// 出队并获取数据bool pop(T data){std::unique_lockstd::mutex lock(_mtx);if (_list.empty() true){return false;}data _list.front();_list.pop_front();return true;}// 移除指定数据void remove(const T data){std::unique_lockstd::mutex lock(_mtx);_list.remove(data);}private:std::listT _list; // 使用双链表来模拟队列(因为有删除指定元素的需求)std::mutex _mtx; // 互斥锁实现线程安全std::condition_variable _cond; // 条件变量主要为了阻塞消费者消费者是从队列中拿数据的当队列中元素 2时阻塞
};
在编写匹配队列类的时候我自己遇到了一个坑就是下面左边的这种写法 错误写法中会导致死锁问题。两个成员函数pop()和empty()都同时申请同一个锁资源即成员变量_mtx就会出现争夺锁资源导致死锁。 匹配管理类实现
玩家进入游戏大厅后向服务器发起Json::Value对象格式的游戏匹配请求。下面列举了开始游戏匹配和停止游戏匹配的Json::Value格式。 | 开始游戏匹配 | {optype: match_start
} // 服务器后台正确处理后回复
{optype: match_start, // 表示成功加入匹配队列result: true
}// 服务器后台处理出错后回复
{optype: match_start,result: false,reason: 具体原因....
} // 匹配成功后给客户端的响应
{optype: match_success, // 表示匹配成功result: true
} | 停止游戏匹配 | {optype: match_stop
} // 服务器后台正确处理后回复
{optype: match_stop,result: true
}// 服务器后台处理出错后回复
{optype: match_stop,result: false,reason: 具体原因....
}class match_manager
{
public:match_manager(room_manager* room_manager, user_table* user_table, online_manager* user_online):_room_manager(room_manager), _user_table(user_table), _user_online(user_online), _bronze_thread(std::thread(match_manager::bronze_thread_entrance, this)), _silver_thread(std::thread(match_manager::silver_thread_entrance, this)), _gold_thread(std::thread(match_manager::gold_thread_entrance, this)){DLOG(游戏匹配管理类初始化完毕);}~match_manager() { DLOG(游戏匹配管理类即将销毁); }// 将玩家添加到匹配队列中bool add(uint64_t user_id){// 根据玩家的分数将玩家添加到不同的匹配队列中// 根据玩家id获取玩家信息读取玩家的分数Json::Value user;bool ret _user_table-select_by_id(user_id, user);if (ret false){DLOG(获取玩家%d 的信息失败, user_id);return false;}// 根据分数将玩家加入到对应的匹配队列中int score user[score].asInt();if (score 2000){_bronze_queue.push(user_id);}else if (score 2000 score 3000){_silver_queue.push(user_id);}else if (score 3000){_gold_queue.push(user_id);}return true;}// 将玩家从匹配队列中移除bool del(uint64_t user_id){Json::Value user;bool ret _user_table-select_by_id(user_id, user);if (ret false){DLOG(获取玩家%d 的信息失败, user_id);return false;}int score user[score].asInt();if (score 2000){_bronze_queue.remove(user_id);}else if (score 2000 score 3000){_silver_queue.remove(user_id);}else if (score 3000){_gold_queue.remove(user_id);}return true;}private:void match_handler(match_queueuint64_t q){while (true) // 匹配线程需要一直处于运行状态{while (q.size() 2) // 匹配队列中玩家不足两人阻塞线程{q.wait();}// 从匹配队列中出队两个玩家uint64_t user_id1 0, user_id2 0;bool ret q.pop(user_id1);if (ret false) continue; // 第一个玩家出队失败跳过后续代码重新执行上述代码ret q.pop(user_id2);if (ret false) // 代码执行至此说明第一个玩家已出队第二个玩家出队失败要将出队的第一个玩家重新添加到匹配队列中{this-add(user_id1);continue;}// 两个玩家都出队成功则检验两个玩家是否都在游戏大厅若玩家A掉线则将玩家B重新添加到匹配队列中websocketsvr_t::connection_ptr conn1 _user_online-get_conn_from_hall(user_id1);if (conn1.get() nullptr){this-add(user_id2);continue;}websocketsvr_t::connection_ptr conn2 _user_online-get_conn_from_hall(user_id2);if (conn2.get() nullptr){this-add(user_id1);continue;}// 判断完两个玩家都在线后给两个玩家创建游戏房间room_ptr proom _room_manager-create_room(user_id1, user_id2);if (proom.get() nullptr) // 创建游戏房间失败{// 将两个玩家重新放回匹配队列中this-add(user_id1);this-add(user_id2);continue;}// 给游戏房间内的两个玩家返回响应Json::Value resp;resp[optype] match_success;resp[result] true;std::string resp_str;json_util::serialize(resp, resp_str);conn1-send(resp_str);conn2-send(resp_str);}}// 青铜匹配队列处理线程void bronze_thread_entrance() { match_handler(_bronze_queue); }// 白银匹配队列处理线程void silver_thread_entrance() { match_handler(_silver_queue); }// 黄金匹配队列处理线程void gold_thread_entrance() { match_handler(_gold_queue); }private:match_queueuint64_t _bronze_queue; // 青铜匹配队列match_queueuint64_t _silver_queue; // 白银匹配队列match_queueuint64_t _gold_queue; // 黄金匹配队列std::thread _bronze_thread; // 青铜线程用来管理青铜匹配队列的操作std::thread _silver_thread; // 白银线程用来管理白银匹配队列的操作std::thread _gold_thread; // 黄金线程用来管理黄金匹配队列的操作room_manager* _room_manager; // 游戏房间管理句柄user_table* _user_table; // 用户数据管理句柄online_manager* _user_online; // 在线用户管理句柄
}; 匹配管理类中的私有成员函数match_handler()有以下几点值得留意 ①match_handler()是三个匹配线程执行的函数该函数要处于一个死循环的状态持续不断的检测是否有玩家进入匹配队列继续游戏匹配。 ②匹配队列中元素个数小于2则说明匹配多列中的玩家不够两人则需要将线程阻塞在当前位置。 ③判断匹配队列中的元素使用了一个while循环这是因为每次向匹配队列中添加一个玩家调用push()时都会唤醒线程即线程从③的位置继续执行回到while循环判断若此时匹配队列元素大于等于2则向下执行代码若匹配队列中小于2则继续阻塞线程等待下次向匹配队列添加玩家时才再次唤醒线程。 | 匹配队列类和匹配管理类整合 |
#pragma once#include list
#include mutex
#include condition_variable
#include thread
#include string#include logger.hpp
#include room.hpp
#include db.hpp
#include online.hpp
#include util.hpptemplateclass T
class match_queue
{
public:// 获取队列中元素个数int size(){std::unique_lockstd::mutex lock(_mtx);return _list.size();}// 判断队列是否为空bool empty(){std::unique_lockstd::mutex lock(_mtx);return _list.empty();}// 阻塞队列所在的线程void wait(){std::unique_lockstd::mutex lock(_mtx);_cond.wait(lock);}// 将数据入队并唤醒线程void push(const T data){std::unique_lockstd::mutex lock(_mtx);_list.push_back(data);_cond.notify_all();}// 出队并获取数据bool pop(T data){std::unique_lockstd::mutex lock(_mtx);if (_list.empty() true){return false;}data _list.front();_list.pop_front();return true;}// 移除指定数据void remove(const T data){std::unique_lockstd::mutex lock(_mtx);_list.remove(data);}private:std::listT _list; // 使用双链表来模拟队列(因为有删除指定元素的需求)std::mutex _mtx; // 互斥锁实现线程安全std::condition_variable _cond; // 条件变量主要为了阻塞消费者消费者是从队列中拿数据的当队列中元素 2时阻塞
};class match_manager
{
public:match_manager(room_manager* room_manager, user_table* user_table, online_manager* user_online):_room_manager(room_manager), _user_table(user_table), _user_online(user_online), _bronze_thread(std::thread(match_manager::bronze_thread_entrance, this)), _silver_thread(std::thread(match_manager::silver_thread_entrance, this)), _gold_thread(std::thread(match_manager::gold_thread_entrance, this)){DLOG(游戏匹配管理类初始化完毕);}~match_manager() { DLOG(游戏匹配管理类即将销毁); }// 将玩家添加到匹配队列中bool add(uint64_t user_id){// 根据玩家的分数将玩家添加到不同的匹配队列中// 根据玩家id获取玩家信息读取玩家的分数Json::Value user;bool ret _user_table-select_by_id(user_id, user);if (ret false){DLOG(获取玩家%d 的信息失败, user_id);return false;}// 根据分数将玩家加入到对应的匹配队列中int score user[score].asInt();if (score 2000){_bronze_queue.push(user_id);}else if (score 2000 score 3000){_silver_queue.push(user_id);}else if (score 3000){_gold_queue.push(user_id);}return true;}// 将玩家从匹配队列中移除bool del(uint64_t user_id){Json::Value user;bool ret _user_table-select_by_id(user_id, user);if (ret false){DLOG(获取玩家%d 的信息失败, user_id);return false;}int score user[score].asInt();if (score 2000){_bronze_queue.remove(user_id);}else if (score 2000 score 3000){_silver_queue.remove(user_id);}else if (score 3000){_gold_queue.remove(user_id);}return true;}private:void match_handler(match_queueuint64_t q){while (true) // 匹配线程需要一直处于运行状态{while (q.size() 2) // 匹配队列中玩家不足两人阻塞线程{q.wait();}// 从匹配队列中出队两个玩家uint64_t user_id1 0, user_id2 0;bool ret q.pop(user_id1);if (ret false) continue; // 第一个玩家出队失败跳过后续代码重新执行上述代码ret q.pop(user_id2);if (ret false) // 代码执行至此说明第一个玩家已出队第二个玩家出队失败要将出队的第一个玩家重新添加到匹配队列中{this-add(user_id1);continue;}// 两个玩家都出队成功则检验两个玩家是否都在游戏大厅若玩家A掉线则将玩家B重新添加到匹配队列中websocketsvr_t::connection_ptr conn1 _user_online-get_conn_from_hall(user_id1);if (conn1.get() nullptr){this-add(user_id2);continue;}websocketsvr_t::connection_ptr conn2 _user_online-get_conn_from_hall(user_id2);if (conn2.get() nullptr){this-add(user_id1);continue;}// 判断完两个玩家都在线后给两个玩家创建游戏房间room_ptr proom _room_manager-create_room(user_id1, user_id2);if (proom.get() nullptr) // 创建游戏房间失败{// 将两个玩家重新放回匹配队列中this-add(user_id1);this-add(user_id2);continue;}// 给游戏房间内的两个玩家返回响应Json::Value resp;resp[optype] match_success;resp[result] true;std::string resp_str;json_util::serialize(resp, resp_str);conn1-send(resp_str);conn2-send(resp_str);}}// 青铜匹配队列处理线程void bronze_thread_entrance() { match_handler(_bronze_queue); }// 白银匹配队列处理线程void silver_thread_entrance() { match_handler(_silver_queue); }// 黄金匹配队列处理线程void gold_thread_entrance() { match_handler(_gold_queue); }private:match_queueuint64_t _bronze_queue; // 青铜匹配队列match_queueuint64_t _silver_queue; // 白银匹配队列match_queueuint64_t _gold_queue; // 黄金匹配队列std::thread _bronze_thread; // 青铜线程用来管理青铜匹配队列的操作std::thread _silver_thread; // 白银线程用来管理白银匹配队列的操作std::thread _gold_thread; // 黄金线程用来管理黄金匹配队列的操作room_manager* _room_manager; // 游戏房间管理句柄user_table* _user_table; // 用户数据管理句柄online_manager* _user_online; // 在线用户管理句柄
}; 服务器类
服务器类是对先前所实现的所有类的一个整合封装对外提供搭建五子棋对战游戏服务器的接口的类。通过实例化出一个服务器对象即可简便的完成一个五子棋游戏服务器的搭建。
该项目只对后端C代码进行讲解和梳理项目中涉及到的前端资源及代码在这里获取前端资源及代码
在与服务器类文件同级目录下创建一个名为wwwroot的文件夹将前端资源及代码放到wwwroot中即可 Restful风格的网络通信接口设计
客户端和服务器之间存在多种请求和响应客户端发送的请求需要统一一种格式同理服务器给客户端发送的响应也需要统一格式。
下面就来设计该项目中统一的网络通信数据格式。 Restful风格依托于HTTP协议来实现。
正文部分采用XML或Json格式进行正文数据的格式组织。 GET - 获取资源 POST - 新增资源 PUT - 更新资源 DELETE - 删除资源 静态资源请求与响应格式
静态资源页面在服务器上就是一个html/css/js文件。服务器对于静态资源请求的处理其实就是读取文件中的内容再发送给客户端。 | 注册页面请求与响应 | 请求
GET /register.html HTTP/1.1响应
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(响应正文)register.html文件的内容数据 | 登录页面请求与响应 | 请求
GET /login.html HTTP/1.1响应
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(响应正文)login.html文件的内容数据 | 游戏大厅页面请求与响应 | 请求
GET /game_hall.html HTTP/1.1响应
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(响应正文)game_hall.html文件的内容数据 | 游戏房间页面请求与响应 | 请求
GET /game_room.html HTTP/1.1响应
HTTP/1.1 200 OK
Content-Length: xxx
Content-Type: text/html
(空行)
(响应正文)game_room.html文件的内容数据 动态资源请求与响应格式 | 用户注册请求与响应 | POST /signup HTTP/1.1
Content-Type: application/json
Content-Length: 32{username:xiaobai, password:123123} // 成功时的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15{result:true} // 失败时的响应
HTTP/1.1 400 Bad Request
Content-Type: application/json
Content-Length: 43{result:false, reason:用户名已经被占用} | 用户登录请求与响应 | POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 32{username:xiaobai, password:123123} // 成功时的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 15{result:true} // 失败时的响应
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 43{result:false, reason:用户还未登录} | 获取客户端信息请求与响应 | GET /userinfo HTTP/1.1
Content-Type: application/json
Content-Length: 0 // 成功时的响应
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 58{id:1, username:xiaobai, score:1000, total_count:4, win_count:2} // 失败时的响应
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Content-Length: 43{result:false, reason:用户还未登录} | WebSocket长连接协议切换请求与响应进入游戏大厅| GET /match HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
...... // 响应
HTTP/1.1 101 Switching
...... | WebSocket长连接协议切换请求与响应进入游戏房间| GET /game HTTP/1.1
Connection: Upgrade
Upgrade: WebSocket
...... // 响应
HTTP/1.1 101 Switching
...... 上面的请求与响应的格式是HTTP协议的格式。 WebSocket协议切换成功后后续的请求与响应格式改为Json格式。 | WebSocket连接建立成功后的响应 | 表示已经成功进入游戏大厅 {optype: hall_ready,result: true,uid: 1
} | WebSocket连接建立成功后的响应 | 表示已经成功进入游戏房间 {optype: room_ready,result: true,room_id: 222, // 游戏房间idself_id: 1, // 当前玩家idwhite_id: 1, // 白棋玩家idblack_id: 2 // 黑棋玩家id
} | 游戏匹配请求与响应 | {optype: match_start
} // 服务器后台正确处理后回复
{optype: match_start, // 表示成功加入匹配队列result: true
}// 服务器后台处理出错后回复
{optype: match_start,result: false,reason: 具体原因....
} // 匹配成功后给客户端的回复
{optype: match_success, // 表示匹配成功result: true
}| 停止匹配请求与响应 | {optype: match_stop
}// 服务器后台正确处理后回复
{optype: match_stop,result: true
}// 服务器后台处理出错后回复
{optype: match_stop,result: false,reason: 具体原因....
} | 下棋请求与响应 | {optype: put_chess, // put_chess表示当前请求是下棋操作room_id: 222, // room_id 表示当前动作属于哪个房间uid: 1, // 当前的下棋操作是哪个用户发起的row: 3, // 当前下棋位置的行号col: 2 // 当前下棋位置的列号
} {optype: put_chess,result: false,reason: 走棋失败具体原因....
} {optype: put_chess,result: true,reason: 对放掉线不战而胜 / 对方/己方五星连珠战无敌/虽败犹荣,room_id: 222,uid: 1,row: 3,col: 2,winner: 0 // 0 -- 未分胜负 !0 -- 已分胜负uid是谁谁就赢了
} | 聊天请求与响应 | {optype: chat,room_id: 222,uid: 1,message: 快点
} {optype: chat,result: false,reason: 发送消息失败的原因
}{optype: chat,result: true,room_id: 222,uid: 1,message: 快点
} 客户端对服务器的请求
在搭建服务器之前首先要清楚客户端对服务器有哪些请求以下列举了用户从开始注册到游戏结束对服务器的所有请求。
HTTP请求
客户端从服务器获取一个注册页面客户端给服务器发送一个用户注册请求提交用户名和密码客户端从服务器获取一个登录页面客户端给服务器发送一个用户登录请求提交用户名和密码客户端从服务器获取一个游戏大厅页面客户端给服务器发送一个获取个人信息的请求展示个人信息
WebSocket请求
客户端给服务器发送一个切换WebSocket协议通信的请求建立游戏大厅长连接客户端给服务器发送一个游戏匹配请求客户端给服务器发送一个停止匹配请求对战匹配成功客户端从服务器获取一个游戏房间页面客户端给服务器发送一个切换WebSocket协议通信的请求建立游戏房间长连接客户端给服务器发送一个下棋请求客户端给服务器发送一个聊天请求游戏结果返回游戏大厅客户端给服务器发送一个获取游戏大厅页面的请求 服务器类实现
服务器类是对前面实现的所有类进行整合并使用的类是该项目中的重中之重
通过服务器类可以实例化出一个服务器对象调用服务器的接口函数即可将游戏服务器运行起来。
由于服务器类中的成员函数繁多下面将会一一实现服务器类中的成员函数。 搭建基本的服务器框架
#pragma once#include string
#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpp#include db.hpp
#include online.hpp
#include room.hpp
#include matcher.hpp
#include session.hpp#define WWWROOT ./wwwroot/class gobang_server
{
public:// 进行成员变量初始化以及设置服务器回调函数gobang_server(const std::string host,const std::string user,const std::string password,const std::string dbname,uint32_t port 3306,const std::string wwwroot WWWROOT):_web_root(wwwroot), _user_table(host, user, password, dbname, port), _room_manager(_user_table, _user_online), _match_manager(_room_manager, _user_table, _user_online), _session_manager(_wssvr){_wssvr.set_access_channels(websocketpp::log::alevel::none);_wssvr.init_asio();_wssvr.set_reuse_addr(true);_wssvr.set_open_handler(std::bind(gobang_server::wsopen_callback, this, std::placeholders::_1));_wssvr.set_close_handler(std::bind(gobang_server::wsclose_callback, this, std::placeholders::_1));_wssvr.set_message_handler(std::bind(gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));_wssvr.set_http_handler(std::bind(gobang_server::http_callback, this, std::placeholders::_1));}// 启动服务器void start(uint16_t port){_wssvr.listen(port);_wssvr.start_accept();_wssvr.run();}private:void wsopen_callback(websocketpp::connection_hdl hdl);void wsclose_callback(websocketpp::connection_hdl hdl);void wsmsg_callback(websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg);void http_callback(websocketpp::connection_hdl hdl);private:std::string _web_root; // 静态资源根目录./wwwroot/ 请求的url为 /register.html会自动将url拼接到_web_root后即./wwwroot/register.htmlwebsocketsvr_t _wssvr; // WebSocket服务器user_table _user_table; // 用户数据管理模块online_manager _user_online; // 在线用户管理模块room_manager _room_manager; // 游戏房间管理模块match_manager _match_manager; // 游戏匹配管理模块session_manager _session_manager; // session管理模块
}; 博客的开头也有讲解如何搭建一个简单的WebSocket服务器。在服务器类中只是把搭建WebSocket服务器的步骤拆分到了构造函数和start()中搭建WebSocket服务器的本质过程还是相同的。 后面主要围绕wsopen_callback()wsclose_callback()wsmsg_callback()http_callback()这四个函数来做文章。 HTTP请求处理函数
void http_callback(websocketpp::connection_hdl hdl)
{websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string method req.get_method();std::string uri req.get_uri();if (method POST uri /signup){signup_handler(conn);}else if (method POST uri /login){login_handler(conn);}else if (method GET uri /userinfo){info_handler(conn);}else{file_handler(conn);}
} 首先调用Websocketpp中的接口get_conn_from_hdl()获取连接conn通过conn获取用户发送的HTTP请求解析HTTP请求获取HTTP首行中的请求方法和uri再根据请求方法和uri的组合调用不同的处理函数。 实现以下函数接口以方便组织HTTP响应返回给用户端
void http_resp(websocketsvr_t::connection_ptr conn, bool result, const std::string reason, websocketpp::http::status_code::value code)
{Json::Value resp;resp[result] result;resp[reason] reason;std::string resp_str;json_util::serialize(resp, resp_str);conn-set_status(code);conn-set_body(resp_str);conn-append_header(Content-Type, application/json);
}
实现以下函数接口以方便组织WebSocket响应返回给用户端
void websocket_resp(websocketsvr_t::connection_ptr conn, Json::Value resp)
{std::string resp_str;json_util::serialize(resp, resp_str);conn-send(resp_str);
} 根据请求方法和uri的组合要实现以下几个4个处理函数
静态资源请求处理函数file_handler()用户注册请求处理函数signup_handler()用户登录请求处理函数login_handler()获取用户信息请求处理函数info_handler() 静态资源请求处理函数
void file_handler(websocketsvr_t::connection_ptr conn)
{// 1.获取http请求的uri -- 资源路径了解客户端请求的页面文件名称websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();// 2.组合出文件实际的路径相对根目录 uristd::string real_path _web_root uri;// 3.如果请求的uri是个目录则增加一个后缀 -- login.htmlif (real_path.back() /) // 表示请求资源路径是一个目录{real_path login.html;}// 4.读取文件内容若文件不存在则返回404std::string body;bool ret file_util::read(real_path, body);if (ret false){body html;body head;body meta charsetUTF-8/;body /head;body body;body h1 Not Found /h1;body /body;conn-set_status(websocketpp::http::status_code::value::not_found);conn-set_body(body);return;}// 5.设置响应正文conn-set_status(websocketpp::http::status_code::value::ok);conn-set_body(body);return;
} 用户注册请求处理函数
void signup_handler(websocketsvr_t::connection_ptr conn)
{// 1. 获取HTTP请求正文std::string req_body conn-get_request_body();// 2. 对HTTP请求正文进行反序列化得到用户名和密码Json::Value signup_info;bool ret json_util::unserialize(req_body, signup_info);if (ret false){DLOG(反序列化注册信息失败);return http_resp(conn, false, 用户注册失败, websocketpp::http::status_code::value::bad_request);}// 3. 进行数据库的用户新增操作成功返回200失败返回400if (signup_info[username].isNull() || signup_info[password].isNull()){DLOG(缺少用户名或密码);return http_resp(conn, false, 缺少用户名或密码, websocketpp::http::status_code::value::bad_request);}ret _user_table.signup(signup_info); // 在数据库中新增用户if (ret false){DLOG(向数据库中添加用户失败);return http_resp(conn, false, 用户注册失败, websocketpp::http::status_code::value::bad_request);}// 用户注册成功返回成功响应return http_resp(conn, true, 用户注册成功, websocketpp::http::status_code::value::ok);
} 用户登录请求处理函数
void login_handler(websocketsvr_t::connection_ptr conn)
{// 1. 获取HTTP请求正文std::string req_body conn-get_request_body();// 2. 对HTTP请求正文进行反序列化得到用户名和密码Json::Value login_info;bool ret json_util::unserialize(req_body, login_info);if (ret false){DLOG(反序列化登录信息失败);return http_resp(conn, false, 用户登录失败, websocketpp::http::status_code::value::bad_request);}// 3. 校验正文完整性进行数据库的用户信息验证失败返回400if (login_info[username].isNull() || login_info[password].isNull()){DLOG(缺少用户名或密码);return http_resp(conn, false, 缺少用户名或密码, websocketpp::http::status_code::value::bad_request);}ret _user_table.login(login_info); // 进行登录验证if (ret false){DLOG(用户登录失败);return http_resp(conn, false, 用户登录失败, websocketpp::http::status_code::value::bad_request);}// 4. 用户信息验证成功则给客户端创建sessionuint64_t user_id login_info[id].asUInt64();session_ptr psession _session_manager.create_session(user_id, LOGIN);if (psession.get() nullptr){DLOG(创建session失败);return http_resp(conn, false, 创建session失败, websocketpp::http::status_code::value::internal_server_error);}_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);// 5. 设置响应头部Set-Cookie将session id通过cookie返回std::string cookie_ssid SSID std::to_string(psession-get_session_id());conn-append_header(Set-Cookie, cookie_ssid);return http_resp(conn, true, 用户登录成功, websocketpp::http::status_code::value::ok);
} 获取用户信息请求处理函数
// 通过HTTP请求头部字段中的Cookie信息获取session id
bool get_cookie_value(const std::string cookie, const std::string key, std::string value)
{// Cookie: SSIDxxx; keyvalue; keyvalue; // 1. 以‘; ’作为间隔对字符串进行分割得到各个单个的Cookie信息std::vectorstd::string cookie_arr;string_util::split(cookie, ; , cookie_arr);// 2. 对单个的cookie字符串以‘’为间隔进行分割得到key和valfor (const auto str : cookie_arr){std::vectorstd::string tmp_arr;string_util::split(str, , tmp_arr);if (tmp_arr.size() ! 2) continue;if (tmp_arr[0] key){value tmp_arr[1];return true;}}return false;
}void info_handler(websocketsvr_t::connection_ptr conn)
{// 1. 获取HTTP请求中的Cookie字段std::string cookie_str conn-get_request_header(Cookie);if (cookie_str.empty()){return http_resp(conn, false, 没有Cookie信息, websocketpp::http::status_code::value::bad_request);}// 从Cookie中获取session idstd::string session_id_str;bool ret get_cookie_value(cookie_str, SSID, session_id_str);if (ret false){return http_resp(conn, false, Cookie中没有session id, websocketpp::http::status_code::value::bad_request);}// 2. 根据session id在session管理中获取对应的sessionsession_ptr psession _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() nullptr){return http_resp(conn, false, session已过期, websocketpp::http::status_code::value::bad_request);}// 3. 通过session获取对应的user id再从数据库中获取用户信息并序列化返回给客户端uint64_t user_id psession-get_user_id();Json::Value user_info;ret _user_table.select_by_id(user_id, user_info);if (ret false){return http_resp(conn, false, 获取用户信息失败, websocketpp::http::status_code::value::bad_request);}std::string user_info_str;json_util::serialize(user_info, user_info_str);conn-set_status(websocketpp::http::status_code::value::ok);conn-set_body(user_info_str);conn-append_header(Content-Type, application/json);// 4. 上述操作访问了session所以要刷新session的过期时间_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);
} WebSocket长连接建立成功后的处理函数
void wsopen_callback(websocketpp::connection_hdl hdl)
{websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();if (uri /hall) // 建立了游戏大厅的长连接{wsopen_game_hall(conn);}else if (uri /room) // 建立了游戏房间的长连接{wsopen_game_room(conn);}
} 根据HTTP请求首行中的uri来判断客户端和服务器建立的是游戏大厅的长连接还是游戏房间的长连接。 用户登录验证函数登录成功则返回用户session
session_ptr get_session_by_cookie(websocketsvr_t::connection_ptr conn)
{Json::Value resp;// 1. 获取HTTP请求中的Cookie字段std::string cookie_str conn-get_request_header(Cookie);if (cookie_str.empty()){resp[optype] hall_ready;resp[result] false;resp[reason] 没有Cookie信息;websocket_resp(conn, resp);return session_ptr();}// 从Cookie中获取session idstd::string session_id_str;bool ret get_cookie_value(cookie_str, SSID, session_id_str);if (ret false){resp[optype] hall_ready;resp[result] false;resp[reason] Cookie中没有session;websocket_resp(conn, resp);return session_ptr();}// 2. 根据session id在session管理中获取对应的sessionsession_ptr psession _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() nullptr){resp[optype] hall_ready;resp[result] false;resp[reason] session已过期;websocket_resp(conn, resp);return session_ptr();}return psession;
} 游戏大厅长连接建立成功的处理函数
void wsopen_game_hall(websocketsvr_t::connection_ptr conn)
{Json::Value resp;// 1. 登录验证判断当前用户是否登录成功session_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 判断当前用户是否重复登录if (_user_online.is_in_game_hall(psession-get_user_id()) || _user_online.is_in_game_room(psession-get_user_id())){resp[optype] hall_ready;resp[result] false;resp[reason] 用户已登录;return websocket_resp(conn, resp);}// 3. 将当前用户和对应的连接加入到游戏大厅_user_online.enter_game_hall(psession-get_user_id(), conn);// 4. 给用户响应游戏大厅建立成功resp[optype] hall_ready;resp[result] true;resp[uid] (Json::UInt64)psession-get_user_id();websocket_resp(conn, resp);// 5. 将session的生命周期设置为永久_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_PERMANENT);
} 游戏房间长连接建立成功的处理函数
void wsopen_game_room(websocketsvr_t::connection_ptr conn)
{Json::Value resp;// 1. 登录验证判断当前用户是否登录成功session_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 判断当前用户是否重复登录if (_user_online.is_in_game_hall(psession-get_user_id()) || _user_online.is_in_game_room(psession-get_user_id())){resp[optype] room_ready;resp[result] false;resp[reason] 用户已登录;return websocket_resp(conn, resp);}// 3. 判断当前用户是否已经创建好了房间room_ptr proom _room_manager.get_room_by_user_id(psession-get_user_id());if (proom.get() nullptr){resp[optype] room_ready;resp[result] false;resp[reason] 通过用户id获取游戏房间失败;return websocket_resp(conn, resp);}// 4. 将当前用户添加到在线用户管理的游戏房间中的用户管理中_user_online.enter_game_room(psession-get_user_id(), conn);// 5. 给用户响应房间创建完成resp[optype] room_ready;resp[result] true;resp[room_id] (Json::UInt64)proom-id();resp[uid] (Json::UInt64)psession-get_user_id();resp[white_id] (Json::UInt64)proom-get_white_player();resp[black_id] (Json::UInt64)proom-get_black_player();websocket_resp(conn, resp);// 6. 将session的生命周期设置为永久_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_PERMANENT);
} WebSocket长连接断开前的处理函数
void wsclose_callback(websocketpp::connection_hdl hdl)
{websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();if (uri /hall) // 游戏大厅长连接断开{wscloes_game_hall(conn);}else if (uri /room) // 游戏房间长连接断开{wscloes_game_room(conn);}
} 根据HTTP请求首行中的uri来判断客户端断开的是游戏大厅的长连接还是游戏房间的长连接。 游戏大厅长连接断开的处理函数
void wscloes_game_hall(websocketsvr_t::connection_ptr conn)
{// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 将用户从游戏大厅中移除_user_online.exit_game_hall(psession-get_user_id());// 3. 将session的生命周期设为定时的超时自动删除_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);
} 游戏房间长连接断开的处理函数
void wscloes_game_room(websocketsvr_t::connection_ptr conn)
{// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 将玩家从在线用户管理中的游戏房间中的玩家中移除_user_online.exit_game_room(psession-get_user_id());// 3. 将玩家从游戏房间中移除(房间中所有玩家都退出了就会销毁房间)_room_manager.remove_player_in_room(psession-get_user_id());// 4. 将session的生命周期设置为定时的超时自动销毁_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);
} WebSocket长连接通信处理函数
void wsmsg_callback(websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg)
{websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();if (uri /hall) // 游戏大厅请求{wsmsg_game_hall(conn, msg); // 游戏大厅请求处理函数}else if (uri /room) // 游戏房间请求{wsmsg_game_room(conn, msg); // 游戏房间请求处理函数}
} 根据HTTP请求首行中的uri来判断当前请求是游戏大厅中的请求还是游戏房间中的请求。 游戏大厅请求处理函数游戏匹配请求/停止匹配请求
void wsmsg_game_hall(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg)
{Json::Value resp;// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 获取WebSocket请求信息std::string req_str msg-get_payload();Json::Value req;bool ret json_util::unserialize(req_str, req);if (ret false){resp[result] false;resp[reason] 解析请求失败;return websocket_resp(conn, resp);}// 3. 对于请求分别进行处理if (!req[optype].isNull() req[optype].asString() match_start){// 开始游戏匹配通过匹配模块将玩家添加到匹配队列中_match_manager.add(psession-get_user_id());resp[optype] match_start;resp[result] true;return websocket_resp(conn, resp);}else if (!req[optype].isNull() req[optype].asString() match_stop){// 停止游戏匹配通过匹配模块将玩家从匹配队列中移除_match_manager.del(psession-get_user_id());resp[optype] match_stop;resp[result] true;return websocket_resp(conn, resp);}resp[optype] unknown;resp[result] false;resp[reason] 未知请求类型;return websocket_resp(conn, resp);
} 游戏房间请求处理函数下棋请求/聊天请求
void wsmsg_game_room(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg){Json::Value resp;// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 获取用户所在的游戏房间信息room_ptr proom _room_manager.get_room_by_user_id(psession-get_user_id());if (proom.get() nullptr){resp[optype] unknown;resp[result] false;resp[reason] 通过用户id获取游戏房间失败;return websocket_resp(conn, resp);}// 3. 对请求进行反序列化std::string req_str msg-get_payload();Json::Value req;bool ret json_util::unserialize(req_str, req);if (ret false){resp[optype] unknown;resp[result] false;resp[reason] 解析请求失败;return websocket_resp(conn, resp);}// 4. 通过游戏房间进行游戏房间请求的处理return proom-handle_request(req);} | 服务器类所有函数整合 |
#pragma once#include string
#include websocketpp/server.hpp
#include websocketpp/config/asio_no_tls.hpp
#include vector#include db.hpp
#include online.hpp
#include room.hpp
#include matcher.hpp
#include session.hpp#define WWWROOT ./wwwroot/class gobang_server
{
public:// 进行成员变量初始化以及设置服务器回调函数gobang_server(const std::string host,const std::string user,const std::string password,const std::string dbname,uint32_t port 3306,const std::string wwwroot WWWROOT):_web_root(wwwroot), _user_table(host, user, password, dbname, port), _room_manager(_user_table, _user_online), _match_manager(_room_manager, _user_table, _user_online), _session_manager(_wssvr){_wssvr.set_access_channels(websocketpp::log::alevel::none);_wssvr.init_asio();_wssvr.set_reuse_addr(true);_wssvr.set_open_handler(std::bind(gobang_server::wsopen_callback, this, std::placeholders::_1));_wssvr.set_close_handler(std::bind(gobang_server::wsclose_callback, this, std::placeholders::_1));_wssvr.set_message_handler(std::bind(gobang_server::wsmsg_callback, this, std::placeholders::_1, std::placeholders::_2));_wssvr.set_http_handler(std::bind(gobang_server::http_callback, this, std::placeholders::_1));}// 启动服务器void start(uint16_t port){_wssvr.listen(port);_wssvr.start_accept();_wssvr.run();}private:// WebSocket长连接建立成功后的处理函数void wsopen_callback(websocketpp::connection_hdl hdl){websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();if (uri /hall) // 建立了游戏大厅的长连接{wsopen_game_hall(conn);}else if (uri /room) // 建立了游戏房间的长连接{wsopen_game_room(conn);}}// WebSocket长连接断开前的处理函数void wsclose_callback(websocketpp::connection_hdl hdl){websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();if (uri /hall) // 游戏大厅长连接断开{wscloes_game_hall(conn);}else if (uri /room) // 游戏房间长连接断开{wscloes_game_room(conn);}}// WebSocket长连接通信处理void wsmsg_callback(websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg){websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();if (uri /hall) // 游戏大厅请求{wsmsg_game_hall(conn, msg); // 游戏大厅请求处理函数}else if (uri /room) // 游戏房间请求{wsmsg_game_room(conn, msg); // 游戏房间请求处理函数}}void http_callback(websocketpp::connection_hdl hdl){websocketsvr_t::connection_ptr conn _wssvr.get_con_from_hdl(hdl);websocketpp::http::parser::request req conn-get_request();std::string method req.get_method();std::string uri req.get_uri();if (method POST uri /signup){signup_handler(conn);}else if (method POST uri /login){login_handler(conn);}else if (method GET uri /userinfo){info_handler(conn);}else{file_handler(conn);}}private:// 组织HTTP响应void http_resp(websocketsvr_t::connection_ptr conn, bool result, const std::string reason, websocketpp::http::status_code::value code){Json::Value resp;resp[result] result;resp[reason] reason;std::string resp_str;json_util::serialize(resp, resp_str);conn-set_status(code);conn-set_body(resp_str);conn-append_header(Content-Type, application/json);}// 组织WebSocket响应void websocket_resp(websocketsvr_t::connection_ptr conn, Json::Value resp){std::string resp_str;json_util::serialize(resp, resp_str);conn-send(resp_str);}// 静态资源请求的处理void file_handler(websocketsvr_t::connection_ptr conn){// 1.获取http请求的uri -- 资源路径了解客户端请求的页面文件名称websocketpp::http::parser::request req conn-get_request();std::string uri req.get_uri();// 2.组合出文件实际的路径相对根目录 uristd::string real_path _web_root uri;// 3.如果请求的uri是个目录则增加一个后缀 -- login.htmlif (real_path.back() /) // 表示请求资源路径是一个目录{real_path login.html;}// 4.读取文件内容若文件不存在则返回404std::string body;bool ret file_util::read(real_path, body);if (ret false){body html;body head;body meta charsetUTF-8/;body /head;body body;body h1 Not Found /h1;body /body;conn-set_status(websocketpp::http::status_code::value::not_found);conn-set_body(body);return;}// 5.设置响应正文conn-set_status(websocketpp::http::status_code::value::ok);conn-set_body(body);return;}// 用户注册请求的处理void signup_handler(websocketsvr_t::connection_ptr conn){// 1. 获取HTTP请求正文std::string req_body conn-get_request_body();// 2. 对HTTP请求正文进行反序列化得到用户名和密码Json::Value signup_info;bool ret json_util::unserialize(req_body, signup_info);if (ret false){DLOG(反序列化注册信息失败);return http_resp(conn, false, 用户注册失败, websocketpp::http::status_code::value::bad_request);}// 3. 进行数据库的用户新增操作成功返回200失败返回400if (signup_info[username].isNull() || signup_info[password].isNull()){DLOG(缺少用户名或密码);return http_resp(conn, false, 缺少用户名或密码, websocketpp::http::status_code::value::bad_request);}ret _user_table.signup(signup_info); // 在数据库中新增用户if (ret false){DLOG(向数据库中添加用户失败);return http_resp(conn, false, 用户注册失败, websocketpp::http::status_code::value::bad_request);}// 用户注册成功返回成功响应return http_resp(conn, true, 用户注册成功, websocketpp::http::status_code::value::ok);}// 用户登录请求的处理void login_handler(websocketsvr_t::connection_ptr conn){// 1. 获取HTTP请求正文std::string req_body conn-get_request_body();// 2. 对HTTP请求正文进行反序列化得到用户名和密码Json::Value login_info;bool ret json_util::unserialize(req_body, login_info);if (ret false){DLOG(反序列化登录信息失败);return http_resp(conn, false, 用户登录失败, websocketpp::http::status_code::value::bad_request);}// 3. 校验正文完整性进行数据库的用户信息验证失败返回400if (login_info[username].isNull() || login_info[password].isNull()){DLOG(缺少用户名或密码);return http_resp(conn, false, 缺少用户名或密码, websocketpp::http::status_code::value::bad_request);}ret _user_table.login(login_info); // 进行登录验证if (ret false){DLOG(用户登录失败);return http_resp(conn, false, 用户登录失败, websocketpp::http::status_code::value::bad_request);}// 4. 用户信息验证成功则给客户端创建sessionuint64_t user_id login_info[id].asUInt64();session_ptr psession _session_manager.create_session(user_id, LOGIN);if (psession.get() nullptr){DLOG(创建session失败);return http_resp(conn, false, 创建session失败, websocketpp::http::status_code::value::internal_server_error);}_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);// 5. 设置响应头部Set-Cookie将session id通过cookie返回std::string cookie_ssid SSID std::to_string(psession-get_session_id());conn-append_header(Set-Cookie, cookie_ssid);return http_resp(conn, true, 用户登录成功, websocketpp::http::status_code::value::ok);}// 通过HTTP请求头部字段中的Cookie信息获取session idbool get_cookie_value(const std::string cookie, const std::string key, std::string value){// Cookie: SSIDxxx; keyvalue; keyvalue; // 1. 以‘; ’作为间隔对字符串进行分割得到各个单个的Cookie信息std::vectorstd::string cookie_arr;string_util::split(cookie, ; , cookie_arr);// 2. 对单个的cookie字符串以‘’为间隔进行分割得到key和valfor (const auto str : cookie_arr){std::vectorstd::string tmp_arr;string_util::split(str, , tmp_arr);if (tmp_arr.size() ! 2) continue;if (tmp_arr[0] key){value tmp_arr[1];return true;}}return false;}// 获取用户信息请求的处理void info_handler(websocketsvr_t::connection_ptr conn){// 1. 获取HTTP请求中的Cookie字段std::string cookie_str conn-get_request_header(Cookie);if (cookie_str.empty()){return http_resp(conn, false, 没有Cookie信息, websocketpp::http::status_code::value::bad_request);}// 从Cookie中获取session idstd::string session_id_str;bool ret get_cookie_value(cookie_str, SSID, session_id_str);if (ret false){return http_resp(conn, false, Cookie中没有session id, websocketpp::http::status_code::value::bad_request);}// 2. 根据session id在session管理中获取对应的sessionsession_ptr psession _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() nullptr){return http_resp(conn, false, session已过期, websocketpp::http::status_code::value::bad_request);}// 3. 通过session获取对应的user id再从数据库中获取用户信息并序列化返回给客户端uint64_t user_id psession-get_user_id();Json::Value user_info;ret _user_table.select_by_id(user_id, user_info);if (ret false){return http_resp(conn, false, 获取用户信息失败, websocketpp::http::status_code::value::bad_request);}std::string user_info_str;json_util::serialize(user_info, user_info_str);conn-set_status(websocketpp::http::status_code::value::ok);conn-set_body(user_info_str);conn-append_header(Content-Type, application/json);// 4. 上述操作访问了session所以要刷新session的过期时间_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);}// 用于验证用户是否登录成功登录成功则返回用户sessionsession_ptr get_session_by_cookie(websocketsvr_t::connection_ptr conn){Json::Value resp;// 1. 获取HTTP请求中的Cookie字段std::string cookie_str conn-get_request_header(Cookie);if (cookie_str.empty()){resp[optype] hall_ready;resp[result] false;resp[reason] 没有Cookie信息;websocket_resp(conn, resp);return session_ptr();}// 从Cookie中获取session idstd::string session_id_str;bool ret get_cookie_value(cookie_str, SSID, session_id_str);if (ret false){resp[optype] hall_ready;resp[result] false;resp[reason] Cookie中没有session;websocket_resp(conn, resp);return session_ptr();}// 2. 根据session id在session管理中获取对应的sessionsession_ptr psession _session_manager.get_session_by_session_id(std::stoul(session_id_str));if (psession.get() nullptr){resp[optype] hall_ready;resp[result] false;resp[reason] session已过期;websocket_resp(conn, resp);return session_ptr();}return psession;}// 游戏大厅长连接建立成功的处理函数void wsopen_game_hall(websocketsvr_t::connection_ptr conn){Json::Value resp;// 1. 登录验证判断当前用户是否登录成功session_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 判断当前用户是否重复登录if (_user_online.is_in_game_hall(psession-get_user_id()) || _user_online.is_in_game_room(psession-get_user_id())){resp[optype] hall_ready;resp[result] false;resp[reason] 用户已登录;return websocket_resp(conn, resp);}// 3. 将当前用户和对应的连接加入到游戏大厅_user_online.enter_game_hall(psession-get_user_id(), conn);// 4. 给用户响应游戏大厅建立成功resp[optype] hall_ready;resp[result] true;resp[uid] (Json::UInt64)psession-get_user_id();websocket_resp(conn, resp);// 5. 将session的生命周期设置为永久_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_PERMANENT);}// 游戏房间长连接建立成功的处理函数void wsopen_game_room(websocketsvr_t::connection_ptr conn){Json::Value resp;// 1. 登录验证判断当前用户是否登录成功session_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 判断当前用户是否重复登录if (_user_online.is_in_game_hall(psession-get_user_id()) || _user_online.is_in_game_room(psession-get_user_id())){resp[optype] room_ready;resp[result] false;resp[reason] 用户已登录;return websocket_resp(conn, resp);}// 3. 判断当前用户是否已经创建好了房间room_ptr proom _room_manager.get_room_by_user_id(psession-get_user_id());if (proom.get() nullptr){resp[optype] room_ready;resp[result] false;resp[reason] 通过用户id获取游戏房间失败;return websocket_resp(conn, resp);}// 4. 将当前用户添加到在线用户管理的游戏房间中的用户管理中_user_online.enter_game_room(psession-get_user_id(), conn);// 5. 给用户响应房间创建完成resp[optype] room_ready;resp[result] true;resp[room_id] (Json::UInt64)proom-id();resp[uid] (Json::UInt64)psession-get_user_id();resp[white_id] (Json::UInt64)proom-get_white_player();resp[black_id] (Json::UInt64)proom-get_black_player();websocket_resp(conn, resp);// 6. 将session的生命周期设置为永久_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_PERMANENT);}// 游戏大厅长连接断开的处理函数void wscloes_game_hall(websocketsvr_t::connection_ptr conn){// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 将用户从游戏大厅中移除_user_online.exit_game_hall(psession-get_user_id());// 3. 将session的生命周期设为定时的超时自动删除_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);}// 游戏房间长连接断开的处理函数void wscloes_game_room(websocketsvr_t::connection_ptr conn){// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 将玩家从在线用户管理中的游戏房间中的玩家中移除_user_online.exit_game_room(psession-get_user_id());// 3. 将玩家从游戏房间中移除(房间中所有玩家都退出了就会销毁房间)_room_manager.remove_player_in_room(psession-get_user_id());// 4. 将session的生命周期设置为定时的超时自动销毁_session_manager.set_session_expiration_time(psession-get_session_id(), SESSION_TEMOPRARY);}// 游戏大厅请求处理函数游戏匹配请求/停止匹配请求void wsmsg_game_hall(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg){Json::Value resp;// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 获取WebSocket请求信息std::string req_str msg-get_payload();Json::Value req;bool ret json_util::unserialize(req_str, req);if (ret false){resp[result] false;resp[reason] 解析请求失败;return websocket_resp(conn, resp);}// 3. 对于请求分别进行处理if (!req[optype].isNull() req[optype].asString() match_start){// 开始游戏匹配通过匹配模块将玩家添加到匹配队列中_match_manager.add(psession-get_user_id());resp[optype] match_start;resp[result] true;return websocket_resp(conn, resp);}else if (!req[optype].isNull() req[optype].asString() match_stop){// 停止游戏匹配通过匹配模块将玩家从匹配队列中移除_match_manager.del(psession-get_user_id());resp[optype] match_stop;resp[result] true;return websocket_resp(conn, resp);}resp[optype] unknown;resp[result] false;resp[reason] 未知请求类型;return websocket_resp(conn, resp);}// 游戏房间请求处理函数下棋请求/聊天请求void wsmsg_game_room(websocketsvr_t::connection_ptr conn, websocketsvr_t::message_ptr msg){Json::Value resp;// 1. 获取用户的sessionsession_ptr psession get_session_by_cookie(conn);if (psession.get() nullptr) return;// 2. 获取用户所在的游戏房间信息room_ptr proom _room_manager.get_room_by_user_id(psession-get_user_id());if (proom.get() nullptr){resp[optype] unknown;resp[result] false;resp[reason] 通过用户id获取游戏房间失败;return websocket_resp(conn, resp);}// 3. 对请求进行反序列化std::string req_str msg-get_payload();Json::Value req;bool ret json_util::unserialize(req_str, req);if (ret false){resp[optype] unknown;resp[result] false;resp[reason] 解析请求失败;return websocket_resp(conn, resp);}// 4. 通过游戏房间进行游戏房间请求的处理return proom-handle_request(req);}private:std::string _web_root; // 静态资源根目录./wwwroot/ 请求的url为 /register.html会自动将url拼接到_web_root后即./wwwroot/register.htmlwebsocketsvr_t _wssvr; // WebSocket服务器user_table _user_table; // 用户数据管理模块online_manager _user_online; // 在线用户管理模块room_manager _room_manager; // 游戏房间管理模块match_manager _match_manager; // 游戏匹配管理模块session_manager _session_manager; // session管理模块
};项目做到这里已经实现了实时聊天的在线匹配五子棋对战游戏的基本功能了。但是每次启动游戏服务器都需要连接Linux服务器主机在终端上运行该程序才能运行该游戏服务器不算真正意义上的网络游戏。
为了实现将程序脱离主机连接和手动启动服务器我们要将该程序进行守护进程化。 守护进程化 守护进程是一种在后台运行的服务进程没有控制终端因此它们独立于任何用户会话。
在Linux中daemon()用于将一个进程变成守护进程。
int daemon(int nochdir, int noclose); 参数说明 nochdir ---- 如果这个参数为0daemon()会将当前工作目录更改为根目录这是因为守护进程不应该与某个具体的文件系统挂载点关联。如果你不希望改变当前工作目录可以将这个参数设置为非零值 noclose ---- 如果这个参数为0daemon()会将标准输入、标准输出和标准错误重定向到 /dev/null这意味着守护进程不会在终端上产生输出。如果你希望保持这些文件描述符不变可以将这个参数设置为非零值。 返回值 返回0表示成功返回-1表示失败并设置errno以指示错误原因 由于要将程序设置为守护进程则先前输出在终端上的日志信息需要写入一个文件中
在src目录下创建一个log.txt文件将日志信息写入到log.txt中。
修改日志宏以达到上述要求
#pragma once#include iostream
#include ctime
#include cstdio#define INF 0
#define DBG 1
#define ERR 2
#define DEFAULT_LOG_LEVEL INF#define LOG(level, format, ...) \do \{ \if (DEFAULT_LOG_LEVEL level) \break; \time_t t time(NULL); \struct tm *lt localtime(t); \char buf[32] {0}; \strftime(buf, 31, %H:%M:%S, lt); \FILE* pf_log fopen(./log.txt, a); \if (pf_log) \{ \fprintf(pf_log, [%s %s:%d] format \n, buf, __FILE__, __LINE__, ##__VA_ARGS__); \fclose(pf_log); \} \else \{ \fprintf(stderr, Failed to open log file\n); \} \} while (false)#define ILOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DLOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(ERR, format, ##__VA_ARGS__) 使用fopen()打开log.txt再调用fprintf()将日志信息格式化写入log.txt中。 创建一个online_gobang.cpp文件文件中实例化一个服务器类gobang_server对象并运行服务器。
#include unistd.h#include server.hppint main()
{// 将游戏服务器设置为守护进程if (daemon(1, 0) -1) {perror(daemon);exit(EXIT_FAILURE);}gobang_server server(127.0.0.1, root, , online_gobang, 3306);server.start(8080);return 0;
} 在main()中的开头调用daemon()将该程序设置为守护进程。 注意不要改变程序的工作目录因为程序中调用的前端资源和log.txt都在同级目录下。 在终端下运行编译生成的可执行程序即是将游戏服务器运行起来了
在浏览器中访问 → 主机id:目标端口号即可体验在线五子棋对战游戏了 项目源码
项目完整源代码https://github.com/NICK03nK/Project/tree/main/Gobang_online_AG