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)
        //  判断该值是否为空 如果为空返回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客户端

注册成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值