当前位置: 首页 > news >正文

QT聊天项目DAY11

1. 验证码服务

1.1 用npm安装redis

npm install redis

1.2 修改config.json配置文件

1.3 新建redis.js

const config_module = require('./config')
const Redis = require("ioredis");// 创建Redis客户端实例
const RedisCli = new Redis({host: config_module.redis_host,             // Redis服务器主机名port: config_module.redis_port,             // Redis服务器端口号password: config_module.redis_passwd,       // Redis密码
});/*** 监听错误信息*/
RedisCli.on("error", function (err) {console.log("RedisCli connect error");RedisCli.quit();
});/*** 根据key获取value* @param {*} key * @returns */
async function GetRedis(key) {try {const result = await RedisCli.get(key)if (result === null) {console.log('result:', '<' + result + '>', 'This key cannot be find...')return null}console.log('Result:', '<' + result + '>', 'Get key success!...');return result} catch (error) {console.log('GetRedis error is', error);return null}
}/*** 根据key查询redis中是否存在key* @param {*} key * @returns */
async function QueryRedis(key) {try {const result = await RedisCli.exists(key)//  判断该值是否为空 如果为空返回nullif (result === 0) {console.log('result:<', '<' + result + '>', 'This key is null...');return null}console.log('Result:', '<' + result + '>', 'With this value!...');return result} catch (error) {console.log('QueryRedis error is', error);return null}
}/*** 设置key和value,并过期时间* @param {*} key * @param {*} value * @param {*} exptime * @returns */
async function SetRedisExpire(key, value, exptime) {try {// 设置键和值await RedisCli.set(key, value)// 设置过期时间(以秒为单位)await RedisCli.expire(key, exptime);return true;} catch (error) {console.log('SetRedisExpire error is', error);return false;}
}/*** 退出函数*/function Quit() {RedisCli.quit();
}
module.exports = { GetRedis, QueryRedis, Quit, SetRedisExpire, }

1.4 修改server.js

添加redis的库,获取验证码之前查询redis,没查到生成uid并且写入redis

async function GetVerifyCode(call, callback) {console.log("email is ", call.request.email)/* 先查询redis中是否有记录的邮箱对应的uuid */let query_res = await redis_module.GetRedis(const_module.code_prefix + call.request.email);         // code_**@163.comconsole.log("query_res is ", query_res);let uniqueId = query_res ? query_res : null;if (uniqueId) {uniqueId = uuidv4();                                                                            // 生成新的uuidif (uniqueId.length > 4)uniqueId = uniqueId.substring(0, 4);}/* 如果redis中没有记录,则生成新的uuid并存入redis */let bres = await redis_module.SetRedisExpire(const_module.code_prefix + call.request.email, uniqueId, 600);if (!bres) {callback(null,{email: call.request.email,error: const_module.Errors.RedisError});return;}try {console.log("uniqueId is ", uniqueId)let text_str = '您的验证码为' + uniqueId + '请三分钟内完成注册'//发送邮件let mailOptions = {from: '13083361602@163.com',to: call.request.email,subject: '=?UTF-8?B?' + Buffer.from('验证码').toString('base64') + '?=',text: text_str,};let send_res = await emailModule.SendMail(mailOptions);console.log("send res is ", send_res)callback(null, {email: call.request.email,error: const_module.Errors.Success});} catch (error) {console.log("catch error is ", error)callback(null, {email: call.request.email,error: const_module.Errors.Exception});}
}

1.5 验证是否正确发送

首先启动redis服务

.\redis-server.exe .\redis.windows.conf

启动grpc服务器

npm run serve

安装ioredis

npm install ioredis

启动QT客户端

启动C++服务器

2. 注册服务

2.1 新增QT客户端确认按钮的槽函数

/* 确认按钮点击 */
void RegisterWidget::OnConfirmButtonClicked()
{if (ui.User_Edit->text() == ""){ShowTipLabel(QString::fromLocal8Bit("用户名不能为空"), "error");return;}if (ui.Email_Edit->text() == ""){ShowTipLabel(QString::fromLocal8Bit("邮箱不能为空"), "error");return;}if (ui.PassWord_Edit->text() == ""){ShowTipLabel(QString::fromLocal8Bit("密码不能为空"), "error");return;}if (ui.Enter_Edit->text() == ""){ShowTipLabel(QString::fromLocal8Bit("确认密码不能为空"), "error");return;}if (ui.PassWord_Edit->text() != ui.Enter_Edit->text()){ShowTipLabel(QString::fromLocal8Bit("两次密码输入不一致"), "error");return;}if (ui.Verify_Edit->text() == ""){ShowTipLabel(QString::fromLocal8Bit("验证码不能为空"), "error");return;}// 发送http请求注册用户QJsonObject jsonObj;jsonObj["user"] = ui.User_Edit->text();jsonObj["email"] = ui.Email_Edit->text();jsonObj["password"] = ui.PassWord_Edit->text();jsonObj["EnterPassword"] = ui.Enter_Edit->text();jsonObj["verify_code"] = ui.Verify_Edit->text();QString urlStr = "http://" + ConfigSettings->value("GateServer/host").toString() + ":" + ConfigSettings->value("GateServer/port").toString() + "//"+ ConfigSettings->value("GateServer/ConfirmUser").toString();QUrl url(urlStr);HttpManager::Instance()->PostHttpReq(url, jsonObj,ReqID::ID_REG_USER,Modules::REGISTERMOD);
}

2.2 绑定注册逻辑完成对应的回调函数

void RegisterWidget::InitHttpHandlers()
{// 注册获取验证码回包的逻辑_handlers.insert(ReqID::ID_GET_VARIFY_CODE, [this](const QJsonObject& jsonObj){int error = jsonObj["error"].toInt();if (error != ErrorCodes::SUCCESS){ShowTipLabel(QString::fromLocal8Bit("参数错误"), "error");return;}auto email = jsonObj["email"].toString();ShowTipLabel(QString::fromLocal8Bit("验证码已发送至邮箱,请注意查找"), "normal");qDebug() << QString::fromLocal8Bit("验证码已发送至邮箱") << email;});// 注册注册用户服务器返回的数据对应的处理逻辑的回调函数_handlers.insert(ReqID::ID_REG_USER, [this](const QJsonObject& jsonObj){int error = jsonObj["error"].toInt();if (error != ErrorCodes::SUCCESS){ShowTipLabel(QString::fromLocal8Bit("注册失败"), "error");return;}auto email = jsonObj["email"].toString();ShowTipLabel(QString::fromLocal8Bit("注册成功,请登录"), "normal");qDebug() << QString::fromLocal8Bit("注册成功") << email;});
}

2.3 梳理一下注册逻辑

整个QT客户端,是由HttpConnect类来管理网络连接,比如向服务器发送请求,接收服务器发来的响应

而对响应的处理实际上是在别的类中处理的,HttpConnect将具体的处理逻辑放在了sig_http_finished 里,这里绑定的是一个回调,再次细分,如果是注册模块的请求,就调用注册模块中被绑定的回调函数,如果是登录模块就调用登录模块的回调函数,实现了面向对象思想,其实所有架构不是一开始都想好的,写的多了自然而然就想到了。

然后在注册模块中再次细分,比如获取验证码,服务器会发响应,注册确认按钮按下时,服务器又会发响应

2.4 服务器处理注册请求

enum ErrorCodes
{SUCCESS = 0,ERROR_JSON = 1001,																		// Json错误RPC_FAILED = 1002,																		// RPC通信失败ERROR_JSON_KEY_EMAIL_LACK = 1003,														// Json中缺少email字段Verify_Expired = 1004,																	// 验证码过期Verify_Not_Match = 1005,																// 验证码不匹配User_Exists = 1006,																		// 用户已存在
};#define CODE_PREFIX "code_"
// 注册用户对应的回调函数
RegisterPost("/ConfirmUser", [](HttpConnection* connection){if (connection){auto bodyStr = boost::beast::buffers_to_string(connection->_request.body().data());	// 获取 Http请求体中的内容cout << "receive body is \n" << bodyStr << endl;connection->_response.set(http::field::content_type, "text/json");					// 设置 Http响应头中的 content-typeJson::Value jsonResonse;															// 响应用的JsonJson::Value jsonResult;																// 请求体解析出来的JsonJson::Reader reader;																// Json解析器bool parseSuccess = reader.parse(bodyStr, jsonResult);								// 将请求体解析为Jsonif (!parseSuccess){cout << "parse json failed" << endl;jsonResonse["error"] = ErrorCodes::ERROR_JSON;									// 设置响应的错误码string jsonStr = jsonResonse.toStyledString();beast::ostream(connection->_response.body()) << jsonStr;						// 向 Http响应体中写入错误码内容return;}/* 查找redis中存储的email对应的验证码是否合理 */string verifyCode = "";bool bGetVerifyCode = RedisManage::GetInstance()->Get(CODE_PREFIX + jsonResult["email"].asString(), verifyCode);if (!bGetVerifyCode){cout << "get verify code Expired " << endl;jsonResonse["error"] = ErrorCodes::Verify_Expired;string jsonStr = jsonResonse.toStyledString();beast::ostream(connection->_response.body()) << jsonStr;return;}/* 判断验证码是否正确 */if (verifyCode != jsonResult["verifyCode"].asString()){cout << "verify code not match" << endl;jsonResonse["error"] = ErrorCodes::Verify_Not_Match;string jsonStr = jsonResonse.toStyledString();beast::ostream(connection->_response.body()) << jsonStr;return;}/* 访问Mysql检查是否已经注册过用户,如果未注册,注册用户,如果已经注册过,返回错误码 */// TODO/* 返回响应报文给客户端 */jsonResonse["error"] = 0;jsonResonse["email"] = jsonResult["email"];jsonResonse["user"] = jsonResult["user"];jsonResonse["password"] = jsonResult["password"];jsonResonse["EnterPassword"] = jsonResult["EnterPassword"];jsonResonse["verifyCode"] = jsonResult["verifyCode"];string jsonStr = jsonResonse.toStyledString();beast::ostream(connection->_response.body()) << jsonStr;							// 向 Http响应体中写入Json内容return;}else{std::cout << "connection is null" << std::endl;}});

2.5 测试注册服务

注册成功

3. Mysql注册用户

3.1 修改Mysql中的My.ini

找到Mysql的安装路径

C:\Program Files\MySQL\MySQL Server 8.0

如果没有my.ini自己定义一个

[mysqld]
# 设置3306端口
port=3306
# 设置mysql的安装目录 ---这里输入你安装的文件路径----
basedir=C:\Program Files\MySQL\MySQL Server 8.0
# 设置mysql数据库的数据的存放目录
datadir=D:\mysql\data
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。
max_connect_errors=10
# 服务端使用的字符集默认为utf8
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用“mysql_native_password”插件认证
#mysql_native_password
default_authentication_plugin=mysql_native_password
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8

启动Mysql服务

连接mysql服务

mysql -uroot -p

如果连接mysql服务显示无法连接指定端口,可以先将my.ini删除,然后登录mysql,调用

SHOW VARIABLES LIKE 'port';

查找mysql服务安装的端口,然后修改my.ini文件

我的mysql密码:123456

3.2 安装图形界面控制Mysql交互

https://pan.baidu.com/s/10jApYUrwaI19j345dpPGNA?pwd=77m2验证码: 77m2

3.3 安装Mysql Connect库

https://pan.baidu.com/s/1XAVhPAAzZpZahsyITua2oQ?pwd=9c1w提取码:9c1w

3.4 配置环境变量

动态库无需链接

将动态库放在项目路径下动态链接

让dll自动拷贝到运行目录

xcopy $(ProjectDir)config.ini  $(SolutionDir)$(Platform)\$(Configuration)\   /y
xcopy $(ProjectDir)*.dll   $(SolutionDir)$(Platform)\$(Configuration)\   /y

4. Mysql连接池

4.1 新建MysqlDAO连接管理类

连接池

class SqlConnection
{
public:SqlConnection(sql::Connection* con, int64_t lastUseTime): _con(con), _lastUseTime(lastUseTime){}sql::Connection* _con;int64_t _lastUseTime;
};class MySqlPool
{
public:MySqlPool(const string& url, const string& user, const string& pwd, const string& schema, int poolSize): _url(url), _user(user), _pwd(pwd), _schema(schema), _poolSize(poolSize), bIsStop(false){for (int i = 0; i < _poolSize; i++){sql::mysql::MySQL_Driver* driver = sql::mysql::get_mysql_driver_instance();sql::Connection* con = driver->connect(_url, _user, _pwd);con->setSchema(_schema);// 获取当前时间戳auto currentTime = chrono::duration_cast<chrono::seconds>(chrono::system_clock::now().time_since_epoch()).count();_pool.push(new SqlConnection(con, currentTime));}_thread = thread([this](){while (!bIsStop){checkConnection();this_thread::sleep_for(chrono::seconds(10));}});_thread.detach();														// 该线程不会阻塞主线程,后台执行}void checkConnection(){lock_guard<mutex> lock(_mutex);// 获取当前时间戳auto currentTime = chrono::duration_cast<chrono::seconds>(chrono::system_clock::now().time_since_epoch()).count();for (int i = 0; i < _poolSize; i++){auto con = _pool.front();_pool.pop();// 如果该连接距离上次使用时间已超过 60 秒,则执行心跳检测if (currentTime - con->_lastUseTime > 60){auto stmt = con->_con->createStatement();stmt->execute("SELECT 1");con->_lastUseTime = currentTime;_pool.push(con);con = nullptr;}}}~MySqlPool(){bIsStop = true;_cv.notify_all();while (!_pool.empty()){SqlConnection* con = _pool.front();_pool.pop();delete con;}}SqlConnection* getConnection(){unique_lock<mutex> lock(_mutex);_cv.wait(lock, [this] {return!_pool.empty();});if(bIsStop)return nullptr;SqlConnection* con = _pool.front();_pool.pop();return con;}void retunConnection(SqlConnection* con){lock_guard<mutex> lock(_mutex);if(bIsStop)return;_pool.push(con);_cv.notify_one();}private:string _url;string _user;string _pwd;string _schema;int _poolSize;queue<SqlConnection*> _pool;mutex _mutex;condition_variable _cv;atomic<bool> bIsStop = false;thread _thread;																	// 检测当前连接是否活跃,不活跃发送一条信息告诉Mysql我还活着
};

class MySqlDAO : public Singletion<MySqlDAO>
{friend class Singletion<MySqlDAO>;
public:MySqlDAO();~MySqlDAO();// 注册用户int RegUser(const string& username, const string& email, const string& password);private:MySqlPool* _pool;
};
#endif // MYSQLDAO_H

4.2 存储过程

数据库中一组预先编写好的SQL语句的集合,可以把存储过程看作数据库中的函数

SQL语言层面的代码封装与重用

示例

DELIMITER //  CREATE PROCEDURE CalculateSquare(IN num INT, OUT result INT)  
BEGIN  SET result = num * num;  
END //  DELIMITER ;

DELIMITER 用于更改命令结束符,以便在存储过程中使用 BEGIN ... END 语句。通常,我们使用 // 作为新的结束符,并在存储过程定义结束后将其改回 ;
CREATE PROCEDURE 用于创建新的存储过程。
CalculateSquare 是存储过程的名称
(IN num INT, OUT result INT) 定义了输入输出参数。在这个例子中,num 是一个输入参数,result 是一个输出参数
BEGIN ... END 之间的部分是存储过程的主体,即要执行的SQL语句

调用存储过程

调用存储过程并获取结果,需要使用CALL语句,并指定一个变量来接收输出参数的值

SET @input = 5;  
SET @output = 0;  CALL CalculateSquare(@input, @output);  SELECT @output;  -- 输出应该是 25

4.3 创建注册用户存储过程

如果处理异常会回滚事务,手动开启事务,保证后续多步操作要么都成功,要么都失败

什么是事务?

首先检查用户名是否已存在,如果存在提交事务

如果用户名没有问题,检查邮箱是否重复,如果存在,提交事务

用户名和邮箱都没有问题,开始注册用户

把新用户插到user表中

设置返回值是新用户ID,并提交事务

CREATE DEFINER=`root`@`localhost` PROCEDURE `reg_user`(IN `new_name` VARCHAR(255),IN `new_email` VARCHAR(255),IN `new_pwd` VARCHAR(255),OUT `result` INT
)
BEGIN-- 如果执行过程中遇到任何错误,回滚事务DECLARE EXIT HANDLER FOR SQLEXCEPTIONBEGIN-- 回滚事务ROLLBACK;-- 设置返回值为-1SET result = -1;END;-- 开始事务START TRANSACTION;-- 检查用户名是否已存在IF EXISTS (SELECT 1 FROM `user` WHERE `name` = new_name) THENSET result = 0;COMMIT;ELSE-- 用户名不存在,检查email是否已存在IF EXISTS (SELECT 1 FROM `user` WHERE `email` = new_email) THENSET result = 0;COMMIT;ELSE-- emial 也不存在,更新user_id 表UPDATE `user_id` SET `id` = `id` + 1;-- 获取更新后的id SELECT `id` INTO @new_id FROM `user_id`;-- 在user表中插入新纪录INSERT INTO `user` (`uid`, `name`, `email`, `pwd`) VALUES (@new_id, new_name, new_email, new_pwd);-- 设置result为新插入的uidSET result = @new_id;COMMIT;END IF;END IF;END

4.4 创建数据表

新建user表

新建user_id表

4.5 新建Mysql管理类

#ifndef MYSQLMANAGE_H
#define MYSQLMANAGE_H
#include "GlobalHead.h"
#include "Singletion.h"
#include "MysqlDAO.h"class MySqlManage : public Singletion<MySqlManage>
{friend class Singletion<MySqlManage>;
public:~MySqlManage();int RegUser(const string& name, const string& email, const string& password);private:MySqlManage();MySqlDAO* m_pDao;
};#endif // MYSQLMANAGE_H

#include "MySqlManage.h"MySqlManage::MySqlManage()
{m_pDao = new MySqlDAO();
}MySqlManage::~MySqlManage()
{}int MySqlManage::RegUser(const string& name, const string& email, const string& password)
{return m_pDao->RegUser(name, email, password);
}

5. 注册用户时向Mysql数据库查询

int uid = MySqlManage::GetInstance()->RegUser(jsonResult["user"].asString(),jsonResult["email"].asString(), jsonResult["password"].asString());
if (uid == -1 || uid == 0)
{cout << "user or email already exist\n";jsonResonse["error"] = ErrorCodes::User_Exists;string jsonStr = jsonResonse.toStyledString();beast::ostream(connection->_response.body()) << jsonStr;return;
}

6. 编译测试

服务器编译失败

链接错误,导入静态库

接着报错,说找不到库,说明附加包含目录错了

将上述库所在的目录添加进去

编译成功

启动redis服务

.\redis-server.exe .\redis.windows.conf

启动grpc服务

npm run serve

启动QT客户端

6.1 分析失败原因

链接被拒绝

导致服务器并没有监听指定的端口

6.2 注册失败

莫名其妙的调用了析构函数?

con连接是空

不再使用.json的方式解析,直接传字符串

可能是添加存储过程时没有保存,重新添加

这里是因为root@'%'这个用户不存在于数据库中

将root@'%' 修改成 `root`@`localhost`

查看存储过程发现

执行期间,遇到任何错误,才会导致返回值是-1

查看到底是哪一个sql语句出问题了

已解决

6.3 重新编译

1.解决字符串无法正确解析的问题

这里返回的是局部变量的值,局部变量是不允许用引用去接的,因为生命周期到期后得到的值就是空的

修改上述代码为

已解决

2. 重新测试

打开redis服务

启动grpc服务

启动c++服务器

启动qt客户端

注册成功

http://www.xdnf.cn/news/508501.html

相关文章:

  • 服务端HttpServletRequest、HttpServletResponse、HttpSession
  • 软件工具:批量图片区域识别+重命名文件的方法,发票识别和区域选择方法参考,基于阿里云实现
  • SuperYOLO:多模态遥感图像中的超分辨率辅助目标检测之论文阅读
  • 05 部署Nginx反向代理
  • Flink Table SQL
  • [SpringBoot]Spring MVC(4.0)
  • TASK03【Datawhale 组队学习】搭建向量知识库
  • 【图像处理基石】OpenCV中都有哪些图像增强的工具?
  • 护网行动——蓝队防守方案指南
  • AI写PPT可以用吗?我测试了3款AI写PPT工具,分享感受
  • Vue 3.0 中的slot及使用场景
  • 【通用智能体】Playwright:跨浏览器自动化工具
  • 微信小程序 地图 使用 射线法 判断目标点是否在多边形内部(可用于判断当前位置是否在某个区域内部)
  • C语言内存函数与数据在内存中的存储
  • ctr查看镜像
  • 掌握版本控制从本地到分布式
  • flat_map, flat_set, flat_multimap, flat_multimap
  • 深入理解位图(Bit - set):概念、实现与应用
  • python中http.cookiejar和http.cookie的区别
  • 深入解析Spring Boot与Kafka集成:构建高性能消息驱动应用
  • 【技海登峰】Kafka漫谈系列(十一)SpringBoot整合Kafka之消费者Consumer
  • 【云原生架构反模式】常见误区与解决方案
  • WPS多级标题编号以及样式控制
  • ES(ES2023/ES14)最新更新内容,及如何减少内耗
  • 大模型微调:从基础模型到专用模型的演进之路
  • IDE/IoT/搭建物联网(LiteOS)集成开发环境,基于 LiteOS Studio + GCC + JLink
  • 为新装的Linux系统配置国内yum源(阿里源)
  • 19. 结合Selenium和YAML对页面实例化PO对象改造
  • 大数据场景下数据导出的架构演进与EasyExcel实战方案
  • 理想AI Talk第二季-重点信息总结