首页 / 资讯中心 / 文章详情

【在线五子棋对战】数据管理模块实现

【在线五子棋对战】数据管理模块实现 ★ FEATURED ARTICLE
目录表结构RegisterLoginSelectByNameSelectByIdSelectOneWInLose数据管理模块用于对登录与注册用户进行管理这里采用以表为单位每个可操作的表为一个类构建该表的接口表结构用于存储用户的表为gomoku数据库下的user表结构如下mysql show create table user\G *************************** 1. row *************************** Table: user Create Table: CREATE TABLE user ( id int unsigned NOT NULL AUTO_INCREMENT COMMENT 编号, username varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs NOT NULL COMMENT 昵称, password_hash char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT SHA-256密码哈希值32字节64位十六进制字符, ladder_points int NOT NULL DEFAULT 1000 COMMENT 天梯积分, number_of_sessions int unsigned NOT NULL DEFAULT 0 COMMENT 场数, win_count int unsigned NOT NULL DEFAULT 0 COMMENT 胜利场次, registration_date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, salt char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 16字节随机盐的十六进制表示, PRIMARY KEY (id) USING BTREE, UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT9 DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci mysql desc user; ----------------------------------------------------------------------------------- | Field | Type | Null | Key | Default | Extra | ----------------------------------------------------------------------------------- | id | int unsigned | NO | PRI | NULL | auto_increment | | username | varchar(32) | NO | UNI | NULL | | | password_hash | char(64) | NO | | NULL | | | ladder_points | int | NO | | 1000 | | | number_of_sessions | int unsigned | NO | | 0 | | | win_count | int unsigned | NO | | 0 | | | registration_date | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | | salt | char(32) | NO | | NULL | | -----------------------------------------------------------------------------------在实用工具模块--密码模块的实现中采用SHA-256 随机盐的方法其中SHA-256结果为32字节(256BIT)盐设定的是16字节// 生成随机盐 bool Password::GenerateSalt(std::string salt) { constexpr size_t SALT_LEN 16; // 编译期常量确认随机盐的长度 16 // ...... }如果用字符数组存储单位长度为1字节SHA-256占32长度盐占16位长度但在密码模块的实现中最后将SHA-256和盐为了传输方便与直观转换成了十六进制字符串2位十六进制长度1字节因此32字节需要64长度password_hash16字节需要32长度salt因此代码要做的工作就是对该表操作的接口封装起来class User_Table{}/* -------------------------------- db.hpp |数据管理模块| MySQL的客户端用于对表进行操作 每个表的操作为一个类 -------------------------------- */ #pragma once #include Util.hpp #include Password.hpp #include mutex #include cassert #include memory #include mutex class User_Table { public: User_Table(const std::string host, const std::string user, const std::string passwd, const std::string db, uint16_t port 3306) { // 初始化句柄 _mysql MySQL_Util::MySQL_Init(host, user, passwd, db, port); assert(_mysql ! nullptr); } ~User_Table() { if (_mysql ! nullptr) { MySQL_Util::MySQL_Destroy(_mysql); _mysql nullptr; } } bool Register(const Json::Value user); // 注册(新增)用户 bool Login(Json::Value user); // 登录验证user返回用户信息 bool SelectByName(const std::string username, Json::Value user); // 通过用户名获取用户信息 bool SelectById(const int id, Json::Value user); // 通过编号获取用户信息 bool Win(const int id); // 胜利时操作 bool Lose(const int id); // 失败时操作 bool SelectOne(const std::string sql, Json::Value user); // 执行只会返回一条记录的Select语句 private: MYSQL *_mysql; // 句柄 std::mutex _mutex; // 互斥锁保证每个接口都是线程安全的 // 虽然mysql的原生接口都是线程安全但封装之后是多个接口一起.... };这里需要采用互斥锁std::mutex确保线程安全保证同时只有一套流程在调用_mysql句柄因为同一个MYSQL*连接句柄被多个业务线程共享。例如现在有两个进程同时调用mysql_query()并获取结果集mysql_store_result()如果顺序是这样的A: query(SQL_A) B: query(SQL_B) A: store_result() B: store_result()就会导致A和B的结果集都是SQL_B的而加锁后就可以确保顺序是A先查询获取结果集B再查询获取结果集A: query A: store_result B: query B: store_resultRegister当有新用户注册时需要用Register接口将字段写入数据库bool User_Table::Register(const Json::Value user) // 注册(新增)用户 { Json::Value root; if (SelectByName(user[username].asString(), root)) { LOG(LogLv::ERROR, 用户名[%s]重复请重新输入, user[username].asCString()); return false; } constexpr char INSERT_SQL[] INSERT INTO user(username, password_hash, salt) VALUES (%s, %s,%s); bool success false; // 如果在后面的do while中没有设置成true就是false do { // 生成随机盐 std::string salt; if (Password::GenerateSalt(salt) false) break; // 生成哈希摘要 std::string hash; if (Password::Hash(user[password].asString(), salt, hash) false) break; // 为数据表user新增用户 std::string sql; // 要插入的sql sql.resize(4096); // 预留好空间防止段错误 // 将用户名、密码哈希、盐写入sql [SQL注入问题] int len snprintf(sql.data(), sql.size(), INSERT_SQL, user[username].asCString(), hash.c_str(), salt.c_str()); if (len 0 || static_castsize_t(len) sql.size()) { LOG(LogLv::ERROR, 格式化打印发生错误或截断); break; } { std::lock_guardstd::mutex lock(_mutex); // 防止多个线程同时调用mysql_query() (操作_mysql句柄) // 执行SQL语句 if (MySQL_Util::MySQL_Query(_mysql, sql.c_str()) false) break; } success true; } while (false); return success; }这里的MySQL_Query就需要加锁防止多个线程同时调用查询、同时对_mysql句柄进行操作LoginLogin接口用于对登录的用户名和密码进行验证bool User_Table::Login(Json::Value user) // 登录验证user返回用户信息 { // 输入检查 if (!(user.isMember(username) user.isMember(password))) { LOG(LogLv::ERROR, 提交参数不全([username][password])); return false; } Json::Value baseinfo; // 用于存数据库内的信息 if (SelectByName(user[username].asString(), baseinfo) false) // 查找数据库内的该用户信息 { LOG(LogLv::ERROR, 没有[%s]用户请先注册, user[username].asCString()); return false; } // 验证用户输入的密码哈希与数据库内哈希是否一致 if (Password::Verify(user[password].asString(), baseinfo[salt].asString(), baseinfo[password_hash].asString()) true) { user baseinfo; // 不要将密码和盐传给前端 user.removeMember(password_hash); user.removeMember(salt); return true; } else { LOG(LogLv::WARNING, 用户[%s]密码错误, user[username].asCString()); return false; } }SelectByName通过username返回该用户其他信息bool User_Table::SelectByName(const std::string username, Json::Value user) // 通过用户名获取用户信息 { constexpr char SELECT_SQL[] SELECT * FROM user WHERE username%s; bool success false; std::string sql; // 要执行的sql sql.resize(4096); // 预留好空间防止段错误 int len snprintf(sql.data(), sql.size(), SELECT_SQL, username.c_str()); if (len 0 || static_castsize_t(len) sql.size()) { LOG(LogLv::ERROR, 格式化打印发生错误或截断); return false; } return SelectOne(sql, user); }SelectById通过id返回用户其他信息bool User_Table::SelectById(const int id, Json::Value user) // 通过编号获取用户信息 { constexpr char SELECT_SQL[] SELECT * FROM user WHERE id%d; std::string sql; // 要执行的sql sql.resize(4096); // 预留好空间防止段错误 int len snprintf(sql.data(), sql.size(), SELECT_SQL, id); if (len 0 || static_castsize_t(len) sql.size()) { LOG(LogLv::ERROR, 格式化打印发生错误或截断); return false; } return SelectOne(sql, user); }SelectOne上面这两个接口都依赖于SelectOne该接口用于通过传入的SQL返回一条记录Json格式这里通过智能指针unique_ptr管理MYSQL_RES*对象防止内存泄漏由于该对象的释放必须通过mysql_free_result()而unique_ptr默认调用delete因此需要自定义删除器MYSQL_RES_Deleternamespace // 匿名命名空间仅供db.cpp使用 { struct MYSQL_RES_Deleter // MYSQL_RES删除器 { void operator()(MYSQL_RES *res) const { if (res ! nullptr) mysql_free_result(res); } }; }通过指定unique_ptrMYSQL_RES, MYSQL_RES_Deleter来指定删除器bool User_Table::SelectOne(const std::string sql, Json::Value user) // 通过sql返回一条记录 { std::unique_ptrMYSQL_RES, MYSQL_RES_Deleter res; { std::lock_guardstd::mutex lock(_mutex); // 对_mysql句柄加锁 // 查询记录 if (!MySQL_Util::MySQL_Query(_mysql, sql.c_str())) return false; // SQL执行成功获取查询结果 // 存储结果集 res.reset(mysql_store_result(_mysql)); if (res nullptr) { LOG(LogLv::ERROR, 存储结果集失败错误码%d错误描述%s, mysql_errno(_mysql), mysql_error(_mysql)); return false; } } // 获取行列数 my_ulonglong rows mysql_num_rows(res.get()); // 正常情况用户不存在比如注册时查重或者输错用户名登录 if (rows 0) { LOG(LogLv::DEBUG, 查询结果为0条); return false; } // 异常情况 if (rows 1) { LOG(LogLv::ERROR, 查询结果记录数异常共[%llu]条, rows); return false; } my_ulonglong fields mysql_num_fields(res.get()); // 获取该记录 MYSQL_ROW row mysql_fetch_row(res.get()); MYSQL_FIELD *field mysql_fetch_fields(res.get()); // 获取该记录所有字段名方便后面进行类型赋值 if (row nullptr || field nullptr) { LOG(LogLv::ERROR, 获取查询结果失败); return false; } user.clear(); // 清除user以前的数据 // 通过类型判断将字段传入user for (my_ulonglong i 0; i fields; i) { if(row[i] nullptr) { user[field[i].name] Json::nullValue; continue; } switch (field[i].type) { case MYSQL_TYPE_TINY: case MYSQL_TYPE_SHORT: case MYSQL_TYPE_LONG: case MYSQL_TYPE_INT24: // 整型字段传整数 user[field[i].name] std::stoi(row[i]); break; case MYSQL_TYPE_LONGLONG: // 长整型字段传长整数 user[field[i].name] Json::Int64(std::stoll(row[i])); break; case MYSQL_TYPE_FLOAT: // 浮点型字段传浮点数 user[field[i].name] std::stof(row[i]); break; case MYSQL_TYPE_DOUBLE: // double字段传double user[field[i].name] std::stod(row[i]); break; default: // 字符串数据 user[field[i].name] row[i]; break; } } return true; }这里将MySQL_Query()与mysql_store_result()加锁保护起来防止在执行完查询后还没拿到结果集就去执行其他_mysql句柄操作该接口为 MySQL C API 常用的类型做了选择器传入正确的JSON字段WIn当用户对战胜利时对数据库的修改操作例如增加天梯积分、战斗场次、胜利场次bool User_Table::Win(const int id) // 胜利时操作 { // 增加天梯积分(30)、战斗场次增加、胜利场次增加 constexpr char UPDATE_SQL[] UPDATE user SET ladder_points ladder_points30, number_of_sessions number_of_sessions 1, win_count win_count 1 WHERE id %d; std::string sql; // 要执行的sql sql.resize(4096); // 预留空间防止空间不足段错误因为下面用的是原始指针不会自己扩容 bool success false; // 若走到最后success则为true do { int len snprintf(sql.data(), sql.size(), UPDATE_SQL, id); if (len 0 || static_castsize_t(len) sql.size()) { LOG(LogLv::ERROR, 格式化打印发生错误或截断); break; } { std::lock_guardstd::mutex lock(_mutex); // 防止多个线程同时调用mysql_query() (操作_mysql句柄) // 执行查询 if (MySQL_Util::MySQL_Query(_mysql, sql.c_str()) false) break; } success true; } while (false); return success; }Lose当用户对战失败时对数据库的修改操作例如减少天梯积分增加战斗场次bool User_Table::Lose(const int id) // 失败时操作 { // 减少天梯积分(-30)、战斗场次增加 constexpr char UPDATE_SQL[] UPDATE user SET ladder_points ladder_points-30, number_of_sessions number_of_sessions 1 WHERE id %d; std::string sql; // 要执行的sql sql.resize(4096); // 预留空间防止空间不足段错误因为下面用的是原始指针不会自己扩容 bool success false; // 若走到最后success则为true do { int len snprintf(sql.data(), sql.size(), UPDATE_SQL, id); if (len 0 || static_castsize_t(len) sql.size()) { LOG(LogLv::ERROR, 格式化打印发生错误或截断); break; } { std::lock_guardstd::mutex lock(_mutex); // 防止多个线程同时调用mysql_query() (操作_mysql句柄) // 执行查询 if (MySQL_Util::MySQL_Query(_mysql, sql.c_str()) false) break; } success true; } while (false); return success; }
阅读完成 · 觉得有帮助?
咨询建站