news 2026/5/15 8:02:29

【C++ 在线五子棋对战】 - 数据库用户表管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【C++ 在线五子棋对战】 - 数据库用户表管理

一、模块概述

数据库模块负责用户数据的持久化存储,包括用户注册、登录验证、信息查询和战绩更新。本模块包含两个文件:

文件功能说明
db.sql数据库建表脚本 — 定义用户表结构
db.hpp用户表操作类 — 封装所有数据库增删改查操作

模块依赖关系:

logger.hpp(日志) ↓ util.hpp(mysql_util / json_util) ↓ db.hpp(本模块,用户表 CRUD 操作) ↓ room.hpp / matcher.hpp / server.hpp(上层模块通过 db.hpp 操作用户数据)

二、数据库表设计 — db.sql

2.1 完整建表脚本

dropdatabaseifexistsgobang;createdatabaseifnotexistsgobang;usegobang;createtableifnotexistsuser(idintprimarykeyauto_increment,usernamevarchar(32)uniquekeynotnull,passwordvarchar(128)notnull,scoreint,total_countint,win_countint);

2.2 字段说明

字段类型约束说明
idint主键,自增用户唯一标识
usernamevarchar(32)唯一,非空用户名,不可重复
passwordvarchar(128)非空密码,存储 MD5 哈希值
scoreint天梯分数,注册默认 1000
total_countint总对战场次,注册默认 0
win_countint胜利场次,注册默认 0

2.3 设计要点

  • auto_increment:用户 ID 由数据库自动分配,从 1 递增,避免并发冲突
  • unique key:用户名唯一约束,数据库层面保证不会出现重复注册
  • password长度 128:存储 MD5 哈希后的密码(32 字符十六进制),留足余量
  • score默认 1000:新注册用户从 1000 分起步,匹配系统以此划分段位

三、用户表操作类 — db.hpp

3.1 类结构总览

classuser_table{private:MYSQL*_mysql;// MySQL 操作句柄std::mutex _mutex;// 互斥锁,保护数据库并发访问public:user_table(host,username,password,dbname,port);// 构造:建立连接~user_table();// 析构:关闭连接boolinsert(Json::Value&user);// 注册:新增用户boollogin(Json::Value&user);// 登录:验证账号密码,返回用户信息boolselect_by_name(name,user);// 按用户名查询boolselect_by_id(id,user);// 按用户ID查询boolwin(uint16_tid);// 胜利:分数+30,场次+1,胜场+1boollose(uint16_tid);// 失败:分数-30,场次+1};

3.2 常见问题

互斥锁与读写锁有啥区别?

  • 互斥锁:任意时刻只允许一个线程访问共享资源,不管是读操作还是写操作,全部互斥串行执行。
  • 读写锁:分为读与读;读与写,写与写。 前者可以同时共享访问,不互斥; 后两者互斥排他,同一时间只能有一个写线程。

为什么本项目选着互斥锁?

  • 共享资源写操作频繁、读写比例差不多;
  • 业务操作执行时间很短,没必要做读并行优化;
  • 要求代码简单、逻辑不容易出错、避免写饥饿问题;
  • 强时序业务(如游戏落子、状态流转)必须串行,不能并发读。

什么时候用读写锁?

  • 读多写极少,大量线程频繁读、很少修改;
  • 读操作耗时较长,希望多线程同时读来提升性能;
  • 允许写操作偶尔等待,不要求即时抢占执行。

3.3 构造与析构

user_table(conststd::string&host,conststd::string&username,conststd::string&password,conststd::string&dbname,uint16_tport=3306){_mysql=mysql_util::mysql_create(host,username,password,dbname,port);assert(_mysql!=NULL);// 连接失败直接断言终止}~user_table(){mysql_util::mysql_destroy(_mysql);_mysql=NULL;}

在util工具类中创建一个Mysql需要传入的数据

  • 构造时通过mysql_util::mysql_create建立数据库连接,assert保证连接必须成功
  • 析构时通过mysql_util::mysql_destroy释放连接资源
  • 整个程序生命周期内只有一个连接,通过互斥锁保护并发访问

3.3 用户注册 — insert

boolinsert(Json::Value&user){#defineINSERT_USER"insert into user values(null, '%s', MD5('%s'), 1000, 0, 0);"if(user["username"].isNull()||user["password"].isNull()){DLOG("INPUT PASSWORD OR USERNAME");returnfalse;}charsql[4096]={0};sprintf(sql,INSERT_USER,user["username"].asCString(),user["password"].asCString());std::unique_lock<std::mutex>lock(_mutex);boolret=mysql_util::mysql_exec(_mysql,sql);if(ret==false){DLOG("insert user info failed!!\n");returnfalse;}returntrue;}

解析:

  • 入参校验:检查用户名和密码字段是否为空,防止插入无效数据

  • SQL 模板insert into user values(null, '%s', MD5('%s'), 1000, 0, 0)

    • null— ID 自动递增
    • MD5('%s')— 密码在 SQL 层面直接做 MD5 哈希,数据库中存储的是哈希值而非明文(密码加密)
    • 1000, 0, 0— 新用户默认分数 1000,场次 0,胜场 0

  • sprintf 拼接 SQL:将用户名和密码填入模板,生成完整的 SQL 语句

    • sprintf 只吃 C 字符串,C++ 字符串它不认,所以必须手动转。
  • 注册失败通常是因为用户名重复(unique key约束)

3.4 用户登录 — login

boollogin(Json::Value&user){#defineLOGIN_USER"select id, score, total_count, win_count from user where username='%s' and password=MD5('%s');"charsql[4096]={0};sprintf(sql,LOGIN_USER,user["username"].asCString(),user["password"].asCString());MYSQL_RES*res=NULL;{std::unique_lock<std::mutex>lock(_mutex);boolret=mysql_util::mysql_exec(_mysql,sql);if(ret==false){returnfalse;}res=mysql_store_result(_mysql);if(res==NULL){returnfalse;}}introw_num=mysql_num_rows(res);if(row_num!=1){returnfalse;}MYSQL_ROW row=mysql_fetch_row(res);user["id"]=std::stol(row[0]);user["score"]=std::stol(row[1]);user["total_count"]=std::stol(row[2]);user["win_count"]=std::stol(row[3]);mysql_free_result(res);returntrue;}

解析:

登录是本模块最复杂的方法,核心流程分为三步:

第一步 — 执行查询:

SQL 语句同时以用户名和 MD5 后的密码作为查询条件。如果用户名或密码任一不匹配,查询结果为空,登录失败。

第二步 — 加锁获取结果集:

std::unique_lock<std::mutex>lock(_mutex);boolret=mysql_util::mysql_exec(_mysql,sql);res=mysql_store_result(_mysql);

这是本模块线程安全的关键。因为多个线程可能同时调用 login、select 等方法共享同一个_mysql连接,MySQL C API 的mysql_store_result必须紧跟在mysql_query之后,中间不能插入其他查询,否则结果集会错乱。所以用_mutex将「执行查询 + 获取结果」作为原子操作保护起来。

只锁【发送 SQL + 获取结果】这两步。

原因:

  1. MySQL 连接必须独占
  2. 拿到结果集 res 之后,就和 MySQL 连接无关了
  3. 锁范围越小,多线程性能越好,不卡玩家

第三步 — 解析结果行:

introw_num=mysql_num_rows(res);// 获取匹配的行数if(row_num!=1){returnfalse;}// 必须恰好一行,0=账号密码错误,>1=数据异常MYSQL_ROW row=mysql_fetch_row(res);// 取出第一行数据user["id"]=std::stol(row[0]);// row[0] = iduser["score"]=std::stol(row[1]);// row[1] = score// ...mysql_free_result(res);// 释放结果集内存
  • MYSQL_ROW本质是char**row[0]row[1]分别对应 SELECT 中的第 1、2 个字段
  • stol把字符串(char /string)转换成 长整型数字(long)
  • 查询结果写入传入的Json::Value对象,供上层模块使用
  • mysql_free_result释放结果集占用的内存,忘记调用会导致内存泄漏

3.5 按用户名查询 — select_by_name

boolselect_by_name(conststd::string&name,Json::Value&user){#defineUSER_BY_NAME"select id, score, total_count, win_count from user where username='%s';"// ... 与 login 类似的加锁查询流程user["id"]=std::stol(row[0]);user["username"]=name;user["score"]=std::stol(row[1]);user["total_count"]=std::stol(row[2]);user["win_count"]=std::stol(row[3]);mysql_free_result(res);returntrue;}

login的区别:

  • 只按用户名查询,不验证密码
  • 结果中额外包含username字段(login 中调用者已知道用户名,不需要返回)

使用场景:匹配模块需要根据用户名获取分数来分配匹配队列。

3.6 按用户ID查询 — select_by_id

// 通过用户id获取用户信息boolselect_by_id(uint16_tid,Json::Value&user){#defineUSER_BY_ID"select username, score, total_count, win_count from user where id='%d';"charsql[4096]={0};sprintf(sql,USER_BY_ID,id);// 存放数据库查询结果MYSQL_RES*res=NULL;// 加锁:保护MySQL连接,多线程安全{std::unique_lock<std::mutex>lock(_mutex);boolret=mysql_util::mysql_exec(_mysql,sql);if(ret==false){DLOG("get user by id failed!!\n");returnfalse;}// 获取结果集res=mysql_store_result(_mysql);if(res==NULL){DLOG("have no user info!!");returnfalse;}}// 必须只能查到 1 条数据(id是主键,唯一)introw_num=mysql_num_rows(res);if(row_num!=1){DLOG("the user information queried is not unique!!");mysql_free_result(res);returnfalse;}// 解析数据,存入 user 对象MYSQL_ROW row=mysql_fetch_row(res);user["id"]=id;user["username"]=row[0];user["score"]=std::stol(row[1]);user["total_count"]=std::stol(row[2]);user["win_count"]=std::stol(row[3]);// 释放数据库结果集内存mysql_free_result(res);returntrue;}

select_by_name结构相同,区别是以id作为查询条件,SELECT 的第一个字段变为username

使用场景:匹配模块根据 WebSocket 连接对应的用户 ID 获取分数和用户名。

3.7 胜负记录更新 — win / lose

// 胜利:分数+30,总场次+1,胜场+1boolwin(uint16_tid){#defineUSER_WIN"update user set score=score+30, total_count=total_count+1, win_count=win_count+1 where id=%d;"charsql[4096]={0};sprintf(sql,USER_WIN,id);std::unique_lock<std::mutex>lock(_mutex);boolret=mysql_util::mysql_exec(_mysql,sql);if(ret==false){returnfalse;}returntrue;}// 失败:分数-30,总场次+1boollose(uint16_tid){#defineUSER_LOSE"update user set score=score-30, total_count=total_count+1 where id=%d;"charsql[4096]={0};sprintf(sql,USER_LOSE,id);std::unique_lock<std::mutex>lock(_mutex);boolret=mysql_util::mysql_exec(_mysql,sql);if(ret==false){returnfalse;}returntrue;}

解析:

  • 使用 SQL 的自增/自减语法(score=score+30),无需先查询再更新,一条语句原子完成
  • 胜利和失败都会增加total_count,只有胜利增加win_count
  • 每局分值变化为+30/-30,影响用户段位划分(匹配模块以此分三档:<2000 / 2000~3000 / ≥3000)

使用场景:房间模块判定胜负后调用,在room.hpphandle_request中:

if(json_resp["winner"].asUInt64()!=0){uint64_twinner_id=json_resp["winner"].asUInt64();uint64_tloser_id=winner_id==_white_id?_black_id:_white_id;_tb_user->win(winner_id);_tb_user->lose(loser_id);_status=GAME_OVER;}

四、设计总结

设计点实现方式
密码安全SQL 层 MD5() 哈希,数据库不存明文
用户名唯一数据库 unique key 约束
并发安全std::mutex 保护 exec + store_result 的原子性
结果集管理mysql_free_result 防止内存泄漏
数据传递统一使用 Json::Value 作为数据容器,与上层模块一致
默认初始值注册时 SQL 中写死 1000/0/0,无需额外代码

五、回顾

  • 本模块基于底层util.hpp的MySQL工具接口,完成五子棋项目用户数据的完整持久化管理;
  • 通过db.sql脚本初始化用户数据表,利用主键、唯一约束保证数据规范。
  • db.hpp封装了用户注册登录校验、按用户名与用户ID查询用户信息、对局胜负战绩分数更新等全套数据库CRUD操作。
  • 模块采用互斥锁保障多线程下共用MySQL连接的并发安全,密码通过SQL层MD5哈希存储保障安全,同时规范管理MySQL结果集避免内存泄漏;
  • 统一以Json::Value作为数据容器和上层模块完成数据交互。

一句话总结:本数据库模块实现了用户数据存储、查询、校验与战绩更新全流程,兼顾数据安全、线程安全,为匹配、房间、对战等上层业务提供稳定的数据支撑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/15 8:01:23

无损精准查缆:鼎讯 G-340A 在铁路高速场景的应用

铁路与高速沿线光缆密集敷设、环境复杂&#xff0c;人工识别目标光缆难度大、效率低&#xff0c;还易损伤在用线路。鼎讯 G-340A 光缆普查仪&#xff08;敲缆仪&#xff09;依托光纤弹光效应&#xff0c;以无损探测、智能便捷的优势&#xff0c;成为铁路高速光缆区分、故障排查…

作者头像 李华
网站建设 2026/5/15 8:01:20

基于Whisper与LLM的视频自动字幕生成与摘要项目实战

1. 项目概述&#xff1a;当AI遇见视频&#xff0c;自动生成字幕与摘要 最近在折腾一个挺有意思的项目&#xff0c;叫 vidscribe 。简单来说&#xff0c;这是一个利用人工智能技术&#xff0c;自动为视频生成字幕&#xff08;SRT文件&#xff09;和内容摘要的工具。如果你像我…

作者头像 李华
网站建设 2026/5/15 7:59:15

单元式幕墙分类及特点

单元式幕墙分类及特点 "单元式幕墙"主要可分为:"单元式幕墙"和"半单元式幕墙"又称坚挺单元式幕墙,半单元式幕墙详分又可分为:立挺分片单元组合式幕墙,窗间墙单元式幕墙。 上述单元式幕墙分类有所不同,但其基本原理完全一致。它和框架式幕…

作者头像 李华
网站建设 2026/5/15 7:57:10

溶剂可及性实战:从DSSP安装到Biopython批量处理

1. 溶剂可及性计算入门指南 第一次接触溶剂可及性计算时&#xff0c;我被那些专业术语搞得一头雾水。简单来说&#xff0c;溶剂可及性&#xff08;Solvent Accessible Surface Area, SASA&#xff09;描述的是蛋白质分子中每个氨基酸残基暴露在溶剂中的程度。这个参数对理解蛋白…

作者头像 李华
网站建设 2026/5/15 7:57:05

本地令牌管理工具tokrepo-cli:安全加密与开发者工作流集成实践

1. 项目概述&#xff1a;一个专为开发者设计的本地令牌管理工具如果你和我一样&#xff0c;日常开发中需要和一堆API密钥、访问令牌、配置文件打交道&#xff0c;那你肯定也经历过这样的混乱&#xff1a;项目A的密钥放在环境变量里&#xff0c;项目B的配置写在某个JSON文件里&a…

作者头像 李华