恋恋风辰的个人博客


  • Home

  • Archives

  • Categories

  • Tags

  • Search

C++ 全栈聊天项目(11) redis环境配置和使用

Posted on 2024-08-31 | In C++聊天项目

注册功能

实现注册功能,先实现客户端发送post请求, 将注册ui中确定按钮改为sure_btn,并为其添加click槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//day11 添加确认槽函数
void RegisterDialog::on_sure_btn_clicked()
{
if(ui->user_edit->text() == ""){
showTip(tr("用户名不能为空"), false);
return;
}

if(ui->email_edit->text() == ""){
showTip(tr("邮箱不能为空"), false);
return;
}

if(ui->pass_edit->text() == ""){
showTip(tr("密码不能为空"), false);
return;
}

if(ui->confirm_edit->text() == ""){
showTip(tr("确认密码不能为空"), false);
return;
}

if(ui->confirm_edit->text() != ui->pass_edit->text()){
showTip(tr("密码和确认密码不匹配"), false);
return;
}

if(ui->varify_edit->text() == ""){
showTip(tr("验证码不能为空"), false);
return;
}

//day11 发送http请求注册用户
QJsonObject json_obj;
json_obj["user"] = ui->user_edit->text();
json_obj["email"] = ui->email_edit->text();
json_obj["passwd"] = ui->pass_edit->text();
json_obj["confirm"] = ui->confirm_edit->text();
json_obj["varifycode"] = ui->varify_edit->text();
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/user_register"),
json_obj, ReqId::ID_REG_USER,Modules::REGISTERMOD);
}

再添加http请求回复后收到处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void RegisterDialog::initHttpHandlers()
{
//...省略
//注册注册用户回包逻辑
_handlers.insert(ReqId::ID_REG_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("用户注册成功"), true);
qDebug()<< "email is " << email ;
});
}

Server端接受注册请求

Server注册user_register逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
RegPost("/user_register", [](std::shared_ptr<HttpConnection> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "receive body is " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}
//先查找redis中email对应的验证码是否合理
std::string varify_code;
bool b_get_varify = RedisMgr::GetInstance()->Get(src_root["email"].asString(), varify_code);
if (!b_get_varify) {
std::cout << " get varify code expired" << std::endl;
root["error"] = ErrorCodes::VarifyExpired;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

if (varify_code != src_root["varifycode"].asString()) {
std::cout << " varify code error" << std::endl;
root["error"] = ErrorCodes::VarifyCodeErr;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//访问redis查找
bool b_usr_exist = RedisMgr::GetInstance()->ExistsKey(src_root["user"].asString());
if (b_usr_exist) {
std::cout << " user exist" << std::endl;
root["error"] = ErrorCodes::UserExist;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//查找数据库判断用户是否存在

root["error"] = 0;
root["email"] = src_root["email"];
root ["user"]= src_root["user"].asString();
root["passwd"] = src_root["passwd"].asString();
root["confirm"] = src_root["confirm"].asString();
root["varifycode"] = src_root["varifycode"].asString();
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

安装Mysql

先介绍Windows环境下安装mysql

点击mysql安装包下载链接:https://dev.mysql.com/downloads/mysql

选择window版本,点击下载按钮,如下所示

https://cdn.llfc.club/4aa44fdafe578d8f2626d3e280d608f.png

不用登录直接下载

https://cdn.llfc.club/1711349001944.jpg

下载好mysql安装包后,将其解压到指定目录,并记下解压的目录,后续用于环境变量配置

https://cdn.llfc.club/1711349518362.jpg

在bin目录同级下创建一个文件,命名为my.ini
编辑my.ini文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[mysqld]
# 设置3308端口
port=3308
# 设置mysql的安装目录 ---这里输入你安装的文件路径----
basedir=D:\cppsoft\mysql
# 设置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=3308
default-character-set=utf8

有两点需要注意修改的:

A、basedir这里输入的是mysql解压存放的文件路径

B、datadir这里设置mysql数据库的数据存放目录

打开cmd进入mysql的bin文件下

https://cdn.llfc.club/1711349826275.jpg

依次执行命令

第一个命令为:

1
2
//安装mysql  安装完成后Mysql会有一个随机密码
.\mysqld.exe --initialize --console

如下图,随机密码要记住,以后我们改密码会用到

https://cdn.llfc.club/83635680847f591980ade3501655f8d.png

接下来在cmd执行第二条命令

1
2
//安装mysql服务并启动   
.\mysqld.exe --install mysql

如果出现以下情况,说明cmd不是以管理员形式执行的,改用为管理员权限执行即可。

https://cdn.llfc.club/2872369cb66fa7803e19575be3cd63b.png

成功如下

https://cdn.llfc.club/87a224f42f4dccb254481470d2f1b8e.png

目前为止安装完毕,大家如果mysql官网下载缓慢,可以去我的网盘下载

https://pan.baidu.com/s/1BTMZB31FWFUq4mZZdzcA9g?pwd=6xlz

提取码:6xlz

修改mysql密码

1 在本机启动mysql服务:

点击桌面我的电脑,右键选择管理进去:

https://cdn.llfc.club/1711350803255.jpg

点击后选择服务

https://cdn.llfc.club/1711350871137.jpg

点击服务后可查看当前计算机启动的所有服务,找到mysql,然后右键点击设为启动,同时也可设置其为自动启动和手动启动

https://cdn.llfc.club/1711350989964.jpg

继续在cmd上执行以下命令

1
mysql -uroot -p

回车后输入上面安装时保存的初始密码,进入mysql里面:

https://cdn.llfc.club/b33134d93210412a6d301c9eedfa8a5.png

在mysql里面继续执行以下命令:

1
2
//修改密码为123mysql
ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';

回车按照指引执行完后,代表密码修改成功,再输入exit;退出即可

配置环境变量

为了方便使用mysql命令,可以将mysql目录配置在环境变量里

新建系统变量:

变量名:MYSQL_HOME

变量值:msql目录

https://cdn.llfc.club/1711352568377.jpg

修改系统的path变量

编辑path,进去后添加 %MYSQL_HOME%\bin

https://cdn.llfc.club/1711352718673.jpg

测试连接

为了方便测试,大家可以使用navicat等桌面工具测试连接。以后增删改查也方便。

可以去官网下载

https://www.navicat.com.cn/

或者我得网盘下载

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

验证码: 77m2

效果如下:

https://cdn.llfc.club/1711531330919.jpg

Docker环境配置mysql

拉取mysql镜像

1
docker pull mysql:8.0

先启动一个测试版本,然后把他的配置文件拷贝出来

1
2
3
docker run --name mysqltest \
-p 3307:3306 -e MYSQL_ROOT_PASSWORD=root \
-d mysql

创建三个目录,我得目录是

1
2
3
mkdir -p /home/zack/llfc/mysql/config
mkdir -p /home/zack/llfc/mysql/data
mkdir -p /home/zack/llfc/mysql/logs

进入docker中

1
docker exec -it mysqltest bash

之后可以通过搜寻找到配置在/etc/mysql/my.cnf

所以接下来退出容器,执行拷贝命令

1
docker cp mysqltest:/etc/mysql/my.cnf  /home/zack/llfc/mysql/config

然后删除测试用的mysql docker

1
docker rm -f mysqltest 

然后启动我们的容器

1
2
3
4
5
docker run --restart=on-failure:3 -d   \
-v /home/zack/llfc/mysql/config/my.cnf:/etc/mysql/my.cnf \
-v /home/zack/llfc/mysql/data/:/var/lib/mysql \
-v /home/zack/llfc/mysql/logs:/logs -p 3308:3306 \
--name llfcmysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0

设置远程访问

进入docker

1
docker exec -it llfcmysql bash

登录mysql

1
mysql -u root -p

设置允许远程访问,我不设置也能访问的,这里介绍一下。

1
2
3
use mysql
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
flush privileges;

再次用navicat连接,是可以连接上了。

完善GateServer配置

添加Redis和Mysql配置

1
2
3
4
5
6
7
8
[Mysql]
Host = 81.68.86.146
Port = 3308
Passwd = 123456
[Redis]
Host = 81.68.86.146
Port = 6380
Passwd = 123456

Mysql Connector C++

尽管Mysql提供了访问数据库的接口,但是都是基于C风格的,为了便于面向对象设计,我们使用Mysql Connector C++ 这个库来访问mysql。

我们先安装这个库,因为我们windows环境代码是debug版本,所以下载connector的debug版本,如果你的开发编译用的release版本,那么就要下载releas版本,否则会报错
terminate called after throwing an instance of 'std::bad_alloc'.

因为我在windows只做debug调试后期会将项目移植到Linux端,所以这里只下载debug版

下载地址

https://dev.mysql.com/downloads/connector/cpp/

如果下载缓慢可以去我的网盘下载
https://pan.baidu.com/s/1XAVhPAAzZpZahsyITua2oQ?pwd=9c1w

提取码:9c1w

https://cdn.llfc.club/1711692126532.jpg

下载后将文件夹解压放在一个自己常用的目录,我放在D:\cppsoft\mysql_connector

https://cdn.llfc.club/1711692478215.jpg

接下来去visual studio中配置项目

VC++ 包含目录添加D:\cppsoft\mysql_connector\include

https://cdn.llfc.club/1711692778937.jpg

库目录包含D:\cppsoft\mysql_connector\lib64\vs14

https://cdn.llfc.club/1711693069494.jpg

然后将D:\cppsoft\mysql_connector\lib64\debug下的mysqlcppconn8-2-vs14.dll和mysqlcppconn9-vs14.dll分别拷贝到项目中

为了让项目自动将dll拷贝到运行目录,可以在生成事件->生成后事件中添加xcopy命令

https://cdn.llfc.club/1711693404656.jpg

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

封装mysql连接池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class MySqlPool {
public:
MySqlPool(const std::string& url, const std::string& user, const std::string& pass, const std::string& schema, int poolSize)
: url_(url), user_(user), pass_(pass), schema_(schema), poolSize_(poolSize), b_stop_(false){
try {
for (int i = 0; i < poolSize_; ++i) {
sql::mysql::MySQL_Driver* driver = sql::mysql::get_mysql_driver_instance();
std::unique_ptr<sql::Connection> con(driver->connect(url_, user_, pass_));
con->setSchema(schema_);
pool_.push(std::move(con));
}
}
catch (sql::SQLException& e) {
// 处理异常
std::cout << "mysql pool init failed" << std::endl;
}
}

std::unique_ptr<sql::Connection> getConnection() {
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait(lock, [this] {
if (b_stop_) {
return true;
}
return !pool_.empty(); });
if (b_stop_) {
return nullptr;
}
std::unique_ptr<sql::Connection> con(std::move(pool_.front()));
pool_.pop();
return con;
}

void returnConnection(std::unique_ptr<sql::Connection> con) {
std::unique_lock<std::mutex> lock(mutex_);
if (b_stop_) {
return;
}
pool_.push(std::move(con));
cond_.notify_one();
}

void Close() {
b_stop_ = true;
cond_.notify_all();
}

~MySqlPool() {
std::unique_lock<std::mutex> lock(mutex_);
while (!pool_.empty()) {
pool_.pop();
}
}

private:
std::string url_;
std::string user_;
std::string pass_;
std::string schema_;
int poolSize_;
std::queue<std::unique_ptr<sql::Connection>> pool_;
std::mutex mutex_;
std::condition_variable cond_;
std::atomic<bool> b_stop_;
};

封装DAO操作层

类的声明

1
2
3
4
5
6
7
8
9
class MysqlDao
{
public:
MysqlDao();
~MysqlDao();
int RegUser(const std::string& name, const std::string& email, const std::string& pwd);
private:
std::unique_ptr<MySqlPool> pool_;
};

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
MysqlDao::MysqlDao()
{
auto & cfg = ConfigMgr::Inst();
const auto& host = cfg["Mysql"]["Host"];
const auto& port = cfg["Mysql"]["Port"];
const auto& pwd = cfg["Mysql"]["Passwd"];
const auto& schema = cfg["Mysql"]["Schema"];
const auto& user = cfg["Mysql"]["User"];
pool_.reset(new MySqlPool(host+":"+port, user, pwd,schema, 5));
}

MysqlDao::~MysqlDao(){
pool_->Close();
}

int MysqlDao::RegUser(const std::string& name, const std::string& email, const std::string& pwd)
{
auto con = pool_->getConnection();
try {
if (con == nullptr) {
pool_->returnConnection(std::move(con));
return false;
}
// 准备调用存储过程
unique_ptr < sql::PreparedStatement > stmt(con->prepareStatement("CALL reg_user(?,?,?,@result)"));
// 设置输入参数
stmt->setString(1, name);
stmt->setString(2, email);
stmt->setString(3, pwd);

// 由于PreparedStatement不直接支持注册输出参数,我们需要使用会话变量或其他方法来获取输出参数的值

// 执行存储过程
stmt->execute();
// 如果存储过程设置了会话变量或有其他方式获取输出参数的值,你可以在这里执行SELECT查询来获取它们
// 例如,如果存储过程设置了一个会话变量@result来存储输出结果,可以这样获取:
unique_ptr<sql::Statement> stmtResult(con->createStatement());
unique_ptr<sql::ResultSet> res(stmtResult->executeQuery("SELECT @result AS result"));
if (res->next()) {
int result = res->getInt("result");
cout << "Result: " << result << endl;
pool_->returnConnection(std::move(con));
return result;
}
pool_->returnConnection(std::move(con));
return -1;
}
catch (sql::SQLException& e) {
pool_->returnConnection(std::move(con));
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return -1;
}
}

新建数据库llfc, llfc数据库添加user表和user_id表

https://cdn.llfc.club/1712109915609.jpg

user表
https://cdn.llfc.club/1712109796859.jpg

user_id就一行数据,用来记录用户id

https://cdn.llfc.club/1712110047125.jpg

这里id用简单计数表示,不考虑以后合服务器和分表分库,如果考虑大家可以采取不同的策略,雪花算法等。

新建存储过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
CREATE DEFINER=`root`@`%` 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; -- email已存在
COMMIT;
ELSE
-- email也不存在,更新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; -- 插入成功,返回新的uid
COMMIT;

END IF;
END IF;

END

数据库管理者

我们需要建立一个数据库管理者用来实现服务层,对接逻辑层的调用

1
2
3
4
5
6
7
8
9
10
11
12
#include "const.h"
#include "MysqlDao.h"
class MysqlMgr: public Singleton<MysqlMgr>
{
friend class Singleton<MysqlMgr>;
public:
~MysqlMgr();
int RegUser(const std::string& name, const std::string& email, const std::string& pwd);
private:
MysqlMgr();
MysqlDao _dao;
};

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "MysqlMgr.h"


MysqlMgr::~MysqlMgr() {

}

int MysqlMgr::RegUser(const std::string& name, const std::string& email, const std::string& pwd)
{
return _dao.RegUser(name, email, pwd);
}

MysqlMgr::MysqlMgr() {
}

逻辑层调用

在逻辑层注册消息处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
RegPost("/user_register", [](std::shared_ptr<HttpConnection> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "receive body is " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

auto email = src_root["email"].asString();
auto name = src_root["user"].asString();
auto pwd = src_root["passwd"].asString();
auto confirm = src_root["confirm"].asString();

if (pwd != confirm) {
std::cout << "password err " << std::endl;
root["error"] = ErrorCodes::PasswdErr;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//先查找redis中email对应的验证码是否合理
std::string varify_code;
bool b_get_varify = RedisMgr::GetInstance()->Get(CODEPREFIX+src_root["email"].asString(), varify_code);
if (!b_get_varify) {
std::cout << " get varify code expired" << std::endl;
root["error"] = ErrorCodes::VarifyExpired;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

if (varify_code != src_root["varifycode"].asString()) {
std::cout << " varify code error" << std::endl;
root["error"] = ErrorCodes::VarifyCodeErr;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//查找数据库判断用户是否存在
int uid = MysqlMgr::GetInstance()->RegUser(name, email, pwd);
if (uid == 0 || uid == -1) {
std::cout << " user or email exist" << std::endl;
root["error"] = ErrorCodes::UserExist;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}
root["error"] = 0;
root["uid"] = uid;
root["email"] = email;
root ["user"]= name;
root["passwd"] = pwd;
root["confirm"] = confirm;
root["varifycode"] = src_root["varifycode"].asString();
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

再次启动客户端测试,可以注册成功

C++ 全栈聊天项目(13) 实现验证码服务和用户注册功能

Posted on 2024-08-31 | In C++聊天项目

重置密码label

当我们在登录忘记密码的时候可以支持重置密码,重置密码label也要实现浮动和点击效果,以及未点击效果。所以我们复用之前的ClickedLabel,
在登录界面中升级forget_label为ClickedLabel。

1
2
3
4
5
6
7
8
9
10
LoginDialog::LoginDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::LoginDialog)
{
ui->setupUi(this);
connect(ui->reg_btn, &QPushButton::clicked, this, &LoginDialog::switchRegister);
ui->forget_label->SetState("normal","hover","","selected","selected_hover","");
ui->forget_label->setCursor(Qt::PointingHandCursor);
connect(ui->forget_label, &ClickedLabel::clicked, this, &LoginDialog::slot_forget_pwd);
}

点击忘记密码发送对应的信号

1
2
3
4
5
void LoginDialog::slot_forget_pwd()
{
qDebug()<<"slot forget pwd";
emit switchReset();
}

我们在mainwindow中连接了重置密码的信号和槽

1
2
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);

实现SlotSwitchReset

1
2
3
4
5
6
7
8
9
10
11
12
void MainWindow::SlotSwitchReset()
{
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_reset_dlg = new ResetDialog(this);
_reset_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_reset_dlg);

_login_dlg->hide();
_reset_dlg->show();
//注册返回登录信号和槽函数
connect(_reset_dlg, &ResetDialog::switchLogin, this, &MainWindow::SlotSwitchLogin2);
}

ResetDialog是我们添加的界面类,新建ResetDialog界面类,界面布局如下

https://cdn.llfc.club/1712826184149.jpg

重置界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "resetdialog.h"
#include "ui_resetdialog.h"
#include <QDebug>
#include <QRegularExpression>
#include "global.h"
#include "httpmgr.h"

ResetDialog::ResetDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ResetDialog)
{
ui->setupUi(this);

connect(ui->user_edit,&QLineEdit::editingFinished,this,[this](){
checkUserValid();
});

connect(ui->email_edit, &QLineEdit::editingFinished, this, [this](){
checkEmailValid();
});

connect(ui->pwd_edit, &QLineEdit::editingFinished, this, [this](){
checkPassValid();
});


connect(ui->varify_edit, &QLineEdit::editingFinished, this, [this](){
checkVarifyValid();
});

//连接reset相关信号和注册处理回调
initHandlers();
connect(HttpMgr::GetInstance().get(), &HttpMgr::sig_reset_mod_finish, this,
&ResetDialog::slot_reset_mod_finish);

}

下面是检测逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
bool ResetDialog::checkUserValid()
{
if(ui->user_edit->text() == ""){
AddTipErr(TipErr::TIP_USER_ERR, tr("用户名不能为空"));
return false;
}

DelTipErr(TipErr::TIP_USER_ERR);
return true;
}


bool ResetDialog::checkPassValid()
{
auto pass = ui->pwd_edit->text();

if(pass.length() < 6 || pass.length()>15){
//提示长度不准确
AddTipErr(TipErr::TIP_PWD_ERR, tr("密码长度应为6~15"));
return false;
}

// 创建一个正则表达式对象,按照上述密码要求
// 这个正则表达式解释:
// ^[a-zA-Z0-9!@#$%^&*]{6,15}$ 密码长度至少6,可以是字母、数字和特定的特殊字符
QRegularExpression regExp("^[a-zA-Z0-9!@#$%^&*]{6,15}$");
bool match = regExp.match(pass).hasMatch();
if(!match){
//提示字符非法
AddTipErr(TipErr::TIP_PWD_ERR, tr("不能包含非法字符"));
return false;;
}

DelTipErr(TipErr::TIP_PWD_ERR);

return true;
}

bool ResetDialog::checkEmailValid()
{
//验证邮箱的地址正则表达式
auto email = ui->email_edit->text();
// 邮箱地址的正则表达式
QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
bool match = regex.match(email).hasMatch(); // 执行正则表达式匹配
if(!match){
//提示邮箱不正确
AddTipErr(TipErr::TIP_EMAIL_ERR, tr("邮箱地址不正确"));
return false;
}

DelTipErr(TipErr::TIP_EMAIL_ERR);
return true;
}

bool ResetDialog::checkVarifyValid()
{
auto pass = ui->varify_edit->text();
if(pass.isEmpty()){
AddTipErr(TipErr::TIP_VARIFY_ERR, tr("验证码不能为空"));
return false;
}

DelTipErr(TipErr::TIP_VARIFY_ERR);
return true;
}

void ResetDialog::AddTipErr(TipErr te, QString tips)
{
_tip_errs[te] = tips;
showTip(tips, false);
}

void ResetDialog::DelTipErr(TipErr te)
{
_tip_errs.remove(te);
if(_tip_errs.empty()){
ui->err_tip->clear();
return;
}

showTip(_tip_errs.first(), false);
}

显示接口

1
2
3
4
5
6
7
8
9
10
11
12
void ResetDialog::showTip(QString str, bool b_ok)
{
if(b_ok){
ui->err_tip->setProperty("state","normal");
}else{
ui->err_tip->setProperty("state","err");
}

ui->err_tip->setText(str);

repolish(ui->err_tip);
}

获取验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ResetDialog::on_varify_btn_clicked()
{
qDebug()<<"receive varify btn clicked ";
auto email = ui->email_edit->text();
auto bcheck = checkEmailValid();
if(!bcheck){
return;
}

//发送http请求获取验证码
QJsonObject json_obj;
json_obj["email"] = email;
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/get_varifycode"),
json_obj, ReqId::ID_GET_VARIFY_CODE,Modules::RESETMOD);
}

初始化回包处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ResetDialog::initHandlers()
{
//注册获取验证码回包逻辑
_handlers.insert(ReqId::ID_GET_VARIFY_CODE, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("验证码已发送到邮箱,注意查收"), true);
qDebug()<< "email is " << email ;
});

//注册注册用户回包逻辑
_handlers.insert(ReqId::ID_RESET_PWD, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("重置成功,点击返回登录"), true);
qDebug()<< "email is " << email ;
qDebug()<< "user uuid is " << jsonObj["uuid"].toString();
});
}

根据返回的id调用不同的回报处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ResetDialog::slot_reset_mod_finish(ReqId id, QString res, ErrorCodes err)
{
if(err != ErrorCodes::SUCCESS){
showTip(tr("网络请求错误"),false);
return;
}

// 解析 JSON 字符串,res需转化为QByteArray
QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());
//json解析错误
if(jsonDoc.isNull()){
showTip(tr("json解析错误"),false);
return;
}

//json解析错误
if(!jsonDoc.isObject()){
showTip(tr("json解析错误"),false);
return;
}


//调用对应的逻辑,根据id回调。
_handlers[id](jsonDoc.object());

return;
}

这里实现发送逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void ResetDialog::on_sure_btn_clicked()
{
bool valid = checkUserValid();
if(!valid){
return;
}

valid = checkEmailValid();
if(!valid){
return;
}

valid = checkPassValid();
if(!valid){
return;
}

valid = checkVarifyValid();
if(!valid){
return;
}

//发送http重置用户请求
QJsonObject json_obj;
json_obj["user"] = ui->user_edit->text();
json_obj["email"] = ui->email_edit->text();
json_obj["passwd"] = xorString(ui->pwd_edit->text());
json_obj["varifycode"] = ui->varify_edit->text();
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/reset_pwd"),
json_obj, ReqId::ID_RESET_PWD,Modules::RESETMOD);
}

注册、重置、登录切换

我们要实现注册、重置、登录三个界面的替换,就需要在MainWindow中添加SlotSwitchLogin2的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//从重置界面返回登录界面
void MainWindow::SlotSwitchLogin2()
{
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog(this);
_login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_login_dlg);

_reset_dlg->hide();
_login_dlg->show();
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);
//连接登录界面注册信号
connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
}

服务端响应重置

在LogicSystem的构造函数中增加注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//重置回调逻辑
RegPost("/reset_pwd", [](std::shared_ptr<HttpConnection> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "receive body is " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

auto email = src_root["email"].asString();
auto name = src_root["user"].asString();
auto pwd = src_root["passwd"].asString();

//先查找redis中email对应的验证码是否合理
std::string varify_code;
bool b_get_varify = RedisMgr::GetInstance()->Get(CODEPREFIX + src_root["email"].asString(), varify_code);
if (!b_get_varify) {
std::cout << " get varify code expired" << std::endl;
root["error"] = ErrorCodes::VarifyExpired;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

if (varify_code != src_root["varifycode"].asString()) {
std::cout << " varify code error" << std::endl;
root["error"] = ErrorCodes::VarifyCodeErr;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}
//查询数据库判断用户名和邮箱是否匹配
bool email_valid = MysqlMgr::GetInstance()->CheckEmail(name, email);
if (!email_valid) {
std::cout << " user email not match" << std::endl;
root["error"] = ErrorCodes::EmailNotMatch;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//更新密码为最新密码
bool b_up = MysqlMgr::GetInstance()->UpdatePwd(name, pwd);
if (!b_up) {
std::cout << " update pwd failed" << std::endl;
root["error"] = ErrorCodes::PasswdUpFailed;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

std::cout << "succeed to update password" << pwd << std::endl;
root["error"] = 0;
root["email"] = email;
root["user"] = name;
root["passwd"] = pwd;
root["varifycode"] = src_root["varifycode"].asString();
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

在Mysql中新增CheckEmail和UpdatePwd函数

1
2
3
4
5
6
7
bool MysqlMgr::CheckEmail(const std::string& name, const std::string& email) {
return _dao.CheckEmail(name, email);
}

bool MysqlMgr::UpdatePwd(const std::string& name, const std::string& pwd) {
return _dao.UpdatePwd(name, pwd);
}

DAO这一层写具体的逻辑, 检测邮箱是否合理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
bool MysqlDao::CheckEmail(const std::string& name, const std::string& email) {
auto con = pool_->getConnection();
try {
if (con == nullptr) {
pool_->returnConnection(std::move(con));
return false;
}

// 准备查询语句
std::unique_ptr<sql::PreparedStatement> pstmt(con->prepareStatement("SELECT email FROM user WHERE name = ?"));

// 绑定参数
pstmt->setString(1, name);

// 执行查询
std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());

// 遍历结果集
while (res->next()) {
std::cout << "Check Email: " << res->getString("email") << std::endl;
if (email != res->getString("email")) {
pool_->returnConnection(std::move(con));
return false;
}
pool_->returnConnection(std::move(con));
return true;
}
}
catch (sql::SQLException& e) {
pool_->returnConnection(std::move(con));
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}

更新密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
bool MysqlDao::UpdatePwd(const std::string& name, const std::string& newpwd) {
auto con = pool_->getConnection();
try {
if (con == nullptr) {
pool_->returnConnection(std::move(con));
return false;
}

// 准备查询语句
std::unique_ptr<sql::PreparedStatement> pstmt(con->prepareStatement("UPDATE user SET pwd = ? WHERE name = ?"));

// 绑定参数
pstmt->setString(2, name);
pstmt->setString(1, newpwd);

// 执行更新
int updateCount = pstmt->executeUpdate();

std::cout << "Updated rows: " << updateCount << std::endl;
pool_->returnConnection(std::move(con));
return true;
}
catch (sql::SQLException& e) {
pool_->returnConnection(std::move(con));
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}

C++ 全栈聊天项目(12) C++封装redis连接池

Posted on 2024-08-31 | In C++聊天项目

增加定时按钮

点击获取验证码后需要让按钮显示倒计时,然后倒计时结束后再次可点击。
添加TimberBtn类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef TIMERBTN_H
#define TIMERBTN_H
#include <QPushButton>
#include <QTimer>

class TimerBtn : public QPushButton
{
public:
TimerBtn(QWidget *parent = nullptr);
~ TimerBtn();

// 重写mouseReleaseEvent
virtual void mouseReleaseEvent(QMouseEvent *e) override;
private:
QTimer *_timer;
int _counter;
};

#endif // TIMERBTN_H

添加实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "timerbtn.h"
#include <QMouseEvent>
#include <QDebug>

TimerBtn::TimerBtn(QWidget *parent):QPushButton(parent),_counter(10)
{
_timer = new QTimer(this);

connect(_timer, &QTimer::timeout, [this](){
_counter--;
if(_counter <= 0){
_timer->stop();
_counter = 10;
this->setText("获取");
this->setEnabled(true);
return;
}
this->setText(QString::number(_counter));
});
}

TimerBtn::~TimerBtn()
{
_timer->stop();
}

void TimerBtn::mouseReleaseEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton) {
// 在这里处理鼠标左键释放事件
qDebug() << "MyButton was released!";
this->setEnabled(false);
this->setText(QString::number(_counter));
_timer->start(1000);
emit clicked();
}
// 调用基类的mouseReleaseEvent以确保正常的事件处理(如点击效果)
QPushButton::mouseReleaseEvent(e);
}

然后将注册界面获取按钮升级为TimerBtn

调整输入框错误提示

在RegisterDialog构造函数中删除原来的输入框editing信号和逻辑,添加editingFinished信号和处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//day11 设定输入框输入后清空字符串
ui->err_tip->clear();

connect(ui->user_edit,&QLineEdit::editingFinished,this,[this](){
checkUserValid();
});

connect(ui->email_edit, &QLineEdit::editingFinished, this, [this](){
checkEmailValid();
});

connect(ui->pass_edit, &QLineEdit::editingFinished, this, [this](){
checkPassValid();
});

connect(ui->confirm_edit, &QLineEdit::editingFinished, this, [this](){
checkConfirmValid();
});

connect(ui->varify_edit, &QLineEdit::editingFinished, this, [this](){
checkVarifyValid();
});

global.h中添加TipErr定义

1
2
3
4
5
6
7
8
9
enum TipErr{
TIP_SUCCESS = 0,
TIP_EMAIL_ERR = 1,
TIP_PWD_ERR = 2,
TIP_CONFIRM_ERR = 3,
TIP_PWD_CONFIRM = 4,
TIP_VARIFY_ERR = 5,
TIP_USER_ERR = 6
};

RegisterDialog声明中添加

1
QMap<TipErr, QString> _tip_errs;

_tip_errs用来缓存各个输入框输入完成后提示的错误,如果该输入框错误清除后就显示剩余的错误,每次只显示一条

实现添加错误和删除错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ResetDialog::AddTipErr(TipErr te, QString tips)
{
_tip_errs[te] = tips;
showTip(tips, false);
}

void ResetDialog::DelTipErr(TipErr te)
{
_tip_errs.remove(te);
if(_tip_errs.empty()){
ui->err_tip->clear();
return;
}

showTip(_tip_errs.first(), false);
}

实现错误检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
bool ResetDialog::checkUserValid()
{
if(ui->user_edit->text() == ""){
AddTipErr(TipErr::TIP_USER_ERR, tr("用户名不能为空"));
return false;
}

DelTipErr(TipErr::TIP_USER_ERR);
return true;
}


bool ResetDialog::checkPassValid()
{
auto pass = ui->pwd_edit->text();

if(pass.length() < 6 || pass.length()>15){
//提示长度不准确
AddTipErr(TipErr::TIP_PWD_ERR, tr("密码长度应为6~15"));
return false;
}

// 创建一个正则表达式对象,按照上述密码要求
// 这个正则表达式解释:
// ^[a-zA-Z0-9!@#$%^&*]{6,15}$ 密码长度至少6,可以是字母、数字和特定的特殊字符
QRegularExpression regExp("^[a-zA-Z0-9!@#$%^&*]{6,15}$");
bool match = regExp.match(pass).hasMatch();
if(!match){
//提示字符非法
AddTipErr(TipErr::TIP_PWD_ERR, tr("不能包含非法字符"));
return false;;
}

DelTipErr(TipErr::TIP_PWD_ERR);

return true;
}



bool ResetDialog::checkEmailValid()
{
//验证邮箱的地址正则表达式
auto email = ui->email_edit->text();
// 邮箱地址的正则表达式
QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
bool match = regex.match(email).hasMatch(); // 执行正则表达式匹配
if(!match){
//提示邮箱不正确
AddTipErr(TipErr::TIP_EMAIL_ERR, tr("邮箱地址不正确"));
return false;
}

DelTipErr(TipErr::TIP_EMAIL_ERR);
return true;
}

bool ResetDialog::checkVarifyValid()
{
auto pass = ui->varify_edit->text();
if(pass.isEmpty()){
AddTipErr(TipErr::TIP_VARIFY_ERR, tr("验证码不能为空"));
return false;
}

DelTipErr(TipErr::TIP_VARIFY_ERR);
return true;
}

除此之外修改之前点击确认按钮的逻辑,改为检测所有条件成立后再发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void ResetDialog::on_sure_btn_clicked()
{
bool valid = checkUserValid();
if(!valid){
return;
}

valid = checkEmailValid();
if(!valid){
return;
}

valid = checkPassValid();
if(!valid){
return;
}

valid = checkVarifyValid();
if(!valid){
return;
}

//发送http重置用户请求
QJsonObject json_obj;
json_obj["user"] = ui->user_edit->text();
json_obj["email"] = ui->email_edit->text();
json_obj["passwd"] = xorString(ui->pwd_edit->text());
json_obj["varifycode"] = ui->varify_edit->text();
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/reset_pwd"),
json_obj, ReqId::ID_RESET_PWD,Modules::RESETMOD);
}

隐藏和显示密码

我们在输入密码时希望能通过点击可见还是不可见,显示密码和隐藏密码,这里先添加图片放入资源中,然后在Register.ui中添加两个label,分别命名为pass_visible和confirm_visible, 用来占据位置。

因为我们要做的点击后图片要有状态切换,以及浮动显示不一样的效果等,所以我们重写ClickedLabel,继承自QLabel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef CLICKEDLABEL_H
#define CLICKEDLABEL_H
#include <QLabel>
#include "global.h"

class ClickedLabel:public QLabel
{
Q_OBJECT
public:
ClickedLabel(QWidget* parent);
virtual void mousePressEvent(QMouseEvent *ev) override;
virtual void enterEvent(QEvent* event) override;
virtual void leaveEvent(QEvent* event) override;
void SetState(QString normal="", QString hover="", QString press="",
QString select="", QString select_hover="", QString select_press="");

ClickLbState GetCurState();
protected:

private:
QString _normal;
QString _normal_hover;
QString _normal_press;

QString _selected;
QString _selected_hover;
QString _selected_press;

ClickLbState _curstate;
signals:
void clicked(void);

};

#endif // CLICKEDLABEL_H

一个Label有六种状态,普通状态,普通的悬浮状态,普通的点击状态,选中状态,选中的悬浮状态,选中的点击状态。

当Label处于普通状态,被点击后,切换为选中状态,再次点击又切换为普通状态。

ClickLbState定义在global.h中,包含两种状态一个是普通状态,一个是选中状态。而Label中的六种状态就是基于这两种状态嵌套实现的。

1
2
3
4
enum ClickLbState{
Normal = 0,
Selected = 1
};

六种状态用qss写好,这样我们只需要根据鼠标事件切换不同的qss就可以实现样式变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pass_visible[state='unvisible']{
border-image: url(:/res/unvisible.png);
}

#pass_visible[state='unvisible_hover']{
border-image: url(:/res/unvisible_hover.png);
}

#pass_visible[state='visible']{
border-image: url(:/res/visible.png);
}

#pass_visible[state='visible_hover']{
border-image: url(:/res/visible_hover.png);
}


#confirm_visible[state='unvisible']{
border-image: url(:/res/unvisible.png);
}

#confirm_visible[state='unvisible_hover']{
border-image: url(:/res/unvisible_hover.png);
}

#confirm_visible[state='visible']{
border-image: url(:/res/visible.png);
}

#confirm_visible[state='visible_hover']{
border-image: url(:/res/visible_hover.png);
}

我们实现ClickedLabel功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "clickedlabel.h"
#include <QMouseEvent>
ClickedLabel::ClickedLabel(QWidget* parent):QLabel (parent),_curstate(ClickLbState::Normal)
{

}


// 处理鼠标点击事件
void ClickedLabel::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if(_curstate == ClickLbState::Normal){
qDebug()<<"clicked , change to selected hover: "<< _selected_hover;
_curstate = ClickLbState::Selected;
setProperty("state",_selected_hover);
repolish(this);
update();

}else{
qDebug()<<"clicked , change to normal hover: "<< _normal_hover;
_curstate = ClickLbState::Normal;
setProperty("state",_normal_hover);
repolish(this);
update();
}
emit clicked();
}
// 调用基类的mousePressEvent以保证正常的事件处理
QLabel::mousePressEvent(event);
}

// 处理鼠标悬停进入事件
void ClickedLabel::enterEvent(QEvent* event) {
// 在这里处理鼠标悬停进入的逻辑
if(_curstate == ClickLbState::Normal){
qDebug()<<"enter , change to normal hover: "<< _normal_hover;
setProperty("state",_normal_hover);
repolish(this);
update();

}else{
qDebug()<<"enter , change to selected hover: "<< _selected_hover;
setProperty("state",_selected_hover);
repolish(this);
update();
}

QLabel::enterEvent(event);
}

// 处理鼠标悬停离开事件
void ClickedLabel::leaveEvent(QEvent* event){
// 在这里处理鼠标悬停离开的逻辑
if(_curstate == ClickLbState::Normal){
qDebug()<<"leave , change to normal : "<< _normal;
setProperty("state",_normal);
repolish(this);
update();

}else{
qDebug()<<"leave , change to normal hover: "<< _selected;
setProperty("state",_selected);
repolish(this);
update();
}
QLabel::leaveEvent(event);
}

void ClickedLabel::SetState(QString normal, QString hover, QString press,
QString select, QString select_hover, QString select_press)
{
_normal = normal;
_normal_hover = hover;
_normal_press = press;

_selected = select;
_selected_hover = select_hover;
_selected_press = select_press;

setProperty("state",normal);
repolish(this);
}

ClickLbState ClickedLabel::GetCurState(){
return _curstate;
}

将label升级为ClickedLabel,然后在RegisterDialog的构造函数中添加label点击的响应函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

//设置浮动显示手形状
ui->pass_visible->setCursor(Qt::PointingHandCursor);
ui->confirm_visible->setCursor(Qt::PointingHandCursor);

ui->pass_visible->SetState("unvisible","unvisible_hover","","visible",
"visible_hover","");

ui->confirm_visible->SetState("unvisible","unvisible_hover","","visible",
"visible_hover","");
//连接点击事件

connect(ui->pass_visible, &ClickedLabel::clicked, this, [this]() {
auto state = ui->pass_visible->GetCurState();
if(state == ClickLbState::Normal){
ui->pass_edit->setEchoMode(QLineEdit::Password);
}else{
ui->pass_edit->setEchoMode(QLineEdit::Normal);
}
qDebug() << "Label was clicked!";
});

connect(ui->confirm_visible, &ClickedLabel::clicked, this, [this]() {
auto state = ui->confirm_visible->GetCurState();
if(state == ClickLbState::Normal){
ui->confirm_edit->setEchoMode(QLineEdit::Password);
}else{
ui->confirm_edit->setEchoMode(QLineEdit::Normal);
}
qDebug() << "Label was clicked!";
});

这样就实现了通过点击切换密码的显示和隐藏。

注册成功提示页面

注册成功后要切换到提示页面,所以在initHandlers函数内实现收到服务器注册回复的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
//注册注册用户回包逻辑
_handlers.insert(ReqId::ID_REG_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("用户注册成功"), true);
qDebug()<< "email is " << email ;
qDebug()<< "user uuid is " << jsonObj["uuid"].toString();
ChangeTipPage();
});

页面切换逻辑

1
2
3
4
5
6
7
8
void RegisterDialog::ChangeTipPage()
{
_countdown_timer->stop();
ui->stackedWidget->setCurrentWidget(ui->page_2);

// 启动定时器,设置间隔为1000毫秒(1秒)
_countdown_timer->start(1000);
}

在RegisterDialog.ui中stackwidget的page2添加标签和返回按钮

https://cdn.llfc.club/1712821604946.jpg

在RegisterDialog构造函数中添加定时器回调

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建定时器
_countdown_timer = new QTimer(this);
// 连接信号和槽
connect(_countdown_timer, &QTimer::timeout, [this](){
if(_countdown==0){
_countdown_timer->stop();
emit sigSwitchLogin();
return;
}
_countdown--;
auto str = QString("注册成功,%1 s后返回登录").arg(_countdown);
ui->tip_lb->setText(str);
});

除此之外在返回按钮的槽函数中停止定时器并发送切换登录的信号

1
2
3
4
5
void RegisterDialog::on_return_btn_clicked()
{
_countdown_timer->stop();
emit sigSwitchLogin();
}

取消注册也发送切换登录信号

1
2
3
4
5
void RegisterDialog::on_cancel_btn_clicked()
{
_countdown_timer->stop();
emit sigSwitchLogin();
}

界面跳转

回到mainwindow,构造函数简化,只做登录界面初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog(this);
_login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_login_dlg);

//连接登录界面注册信号
connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);
}

在点击注册按钮的槽函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
void MainWindow::SlotSwitchReg()
{
_reg_dlg = new RegisterDialog(this);
_reg_dlg->hide();

_reg_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);

//连接注册界面返回登录信号
connect(_reg_dlg, &RegisterDialog::sigSwitchLogin, this, &MainWindow::SlotSwitchLogin);
setCentralWidget(_reg_dlg);
_login_dlg->hide();
_reg_dlg->show();
}

切换登录界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//从注册界面返回登录界面
void MainWindow::SlotSwitchLogin()
{
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog(this);
_login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_login_dlg);

_reg_dlg->hide();
_login_dlg->show();
//连接登录界面注册信号
connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);
}

这样登录界面和注册界面的切换逻辑就写完了。

C++ 全栈聊天项目(14) 封装Mysql连接池

Posted on 2024-08-31 | In C++聊天项目

客户端登录功能

登录界面新增err_tip,用来提示用户登陆结果。至于密码输入框大家可以根据注册界面的逻辑实现隐藏和显示的功能。这里留给大家自己实现。

https://cdn.llfc.club/1713228480446.jpg

点击登录需要发送http 请求到GateServer,GateServer先验证登录密码,再调用grpc请求给StatusServer,获取聊天服务器ip信息和token信息反馈给客户端。

结构图如下

https://cdn.llfc.club/1713230325540.jpg

我们先实现客户端登录,为登录按钮添加槽函数响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void LoginDialog::on_login_btn_clicked()
{
qDebug()<<"login btn clicked";
if(checkUserValid() == false){
return;
}

if(checkPwdValid() == false){
return ;
}

auto user = ui->user_edit->text();
auto pwd = ui->pass_edit->text();
//发送http请求登录
QJsonObject json_obj;
json_obj["user"] = user;
json_obj["passwd"] = xorString(pwd);
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/user_login"),
json_obj, ReqId::ID_LOGIN_USER,Modules::LOGINMOD);
}

增加检测函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool LoginDialog::checkUserValid(){

auto user = ui->user_edit->text();
if(user.isEmpty()){
qDebug() << "User empty " ;
return false;
}

return true;
}

bool LoginDialog::checkPwdValid(){
auto pwd = ui->pass_edit->text();
if(pwd.length() < 6 || pwd.length() > 15){
qDebug() << "Pass length invalid";
return false;
}

return true;
}

在HttpMgr中添加sig_login_mod_finish信号,收到http请求完成回包的槽函数中添加登录模块的响应,将登录模块的消息发送到登录界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void HttpMgr::slot_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod)
{
if(mod == Modules::REGISTERMOD){
//发送信号通知指定模块http响应结束
emit sig_reg_mod_finish(id, res, err);
}

if(mod == Modules::RESETMOD){
//发送信号通知指定模块http响应结束
emit sig_reset_mod_finish(id, res, err);
}

if(mod == Modules::LOGINMOD){
emit sig_login_mod_finish(id, res, err);
}
}

在LoginDialog的构造函数中添加消息对应的槽函数连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LoginDialog::LoginDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::LoginDialog)
{
ui->setupUi(this);
connect(ui->reg_btn, &QPushButton::clicked, this, &LoginDialog::switchRegister);
ui->forget_label->SetState("normal","hover","","selected","selected_hover","");
ui->forget_label->setCursor(Qt::PointingHandCursor);
connect(ui->forget_label, &ClickedLabel::clicked, this, &LoginDialog::slot_forget_pwd);
initHttpHandlers();
//连接登录回包信号
connect(HttpMgr::GetInstance().get(), &HttpMgr::sig_login_mod_finish, this,
&LoginDialog::slot_login_mod_finish);
}

initHttpHandlers为初始化http回调逻辑, 并添加_handlers成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void LoginDialog::initHttpHandlers()
{
//注册获取登录回包逻辑
_handlers.insert(ReqId::ID_LOGIN_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto user = jsonObj["user"].toString();
showTip(tr("登录成功"), true);
qDebug()<< "user is " << user ;
});
}

在LoginDialog中添加槽函数slot_login_mod_finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void LoginDialog::slot_login_mod_finish(ReqId id, QString res, ErrorCodes err)
{
if(err != ErrorCodes::SUCCESS){
showTip(tr("网络请求错误"),false);
return;
}

// 解析 JSON 字符串,res需转化为QByteArray
QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());
//json解析错误
if(jsonDoc.isNull()){
showTip(tr("json解析错误"),false);
return;
}

//json解析错误
if(!jsonDoc.isObject()){
showTip(tr("json解析错误"),false);
return;
}


//调用对应的逻辑,根据id回调。
_handlers[id](jsonDoc.object());

return;
}

到此客户端登陆请求发送的模块封装完了

GateServer完善登陆逻辑

在LogicSystem的构造函数中添加登陆请求的注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//用户登录逻辑
RegPost("/user_login", [](std::shared_ptr<HttpConnection> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "receive body is " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

auto name = src_root["user"].asString();
auto pwd = src_root["passwd"].asString();
UserInfo userInfo;
//查询数据库判断用户名和密码是否匹配
bool pwd_valid = MysqlMgr::GetInstance()->CheckPwd(name, pwd, userInfo);
if (!pwd_valid) {
std::cout << " user pwd not match" << std::endl;
root["error"] = ErrorCodes::PasswdInvalid;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//查询StatusServer找到合适的连接
auto reply = StatusGrpcClient::GetInstance()->GetChatServer(userInfo.uid);
if (reply.error()) {
std::cout << " grpc get chat server failed, error is " << reply.error()<< std::endl;
root["error"] = ErrorCodes::RPCGetFailed;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

std::cout << "succeed to load userinfo uid is " << userInfo.uid << std::endl;
root["error"] = 0;
root["user"] = name;
root["uid"] = userInfo.uid;
root["token"] = reply.token();
root["host"] = reply.host();
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

在MysqlMgr中添加CheckPwd函数

1
2
3
bool MysqlMgr::CheckPwd(const std::string& name, const std::string& pwd, UserInfo& userInfo) {
return _dao.CheckPwd(name, pwd, userInfo);
}

在DAO层添加根据用户名查询sql逻辑,并且判断pwd是否匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
bool MysqlDao::CheckPwd(const std::string& name, const std::string& pwd, UserInfo& userInfo) {
auto con = pool_->getConnection();
Defer defer([this, &con]() {
pool_->returnConnection(std::move(con));
});

try {
if (con == nullptr) {
return false;
}

// 准备SQL语句
std::unique_ptr<sql::PreparedStatement> pstmt(con->prepareStatement("SELECT * FROM user WHERE name = ?"));
pstmt->setString(1, name); // 将username替换为你要查询的用户名

// 执行查询
std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());
std::string origin_pwd = "";
// 遍历结果集
while (res->next()) {
origin_pwd = res->getString("pwd");
// 输出查询到的密码
std::cout << "Password: " << origin_pwd << std::endl;
break;
}

if (pwd != origin_pwd) {
return false;
}
userInfo.name = name;
userInfo.email = res->getString("email");
userInfo.uid = res->getInt("uid");
userInfo.pwd = origin_pwd;
return true;
}
catch (sql::SQLException& e) {
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}

因为要调用grpc访问StatusServer,所以我们这里先完善协议proto文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
syntax = "proto3";

package message;

service VarifyService {
rpc GetVarifyCode (GetVarifyReq) returns (GetVarifyRsp) {}
}

message GetVarifyReq {
string email = 1;
}

message GetVarifyRsp {
int32 error = 1;
string email = 2;
string code = 3;
}

message GetChatServerReq {
int32 uid = 1;
}

message GetChatServerRsp {
int32 error = 1;
string host = 2;
string port = 3;
string token = 4;
}

service StatusService {
rpc GetChatServer (GetChatServerReq) returns (GetChatServerRsp) {}
}

我们用下面两条命令重新生成pb.h和grpc.pb.h

1
D:\cppsoft\grpc\visualpro\third_party\protobuf\Debug\protoc.exe --cpp_out=. "message.proto"

生成grpc.pb.h

1
D:\cppsoft\grpc\visualpro\third_party\protobuf\Debug\protoc.exe  -I="." --grpc_out="." --plugin=protoc-gen-grpc="D:\cppsoft\grpc\visualpro\Debug\grpc_cpp_plugin.exe" "message.proto"

这俩命令执行完成后总计生成四个文件

https://cdn.llfc.club/1713239066360.jpg

实现StatusGrpcClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "const.h"
#include "Singleton.h"
#include "ConfigMgr.h"

using grpc::Channel;
using grpc::Status;
using grpc::ClientContext;

using message::GetChatServerReq;
using message::GetChatServerRsp;
using message::StatusService;

class StatusGrpcClient :public Singleton<StatusGrpcClient>
{
friend class Singleton<StatusGrpcClient>;
public:
~StatusGrpcClient() {

}
GetChatServerRsp GetChatServer(int uid);

private:
StatusGrpcClient();
std::unique_ptr<StatusConPool> pool_;

};

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "StatusGrpcClient.h"

GetChatServerRsp StatusGrpcClient::GetChatServer(int uid)
{
ClientContext context;
GetChatServerRsp reply;
GetChatServerReq request;
request.set_uid(uid);
auto stub = pool_->getConnection();
Status status = stub->GetChatServer(&context, request, &reply);
Defer defer([&stub, this]() {
pool_->returnConnection(std::move(stub));
});
if (status.ok()) {
return reply;
}
else {
reply.set_error(ErrorCodes::RPCFailed);
return reply;
}
}

StatusGrpcClient::StatusGrpcClient()
{
auto& gCfgMgr = ConfigMgr::Inst();
std::string host = gCfgMgr["StatusServer"]["Host"];
std::string port = gCfgMgr["StatusServer"]["Port"];
pool_.reset(new StatusConPool(5, host, port));
}

当然GateServer的config.ini文件也要做更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[GateServer]
Port = 8080
[VarifyServer]
Host = 127.0.0.1
Port = 50051
[StatusServer]
Host = 127.0.0.1
Port = 50052
[Mysql]
Host = 81.68.86.146
Port = 3308
User = root
Passwd = 123456
Schema = llfc
[Redis]
Host = 81.68.86.146
Port = 6380
Passwd = 123456

StatusGrpcClient用到了StatusConPool, 将其实现放在StatusGrpcClient类之上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class StatusConPool {
public:
StatusConPool(size_t poolSize, std::string host, std::string port)
: poolSize_(poolSize), host_(host), port_(port), b_stop_(false) {
for (size_t i = 0; i < poolSize_; ++i) {

std::shared_ptr<Channel> channel = grpc::CreateChannel(host + ":" + port,
grpc::InsecureChannelCredentials());

connections_.push(StatusService::NewStub(channel));
}
}

~StatusConPool() {
std::lock_guard<std::mutex> lock(mutex_);
Close();
while (!connections_.empty()) {
connections_.pop();
}
}

std::unique_ptr<StatusService::Stub> getConnection() {
std::unique_lock<std::mutex> lock(mutex_);
cond_.wait(lock, [this] {
if (b_stop_) {
return true;
}
return !connections_.empty();
});
//如果停止则直接返回空指针
if (b_stop_) {
return nullptr;
}
auto context = std::move(connections_.front());
connections_.pop();
return context;
}

void returnConnection(std::unique_ptr<StatusService::Stub> context) {
std::lock_guard<std::mutex> lock(mutex_);
if (b_stop_) {
return;
}
connections_.push(std::move(context));
cond_.notify_one();
}

void Close() {
b_stop_ = true;
cond_.notify_all();
}

private:
atomic<bool> b_stop_;
size_t poolSize_;
std::string host_;
std::string port_;
std::queue<std::unique_ptr<StatusService::Stub>> connections_;
std::mutex mutex_;
std::condition_variable cond_;
};

StatusServer状态服务

我们要实现状态服务,主要是用来监听其他服务器的查询请求, 用visual studio创建项目,名字为StatusServer.

在主函数所在文件StatusServer.cpp中实现如下逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>
#include "const.h"
#include "ConfigMgr.h"
#include "hiredis.h"
#include "RedisMgr.h"
#include "MysqlMgr.h"
#include "AsioIOServicePool.h"
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <boost/asio.hpp>
#include "StatusServiceImpl.h"
void RunServer() {
auto & cfg = ConfigMgr::Inst();

std::string server_address(cfg["StatusServer"]["Host"]+":"+ cfg["StatusServer"]["Port"]);
StatusServiceImpl service;

grpc::ServerBuilder builder;
// 监听端口和添加服务
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);

// 构建并启动gRPC服务器
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;

// 创建Boost.Asio的io_context
boost::asio::io_context io_context;
// 创建signal_set用于捕获SIGINT
boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);

// 设置异步等待SIGINT信号
signals.async_wait([&server](const boost::system::error_code& error, int signal_number) {
if (!error) {
std::cout << "Shutting down server..." << std::endl;
server->Shutdown(); // 优雅地关闭服务器
}
});

// 在单独的线程中运行io_context
std::thread([&io_context]() { io_context.run(); }).detach();

// 等待服务器关闭
server->Wait();
io_context.stop(); // 停止io_context
}

int main(int argc, char** argv) {
try {
RunServer();
}
catch (std::exception const& e) {
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}

return 0;
}

在开始逻辑之前,我们需要先更新下config.ini文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[StatusServer]
Port = 50052
Host = 0.0.0.0
[Mysql]
Host = 81.68.86.146
Port = 3308
User = root
Passwd = 123456
Schema = llfc
[Redis]
Host = 81.68.86.146
Port = 6380
Passwd = 123456
[ChatServer1]
Host = 127.0.0.1
Port = 8090
[ChatServer2]
Host = 127.0.0.1
Port = 8091

然后我们将GateServer之前生成的pb文件和proto文件拷贝到StatusServer中。并且加入到项目中。

我们在项目中添加一个新的类StatusServiceImpl,该类主要继承自StatusService::Service。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <grpcpp/grpcpp.h>
#include "message.grpc.pb.h"

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using message::GetChatServerReq;
using message::GetChatServerRsp;
using message::StatusService;

struct ChatServer {
std::string host;
std::string port;
};
class StatusServiceImpl final : public StatusService::Service
{
public:
StatusServiceImpl();
Status GetChatServer(ServerContext* context, const GetChatServerReq* request,
GetChatServerRsp* reply) override;

std::vector<ChatServer> _servers;
int _server_index;
};

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "StatusServiceImpl.h"
#include "ConfigMgr.h"
#include "const.h"

std::string generate_unique_string() {
// 创建UUID对象
boost::uuids::uuid uuid = boost::uuids::random_generator()();

// 将UUID转换为字符串
std::string unique_string = to_string(uuid);

return unique_string;
}

Status StatusServiceImpl::GetChatServer(ServerContext* context, const GetChatServerReq* request, GetChatServerRsp* reply)
{
std::string prefix("llfc status server has received : ");
_server_index = (_server_index++) % (_servers.size());
auto &server = _servers[_server_index];
reply->set_host(server.host);
reply->set_port(server.port);
reply->set_error(ErrorCodes::Success);
reply->set_token(generate_unique_string());
return Status::OK;
}

StatusServiceImpl::StatusServiceImpl():_server_index(0)
{
auto& cfg = ConfigMgr::Inst();
ChatServer server;
server.port = cfg["ChatServer1"]["Port"];
server.host = cfg["ChatServer1"]["Host"];
_servers.push_back(server);

server.port = cfg["ChatServer2"]["Port"];
server.host = cfg["ChatServer2"]["Host"];
_servers.push_back(server);
}

其余的文件为了保持复用,我们不重复开发,将GateServer中的RedisMgr,MysqlMgr,Singleton,IOSerivePool等统统拷贝过来并添加到项目中。

联调测试

我们启动StatusServer,GateServer以及QT客户端,输入密码和用户名,点击登陆,会看到前端收到登陆成功的消息

https://cdn.llfc.club/1713248019373.jpg

C++ 全栈聊天项目(16) 重置密码功能

Posted on 2024-08-31 | In C++聊天项目

ChatServer

一个TCP服务器必然会有连接的接收,维持,收发数据等逻辑。那我们就要基于asio完成这个服务的搭建。主服务是这个样子的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "LogicSystem.h"
#include <csignal>
#include <thread>
#include <mutex>
#include "AsioIOServicePool.h"
#include "CServer.h"
#include "ConfigMgr.h"
using namespace std;
bool bstop = false;
std::condition_variable cond_quit;
std::mutex mutex_quit;

int main()
{
try {
auto &cfg = ConfigMgr::Inst();
auto pool = AsioIOServicePool::GetInstance();
boost::asio::io_context io_context;
boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait([&io_context, pool](auto, auto) {
io_context.stop();
pool->Stop();
});
auto port_str = cfg["SelfServer"]["Port"];
CServer s(io_context, atoi(port_str.c_str()));
io_context.run();
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}

}

CServer类的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <boost/asio.hpp>
#include "CSession.h"
#include <memory.h>
#include <map>
#include <mutex>
using namespace std;
using boost::asio::ip::tcp;
class CServer
{
public:
CServer(boost::asio::io_context& io_context, short port);
~CServer();
void ClearSession(std::string);
private:
void HandleAccept(shared_ptr<CSession>, const boost::system::error_code & error);
void StartAccept();
boost::asio::io_context &_io_context;
short _port;
tcp::acceptor _acceptor;
std::map<std::string, shared_ptr<CSession>> _sessions;
std::mutex _mutex;
};

构造函数中监听对方连接

1
2
3
4
5
6
CServer::CServer(boost::asio::io_context& io_context, short port):_io_context(io_context), _port(port),
_acceptor(io_context, tcp::endpoint(tcp::v4(),port))
{
cout << "Server start success, listen on port : " << _port << endl;
StartAccept();
}

接受连接的函数

1
2
3
4
5
void CServer::StartAccept() {
auto &io_context = AsioIOServicePool::GetInstance()->GetIOService();
shared_ptr<CSession> new_session = make_shared<CSession>(io_context, this);
_acceptor.async_accept(new_session->GetSocket(), std::bind(&CServer::HandleAccept, this, new_session, placeholders::_1));
}

AsioIOServicePool

从AsioIOServicePool中返回一个可用的iocontext构造Session,然后将接受的新链接的socket写入这个Session保管。

AsioIOServicePool已经在前面讲解很多次了,它的声明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <vector>
#include <boost/asio.hpp>
#include "Singleton.h"
class AsioIOServicePool:public Singleton<AsioIOServicePool>
{
friend Singleton<AsioIOServicePool>;
public:
using IOService = boost::asio::io_context;
using Work = boost::asio::io_context::work;
using WorkPtr = std::unique_ptr<Work>;
~AsioIOServicePool();
AsioIOServicePool(const AsioIOServicePool&) = delete;
AsioIOServicePool& operator=(const AsioIOServicePool&) = delete;
// 使用 round-robin 的方式返回一个 io_service
boost::asio::io_context& GetIOService();
void Stop();
private:
AsioIOServicePool(std::size_t size = std::thread::hardware_concurrency());
std::vector<IOService> _ioServices;
std::vector<WorkPtr> _works;
std::vector<std::thread> _threads;
std::size_t _nextIOService;
};

AsioIOServicePool具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "AsioIOServicePool.h"
#include <iostream>
using namespace std;
AsioIOServicePool::AsioIOServicePool(std::size_t size):_ioServices(size),
_works(size), _nextIOService(0){
for (std::size_t i = 0; i < size; ++i) {
_works[i] = std::unique_ptr<Work>(new Work(_ioServices[i]));
}

//遍历多个ioservice,创建多个线程,每个线程内部启动ioservice
for (std::size_t i = 0; i < _ioServices.size(); ++i) {
_threads.emplace_back([this, i]() {
_ioServices[i].run();
});
}
}

AsioIOServicePool::~AsioIOServicePool() {
std::cout << "AsioIOServicePool destruct" << endl;
}

boost::asio::io_context& AsioIOServicePool::GetIOService() {
auto& service = _ioServices[_nextIOService++];
if (_nextIOService == _ioServices.size()) {
_nextIOService = 0;
}
return service;
}

void AsioIOServicePool::Stop(){
//因为仅仅执行work.reset并不能让iocontext从run的状态中退出
//当iocontext已经绑定了读或写的监听事件后,还需要手动stop该服务。
for (auto& work : _works) {
//把服务先停止
work->get_io_context().stop();
work.reset();
}

for (auto& t : _threads) {
t.join();
}
}

CServer的处理连接逻辑

1
2
3
4
5
6
7
8
9
10
11
12
void CServer::HandleAccept(shared_ptr<CSession> new_session, const boost::system::error_code& error){
if (!error) {
new_session->Start();
lock_guard<mutex> lock(_mutex);
_sessions.insert(make_pair(new_session->GetUuid(), new_session));
}
else {
cout << "session accept failed, error is " << error.what() << endl;
}

StartAccept();
}

Session层

上面的逻辑接受新链接后执行Start函数,新链接接受数据,然后Server继续监听新的连接

1
2
3
void CSession::Start(){
AsyncReadHead(HEAD_TOTAL_LEN);
}

先读取头部数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
void CSession::AsyncReadHead(int total_len)
{
auto self = shared_from_this();
asyncReadFull(HEAD_TOTAL_LEN, [self, this](const boost::system::error_code& ec, std::size_t bytes_transfered) {
try {
if (ec) {
std::cout << "handle read failed, error is " << ec.what() << endl;
Close();
_server->ClearSession(_uuid);
return;
}

if (bytes_transfered < HEAD_TOTAL_LEN) {
std::cout << "read length not match, read [" << bytes_transfered << "] , total ["
<< HEAD_TOTAL_LEN << "]" << endl;
Close();
_server->ClearSession(_uuid);
return;
}

_recv_head_node->Clear();
memcpy(_recv_head_node->_data, _data, bytes_transfered);

//获取头部MSGID数据
short msg_id = 0;
memcpy(&msg_id, _recv_head_node->_data, HEAD_ID_LEN);
//网络字节序转化为本地字节序
msg_id = boost::asio::detail::socket_ops::network_to_host_short(msg_id);
std::cout << "msg_id is " << msg_id << endl;
//id非法
if (msg_id > MAX_LENGTH) {
std::cout << "invalid msg_id is " << msg_id << endl;
_server->ClearSession(_uuid);
return;
}
short msg_len = 0;
memcpy(&msg_len, _recv_head_node->_data + HEAD_ID_LEN, HEAD_DATA_LEN);
//网络字节序转化为本地字节序
msg_len = boost::asio::detail::socket_ops::network_to_host_short(msg_len);
std::cout << "msg_len is " << msg_len << endl;

//id非法
if (msg_len > MAX_LENGTH) {
std::cout << "invalid data length is " << msg_len << endl;
_server->ClearSession(_uuid);
return;
}

_recv_msg_node = make_shared<RecvNode>(msg_len, msg_id);
AsyncReadBody(msg_len);
}
catch (std::exception& e) {
std::cout << "Exception code is " << e.what() << endl;
}
});
}

上面的逻辑里调用asyncReadFull读取整个长度,然后解析收到的数据,前两个字节为id,之后两个字节为长度,最后n个长度字节为消息内容。

1
2
3
4
5
6
//读取完整长度
void CSession::asyncReadFull(std::size_t maxLength, std::function<void(const boost::system::error_code&, std::size_t)> handler )
{
::memset(_data, 0, MAX_LENGTH);
asyncReadLen(0, maxLength, handler);
}

读取指定长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//读取指定字节数
void CSession::asyncReadLen(std::size_t read_len, std::size_t total_len,
std::function<void(const boost::system::error_code&, std::size_t)> handler)
{
auto self = shared_from_this();
_socket.async_read_some(boost::asio::buffer(_data + read_len, total_len-read_len),
[read_len, total_len, handler, self](const boost::system::error_code& ec, std::size_t bytesTransfered) {
if (ec) {
// 出现错误,调用回调函数
handler(ec, read_len + bytesTransfered);
return;
}

if (read_len + bytesTransfered >= total_len) {
//长度够了就调用回调函数
handler(ec, read_len + bytesTransfered);
return;
}

// 没有错误,且长度不足则继续读取
self->asyncReadLen(read_len + bytesTransfered, total_len, handler);
});
}

读取头部成功后,其回调函数内部调用了读包体的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void CSession::AsyncReadBody(int total_len)
{
auto self = shared_from_this();
asyncReadFull(total_len, [self, this, total_len](const boost::system::error_code& ec, std::size_t bytes_transfered) {
try {
if (ec) {
std::cout << "handle read failed, error is " << ec.what() << endl;
Close();
_server->ClearSession(_uuid);
return;
}

if (bytes_transfered < total_len) {
std::cout << "read length not match, read [" << bytes_transfered << "] , total ["
<< total_len<<"]" << endl;
Close();
_server->ClearSession(_uuid);
return;
}

memcpy(_recv_msg_node->_data , _data , bytes_transfered);
_recv_msg_node->_cur_len += bytes_transfered;
_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
cout << "receive data is " << _recv_msg_node->_data << endl;
//此处将消息投递到逻辑队列中
LogicSystem::GetInstance()->PostMsgToQue(make_shared<LogicNode>(shared_from_this(), _recv_msg_node));
//继续监听头部接受事件
AsyncReadHead(HEAD_TOTAL_LEN);
}
catch (std::exception& e) {
std::cout << "Exception code is " << e.what() << endl;
}
});
}

读取包体完成后,在回调中继续读包头。以此循环往复直到读完所有数据。如果对方不发送数据,则回调函数就不会触发。不影响程序执行其他工作,因为我们采用的是asio异步的读写操作。

当然我们解析完包体后会调用LogicSystem单例将解析好的消息封装为逻辑节点传递给逻辑层进行处理。

LogicSystem

我们在逻辑层处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void LogicSystem::RegisterCallBacks() {
_fun_callbacks[MSG_CHAT_LOGIN] = std::bind(&LogicSystem::LoginHandler, this,
placeholders::_1, placeholders::_2, placeholders::_3);
}

void LogicSystem::LoginHandler(shared_ptr<CSession> session, const short &msg_id, const string &msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
std::cout << "user login uid is " << root["uid"].asInt() << " user token is "
<< root["token"].asString() << endl;

std::string return_str = root.toStyledString();
session->Send(return_str, msg_id);
}

并在构造函数中注册这些处理流程

1
2
3
4
LogicSystem::LogicSystem():_b_stop(false){
RegisterCallBacks();
_worker_thread = std::thread (&LogicSystem::DealMsg, this);
}

总结

到此,完成了ChatServer收到QT客户端发送过来的长链接请求,并解析读取的数据,将收到的数据通过tcp发送给对端。接下来还要做ChatServer到GateServer的token验证,判断是否合理,这个教给之后的文章处理。

C++ 全栈聊天项目(15) 实现自定义可点击Label以及定时按钮

Posted on 2024-08-31 | In C++聊天项目

客户端TCP管理者

因为聊天服务要维持一个长链接,方便服务器和客户端双向通信,那么就需要一个TCPMgr来管理TCP连接。

而实际开发中网络模块一般以单例模式使用,那我们就基于单例基类和可被分享类创建一个自定义的TcpMgr类,在QT工程中新建TcpMgr类,会生成头文件和源文件,头文件修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef TCPMGR_H
#define TCPMGR_H
#include <QTcpSocket>
#include "singleton.h"
#include "global.h"
class TcpMgr:public QObject, public Singleton<TcpMgr>,
public std::enable_shared_from_this<TcpMgr>
{
Q_OBJECT
public:
TcpMgr();
private:
QTcpSocket _socket;
QString _host;
uint16_t _port;
QByteArray _buffer;
bool _b_recv_pending;
quint16 _message_id;
quint16 _message_len;
public slots:
void slot_tcp_connect(ServerInfo);
void slot_send_data(ReqId reqId, QString data);
signals:
void sig_con_success(bool bsuccess);
void sig_send_data(ReqId reqId, QString data);
};

#endif // TCPMGR_H

接下来我们在构造函数中连接网络请求的各种信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
TcpMgr::TcpMgr():_host(""),_port(0),_b_recv_pending(false),_message_id(0),_message_len(0)
{
QObject::connect(&_socket, &QTcpSocket::connected, [&]() {
qDebug() << "Connected to server!";
// 连接建立后发送消息
emit sig_con_success(true);
});

QObject::connect(&_socket, &QTcpSocket::readyRead, [&]() {
// 当有数据可读时,读取所有数据
// 读取所有数据并追加到缓冲区
_buffer.append(_socket.readAll());

QDataStream stream(&_buffer, QIODevice::ReadOnly);
stream.setVersion(QDataStream::Qt_5_0);

forever {
//先解析头部
if(!_b_recv_pending){
// 检查缓冲区中的数据是否足够解析出一个消息头(消息ID + 消息长度)
if (_buffer.size() < static_cast<int>(sizeof(quint16) * 2)) {
return; // 数据不够,等待更多数据
}

// 预读取消息ID和消息长度,但不从缓冲区中移除
stream >> _message_id >> _message_len;

//将buffer 中的前四个字节移除
_buffer = _buffer.mid(sizeof(quint16) * 2);

// 输出读取的数据
qDebug() << "Message ID:" << _message_id << ", Length:" << _message_len;

}

//buffer剩余长读是否满足消息体长度,不满足则退出继续等待接受
if(_buffer.size() < _message_len){
_b_recv_pending = true;
return;
}

_b_recv_pending = false;
// 读取消息体
QByteArray messageBody = _buffer.mid(0, _message_len);
qDebug() << "receive body msg is " << messageBody ;

_buffer = _buffer.mid(_message_len);
}

});

//5.15 之后版本
// QObject::connect(&_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred), [&](QAbstractSocket::SocketError socketError) {
// Q_UNUSED(socketError)
// qDebug() << "Error:" << _socket.errorString();
// });

// 处理错误(适用于Qt 5.15之前的版本)
QObject::connect(&_socket, static_cast<void (QTcpSocket::*)(QTcpSocket::SocketError)>(&QTcpSocket::error),
[&](QTcpSocket::SocketError socketError) {
qDebug() << "Error:" << _socket.errorString() ;
switch (socketError) {
case QTcpSocket::ConnectionRefusedError:
qDebug() << "Connection Refused!";
emit sig_con_success(false);
break;
case QTcpSocket::RemoteHostClosedError:
qDebug() << "Remote Host Closed Connection!";
break;
case QTcpSocket::HostNotFoundError:
qDebug() << "Host Not Found!";
emit sig_con_success(false);
break;
case QTcpSocket::SocketTimeoutError:
qDebug() << "Connection Timeout!";
emit sig_con_success(false);
break;
case QTcpSocket::NetworkError:
qDebug() << "Network Error!";
break;
default:
qDebug() << "Other Error!";
break;
}
});

// 处理连接断开
QObject::connect(&_socket, &QTcpSocket::disconnected, [&]() {
qDebug() << "Disconnected from server.";
});

QObject::connect(this, &TcpMgr::sig_send_data, this, &TcpMgr::slot_send_data);
}

连接对端服务器

1
2
3
4
5
6
7
8
9
void TcpMgr::slot_tcp_connect(ServerInfo si)
{
qDebug()<< "receive tcp connect signal";
// 尝试连接到服务器
qDebug() << "Connecting to server...";
_host = si.Host;
_port = static_cast<uint16_t>(si.Port.toUInt());
_socket.connectToHost(si.Host, _port);
}

因为客户端发送数据可能在任何线程,为了保证线程安全,我们在要发送数据时发送TcpMgr的sig_send_data信号,然后实现接受这个信号的槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void TcpMgr::slot_send_data(ReqId reqId, QString data)
{
uint16_t id = reqId;

// 将字符串转换为UTF-8编码的字节数组
QByteArray dataBytes = data.toUtf8();

// 计算长度(使用网络字节序转换)
quint16 len = static_cast<quint16>(data.size());

// 创建一个QByteArray用于存储要发送的所有数据
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);

// 设置数据流使用网络字节序
out.setByteOrder(QDataStream::BigEndian);

// 写入ID和长度
out << id << len;

// 添加字符串数据
block.append(data);

// 发送数据
_socket.write(block);
}

然后修改LoginDialog中的initHandlers中的收到服务器登陆回复后的逻辑,这里发送信号准备发起长链接到聊天服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void LoginDialog::initHttpHandlers()
{
//注册获取登录回包逻辑
_handlers.insert(ReqId::ID_LOGIN_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
enableBtn(true);
return;
}
auto user = jsonObj["user"].toString();

//发送信号通知tcpMgr发送长链接
ServerInfo si;
si.Uid = jsonObj["uid"].toInt();
si.Host = jsonObj["host"].toString();
si.Port = jsonObj["port"].toString();
si.Token = jsonObj["token"].toString();

_uid = si.Uid;
_token = si.Token;
qDebug()<< "user is " << user << " uid is " << si.Uid <<" host is "
<< si.Host << " Port is " << si.Port << " Token is " << si.Token;
emit sig_connect_tcp(si);
});
}

在LoginDialog构造函数中连接信号,包括建立tcp连接,以及收到TcpMgr连接成功或者失败的信号处理

1
2
3
4
//连接tcp连接请求的信号和槽函数
connect(this, &LoginDialog::sig_connect_tcp, TcpMgr::GetInstance().get(), &TcpMgr::slot_tcp_connect);
//连接tcp管理者发出的连接成功信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_con_success, this, &LoginDialog::slot_tcp_con_finish);

LoginDialog收到连接结果的槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void LoginDialog::slot_tcp_con_finish(bool bsuccess)
{

if(bsuccess){
showTip(tr("聊天服务连接成功,正在登录..."),true);
QJsonObject jsonObj;
jsonObj["uid"] = _uid;
jsonObj["token"] = _token;

QJsonDocument doc(jsonObj);
QString jsonString = doc.toJson(QJsonDocument::Indented);

//发送tcp请求给chat server
TcpMgr::GetInstance()->sig_send_data(ReqId::ID_CHAT_LOGIN, jsonString);

}else{
showTip(tr("网络异常"),false);
enableBtn(true);
}

}

在这个槽函数中我们发送了sig_send_data信号并且通知TcpMgr将数据发送给服务器。

这样TcpMgr发送完数据收到服务器的回复后就可以进一步根据解析出来的信息处理不同的情况了。我们先到此为止。具体如何处理后续再讲。

C++ 全栈聊天项目(17) 登录验证和状态服务

Posted on 2024-08-31 | In C++聊天项目

完善proto

在proto文件里新增登陆验证服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message LoginReq{
int32 uid = 1;
string token= 2;
}

message LoginRsp {
int32 error = 1;
int32 uid = 2;
string token = 3;
}

service StatusService {
rpc GetChatServer (GetChatServerReq) returns (GetChatServerRsp) {}
rpc Login(LoginReq) returns(LoginRsp);
}

接下来是调用grpc命令生成新的pb文件覆盖原有的,并且也拷贝给StatusServer一份

我们完善登陆逻辑,先去StatusServer验证token是否合理,如果合理再从内存中寻找用户信息,如果没找到则从数据库加载一份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void LogicSystem::LoginHandler(shared_ptr<CSession> session, const short &msg_id, const string &msg_data) {
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
auto uid = root["uid"].asInt();
std::cout << "user login uid is " << uid << " user token is "
<< root["token"].asString() << endl;
//从状态服务器获取token匹配是否准确
auto rsp = StatusGrpcClient::GetInstance()->Login(uid, root["token"].asString());
Json::Value rtvalue;
Defer defer([this, &rtvalue, session]() {
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, MSG_CHAT_LOGIN_RSP);
});

rtvalue["error"] = rsp.error();
if (rsp.error() != ErrorCodes::Success) {
return;
}

//内存中查询用户信息
auto find_iter = _users.find(uid);
std::shared_ptr<UserInfo> user_info = nullptr;
if (find_iter == _users.end()) {
//查询数据库
user_info = MysqlMgr::GetInstance()->GetUser(uid);
if (user_info == nullptr) {
rtvalue["error"] = ErrorCodes::UidInvalid;
return;
}

_users[uid] = user_info;
}
else {
user_info = find_iter->second;
}

rtvalue["uid"] = uid;
rtvalue["token"] = rsp.token();
rtvalue["name"] = user_info->name;
}

StatusServer验证token

在StatusServer验证token之前,我们需要在StatusServer中的GetServer的服务里将token写入内存

1
2
3
4
5
6
7
8
9
10
11
Status StatusServiceImpl::GetChatServer(ServerContext* context, const GetChatServerReq* request, GetChatServerRsp* reply)
{
std::string prefix("llfc status server has received : ");
const auto& server = getChatServer();
reply->set_host(server.host);
reply->set_port(server.port);
reply->set_error(ErrorCodes::Success);
reply->set_token(generate_unique_string());
insertToken(request->uid(), reply->token());
return Status::OK;
}

接下来我们实现登陆验证服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Status StatusServiceImpl::Login(ServerContext* context, const LoginReq* request, LoginRsp* reply)
{
auto uid = request->uid();
auto token = request->token();
std::lock_guard<std::mutex> guard(_token_mtx);
auto iter = _tokens.find(uid);
if (iter == _tokens.end()) {
reply->set_error(ErrorCodes::UidInvalid);
return Status::OK;
}
if (iter->second != token) {
reply->set_error(ErrorCodes::TokenInvalid);
return Status::OK;
}
reply->set_error(ErrorCodes::Success);
reply->set_uid(uid);
reply->set_token(token);
return Status::OK;
}

这样当GateServer访问StatusServer的Login服务做验证后,就可以将数据返回给QT前端了。

客户端处理登陆回包

QT 的客户端TcpMgr收到请求后要进行对应的逻辑处理。所以我们在TcpMgr的构造函数中调用initHandlers注册消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void TcpMgr::initHandlers()
{
//auto self = shared_from_this();
_handlers.insert(ID_CHAT_LOGIN_RSP, [this](ReqId id, int len, QByteArray data){
qDebug()<< "handle id is "<< id << " data is " << data;
// 将QByteArray转换为QJsonDocument
QJsonDocument jsonDoc = QJsonDocument::fromJson(data);

// 检查转换是否成功
if(jsonDoc.isNull()){
qDebug() << "Failed to create QJsonDocument.";
return;
}

QJsonObject jsonObj = jsonDoc.object();

if(!jsonObj.contains("error")){
int err = ErrorCodes::ERR_JSON;
qDebug() << "Login Failed, err is Json Parse Err" << err ;
emit sig_login_failed(err);
return;
}

int err = jsonObj["error"].toInt();
if(err != ErrorCodes::SUCCESS){
qDebug() << "Login Failed, err is " << err ;
emit sig_login_failed(err);
return;
}

UserMgr::GetInstance()->SetUid(jsonObj["uid"].toInt());
UserMgr::GetInstance()->SetName(jsonObj["name"].toString());
UserMgr::GetInstance()->SetToken(jsonObj["token"].toString());
emit sig_swich_chatdlg();
});
}

并且增加处理请求

1
2
3
4
5
6
7
8
9
10
void TcpMgr::handleMsg(ReqId id, int len, QByteArray data)
{
auto find_iter = _handlers.find(id);
if(find_iter == _handlers.end()){
qDebug()<< "not found id ["<< id << "] to handle";
return ;
}

find_iter.value()(id,len,data);
}

用户管理

为管理用户数据,需要创建一个UserMgr类,统一管理用户数据,我们这么声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef USERMGR_H
#define USERMGR_H
#include <QObject>
#include <memory>
#include <singleton.h>

class UserMgr:public QObject,public Singleton<UserMgr>,
public std::enable_shared_from_this<UserMgr>
{
Q_OBJECT
public:
friend class Singleton<UserMgr>;
~ UserMgr();
void SetName(QString name);
void SetUid(int uid);
void SetToken(QString token);
private:
UserMgr();
QString _name;
QString _token;
int _uid;
};

#endif // USERMGR_H

简单实现几个功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "usermgr.h"

UserMgr::~UserMgr()
{

}

void UserMgr::SetName(QString name)
{
_name = name;
}

void UserMgr::SetUid(int uid)
{
_uid = uid;
}

void UserMgr::SetToken(QString token)
{
_token = token;
}

UserMgr::UserMgr()
{

}

详细和复杂的管理后续不断往这里补充就行了。

登陆界面

登陆界面响应TcpMgr返回的登陆请求,在其构造函数中添加

1
2
//连接tcp管理者发出的登陆失败信号
connect(TcpMgr::GetInstance().get(), &TcpMgr::sig_login_failed, this, &LoginDialog::slot_login_failed);

并实现槽函数

1
2
3
4
5
6
7
void LoginDialog::slot_login_failed(int err)
{
QString result = QString("登录失败, err is %1")
.arg(err);
showTip(result,false);
enableBtn(true);
}

到此完成了登陆的请求和响应,接下来要实现响应登陆成功后跳转到聊天界面。下一篇先实现聊天布局。

聊天项目(18) 聊天界面布局以及可点击按钮的实现

Posted on 2024-08-31 | In C++聊天项目

聊天界面整体展示

我们先看下整体的聊天界面,方便以后逐个功能添加
https://cdn.llfc.club/1716523002662.jpg

QT Designer中 ui 设计如下
https://cdn.llfc.club/1716528347175.jpg

将聊天对话框分为几个部分:

1 处为左侧功能切换区域,包括聊天,联系人等。

2 处为搜索区域,可以搜索联系人,聊天记录等, 搜索框后面的按钮是快速拉人创建群聊的功能。

3 处为近期聊天联系列表

4 处为搜索结果列表,包括匹配到的联系人,聊天记录,以及添加联系人的按钮。

5 处为聊天对象名称和头像的显示区域,这里先做简单演示写死。

6 处为聊天记录区域。

7 处为工具栏区域。

8 处为输入区域

9 处为发送按钮区域。

大家可以按照这个布局拖动和修改,达到我的布局效果。

创建ChatDialog

右键项目,选择创建设计师界面类,选择创建QDialog without buttons。对话框的名字为ChatDialog

创建完成后,在之前登录成功的回调里,跳转到这个对话框。在MainWindow里添加槽函数

1
2
3
4
5
6
7
8
9
10
void MainWindow::SlotSwitchChat()
{
_chat_dlg = new ChatDialog();
_chat_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_chat_dlg);
_chat_dlg->show();
_login_dlg->hide();
this->setMinimumSize(QSize(1050,900));
this->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
}

在MainWindow的构造函数中添加信号和槽的链接

1
2
3
//连接创建聊天界面信号
connect(TcpMgr::GetInstance().get(),&TcpMgr::sig_swich_chatdlg, this, &MainWindow::SlotSwitchChat);

并且在TcpMgr中添加信号

1
void sig_swich_chatdlg();

为了方便测试,我们在MainWindow的构造函数中直接发送sig_switch_chatdlg信号,这样程序运行起来就会直接跳转到聊天界面

ChatDialog ui文件

ui文件的结构可以参考我的源码中的结构,这里我们可以简单看下

https://cdn.llfc.club/1716603750779.jpg

按照这个布局拖动并设置宽高即可,接下来需要设置下qss调整颜色

1
2
3
#side_bar{
background-color:rgb(46,46,46);
}

重写点击按钮

为了实现点击效果,我们继承QPushButton实现按钮的点击效果,包括普通状态,悬浮状态,以及按下状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ClickedBtn:public QPushButton
{
Q_OBJECT
public:
ClickedBtn(QWidget * parent = nullptr);
~ClickedBtn();
void SetState(QString nomal, QString hover, QString press);
protected:
virtual void enterEvent(QEvent *event) override; // 鼠标进入
virtual void leaveEvent(QEvent *event) override;// 鼠标离开
virtual void mousePressEvent(QMouseEvent *event) override; // 鼠标按下
virtual void mouseReleaseEvent(QMouseEvent *event) override; // 鼠标释放
private:
QString _normal;
QString _hover;
QString _press;
};

接下来实现其按下,离开进入等资源加载,并且重写这些事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
ClickedBtn::ClickedBtn(QWidget *parent):QPushButton (parent)
{
setCursor(Qt::PointingHandCursor); // 设置光标为小手
}

ClickedBtn::~ClickedBtn(){

}

void ClickedBtn::SetState(QString normal, QString hover, QString press)
{
_hover = hover;
_normal = normal;
_press = press;
setProperty("state",normal);
repolish(this);
update();
}

void ClickedBtn::enterEvent(QEvent *event)
{
setProperty("state",_hover);
repolish(this);
update();
QPushButton::enterEvent(event);
}

void ClickedBtn::mousePressEvent(QMouseEvent *event)
{
setProperty("state",_press);
repolish(this);
update();
QPushButton::mousePressEvent(event);
}

void ClickedBtn::mouseReleaseEvent(QMouseEvent *event)
{
setProperty("state",_hover);
repolish(this);
update();
QPushButton::mouseReleaseEvent(event);
}

回到chatdialog.ui文件,将add_btn升级为ClickedBtn

接着在qss文件中添加样式

1
2
3
4
5
6
7
8
9
10
11
12
#add_btn[state='normal']{
border-image: url(:/res/add_friend_normal.png);
}

#add_btn[state='hover']{
border-image: url(:/res/add_friend_hover.png);

}

#add_btn[state='press']{
border-image: url(:/res/add_friend_hover.png);
}

add_btn的样式一定要显示设置一下三个样式,所以我们回到ChatDialog的构造函数中设置样式

1
ui->add_btn->SetState("normal","hover","press");

再次启动运行,可以看到添加群组的按钮样式和sidebar的样式生效了。

为了美观显示,我们去mainwindow.ui中移除状态栏和菜单栏。

聊天项目(19) 实现搜索框和聊天列表

Posted on 2024-08-31 | In C++聊天项目

搜索框

我们需要实现如下效果

https://cdn.llfc.club/1717211817129.jpg

输入框默认不显示关闭按钮,当输入文字后显示关闭按钮,点击关闭按钮清空文字

添加CustomizeEdit类,头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#ifndef CUSTOMIZEEDIT_H
#define CUSTOMIZEEDIT_H
#include <QLineEdit>
#include <QDebug>

class CustomizeEdit: public QLineEdit
{
Q_OBJECT
public:
CustomizeEdit(QWidget *parent = nullptr);
void SetMaxLength(int maxLen);
protected:
void focusOutEvent(QFocusEvent *event) override
{
// 执行失去焦点时的处理逻辑
//qDebug() << "CustomizeEdit focusout";
// 调用基类的focusOutEvent()方法,保证基类的行为得到执行
QLineEdit::focusOutEvent(event);
//发送失去焦点得信号
emit sig_foucus_out();
}
private:
void limitTextLength(QString text) {
if(_max_len <= 0){
return;
}

QByteArray byteArray = text.toUtf8();

if (byteArray.size() > _max_len) {
byteArray = byteArray.left(_max_len);
this->setText(QString::fromUtf8(byteArray));
}
}

int _max_len;
signals:
void sig_foucus_out();
};

#endif // CUSTOMIZEEDIT_H

源文件

1
2
3
4
5
6
7
8
9
10
11
#include "customizeedit.h"

CustomizeEdit::CustomizeEdit(QWidget *parent):QLineEdit (parent),_max_len(0)
{
connect(this, &QLineEdit::textChanged, this, &CustomizeEdit::limitTextLength);
}

void CustomizeEdit::SetMaxLength(int maxLen)
{
_max_len = maxLen;
}

设计师界面类里将ui->search_edit提升为CustomizeEdit

在ChatDialog的构造函数中设置输入的长度限制以及关闭等图标的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
QAction *searchAction = new QAction(ui->search_edit);
searchAction->setIcon(QIcon(":/res/search.png"));
ui->search_edit->addAction(searchAction,QLineEdit::LeadingPosition);
ui->search_edit->setPlaceholderText(QStringLiteral("搜索"));


// 创建一个清除动作并设置图标
QAction *clearAction = new QAction(ui->search_edit);
clearAction->setIcon(QIcon(":/res/close_transparent.png"));
// 初始时不显示清除图标
// 将清除动作添加到LineEdit的末尾位置
ui->search_edit->addAction(clearAction, QLineEdit::TrailingPosition);

// 当需要显示清除图标时,更改为实际的清除图标
connect(ui->search_edit, &QLineEdit::textChanged, [clearAction](const QString &text) {
if (!text.isEmpty()) {
clearAction->setIcon(QIcon(":/res/close_search.png"));
} else {
clearAction->setIcon(QIcon(":/res/close_transparent.png")); // 文本为空时,切换回透明图标
}

});

// 连接清除动作的触发信号到槽函数,用于清除文本
connect(clearAction, &QAction::triggered, [this, clearAction]() {
ui->search_edit->clear();
clearAction->setIcon(QIcon(":/res/close_transparent.png")); // 清除文本后,切换回透明图标
ui->search_edit->clearFocus();
//清除按钮被按下则不显示搜索框
//ShowSearch(false);
});

ui->search_edit->SetMaxLength(15);

stylesheet.qss 中修改样式

1
2
3
4
5
6
7
#search_wid{
background-color:rgb(247,247,247);
}

#search_edit {
border: 2px solid #f1f1f1;
}

聊天记录列表

创建C++ 类ChatUserList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef CHATUSERLIST_H
#define CHATUSERLIST_H
#include <QListWidget>
#include <QWheelEvent>
#include <QEvent>
#include <QScrollBar>
#include <QDebug>

class ChatUserList: public QListWidget
{
Q_OBJECT
public:
ChatUserList(QWidget *parent = nullptr);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;

signals:
void sig_loading_chat_user();
};

#endif // CHATUSERLIST_H

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include "chatuserlist.h"

ChatUserList::ChatUserList(QWidget *parent):QListWidget(parent)
{
Q_UNUSED(parent);
this->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
this->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
// 安装事件过滤器
this->viewport()->installEventFilter(this);
}

bool ChatUserList::eventFilter(QObject *watched, QEvent *event)
{
// 检查事件是否是鼠标悬浮进入或离开
if (watched == this->viewport()) {
if (event->type() == QEvent::Enter) {
// 鼠标悬浮,显示滚动条
this->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
} else if (event->type() == QEvent::Leave) {
// 鼠标离开,隐藏滚动条
this->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
}
}

// 检查事件是否是鼠标滚轮事件
if (watched == this->viewport() && event->type() == QEvent::Wheel) {
QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);
int numDegrees = wheelEvent->angleDelta().y() / 8;
int numSteps = numDegrees / 15; // 计算滚动步数

// 设置滚动幅度
this->verticalScrollBar()->setValue(this->verticalScrollBar()->value() - numSteps);

// 检查是否滚动到底部
QScrollBar *scrollBar = this->verticalScrollBar();
int maxScrollValue = scrollBar->maximum();
int currentValue = scrollBar->value();
//int pageSize = 10; // 每页加载的联系人数量

if (maxScrollValue - currentValue <= 0) {
// 滚动到底部,加载新的联系人
qDebug()<<"load more chat user";
//发送信号通知聊天界面加载更多聊天内容
emit sig_loading_chat_user();
}

return true; // 停止事件传递
}

return QListWidget::eventFilter(watched, event);
}

在设计师界面类里提升ui->chat_user_list为ChatUserList

在ChatDialog构造函数和搜索清除按钮的回调中增加

1
ShowSearch(false);

该函数的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void ChatDialog::ShowSearch(bool bsearch)
{
if(bsearch){
ui->chat_user_list->hide();
ui->con_user_list->hide();
ui->search_list->show();
_mode = ChatUIMode::SearchMode;
}else if(_state == ChatUIMode::ChatMode){
ui->chat_user_list->show();
ui->con_user_list->hide();
ui->search_list->hide();
_mode = ChatUIMode::ChatMode;
}else if(_state == ChatUIMode::ContactMode){
ui->chat_user_list->hide();
ui->search_list->hide();
ui->con_user_list->show();
_mode = ChatUIMode::ContactMode;
}
}

ChatDialog类中声明添加

1
2
3
ChatUIMode _mode;
ChatUIMode _state;
bool _b_loading;

构造函数的初始化列表初始化这些模式和状态

1
2
3
4
ChatDialog::ChatDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ChatDialog),_mode(ChatUIMode::ChatMode),
_state(ChatUIMode::ChatMode),_b_loading(false){//...}

为了让用户聊天列表更美观,修改qss文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#chat_user_wid{
background-color:rgb(0,0,0);
}

#chat_user_list {
background-color: rgb(247,247,248);
border: none;
}


#chat_user_list::item:selected {
background-color: #d3d7d4;
border: none;
outline: none;
}

#chat_user_list::item:hover {
background-color: rgb(206,207,208);
border: none;
outline: none;
}

#chat_user_list::focus {
border: none;
outline: none;
}

添加聊天item

我们要为聊天列表添加item,每个item包含的样式为

https://cdn.llfc.club/1717215988933.jpg

对于这样的列表元素,我们采用设计师界面类设计非常方便, 新建设计师界面类ChatUserWid, 在ChatUserWid.ui中拖动布局如下

https://cdn.llfc.club/1717217007100.jpg

我们定义一个基类ListItemBase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef LISTITEMBASE_H
#define LISTITEMBASE_H
#include <QWidget>
#include "global.h"

class ListItemBase : public QWidget
{
Q_OBJECT
public:
explicit ListItemBase(QWidget *parent = nullptr);
void SetItemType(ListItemType itemType);

ListItemType GetItemType();

private:
ListItemType _itemType;

public slots:

signals:


};

#endif // LISTITEMBASE_H

我们实现这个基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "listitembase.h"

ListItemBase::ListItemBase(QWidget *parent) : QWidget(parent)
{

}

void ListItemBase::SetItemType(ListItemType itemType)
{
_itemType = itemType;
}

ListItemType ListItemBase::GetItemType()
{
return _itemType;
}

我们实现ChatUserWid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#ifndef CHATUSERWID_H
#define CHATUSERWID_H

#include <QWidget>
#include "listitembase.h"
namespace Ui {
class ChatUserWid;
}

class ChatUserWid : public ListItemBase
{
Q_OBJECT

public:
explicit ChatUserWid(QWidget *parent = nullptr);
~ChatUserWid();

QSize sizeHint() const override {
return QSize(250, 70); // 返回自定义的尺寸
}

void SetInfo(QString name, QString head, QString msg);

private:
Ui::ChatUserWid *ui;
QString _name;
QString _head;
QString _msg;
};

#endif // CHATUSERWID_H

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "chatuserwid.h"
#include "ui_chatuserwid.h"

ChatUserWid::ChatUserWid(QWidget *parent) :
ListItemBase(parent),
ui(new Ui::ChatUserWid)
{
ui->setupUi(this);
SetItemType(ListItemType::CHAT_USER_ITEM);
}

ChatUserWid::~ChatUserWid()
{
delete ui;
}

void ChatUserWid::SetInfo(QString name, QString head, QString msg)
{
_name = name;
_head = head;
_msg = msg;
// 加载图片
QPixmap pixmap(_head);

// 设置图片自动缩放
ui->icon_lb->setPixmap(pixmap.scaled(ui->icon_lb->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
ui->icon_lb->setScaledContents(true);

ui->user_name_lb->setText(_name);
ui->user_chat_lb->setText(_msg);
}

在ChatDialog里定义一些全局的变量用来做测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
std::vector<QString>  strs ={"hello world !",
"nice to meet u",
"New year,new life",
"You have to love yourself",
"My love is written in the wind ever since the whole world is you"};

std::vector<QString> heads = {
":/res/head_1.jpg",
":/res/head_2.jpg",
":/res/head_3.jpg",
":/res/head_4.jpg",
":/res/head_5.jpg"
};

std::vector<QString> names = {
"llfc",
"zack",
"golang",
"cpp",
"java",
"nodejs",
"python",
"rust"
};

这些数据只是测试数据,实际数据是后端传输过来的,我们目前只测试界面功能,用测试数据即可,写一个函数根据上面的数据添加13条item记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ChatDialog::addChatUserList()
{
// 创建QListWidgetItem,并设置自定义的widget
for(int i = 0; i < 13; i++){
int randomValue = QRandomGenerator::global()->bounded(100); // 生成0到99之间的随机整数
int str_i = randomValue%strs.size();
int head_i = randomValue%heads.size();
int name_i = randomValue%names.size();

auto *chat_user_wid = new ChatUserWid();
chat_user_wid->SetInfo(names[name_i], heads[head_i], strs[str_i]);
QListWidgetItem *item = new QListWidgetItem;
//qDebug()<<"chat_user_wid sizeHint is " << chat_user_wid->sizeHint();
item->setSizeHint(chat_user_wid->sizeHint());
ui->chat_user_list->addItem(item);
ui->chat_user_list->setItemWidget(item, chat_user_wid);
}
}

在ChatDialog构造函数中添加

1
addChatUserList();

完善界面效果,新增qss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ChatUserWid {
background-color:rgb(247,247,247);
border: none;
}

#user_chat_lb{
color:rgb(153,153,153);
font-size: 12px;
font-family: "Microsoft YaHei";
}

#user_name_lb{
color:rgb(0,0,0);
font-size: 14px;
font-weight: normal;
font-family: "Microsoft YaHei";
}

#time_wid #time_lb{
color:rgb(140,140,140);
font-size: 12px;
font-family: "Microsoft YaHei";
}

QScrollBar:vertical {
background: transparent; /* 将轨道背景设置为透明 */
width: 8px; /* 滚动条宽度,根据需要调整 */
margin: 0px; /* 移除滚动条与滑块之间的间距 */
}

QScrollBar::handle:vertical {
background: rgb(173,170,169); /* 滑块颜色 */
min-height: 10px; /* 滑块最小高度,根据需要调整 */
border-radius: 4px; /* 滑块边缘圆角,调整以形成椭圆形状 */
}

QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px; /* 移除上下按钮 */
border: none; /* 移除边框 */
background: transparent; /* 背景透明 */
}

QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: none; /* 页面滚动部分背景透明 */
}

测试效果

https://cdn.llfc.club/1717218961063.jpg

源码链接

https://gitee.com/secondtonone1/llfcchat

视频链接

https://www.bilibili.com/video/BV13Z421W7WA/?spm_id_from=333.788&vd_source=8be9e83424c2ed2c9b2a3ed1d01385e9

C++ 全栈聊天项目(2) 内存泄漏修复和qss样式切换

Posted on 2024-08-31 | In C++聊天项目

完善注册类界面

先在注册类构造函数里添加lineEdit的模式为密码模式

1
2
ui->lineEdit_Passwd->setEchoMode(QLineEdit::Password);
ui->lineEdit_Confirm->setEchoMode(QLineEdit::Password);

我们在注册界面的ui里添加一个widget,widget内部包含一个tip居中显示,用来提示错误。设置label的显示为文字居中。

https://cdn.llfc.club/1709103910427.jpg

我们在qss里添加err_tip样式,根据不同的状态做字体显示

1
2
3
4
5
6
7
#err_tip[state='normal']{
color: green;
}

#err_tip[state='err']{
color: red;
}

接下来项目中添加global.h和global.cpp文件,global.h声明repolish函数,global.cpp用来定义这个函数。

.h中的声明

1
2
3
4
5
6
7
8
#ifndef GLOBAL_H
#define GLOBAL_H
#include <QWidget>
#include <functional>
#include "QStyle"
extern std::function<void(QWidget*)> repolish;

#endif // GLOBAL_H

.cpp中的定义

1
2
3
4
5
6
7
#include "global.h"

std::function<void(QWidget*)> repolish =[](QWidget *w){
w->style()->unpolish(w);
w->style()->polish(w);
};

在Register的构造函数中添加样式设置。

1
2
ui->err_tip->setProperty("state","normal");
repolish(ui->err_tip);

接下来实现获取验证码的逻辑,ui里关联get_code按钮的槽事件,并实现槽函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void RegisterDialog::on_get_code_clicked()
{
//验证邮箱的地址正则表达式
auto email = ui->email_edit->text();
// 邮箱地址的正则表达式
QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
bool match = regex.match(email).hasMatch(); // 执行正则表达式匹配
if(match){
//发送http请求获取验证码
}else{
//提示邮箱不正确
showTip(tr("邮箱地址不正确"));
}
}

在RegisterDialog中添加showTip函数

1
2
3
4
5
6
void RegisterDialog::showTip(QString str)
{
ui->err_tip->setText(str);
ui->err_tip->setProperty("state","err");
repolish(ui->err_tip);
}

好了,这样就完成了。测试提示功能正确,下面要实现判断邮箱正确后发送http请求。

单例类封装

网络请求类要做成一个单例类,这样方便在任何需要发送http请求的时候调用,我们先实现单例类,添加singleton.h实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <memory>
#include <mutex>
#include <iostream>
using namespace std;
template <typename T>
class Singleton {
protected:
Singleton() = default;
Singleton(const Singleton<T>&) = delete;
Singleton& operator=(const Singleton<T>& st) = delete;

static std::shared_ptr<T> _instance;
public:
static std::shared_ptr<T> GetInstance() {
static std::once_flag s_flag;
std::call_once(s_flag, [&]() {
_instance = shared_ptr<T>(new T);
});

return _instance;
}
void PrintAddress() {
std::cout << _instance.get() << endl;
}
~Singleton() {
std::cout << "this is singleton destruct" << std::endl;
}
};

template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

http管理类

http管理类主要用来管理http发送接收等请求得,我们需要在pro中添加网络库

1
QT       += core gui network

在pro中添加C++类,命名为HttpMgr,然后头文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "singleton.h"
#include <QString>
#include <QUrl>
#include <QObject>
#include <QNetworkAccessManager>
#include "global.h"
#include <memory>
#include <QJsonObject>
#include <QJsonDocument>
class HttpMgr:public QObject, public Singleton<HttpMgr>,
public std::enable_shared_from_this<HttpMgr>
{
Q_OBJECT

public:
~HttpMgr();
private:
friend class Singleton<HttpMgr>;
HttpMgr();
QNetworkAccessManager _manager;
signals:
void sig_http_finish();
};

我们先实现PostHttpReq请求的函数,也就是发送http的post请求, 发送请求要用到请求的url,请求的数据(json或者protobuf序列化),以及请求的id,以及哪个模块发出的请求mod,那么一个请求接口应该是这样的

1
void PostHttpReq(QUrl url, QJsonObject json, ReqId req_id, Modules mod);

我们去global.h定义ReqId枚举类型

1
2
3
4
enum ReqId{
ID_GET_VARIFY_CODE = 1001, //获取验证码
ID_REG_USER = 1002, //注册用户
};

在global.h定义ErrorCodes

1
2
3
4
5
enum ErrorCodes{
SUCCESS = 0,
ERR_JSON = 1, //Json解析失败
ERR_NETWORK = 2,
};

在global.h中定义模块

1
2
3
enum Modules{
REGISTERMOD = 0,
};

还需要修改下要发送的信号,在HttpMgr的头文件里,让他携带参数

1
void sig_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod);

我们实现PostHttpReq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void HttpMgr::PostHttpReq(QUrl url, QJsonObject json, ReqId req_id, Modules mod)
{
//创建一个HTTP POST请求,并设置请求头和请求体
QByteArray data = QJsonDocument(json).toJson();
//通过url构造请求
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setHeader(QNetworkRequest::ContentLengthHeader, QByteArray::number(data.length()));
//发送请求,并处理响应, 获取自己的智能指针,构造伪闭包并增加智能指针引用计数
auto self = shared_from_this();
QNetworkReply * reply = _manager.post(request, data);
//设置信号和槽等待发送完成
QObject::connect(reply, &QNetworkReply::finished, [reply, self, req_id, mod](){
//处理错误的情况
if(reply->error() != QNetworkReply::NoError){
qDebug() << reply->errorString();
//发送信号通知完成
emit self->sig_http_finish(req_id, "", ErrorCodes::ERR_NETWORK, mod);
reply->deleteLater();
return;
}

//无错误则读回请求
QString res = reply->readAll();

//发送信号通知完成
emit self->sig_http_finish(req_id, res, ErrorCodes::SUCCESS,mod);
reply->deleteLater();
return;
});
}

加下来HttpMgr内实现一个slot_http_finish的槽函数用来接收sig_http_finish信号。

1
2
3
4
5
6
7
void HttpMgr::slot_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod)
{
if(mod == Modules::REGISTERMOD){
//发送信号通知指定模块http响应结束
emit sig_reg_mod_finish(id, res, err);
}
}

我们在HttpMgr.h中添加信号sig_reg_mod_finish,

1
2
3
4
5
6
7
8
9
10
11
class HttpMgr:public QObject, public Singleton<HttpMgr>,
public std::enable_shared_from_this<HttpMgr>
{
Q_OBJECT

public:
//...省略
signals:
void sig_http_finish(ReqId id, QString res, ErrorCodes err, Modules mod);
void sig_reg_mod_finish(ReqId id, QString res, ErrorCodes err);
};

并且在cpp文件中连接slot_http_finish和sig_http_finish.

1
2
3
4
5
HttpMgr::HttpMgr()
{
//连接http请求和完成信号,信号槽机制保证队列消费
connect(this, &HttpMgr::sig_http_finish, this, &HttpMgr::slot_http_finish);
}

我们在注册界面连接sig_reg_mod_finish信号

1
2
3
4
5
6
7
RegisterDialog::RegisterDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::RegisterDialog)
{
//省略...
connect(HttpMgr::GetInstance().get(), &HttpMgr::sig_reg_mod_finish, this, &RegisterDialog::slot_reg_mod_finish);
}

接下俩实现slot_reg_mod_finish函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void RegisterDialog::slot_reg_mod_finish(ReqId id, QString res, ErrorCodes err)
{
if(err != ErrorCodes::SUCCESS){
showTip(tr("网络请求错误"),false);
return;
}

// 解析 JSON 字符串,res需转化为QByteArray
QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());
//json解析错误
if(jsonDoc.isNull()){
showTip(tr("json解析错误"),false);
return;
}

//json解析错误
if(!jsonDoc.isObject()){
showTip(tr("json解析错误"),false);
return;
}

QJsonObject jsonObj = jsonDoc.object();

//调用对应的逻辑
return;
}

showTip逻辑稍作修改,增加bool类型参数

1
2
3
4
5
6
7
8
9
10
11
12
void RegisterDialog::showTip(QString str, bool b_ok)
{
if(b_ok){
ui->err_tip->setProperty("state","err");
}else{
ui->err_tip->setProperty("state","normal");
}

ui->err_tip->setText(str);

repolish(ui->err_tip);
}

注册消息处理

我们需要对RegisterDialog注册消息处理,头文件声明

1
QMap<ReqId, std::function<void(const QJsonObject&)>> _handlers;

在RegisterDialog中添加注册消息处理的声明和定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void RegisterDialog::initHttpHandlers()
{
//注册获取验证码回包逻辑
_handlers.insert(ReqId::ID_GET_VARIFY_CODE, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("验证码已发送到邮箱,注意查收"), true);
qDebug()<< "email is " << email ;
});
}

回到slot_reg_mod_finish函数添加根据id调用函数处理对应逻辑

1
2
3
4
5
6
7
8
void RegisterDialog::slot_reg_mod_finish(ReqId id, QString res, ErrorCodes err)
{
//前面逻辑省略...
//调用对应的逻辑,根据id回调。
_handlers[id](jsonDoc.object());

return;
}
<1…789…41>

401 posts
18 categories
21 tags
RSS
GitHub ZhiHu
© 2026 恋恋风辰 本站总访问量次 | 本站访客数人
Powered by Hexo
|
Theme — NexT.Muse v5.1.3