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客户端
注册成功