前置知识
WebSocket协议
与Http相比,服务端可以主动给客户端发送消息,从短连接变成长连接
WebSocket协议切换过程

websocketpp
WebSocketpp同时支持HTTP和Websocket两种网络协议,适合本项目的实现,需要用到的接口如下:
日志相关接口
设置日志打印等级:void set_access_channels(log::level channels); 由于该库日志打印比较复杂,不适合于该项目实现,所以我们需要将打印等级设置为websocketpp::log::alevel
::none ,即禁止打印所有日志。
回调函数相关接口
针对不同事件设置不同的业务处理函数。用websocketpp库搭建服务器后,需要给不同事件设置不同的处理函数指针,当服务器收到指定的数据,就会触发对应的事件然后通过刚刚设置的函数指针去调用这些业务处理函数。常用的有四个接口
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请求回调处理函数
通信连接相关接口
- send 给客户端发送消息
- close 关闭连接
- get_con_from_hdl 获取connection_hdl 对应连接的connection_ptr
搭建服务器的其它接口
- init_asio() 初始化asio框架
- listen 设置绑定监听端口
- set_reuse_add 是否启动地址重用
- run 启动服务器
- start_accept 开始获取新建连接
- set_timer 设置定时任务
服务器初步搭建
搭建流程
- 实例化server对象
- 设置日志输出等级
- 初始化asio框架中的io_service调度器
- 设置业务处理回调函数(用户自己实现)
- 设置服务器监听端口
- 开始获取新建连接
- 启动服务器
代码
#include <iostream>
#include <string>
#include <websocketpp/server.hpp>
#include <websocketpp/config/asio_no_tls.hpp>
typedef websocketpp::server<websocketpp::config::asio> wsserver_t;
void http_callback(wsserver_t *svr, websocketpp::connection_hdl hdl)
{
wsserver_t::connection_ptr conn = svr->get_con_from_hdl(hdl);
// 获取http请求信息
websocketpp::http::parser::request req = conn->get_request();
std::cout << "method: " << req.get_method() << std::endl;
std::cout << "uri: " << req.get_uri() << std::endl;
std::cout << "body: " << conn->get_request_body() << std::endl;
// 返回helloworld界面
std::string body = "<html><body><h1>Hello World</h1></body></html>";
conn->set_body(body);
conn->set_status(websocketpp::http::status_code::ok);
conn->append_header("Content-Type", "text/html");
}
void wbopen_callback(wsserver_t *svr, websocketpp::connection_hdl hdl)
{
std::cout << "握手成功" << std::endl;
}
void wbmsg_callback(wsserver_t *svr, websocketpp::connection_hdl hdl, wsserver_t::message_ptr pmsg)
{
wsserver_t::connection_ptr conn = svr->get_con_from_hdl(hdl);
std::cout << "wsmessage: " << pmsg->get_payload() << std::endl;
std::string rsp = "你好,客户端";
conn->send(rsp, websocketpp::frame::opcode::text);
}
void wbclose_callback(wsserver_t *svr, websocketpp::connection_hdl hdl)
{
std::cout << "连接关闭" << std::endl;
}
int main()
{
// 1.实例化server对象
wsserver_t wbserver;
// 2.设置日志输出等级
wbserver.set_access_channels(websocketpp::log::alevel::none);
// 3.初始化asio框架中的调度器
wbserver.init_asio();
// 4.设置业务处理回调函数
wbserver.set_http_handler(std::bind(http_callback, &wbserver, std::placeholders::_1));
wbserver.set_open_handler(std::bind(wbopen_callback, &wbserver, std::placeholders::_1));
wbserver.set_message_handler(std::bind(wbmsg_callback, &wbserver, std::placeholders::_1, std::placeholders::_2));
wbserver.set_close_handler(std::bind(wbclose_callback, &wbserver, std::placeholders::_1));
// 5.设置服务监听端口
wbserver.listen(8080);
// 6.开始获取新建连接:告诉Asio “可以接受新连接了”,但无实际执行
wbserver.start_accept();
// 7.启动服务器:启动无限循环,检测连接/处理消息/执行回调
wbserver.run();
return 0;
}
bind接口
typedef lib::function<void(connection_hdl,message_ptr)> message_handler;
void set_message_handler(message_handler h);
set_message_handler需要void(connection_hdl,message_ptr)的回调函数类型,而业务处理函数是void OnMessage(websocketsvr *, websocketpp::connection_hdl, message_ptr ),参数多了一个。
std::bind 的作用就是补齐参数,把多参数的函数适配成少参数的可调用对象,用std::bind(&OnMessage, &server, _1, _2)提前绑定第一个参数 &server,剩下 _1(对应 connection_hdl)、_2(对应 message_ptr);最终适配成 std::function<void (connection_hdl, message_ptr)>类型,正好匹配 set_message_handler 的要求。
Json序列化和反序列化

这里没有什么难点,主要是注意序列化和反序列化的流程,以及及时释放Json::StreamWriter对象以及Json::CharReader对象,下面的测试代码里也可以直接用智能指针std::unique<Json::StreamWriter>和std::unique<Json::CharReader>将它们管理起来,就不需要手动delete。
#include <iostream>
#include <jsoncpp/json/json.h>
#include <sstream>
std::string Serialize()
{
//1.将需要进行序列号的数据,存储在Json::Value 对象
Json::Value val;
val["name"] = "张三";
val["age"] = 18;
val["score"].append(88);
val["score"].append(98);
val["score"].append(76);
//2.实例化一个StreamWriterBuilder工厂类对象
Json::StreamWriterBuilder swb;
//3.通过StreamWriterBuilder工厂类对象生产一个StreamWriter对象
Json::StreamWriter *sw = swb.newStreamWriter();
//4.使用StreamWriter对象,对Json::Value对象存储的数据进行序列化
std::stringstream ss;
int ret = sw->write(val, &ss);
if(ret != 0)
{
std::cout << "json serialize failed" << std::endl;
delete sw;
return "";
}
std::cout << ss.str() << std::endl;
delete sw;
return ss.str();
}
void UnSerialize(const std::string &str)
{
//1.实例化一个CharReaderBuilder工厂类对象
Json::CharReaderBuilder crb;
//2.通过CharReaderBuilder工厂类生产一个CharReader对象
Json::CharReader *cr = crb.newCharReader();
//3.定义一个Json::Value对象存储解析后的数据
Json::Value root;
std::string err;
//4.使用CharReader对象进行json格式字符串str的反序列化
bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);
if(ret == false)
{
std::cout << "json unserialize failed : " << err << std::endl;
delete cr;
return;
}
//5.逐个元素去访问Json::Value中的数据
std::cout << "name: " << root["name"].asString() << std::endl;
std::cout << "age: " << root["age"].asInt() << std::endl;
int n = root["score"].size();
for(int i = 0; i < n; i++)
{
std::cout << "score" << root["score"][i] << std::endl;
}
delete cr;
}
int main()
{
std::string str = Serialize();
UnSerialize(str);
return 0;
}
mysql客户端
在对用户信息管理时,我们总不能手动操作mysql客户端,需要自己实现一个mysql客户端去访问mysql服务器,而不是通过之前用指令创建子进程的方式去访问:mysql -uroot -p
要实现mysqlclient,需要了解C语言涉及mysql客户端的相关接口以及搭建客户端的流程:
1.初始化mysql操作句柄
2.连接mysql服务器
3.设置客户端字符集
4.选择想要操作的数据库
5.执行sql语句(增删改不需要获取结果集)
(如果是查询)6.将查询结果保存到本地
7.获取查询结果集中的结果条数
8.遍历获取结果集中的每一条数据进行处理
9.释放结果集
10.释放mysql操作句柄
注意:
mysql服务器默认情况不允许远程登录,就是不允许公网访问,下面的测试就用本地地址127.0.0.1mysql_error:记录上一次mysql操作失败的原因
pthead、websocketpp库文件都是默认在/usr/lib64路径下查找库,所以编译写-lpthead、-lwebsocketpp;
而mysqlclient库是在/usr/lib64/mysql/查找,所以编译要先加上-L /usr/lib64/mysql/ 再加上 -lmysqlclient
#include <iostream>
#include <mysql/mysql.h>
#define HOST "127.0.0.1"
#define USER "root"
#define DB "test"
#define PORT 3306
int main()
{
//1.初始化mysql操作句柄
MYSQL *mysql = mysql_init(NULL);//传空会在堆上new MYSQL
if(mysql == nullptr)
{
std::cout << "mysql init failed!" << std::endl;
return -1;
}
//2.连接mysql服务器
if(mysql_real_connect(mysql, HOST, USER, PASSWD, DB, PORT, nullptr, 0) == nullptr)
{
std::cout << "connect mysql server failed : " << mysql_error(mysql) << std::endl;
mysql_close(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);
return -1;
}
//4.选择想要操作的数据库
//mysql_select_db(mysql, DB);
//5.执行sql语句(增删改不需要结果)
//const char* sql = "insert stu values(null, '张三', 18, 56.67, 67.89, 78.88)";
//const char* sql = "update stu set ch=ch+1 where num=1;";
// const char* sql = "select * from stu;";
const char* sql = "delete from stu where num = 1;";
if(mysql_query(mysql, sql) != 0)
{
printf("mysql query failed : %s", mysql_error(mysql));
mysql_close(mysql);
return -1;
}
// //6.将查询结果保存到本地
// MYSQL_RES *res = mysql_store_result(mysql);
// if(res == NULL) //没有结果集
// {
// mysql_close(mysql);
// return -1;
// }
// //7.获取查询结果集中的结果条数
// int num_row = mysql_num_rows(res);
// int num_col = mysql_num_fields(res);
// //8.遍历获取结果集中的每一条数据进行处理
// for(int i = 0; i < num_row; i++)
// {
// MYSQL_ROW row = mysql_fetch_row(res);
// for(int j = 0; j < num_col; j++)
// {
// printf("%s\t", row[j]);
// }
// printf("\n");
// }
// //9.释放结果集
// mysql_free_result(res);
//10.释放mysql操作句柄
mysql_close(mysql);
return 0;
}
工具接口
项目需要的工具接口如下:
- 日志宏: 前面已经将websocket的日志打印屏蔽,需要自己实现程序日志打印
- mysql_util: 数据库句柄的初始化以及连接,语句的执行(查询语句的结果集获取另写),mysql句柄的销毁
- json_util: 封装json的序列化和反序列化
- string_util: 对字符串分割的封装(http请求的cookies字段需要按照特定规则分割)
- file_util: 对文件数据读取的封装(对html文件数据进行读取)
#ifndef UTIL_HPP_ #define UTIL_HPP_ #include "Log.hpp" #include <mysql/mysql.h> #include <string> #include <jsoncpp/json/json.h> #include <vector> #include <fstream> class mysql_util { public: static MYSQL *mysql_create(const std::string &host, const std::string &username, const std::string &passwd, const std::string &db, int port) { MYSQL *mysql = mysql_init(NULL); // 传空会在堆上new MYSQL if (mysql == nullptr) { ELOG("mysql init failed!"); return nullptr; } // 2.连接mysql服务器 if (mysql_real_connect(mysql, host.c_str(), username.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0) == nullptr) { ELOG("connect mysql server failed : %s", mysql_error(mysql)); mysql_close(mysql); return nullptr; } // 3.设置客户端字符集 if (mysql_set_character_set(mysql, "utf8") != 0) { ELOG("set client character failed : %s", mysql_error(mysql)); mysql_close(mysql); return nullptr; } return mysql; } static bool mysql_exec(MYSQL *mysql, const std::string &sql) { if (mysql == nullptr) return false; if (mysql_query(mysql, sql.c_str()) != 0) { ELOG("mysql query failed : %s", mysql_error(mysql)); return false; } return true; } static void mysql_destroy(MYSQL *mysql) { if (mysql) mysql_close(mysql); } }; class json_util { public: static bool serialize(const Json::Value &root, std::string &str) { Json::StreamWriterBuilder swb; std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 确保delete StreamWriter std::stringstream ss; int ret = sw->write(root, &ss); if (ret != 0) { ELOG("json serialize fail!"); return false; } str = ss.str(); return true; } static bool unserialize(Json::Value &root, const std::string &str) { Json::CharReaderBuilder crb; std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); std::string err; bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err); if (ret == false) { ELOG("json unserialize failed : %s", err.c_str()); return false; } return true; } }; class str_util { public: static int split(std::string &src, const std::string &sep, std::vector<std::string> &arry) { //,12,,,11,34,eee size_t offset = 0, pos = 0; while (offset < src.size()) { pos = src.find(sep, offset); if (pos == std::string::npos) { arry.push_back(src.substr(offset)); return arry.size(); } if (pos == offset) offset = pos + sep.size(); else { arry.push_back(src.substr(offset, pos - offset)); offset = pos + sep.size(); } } return arry.size(); } }; class file_util { public: static bool read(const std::string &filename, std::string &dst) { // 打开文件 std::ifstream ifs(filename, std::ios::binary); if (ifs.is_open() == false) { ELOG("file %s open failed!", filename.c_str()); return false; } // 获取文件大小 size_t fsize = 0; ifs.seekg(0, std::ios::end); fsize = ifs.tellg(); ifs.seekg(0, std::ios::beg); dst.resize(fsize); ifs.read(&dst[0], fsize); if (ifs.good() == false) { ELOG("file %s read failed!", filename.c_str()); ifs.close(); return false; } ifs.close(); return true; } }; #endif
数据管理模块
自己实现一个mysql客户端来访问服务器进行数据库操作:针对gobang库管理用户表,设计一个user类,通过user类对象管理数据库的用户表。
注意:在类的成员变量这里除了需要MYSQL句柄外,还需要互斥锁-->虽然MySQL单个操作是线程安全的,但是进行多个操作时就会出现问题,比如:
线程A在读取结果之前,B对同一个连接进行操作,可能会出现A因为B的访问停止读取结果,B又因为数据库特定的流程(需要把mysql_store_result操作执行完,拿到结果集)而在等待A结束,从而导致假死锁。通过互斥锁将连接_mysql这个临界资源(涉及多个_mysql操作才需要,一个操作本身是线程安全的)保护起来:当A获取结果集时,B不能访问_mysql,就不会出现多个线程同时访问同一个临界资源的情况。
功能
- 注册时新增用户
- 进行登录验证,并返回详细的用户信息
- 通过用户名获取用户信息
- 通过用户id获取用户信息
- 胜利时天梯分数增加,战斗场次增加,胜利场次增加
- 失败时天梯分数减少,战斗场次增加
代码实现
#ifndef DB_HPP_
#define DB_HPP_
#include "Util.hpp"
#include <mutex>
#include <cassert>
class user_table
{
private:
MYSQL *_mysql;
std::mutex _mutex; // 涉及多个mysql操作需要加锁
public:
user_table(const std::string &host,
const std::string &username,
const std::string &passwd,
const std::string &db,
int port)
{
_mysql = mysql_util::mysql_create(host, username, passwd, db, port);
assert(_mysql != nullptr);
};
~user_table()
{
mysql_util::mysql_destroy(_mysql);
};
// 注册时新增用户
bool insert(Json::Value &user) // 用Json::Value减少参数压栈
{
#define USER_INSERT "insert user values(null, '%s', MD5('%s'), 1000, 0, 0);"
if (user["username"].isNull() || user["password"].isNull())
{
ELOG("username or password is incomplete!");
return false;
}
char sql[4096] = {0};
sprintf(sql, USER_INSERT, user["username"].asCString(), user["password"].asCString());
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
ELOG("insert user failed!");
return false;
}
return true;
}
// 登录验证,并返回详细的用户信息
bool login(Json::Value &user)
{
#define USER_LOGIN "select id, score, total_count, win_count from user where username = '%s' and password = MD5('%s');"
if (user["username"].isNull() || user["password"].isNull())
{
ELOG("username or password is incomplete!");
return false;
}
char sql[4096] = {0};
sprintf(sql, USER_LOGIN, user["username"].asCString(), user["password"].asCString());
MYSQL_RES *res;
{
std::unique_lock<std::mutex> lock(_mutex);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
ELOG("login user failed!");
return false;
}
// 获取结果集
res = mysql_store_result(_mysql);
if (res == nullptr) // 读取结果集出错(内存不足/连接错误)
{
DLOG("Failed to retrieve result set: %s\n", mysql_error(_mysql));
}
}
int row_num = mysql_num_rows(res);
// 登录查询结果只能一行
if (row_num != 1)
{
DLOG("Invalid username or password!");
mysql_free_result(res);
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
user["id"] = (Json::UInt64)std::stol(row[0]);
user["score"] = (Json::UInt64)std::stol(row[1]);
user["total_count"] = std::stoi(row[2]);
user["win_count"] = std::stoi(row[3]);
mysql_free_result(res);
return true;
}
// 通过用户名获取用户信息
bool select_by_name(const std::string &name, Json::Value &user)
{
#define SEL_BY_NAME "select id, score, total_count, win_count from user where username = '%s';"
char sql[4096] = {0};
sprintf(sql, SEL_BY_NAME, name.c_str());
MYSQL_RES *res;
{
std::unique_lock<std::mutex> lock(_mutex);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
ELOG("select user by name failed!");
return false;
}
// 获取结果集
res = mysql_store_result(_mysql);
if (res == nullptr) // 读取结果集出错(内存不足/连接错误)
{
DLOG("Failed to retrieve result set: %s\n", mysql_error(_mysql));
}
}
int row_num = mysql_num_rows(res);
// 登录查询结果只能一行
if (row_num != 1)
{
DLOG("Invalid username");
mysql_free_result(res);
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
user["id"] = (Json::UInt64)std::stol(row[0]);
user["score"] = (Json::UInt64)std::stol(row[1]);
user["total_count"] = std::stoi(row[2]);
user["win_count"] = std::stoi(row[3]);
user["username"] = name;
mysql_free_result(res);
return true;
}
// 通过用户id获取用户信息
bool select_by_id(uint64_t id, Json::Value &user)
{
#define SEL_BY_ID "select username, score, total_count, win_count from user where id = %lu;"
char sql[4096] = {0};
sprintf(sql, SEL_BY_ID, id);
MYSQL_RES *res;
{
std::unique_lock<std::mutex> lock(_mutex);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
ELOG("select user by id failed!");
return false;
}
// 获取结果集
res = mysql_store_result(_mysql);
if (res == nullptr) // 读取结果集出错(内存不足/连接错误)
{
DLOG("Failed to retrieve result set: %s\n", mysql_error(_mysql));
}
}
int row_num = mysql_num_rows(res);
// 登录查询结果只能一行
if (row_num != 1)
{
DLOG("Invalid username");
mysql_free_result(res);
return false;
}
MYSQL_ROW row = mysql_fetch_row(res);
user["username"] = row[0];
user["score"] = (Json::UInt64)std::stol(row[1]);
user["total_count"] = std::stoi(row[2]);
user["win_count"] = std::stoi(row[3]);
user["id"] = id;
mysql_free_result(res);
return true;
}
// 胜利时天梯分数增加,战斗场次增加,胜利场次增加
bool win(uint64_t id)
{
#define WIN_UPDATE "update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id = %lu;"
char sql[4096] = {0};
sprintf(sql, WIN_UPDATE, id);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
ELOG("update user win failed!");
return false;
}
return true;
}
// 失败时天梯分数减少,战斗场次增加
bool lose(uint64_t id)
{
#define LOSE_UPDATE "update user set score=score-30, total_count=total_count+1 where id = %lu;"
char sql[4096] = {0};
sprintf(sql, LOSE_UPDATE, id);
bool ret = mysql_util::mysql_exec(_mysql, sql);
if (ret == false)
{
ELOG("update user lose failed!");
return false;
}
return true;
}
};
#endif
&spm=1001.2101.3001.5002&articleId=157914595&d=1&t=3&u=8b0079ce562c4785b6e602fe337fe8b0)
1万+

被折叠的 条评论
为什么被折叠?



