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)
// 判断该值是否为空 如果为空返回null
if (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.com
console.log("query_res is ", query_res);
let uniqueId = query_res ? query_res : null;
if (uniqueId) {
uniqueId = uuidv4(); // 生成新的uuid
if (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-type
Json::Value jsonResonse; // 响应用的Json
Json::Value jsonResult; // 请求体解析出来的Json
Json::Reader reader; // Json解析器
bool parseSuccess = reader.parse(bodyStr, jsonResult); // 将请求体解析为Json
if (!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 SQLEXCEPTION
BEGIN
-- 回滚事务
ROLLBACK;
-- 设置返回值为-1
SET result = -1;
END;
-- 开始事务
START TRANSACTION;
-- 检查用户名是否已存在
IF EXISTS (SELECT 1 FROM `user` WHERE `name` = new_name) THEN
SET result = 0;
COMMIT;
ELSE
-- 用户名不存在,检查email是否已存在
IF EXISTS (SELECT 1 FROM `user` WHERE `email` = new_email) THEN
SET 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为新插入的uid
SET 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客户端
注册成功