恋恋风辰的个人博客


  • Home

  • Archives

  • Categories

  • Tags

  • Search

聊天项目(24) EventFilter实现搜索隐藏

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

事件过滤器

我们为了实现点击界面某个位置判断是否隐藏搜索框的功能。我们期待当鼠标点击搜索列表之外的区域时显示隐藏搜索框恢复聊天界面。
点击搜索列表则不隐藏搜索框。可以通过重载ChatDialog的EventFilter函数实现点击功能

1
2
3
4
5
6
7
8
bool ChatDialog::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::MouseButtonPress) {
QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
handleGlobalMousePress(mouseEvent);
}
return QDialog::eventFilter(watched, event);
}

具体判断全局鼠标按下位置和功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void ChatDialog::handleGlobalMousePress(QMouseEvent *event)
{
// 实现点击位置的判断和处理逻辑
// 先判断是否处于搜索模式,如果不处于搜索模式则直接返回
if( _mode != ChatUIMode::SearchMode){
return;
}

// 将鼠标点击位置转换为搜索列表坐标系中的位置
QPoint posInSearchList = ui->search_list->mapFromGlobal(event->globalPos());
// 判断点击位置是否在聊天列表的范围内
if (!ui->search_list->rect().contains(posInSearchList)) {
// 如果不在聊天列表内,清空输入框
ui->search_edit->clear();
ShowSearch(false);
}
}

在ChatDialog构造函数中添加事件过滤器

1
2
3
4
5
//检测鼠标点击位置判断是否要清空搜索框
this->installEventFilter(this); // 安装事件过滤器

//设置聊天label选中状态
ui->side_chat_lb->SetSelected(true);

这样就可以实现在ChatDialog中点击其他位置隐藏SearchList列表了。

查找结果

在项目中添加FindSuccessDlg设计师界面类,其布局如下

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

属性管理界面如下

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

FindSuccessDlg声明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FindSuccessDlg : public QDialog
{
Q_OBJECT

public:
explicit FindSuccessDlg(QWidget *parent = nullptr);
~FindSuccessDlg();
void SetSearchInfo(std::shared_ptr<SearchInfo> si);
private slots:
void on_add_friend_btn_clicked();

private:
Ui::FindSuccessDlg *ui;
QWidget * _parent;
std::shared_ptr<SearchInfo> _si;
};

FindSuccessDlg实现如下

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
FindSuccessDlg::FindSuccessDlg(QWidget *parent) :
QDialog(parent),
ui(new Ui::FindSuccessDlg)
{
ui->setupUi(this);
// 设置对话框标题
setWindowTitle("添加");
// 隐藏对话框标题栏
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
// 获取当前应用程序的路径
QString app_path = QCoreApplication::applicationDirPath();
QString pix_path = QDir::toNativeSeparators(app_path +
QDir::separator() + "static"+QDir::separator()+"head_1.jpg");
QPixmap head_pix(pix_path);
head_pix = head_pix.scaled(ui->head_lb->size(),
Qt::KeepAspectRatio, Qt::SmoothTransformation);
ui->head_lb->setPixmap(head_pix);
ui->add_friend_btn->SetState("normal","hover","press");
this->setModal(true);
}

FindSuccessDlg::~FindSuccessDlg()
{
qDebug()<<"FindSuccessDlg destruct";
delete ui;
}

void FindSuccessDlg::SetSearchInfo(std::shared_ptr<SearchInfo> si)
{
ui->name_lb->setText(si->_name);
_si = si;
}

void FindSuccessDlg::on_add_friend_btn_clicked()
{
//todo... 添加好友界面弹出
}

在SearchList 的slot_item_clicked函数中添加点击条目处理逻辑

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
void SearchList::slot_item_clicked(QListWidgetItem *item)
{
QWidget *widget = this->itemWidget(item); //获取自定义widget对象
if(!widget){
qDebug()<< "slot item clicked widget is nullptr";
return;
}

// 对自定义widget进行操作, 将item 转化为基类ListItemBase
ListItemBase *customItem = qobject_cast<ListItemBase*>(widget);
if(!customItem){
qDebug()<< "slot item clicked widget is nullptr";
return;
}

auto itemType = customItem->GetItemType();
if(itemType == ListItemType::INVALID_ITEM){
qDebug()<< "slot invalid item clicked ";
return;
}

if(itemType == ListItemType::ADD_USER_TIP_ITEM){

//todo ...
_find_dlg = std::make_shared<FindSuccessDlg>(this);
auto si = std::make_shared<SearchInfo>(0,"llfc","llfc","hello , my friend!",0);
(std::dynamic_pointer_cast<FindSuccessDlg>(_find_dlg))->SetSearchInfo(si);
_find_dlg->show();
return;
}

//清楚弹出框
CloseFindDlg();

}

这样我们在输入框输入文字,点击搜索列表中搜索添加好友的item,就能弹出搜索结果对话框了。这里只做界面演示,之后会改为像服务器发送请求获取搜索结果。

pro的改写

我们对项目的pro做了调整,更新了static文件夹的拷贝以及编码utf-8的设定

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
QT       += core gui network

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = llfcchat
TEMPLATE = app
RC_ICONS = icon.ico
DESTDIR = ./bin
DEFINES += QT_DEPRECATED_WARNINGS
CONFIG += c++11

SOURCES += \
adduseritem.cpp \
bubbleframe.cpp \
chatdialog.cpp \
chatitembase.cpp \
chatpage.cpp \
chatuserlist.cpp \
chatuserwid.cpp \
chatview.cpp \
clickedbtn.cpp \
clickedlabel.cpp \
customizeedit.cpp \
findsuccessdlg.cpp \
global.cpp \
httpmgr.cpp \
listitembase.cpp \
loadingdlg.cpp \
logindialog.cpp \
main.cpp \
mainwindow.cpp \
messagetextedit.cpp \
picturebubble.cpp \
registerdialog.cpp \
resetdialog.cpp \
searchlist.cpp \
statewidget.cpp \
tcpmgr.cpp \
textbubble.cpp \
timerbtn.cpp \
userdata.cpp \
usermgr.cpp

HEADERS += \
adduseritem.h \
bubbleframe.h \
chatdialog.h \
chatitembase.h \
chatpage.h \
chatuserlist.h \
chatuserwid.h \
chatview.h \
clickedbtn.h \
clickedlabel.h \
customizeedit.h \
findsuccessdlg.h \
global.h \
httpmgr.h \
listitembase.h \
loadingdlg.h \
logindialog.h \
mainwindow.h \
messagetextedit.h \
picturebubble.h \
registerdialog.h \
resetdialog.h \
searchlist.h \
singleton.h \
statewidget.h \
tcpmgr.h \
textbubble.h \
timerbtn.h \
userdata.h \
usermgr.h

FORMS += \
adduseritem.ui \
chatdialog.ui \
chatpage.ui \
chatuserwid.ui \
findsuccessdlg.ui \
loadingdlg.ui \
logindialog.ui \
mainwindow.ui \
registerdialog.ui \
resetdialog.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

RESOURCES += \
rc.qrc

DISTFILES += \
config.ini


CONFIG(debug, debug | release) {
#指定要拷贝的文件目录为工程目录下release目录下的所有dll、lib文件,例如工程目录在D:\QT\Test
#PWD就为D:/QT/Test,DllFile = D:/QT/Test/release/*.dll
TargetConfig = $${PWD}/config.ini
#将输入目录中的"/"替换为"\"
TargetConfig = $$replace(TargetConfig, /, \\)
#将输出目录中的"/"替换为"\"
OutputDir = $${OUT_PWD}/$${DESTDIR}
OutputDir = $$replace(OutputDir, /, \\)
//执行copy命令
QMAKE_POST_LINK += copy /Y \"$$TargetConfig\" \"$$OutputDir\" &

# 首先,定义static文件夹的路径
StaticDir = $${PWD}/static
# 将路径中的"/"替换为"\"
StaticDir = $$replace(StaticDir, /, \\)
#message($${StaticDir})
# 使用xcopy命令拷贝文件夹,/E表示拷贝子目录及其内容,包括空目录。/I表示如果目标不存在则创建目录。/Y表示覆盖现有文件而不提示。
QMAKE_POST_LINK += xcopy /Y /E /I \"$$StaticDir\" \"$$OutputDir\\static\\\"

}else{
#release
message("release mode")
#指定要拷贝的文件目录为工程目录下release目录下的所有dll、lib文件,例如工程目录在D:\QT\Test
#PWD就为D:/QT/Test,DllFile = D:/QT/Test/release/*.dll
TargetConfig = $${PWD}/config.ini
#将输入目录中的"/"替换为"\"
TargetConfig = $$replace(TargetConfig, /, \\)
#将输出目录中的"/"替换为"\"
OutputDir = $${OUT_PWD}/$${DESTDIR}
OutputDir = $$replace(OutputDir, /, \\)
//执行copy命令
QMAKE_POST_LINK += copy /Y \"$$TargetConfig\" \"$$OutputDir\"

# 首先,定义static文件夹的路径
StaticDir = $${PWD}/static
# 将路径中的"/"替换为"\"
StaticDir = $$replace(StaticDir, /, \\)
#message($${StaticDir})
# 使用xcopy命令拷贝文件夹,/E表示拷贝子目录及其内容,包括空目录。/I表示如果目标不存在则创建目录。/Y表示覆盖现有文件而不提示。
QMAKE_POST_LINK += xcopy /Y /E /I \"$$StaticDir\" \"$$OutputDir\\static\\\"
}

win32-msvc*:QMAKE_CXXFLAGS += /wd"4819" /utf-8

视频

https://www.bilibili.com/video/BV1ww4m1e72G/

源码链接

https://gitee.com/secondtonone1/llfcchat

聊天项目(27) 分布式聊天服务设计

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

简介

本文介绍如何将chatserver设置为分布式服务,并且实现statusserver的负载均衡处理,根据每个chatserver现有的连接数匹配最小的chatserver返回给GateServer并返回给客户端。

为了实现这一系列分布式设计,我们需要先完善chatserver,增加grpc客户端和服务端。这样能实现两个chatserver之间端对端的通信。

visual studio中右键chatserver项目选择添加新文件ChatGrpcClient, 会为我们生成ChatGrpcClient.h和ChatGrpcClient.cpp文件。

连接池客户端

先实现ChatConPool连接池

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
class ChatConPool {
public:
ChatConPool(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(ChatService::NewStub(channel));
}
}

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

std::unique_ptr<ChatService::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<ChatService::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<ChatService::Stub> > connections_;
std::mutex mutex_;
std::condition_variable cond_;
};

然后利用单例模式实现grpc通信的客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ChatGrpcClient: public Singleton<ChatGrpcClient>
{
friend class Singleton<ChatGrpcClient>;

public:
~ChatGrpcClient() {

}

AddFriendRsp NotifyAddFriend(std::string server_ip, const AddFriendReq& req);
AuthFriendRsp NotifyAuthFriend(std::string server_ip, const AuthFriendReq& req);
bool GetBaseInfo(std::string base_key, int uid, std::shared_ptr<UserInfo>& userinfo);
TextChatMsgRsp NotifyTextChatMsg(std::string server_ip, const TextChatMsgReq& req, const Json::Value& rtvalue);
private:
ChatGrpcClient();
unordered_map<std::string, std::unique_ptr<ChatConPool>> _pools;
};

实现具体的ChatGrpcClient

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
ChatGrpcClient::ChatGrpcClient()
{
auto& cfg = ConfigMgr::Inst();
auto server_list = cfg["PeerServer"]["Servers"];

std::vector<std::string> words;

std::stringstream ss(server_list);
std::string word;

while (std::getline(ss, word, ',')) {
words.push_back(word);
}

for (auto& word : words) {
if (cfg[word]["Name"].empty()) {
continue;
}
_pools[cfg[word]["Name"]] = std::make_unique<ChatConPool>(5, cfg[word]["Host"], cfg[word]["Port"]);
}

}

AddFriendRsp ChatGrpcClient::NotifyAddFriend(std::string server_ip, const AddFriendReq& req) {
AddFriendRsp rsp;
return rsp;
}

AuthFriendRsp ChatGrpcClient::NotifyAuthFriend(std::string server_ip, const AuthFriendReq& req) {
AuthFriendRsp rsp;
return rsp;
}

bool ChatGrpcClient::GetBaseInfo(std::string base_key, int uid, std::shared_ptr<UserInfo>& userinfo) {
return true;
}

TextChatMsgRsp ChatGrpcClient::NotifyTextChatMsg(std::string server_ip,
const TextChatMsgReq& req, const Json::Value& rtvalue) {

TextChatMsgRsp rsp;
return rsp;
}

连接池服务端

向ChatServer中添加ChatServiceImpl类,自动生成头文件和源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ChatServiceImpl final : public ChatService::Service
{
public:
ChatServiceImpl();
Status NotifyAddFriend(ServerContext* context, const AddFriendReq* request,
AddFriendRsp* reply) override;

Status NotifyAuthFriend(ServerContext* context,
const AuthFriendReq* request, AuthFriendRsp* response) override;

Status NotifyTextChatMsg(::grpc::ServerContext* context,
const TextChatMsgReq* request, TextChatMsgRsp* response) override;

bool GetBaseInfo(std::string base_key, int uid, std::shared_ptr<UserInfo>& userinfo);

private:
};

实现服务逻辑,先简单写成不处理直接返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ChatServiceImpl::ChatServiceImpl()
{

}

Status ChatServiceImpl::NotifyAddFriend(ServerContext* context, const AddFriendReq* request,
AddFriendRsp* reply) {
return Status::OK;
}

Status ChatServiceImpl::NotifyAuthFriend(ServerContext* context,
const AuthFriendReq* request, AuthFriendRsp* response) {
return Status::OK;
}

Status ChatServiceImpl::NotifyTextChatMsg(::grpc::ServerContext* context,
const TextChatMsgReq* request, TextChatMsgRsp* response) {
return Status::OK;
}

bool ChatServiceImpl::GetBaseInfo(std::string base_key, int uid, std::shared_ptr<UserInfo>& userinfo) {
return true;
}

并且完善chatserver配置

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
[GateServer]
Port = 8080
[VarifyServer]
Host = 127.0.0.1
Port = 50051
[StatusServer]
Host = 127.0.0.1
Port = 50052
[SelfServer]
Name = chatserver1
Host = 0.0.0.0
Port = 8090
RPCPort = 50055
[Mysql]
Host = 81.68.86.146
Port = 3308
User = root
Passwd = 123456.
Schema = llfc
[Redis]
Host = 81.68.86.146
Port = 6380
Passwd = 123456
[PeerServer]
Servers = chatserver2
[chatserver2]
Name = chatserver2
Host = 127.0.0.1
Port = 50056

增加了PeerServer字段,存储对端server列表,通过逗号分隔,可以通过逗号切割对端服务器名字,再根据名字去配置里查找对应字段。

对应的chatserver复制一份,改名为chatserver2,然后修改config.ini配置。要和server1配置不同,实现端对端的配置。具体详见服务器代码。

服务器连接数管理

每当服务器chatserver启动后,都要重新设置一下用户连接数管理,并且我们每个chatserver既要有tcp服务监听也要有grpc服务监听

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
using namespace std;
bool bstop = false;
std::condition_variable cond_quit;
std::mutex mutex_quit;

int main()
{
auto& cfg = ConfigMgr::Inst();
auto server_name = cfg["SelfServer"]["Name"];
try {
auto pool = AsioIOServicePool::GetInstance();
//将登录数设置为0
RedisMgr::GetInstance()->HSet(LOGIN_COUNT, server_name, "0");
//定义一个GrpcServer

std::string server_address(cfg["SelfServer"]["Host"] + ":" + cfg["SelfServer"]["RPCPort"]);
ChatServiceImpl service;
grpc::ServerBuilder builder;
// 监听端口和添加服务
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
// 构建并启动gRPC服务器
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "RPC Server listening on " << server_address << std::endl;


//单独启动一个线程处理grpc服务
std::thread grpc_server_thread([&server]() {
server->Wait();
});


boost::asio::io_context io_context;
boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
signals.async_wait([&io_context, pool, &server](auto, auto) {
io_context.stop();
pool->Stop();
server->Shutdown();
});

auto port_str = cfg["SelfServer"]["Port"];
CServer s(io_context, atoi(port_str.c_str()));
io_context.run();

RedisMgr::GetInstance()->HDel(LOGIN_COUNT, server_name);
RedisMgr::GetInstance()->Close();
grpc_server_thread.join();
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
}

我们在服务器启动后将本服务器的登录数量设置为0.

同样的道理,我们将服务器关闭后,也要删除对应key。

用户连接管理

因为我们用户登录后,要将连接(session)和用户uid绑定。为以后登陆踢人做准备。所以新增UserMgr管理类.

其声明如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CSession;
class UserMgr : public Singleton<UserMgr>
{
friend class Singleton<UserMgr>;
public:
~UserMgr();
std::shared_ptr<CSession> GetSession(int uid);
void SetUserSession(int uid, std::shared_ptr<CSession> session);
void RmvUserSession(int uid);

private:
UserMgr();
std::mutex _session_mtx;
std::unordered_map<int, std::shared_ptr<CSession>> _uid_to_session;
};

其实现如下

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
UserMgr:: ~UserMgr() {
_uid_to_session.clear();
}

std::shared_ptr<CSession> UserMgr::GetSession(int uid)
{
std::lock_guard<std::mutex> lock(_session_mtx);
auto iter = _uid_to_session.find(uid);
if (iter == _uid_to_session.end()) {
return nullptr;
}

return iter->second;
}

void UserMgr::SetUserSession(int uid, std::shared_ptr<CSession> session)
{
std::lock_guard<std::mutex> lock(_session_mtx);
_uid_to_session[uid] = session;
}

void UserMgr::RmvUserSession(int uid)
{
auto uid_str = std::to_string(uid);
//因为再次登录可能是其他服务器,所以会造成本服务器删除key,其他服务器注册key的情况
// 有可能其他服务登录,本服删除key造成找不到key的情况
//RedisMgr::GetInstance()->Del(USERIPPREFIX + uid_str);

{
std::lock_guard<std::mutex> lock(_session_mtx);
_uid_to_session.erase(uid);
}

}

UserMgr::UserMgr()
{

}

RmvUserSession 暂时屏蔽,以后做登录踢人后能保证有序移除用户ip操作。

当有连接异常时,可以调用移除用户Session的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
void CServer::ClearSession(std::string session_id) {

if (_sessions.find(session_id) != _sessions.end()) {
//移除用户和session的关联
UserMgr::GetInstance()->RmvUserSession(_sessions[session_id]->GetUserId());
}

{
lock_guard<mutex> lock(_mutex);
_sessions.erase(session_id);
}

}

聊天服务完善用户登录,当用户登录后, 设置其uid对应的serverip。以及更新其所在服务器的连接数。

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
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();
auto token = root["token"].asString();
std::cout << "user login uid is " << uid << " user token is "
<< token << endl;

Json::Value rtvalue;
Defer defer([this, &rtvalue, session]() {
std::string return_str = rtvalue.toStyledString();
session->Send(return_str, MSG_CHAT_LOGIN_RSP);
});

//从redis获取用户token是否正确
std::string uid_str = std::to_string(uid);
std::string token_key = USERTOKENPREFIX + uid_str;
std::string token_value = "";
bool success = RedisMgr::GetInstance()->Get(token_key, token_value);
if (!success) {
rtvalue["error"] = ErrorCodes::UidInvalid;
return;
}

if (token_value != token) {
rtvalue["error"] = ErrorCodes::TokenInvalid;
return;
}

rtvalue["error"] = ErrorCodes::Success;

std::string base_key = USER_BASE_INFO + uid_str;
auto user_info = std::make_shared<UserInfo>();
bool b_base = GetBaseInfo(base_key, uid, user_info);
if (!b_base) {
rtvalue["error"] = ErrorCodes::UidInvalid;
return;
}
rtvalue["uid"] = uid;
rtvalue["pwd"] = user_info->pwd;
rtvalue["name"] = user_info->name;
rtvalue["email"] = user_info->email;
rtvalue["nick"] = user_info->nick;
rtvalue["desc"] = user_info->desc;
rtvalue["sex"] = user_info->sex;
rtvalue["icon"] = user_info->icon;

//从数据库获取申请列表

//获取好友列表

auto server_name = ConfigMgr::Inst().GetValue("SelfServer", "Name");
//将登录数量增加
auto rd_res = RedisMgr::GetInstance()->HGet(LOGIN_COUNT, server_name);
int count = 0;
if (!rd_res.empty()) {
count = std::stoi(rd_res);
}

count++;

auto count_str = std::to_string(count);
RedisMgr::GetInstance()->HSet(LOGIN_COUNT, server_name, count_str);

//session绑定用户uid
session->SetUserId(uid);

//为用户设置登录ip server的名字
std::string ipkey = USERIPPREFIX + uid_str;
RedisMgr::GetInstance()->Set(ipkey, server_name);

//uid和session绑定管理,方便以后踢人操作
UserMgr::GetInstance()->SetUserSession(uid, session);

return;
}

状态服务

状态服务更新配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[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
[chatservers]
Name = chatserver1,chatserver2
[chatserver1]
Name = chatserver1
Host = 127.0.0.1
Port = 8090
[chatserver2]
Name = chatserver2
Host = 127.0.0.1
Port = 8091

配置文件同样增加了chatservers列表,用来管理多个服务,接下来实现根据连接数动态返回chatserverip的功能

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

getChatServer用来获取最小连接数的chatserver 名字

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
ChatServer StatusServiceImpl::getChatServer() {
std::lock_guard<std::mutex> guard(_server_mtx);
auto minServer = _servers.begin()->second;
auto count_str = RedisMgr::GetInstance()->HGet(LOGIN_COUNT, minServer.name);
if (count_str.empty()) {
//不存在则默认设置为最大
minServer.con_count = INT_MAX;
}
else {
minServer.con_count = std::stoi(count_str);
}


// 使用范围基于for循环
for (auto& server : _servers) {

if (server.second.name == minServer.name) {
continue;
}

auto count_str = RedisMgr::GetInstance()->HGet(LOGIN_COUNT, server.second.name);
if (count_str.empty()) {
server.second.con_count = INT_MAX;
}
else {
server.second.con_count = std::stoi(count_str);
}

if (server.second.con_count < minServer.con_count) {
minServer = server.second;
}
}

return minServer;
}

测试

分别启动两个chatserver,gateserver,以及statusserver,并且启动两个客户端登录,

分别查看登录信息,发现两个客户端被分配到不同的chatserver了,说明我们实现了负载均衡的分配方式。

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

源码连接

https://gitee.com/secondtonone1/llfcchat

视频连接

https://www.bilibili.com/video/BV17r421K7Px/?spm_id_from=333.999.0.0&vd_source=8be9e83424c2ed2c9b2a3ed1d01385e9

C++ 全栈聊天项目(3) CRTP实现Http管理者

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

GateServer

网关服务器主要应答客户端基本的连接请求,包括根据服务器负载情况选择合适服务器给客户端登录,注册,获取验证服务等,接收http请求并应答。

boost库安装

boost库的安装分为Windows和Linux两部分,Linux部分放在后面再讲解。因为Windows比较直观,便于我们编写代码,所以优先在windows平台搭建环境并编写代码,测试无误后再移植到linux。

boost官网地址:

Boost库官网https://www.boost.org/,首先进入官网下载对应的Boost库文件。点击下图所示红框中Download进入下载页面。更多版本点击链接下载。

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

点击进入页面后,接下来选择7z或者zip文件都可以。

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

如果下载缓慢,大家可以去我的网盘下载
链接:https://pan.baidu.com/s/1Uf-7gZxWpCOl7dnYzlYRHg?pwd=xt01

提取码:xt01

我的是boost_1_81_0版本,大家可以和我的版本匹配,也可以自己用最新版。

下载好后解压, 其中booststrap.bat点击后生成编译程序

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

点击后,生成b2.exe,我们执行如下命令

1
.\b2.exe install --toolset=msvc-14.2 --build-type=complete --prefix="D:\cppsoft\boost_1_81_0" link=static runtime-link=shared threading=multi debug release

先逐一解释各参数含义

  1. install可以更改为stage, stage表示只生成库(dll和lib), install还会生成包含头文件的include目录。一般来说用stage就可以了,我们将生成的lib和下载的源码包的include头文件夹放到项目要用的地方即可。

  2. toolset 指定编译器,gcc用来编译生成linux用的库,msvc-14.2(VS2019)用来编译windows使用的库,版本号看你的编译器比如msvc-10.0(VS2010),我的是VS2019所以是msvc-14.2。

  3. 如果选择的是install 命令,指定生成的库文件夹要用--prefix,如果使用的是stage命令,需要用--stagedir指定。

  4. link 表示生成动态库还是静态库,static表示生成lib库,shared表示生成dll库。

  5. runtime-link 表示用于指定运行时链接方式为静态库还是动态库,指定为static就是MT模式,指定shared就是MD模式。MD 和 MT 是微软 Visual C++ 编译器的选项,用于指定运行时库的链接方式。这两个选项有以下区别:

    • /MD:表示使用多线程 DLL(Dynamic Link Library)版本的运行时库。这意味着你的应用程序将使用动态链接的运行时库(MSVCRT.dll)。这样的设置可以减小最终可执行文件的大小,并且允许应用程序与其他使用相同运行时库版本的程序共享代码和数据。
    • /MT:表示使用多线程静态库(Static Library)版本的运行时库。这意味着所有的运行时函数将被静态链接到应用程序中,使得应用程序不再依赖于动态链接的运行时库。这样可以确保应用程序在没有额外依赖的情况下独立运行,但可能会导致最终可执行文件的体积增大。

执行上述命令后就会在指定目录生成lib库了,我们将lib库拷贝到要使用的地方即可。

一句话简化上面的含义,就是我们生成的是lib库,运行时采用的md加载模式。

下面是编译界面

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

编译后生成如下目录和文件, 我的是D盘 cppsoft目录下的boost文件夹,大家可以根据自己的设置去指定文件夹查看。

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

为了兼容我之前做的旧项目,我创建了一个stage文件夹,将lib文件夹和内容移动到stage中了。然后将include文件夹下的boost文件夹移出到boost_1_81_0目录下,整体看就就是如下

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

接下来我们创建项目并配置boost

配置boost

打开visual studio 创建项目

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

接下来配置boost到项目中,右键项目选择属性,配置VC++包含目录,添加D:\cppsoft\boost_1_81_0(根据你自己的boost目录配置)

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

再配置VC++库目录, 添加D:\cppsoft\boost_1_81_0\stage\lib

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

写个代码测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>
#include "boost/lexical_cast.hpp"
int main()
{
using namespace std;
cout << "Enter your weight: ";
float weight;
cin >> weight;
string gain = "A 10% increase raises ";
string wt = boost::lexical_cast<string> (weight);
gain = gain + wt + " to "; // string operator()
weight = 1.1 * weight;
gain = gain + boost::lexical_cast<string>(weight) + ".";
cout << gain << endl;
system("pause");
return 0;
}

运行成功,可以看到弹出了窗口

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

配置jsoncpp

因为要用到json解析,所以我们选择jsoncpp来做后端json解析工作

jsoncpp下载地址:
https://github.com/open-source-parsers/jsoncpp
官方文档:
http://jsoncpp.sourceforge.net/old.html

选择windows版本的下载。

如果下载速度很慢,可以去我的网盘地址下载
https://pan.baidu.com/s/1Yg9Usdc3T-CYhyr9GiePCw?pwd=ng6x

验证码ng6x
下载后我们解压文件夹,解压后文件夹如下图
https://cdn.llfc.club/1684638346874.jpg

然后进行编译,编译需要进入makefile文件夹下

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

找到jsoncpp.sln文件,用visual studio打开,因为我的是visual studio2019版本,所以会提示我升级。

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

点击确定升级,之后我们选择编译lib_json即可,当然偷懒可以选择编译整个解决方案。
https://cdn.llfc.club/1684639169065.jpg

然后我们配置编译属性,我想要用x64位的,所以配置编译平台为X64位,编译模式为debug模式,大家最好把release版和debug版都编译一遍。

右键lib_json属性里选择C++,再选择代码生成,之后在右侧选择运行库,选择md(release版), mdd(debug版).

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

编译生成后,我们的json库生成在项目同级目录的x64文件夹下的debug目录下
https://cdn.llfc.club/1684640251160.jpg

接下来我们在D盘cppsoft新建一个文件夹libjson,然后在其内部分别建立include和lib文件夹

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

将jsoncpp-src-0.5.0源码文件夹下include文件夹里的内容copy到libjson下的include文件夹内。

将jsoncpp-src-0.5.0源码文件夹下x64位debug文件夹和Release文件夹下生成的库copy到libjson下的lib文件夹内。

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

我们生成的是mdd和md版本的库,但是名字却是mt,这个是visual studio生成的小bug先不管了。

接下来我们新建一个项目,在项目属性中配置jsoncpp

项目属性中,VC++包含目录设置为 D:\cppsoft\libjson\include

库目录选择为 VC++库目录设置为 D:\cppsoft\libjson\lib

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

另外我们还要设置链接器->输入->附加依赖项里设置json_vc71_libmtd.lib

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

我们写个程序测试一下json库安装的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>

int main()
{
Json::Value root;
root["id"] = 1001;
root["data"] = "hello world";
std::string request = root.toStyledString();
std::cout << "request is " << request << std::endl;

Json::Value root2;
Json::Reader reader;
reader.parse(request, root2);
std::cout << "msg id is " << root2["id"] << " msg is " << root2["data"] << std::endl;
}

从这段代码中,我们先将root序列化为字符串,再将字符串反序列化为root2.

输出如下

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

C++ 全栈聊天项目(4) visualstudio配置boost与jsoncpp

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

绑定和监听连接

我们利用visual studio创建一个空项目,项目名字为GateServer,然后按照day03的方法配置boost库和jsoncpp配置好后,我们添加一个新的类,名字叫CServer。添加成功后生成的CServer.h和CServer.cpp也会自动加入到项目中。

CServer类构造函数接受一个端口号,创建acceptor接受新到来的链接。

CServer.h包含必要的头文件,以及简化作用域声明

1
2
3
4
5
6
7
8
#include <boost/beast/http.hpp>
#include <boost/beast.hpp>
#include <boost/asio.hpp>

namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>

CServer.h中声明acceptor, 以及用于事件循环的上下文iocontext,和构造函数

1
2
3
4
5
6
7
8
9
10
class CServer:public std::enable_shared_from_this<CServer>
{
public:
CServer(boost::asio::io_context& ioc, unsigned short& port);
void Start();
private:
tcp::acceptor _acceptor;
net::io_context& _ioc;
boost::asio::ip::tcp::socket _socket;
};

cpp中实现构造函数如下

1
2
3
4
CServer::CServer(boost::asio::io_context& ioc, unsigned short& port) :_ioc(ioc),
_acceptor(ioc, tcp::endpoint(tcp::v4(), port)),_socket(ioc) {

}

接下来我们实现Start函数,用来监听新链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CServer::Start()
{
auto self = shared_from_this();
_acceptor.async_accept(_socket, [self](beast::error_code ec) {
try {
//出错则放弃这个连接,继续监听新链接
if (ec) {
self->Start();
return;
}

//处理新链接,创建HpptConnection类管理新连接
std::make_shared<HttpConnection>(std::move(self->_socket))->Start();
//继续监听
self->Start();
}
catch (std::exception& exp) {
std::cout << "exception is " << exp.what() << std::endl;
self->Start();
}
});
}

Start函数内创建HttpConnection类型智能指针,将_socket内部数据转移给HttpConnection管理,_socket继续用来接受写的链接。

我们创建const.h将文件件和一些作用于声明放在const.h里,这样以后创建的文件包含这个const.h即可,不用写那么多头文件了。

1
2
3
4
5
6
7
8
#include <boost/beast/http.hpp>
#include <boost/beast.hpp>
#include <boost/asio.hpp>

namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>

新建HttpConnection类文件,在头文件添加声明

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
#include "const.h"

class HttpConnection: public std::enable_shared_from_this<HttpConnection>
{
friend class LogicSystem;
public:
HttpConnection(tcp::socket socket);
void Start();

private:
void CheckDeadline();
void WriteResponse();
void HandleReq();
tcp::socket _socket;
// The buffer for performing reads.
beast::flat_buffer _buffer{ 8192 };

// The request message.
http::request<http::dynamic_body> _request;

// The response message.
http::response<http::dynamic_body> _response;

// The timer for putting a deadline on connection processing.
net::steady_timer deadline_{
_socket.get_executor(), std::chrono::seconds(60) };
};

_buffer 用来接受数据

_request 用来解析请求

_response 用来回应客户端

_deadline 用来做定时器判断请求是否超时

实现HttpConnection构造函数

1
2
3
HttpConnection::HttpConnection(tcp::socket socket)
: _socket(std::move(socket)) {
}

我们考虑在HttpConnection::Start内部调用http::async_read函数,其源码为

1
2
3
4
5
async_read(
AsyncReadStream& stream,
DynamicBuffer& buffer,
basic_parser<isRequest>& parser,
ReadHandler&& handler)

第一个参数为异步可读的数据流,大家可以理解为socket.

第二个参数为一个buffer,用来存储接受的数据,因为http可接受文本,图像,音频等多种资源文件,所以是Dynamic动态类型的buffer。

第三个参数是请求参数,我们一般也要传递能接受多种资源类型的请求参数。

第四个参数为回调函数,接受成功或者失败,都会触发回调函数,我们用lambda表达式就可以了。

我们已经将1,2,3这几个参数写到HttpConnection类的成员声明里了

实现HttpConnection的Start函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void HttpConnection::Start()
{
auto self = shared_from_this();
http::async_read(_socket, _buffer, _request, [self](beast::error_code ec,
std::size_t bytes_transferred) {
try {
if (ec) {
std::cout << "http read err is " << ec.what() << std::endl;
return;
}

//处理读到的数据
boost::ignore_unused(bytes_transferred);
self->HandleReq();
self->CheckDeadline();
}
catch (std::exception& exp) {
std::cout << "exception is " << exp.what() << std::endl;
}
}
);
}

我们实现HandleReq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void HttpConnection::HandleReq() {
//设置版本
_response.version(_request.version());
//设置为短链接
_response.keep_alive(false);

if (_request.method() == http::verb::get) {
bool success = LogicSystem::GetInstance()->HandleGet(_request.target(), shared_from_this());
if (!success) {
_response.result(http::status::not_found);
_response.set(http::field::content_type, "text/plain");
beast::ostream(_response.body()) << "url not found\r\n";
WriteResponse();
return;
}

_response.result(http::status::ok);
_response.set(http::field::server, "GateServer");
WriteResponse();
return;
}
}

为了方便我们先实现Get请求的处理,根据请求类型为get调用LogicSystem的HandleGet接口处理get请求,根据处理成功还是失败回应数据包给对方。

我们先实现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
#include <memory>
#include <mutex>
#include <iostream>
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;

实现LogicSystem单例类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "Singleton.h"
#include <functional>
#include <map>
#include "const.h"

class HttpConnection;
typedef std::function<void(std::shared_ptr<HttpConnection>)> HttpHandler;
class LogicSystem :public Singleton<LogicSystem>
{
friend class Singleton<LogicSystem>;
public:
~LogicSystem();
bool HandleGet(std::string, std::shared_ptr<HttpConnection>);
void RegGet(std::string, HttpHandler handler);
private:
LogicSystem();
std::map<std::string, HttpHandler> _post_handlers;
std::map<std::string, HttpHandler> _get_handlers;
};

_post_handlers和_get_handlers分别是post请求和get请求的回调函数map,key为路由,value为回调函数。

我们实现RegGet函数,接受路由和回调函数作为参数

1
2
3
void LogicSystem::RegGet(std::string url, HttpHandler handler) {
_get_handlers.insert(make_pair(url, handler));
}

在构造函数中实现具体的消息注册

1
2
3
4
5
LogicSystem::LogicSystem() {
RegGet("/get_test", [](std::shared_ptr<HttpConnection> connection) {
beast::ostream(connection->_response.body()) << "receive get_test req";
});
}

为防止互相引用,以及LogicSystem能够成功访问HttpConnection,在LogicSystem.cpp中包含HttpConnection头文件

并且在HttpConnection中添加友元类LogicSystem, 且在HttpConnection.cpp中包含LogicSystem.h文件

1
2
3
4
5
6
7
8
bool LogicSystem::HandleGet(std::string path, std::shared_ptr<HttpConnection> con) {
if (_get_handlers.find(path) == _get_handlers.end()) {
return false;
}

_get_handlers[path](con);
return true;
}

这样我们在HttpConnection里实现WriteResponse函数

1
2
3
4
5
6
7
8
9
10
11
12
void HttpConnection::WriteResponse() {
auto self = shared_from_this();
_response.content_length(_response.body().size());
http::async_write(
_socket,
_response,
[self](beast::error_code ec, std::size_t)
{
self->_socket.shutdown(tcp::socket::shutdown_send, ec);
self->deadline_.cancel();
});
}

因为http是短链接,所以发送完数据后不需要再监听对方链接,直接断开发送端即可。

另外,http处理请求需要有一个时间约束,发送的数据包不能超时。所以在发送时我们启动一个定时器,收到发送的回调后取消定时器。

我们实现检测超时的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
void HttpConnection::CheckDeadline() {
auto self = shared_from_this();

deadline_.async_wait(
[self](beast::error_code ec)
{
if (!ec)
{
// Close socket to cancel any outstanding operation.
self->_socket.close(ec);
}
});
}

我们在主函数中初始化上下文iocontext以及启动信号监听ctr-c退出事件, 并且启动iocontext服务

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
int main()
{
try
{
unsigned short port = static_cast<unsigned short>(8080);
net::io_context ioc{ 1 };
boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
signals.async_wait([&ioc](const boost::system::error_code& error, int signal_number) {

if (error) {
return;
}
ioc.stop();
});
std::make_shared<CServer>(ioc, port)->Start();
ioc.run();
}
catch (std::exception const& e)
{
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
}
``
启动服务器,在浏览器输入`http://localhost:8080/get_test`

会看到服务器回包`receive get_test req`

如果我们输入带参数的url请求`http://localhost:8080/get_test?key1=value1&key2=value2`

会收到服务器反馈`url not found`

所以对于get请求带参数的情况我们要实现参数解析,我们可以自己实现简单的url解析函数

``` cpp
//char 转为16进制
unsigned char ToHex(unsigned char x)
{
return x > 9 ? x + 55 : x + 48;
}

将十进制的char转为16进制,如果是数字不超过9则加48转为对应的ASCII码的值

如果字符是大于9的,比如AZ, az等则加55,获取到对应字符的ASCII码值

详细的ASCII码表大家可以看这个https://c.biancheng.net/c/ascii/

接下来实现从16进制转为十进制的char的方法

1
2
3
4
5
6
7
8
9
unsigned char FromHex(unsigned char x)
{
unsigned char y;
if (x >= 'A' && x <= 'Z') y = x - 'A' + 10;
else if (x >= 'a' && x <= 'z') y = x - 'a' + 10;
else if (x >= '0' && x <= '9') y = x - '0';
else assert(0);
return y;
}

接下来我们实现url编码工作

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
std::string UrlEncode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
//判断是否仅有数字和字母构成
if (isalnum((unsigned char)str[i]) ||
(str[i] == '-') ||
(str[i] == '_') ||
(str[i] == '.') ||
(str[i] == '~'))
strTemp += str[i];
else if (str[i] == ' ') //为空字符
strTemp += "+";
else
{
//其他字符需要提前加%并且高四位和低四位分别转为16进制
strTemp += '%';
strTemp += ToHex((unsigned char)str[i] >> 4);
strTemp += ToHex((unsigned char)str[i] & 0x0F);
}
}
return strTemp;
}

我们先判断str[i]是否为字母或者数字,或者一些简单的下划线,如果是泽直接拼接,否则判断是否为空字符,如果为空则换成’+’拼接。否则就是特殊字符,我们需要将特殊字符转化为’%’和两个十六进制字符拼接。现拼接’%’,再将字符的高四位拼接到strTemp上,最后将低四位拼接到strTemp上。

url解码的工作正好相反

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string UrlDecode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
//还原+为空
if (str[i] == '+') strTemp += ' ';
//遇到%将后面的两个字符从16进制转为char再拼接
else if (str[i] == '%')
{
assert(i + 2 < length);
unsigned char high = FromHex((unsigned char)str[++i]);
unsigned char low = FromHex((unsigned char)str[++i]);
strTemp += high * 16 + low;
}
else strTemp += str[i];
}
return strTemp;
}

接下来实现get请求的参数解析, 在HttpConnection里添加两个成员

1
2
std::string _get_url;
std::unordered_map<std::string, std::string> _get_params;

参数解析如下

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
void HttpConnection::PreParseGetParam() {
// 提取 URI
auto uri = _request.target();
// 查找查询字符串的开始位置(即 '?' 的位置)
auto query_pos = uri.find('?');
if (query_pos == std::string::npos) {
_get_url = uri;
return;
}

_get_url = uri.substr(0, query_pos);
std::string query_string = uri.substr(query_pos + 1);
std::string key;
std::string value;
size_t pos = 0;
while ((pos = query_string.find('&')) != std::string::npos) {
auto pair = query_string.substr(0, pos);
size_t eq_pos = pair.find('=');
if (eq_pos != std::string::npos) {
key = UrlDecode(pair.substr(0, eq_pos)); // 假设有 url_decode 函数来处理URL解码
value = UrlDecode(pair.substr(eq_pos + 1));
_get_params[key] = value;
}
query_string.erase(0, pos + 1);
}
// 处理最后一个参数对(如果没有 & 分隔符)
if (!query_string.empty()) {
size_t eq_pos = query_string.find('=');
if (eq_pos != std::string::npos) {
key = UrlDecode(query_string.substr(0, eq_pos));
value = UrlDecode(query_string.substr(eq_pos + 1));
_get_params[key] = value;
}
}
}

HttpConnection::HandleReq函数略作修改

1
2
3
4
5
6
7
8
void HttpConnection::HandleReq() {
//...省略
if (_request.method() == http::verb::get) {
PreParseGetParam();
bool success = LogicSystem::GetInstance()->HandleGet(_get_url, shared_from_this());
}
//...省略
}

我们修改LogicSytem构造函数,在get_test的回调里返回参数给对端

1
2
3
4
5
6
7
8
9
10
11
LogicSystem::LogicSystem() {
RegGet("/get_test", [](std::shared_ptr<HttpConnection> connection) {
beast::ostream(connection->_response.body()) << "receive get_test req " << std::endl;
int i = 0;
for (auto& elem : connection->_get_params) {
i++;
beast::ostream(connection->_response.body()) << "param" << i << " key is " << elem.first;
beast::ostream(connection->_response.body()) << ", " << " value is " << elem.second << std::endl;
}
});
}

在浏览器输入http://localhost:8080/get_test?key1=value1&key2=value2

看到浏览器收到如下图信息,说明我们的get请求逻辑处理完了

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

C++ 全栈聊天项目(5) Beast实现http get请求处理

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

注册Post请求

我们实现RegPost函数

1
2
3
void LogicSystem::RegPost(std::string url, HttpHandler handler) {
_post_handlers.insert(make_pair(url, handler));
}

在const.h中添加ErrorCodes定义并且包含JsonCpp相关的头文件

1
2
3
4
5
6
7
8
9
#include <json/json.h>
#include <json/value.h>
#include <json/reader.h>

enum ErrorCodes {
Success = 0,
Error_Json = 1001, //Json解析错误
RPCFailed = 1002, //RPC请求错误
};

然后在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
RegPost("/get_varifycode", [](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();
cout << "email is " << email << endl;
root["error"] = 0;
root["email"] = src_root["email"];
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

然后我们在LogicSystem中添加Post请求的处理

1
2
3
4
5
6
7
8
bool LogicSystem::HandlePost(std::string path, std::shared_ptr<HttpConnection> con) {
if (_post_handlers.find(path) == _post_handlers.end()) {
return false;
}

_post_handlers[path](con);
return true;
}

在HttpConnection的HandleReq中添加post请求处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void HttpConnection::HandleReq() {
//省略...
if (_request.method() == http::verb::post) {
bool success = LogicSystem::GetInstance()->HandlePost(_request.target(), shared_from_this());
if (!success) {
_response.result(http::status::not_found);
_response.set(http::field::content_type, "text/plain");
beast::ostream(_response.body()) << "url not found\r\n";
WriteResponse();
return;
}

_response.result(http::status::ok);
_response.set(http::field::server, "GateServer");
WriteResponse();
return;
}

}

然后我们启动服务器,然后下载postman,大家可以去官网下载,如果速度慢可以去我的网盘下载
https://pan.baidu.com/s/1DBIf7Y6G3v0XYfW5LyDKMg?pwd=kjxz

提取码:kjxz

打开postman,将请求修改为post

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

绿色的为post请求的json参数,红色的为服务器返回的json数据包。

我们看服务器打印的日志

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

客户端增加post逻辑

我们之前在客户端实现了httpmgr的post请求,在点击获取验证码的槽函数里添加发送http的post请求即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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请求获取验证码
QJsonObject json_obj;
json_obj["email"] = email;
HttpMgr::GetInstance()->PostHttpReq(QUrl("http://localhost:8080/get_varifycode"),
json_obj, ReqId::ID_GET_VARIFY_CODE,Modules::REGISTERMOD);

}else{
//提示邮箱不正确
showTip(tr("邮箱地址不正确"),false);
}
}

当服务器不启动,客户端输入邮箱,点击获取验证码,客户端会收到网络连接失败的提示

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

启动服务器后,再次获取验证码,就显示正确提示了,而且客户端输出了服务器回传的邮箱地址email is "secondtonone1@163.com",界面也刷新为正确显示

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

客户端配置管理

我们发现客户端代码中很多参数都是写死的,最好通过配置文件管理,我们在代码所在目录中新建一个config.ini文件, 内部添加配置

1
2
3
[GateServer]
host=localhost
port=8080

接着右键项目添加现有文件config.ini即可加入项目中。

因为我们的程序最终会输出的bin目录,所以在pro中添加拷贝脚本将配置也拷贝到bin目录

1
2
3
4
5
6
7
8
9
10
11
12
13
win32:CONFIG(release, debug | release)
{
#指定要拷贝的文件目录为工程目录下release目录下的所有dll、lib文件,例如工程目录在D:\QT\Test
#PWD就为D:/QT/Test,DllFile = D:/QT/Test/release/*.dll
TargetConfig = $${PWD}/config.ini
#将输入目录中的"/"替换为"\"
TargetConfig = $$replace(TargetConfig, /, \\)
#将输出目录中的"/"替换为"\"
OutputDir = $${OUT_PWD}/$${DESTDIR}
OutputDir = $$replace(OutputDir, /, \\)
//执行copy命令
QMAKE_POST_LINK += copy /Y \"$$TargetConfig\" \"$$OutputDir\"
}

global.h中添加声明

1
extern QString gate_url_prefix;

在cpp中添加定义

1
QString gate_url_prefix = "";

在main函数中添加解析配置的逻辑

1
2
3
4
5
6
7
8
9
10
11
// 获取当前应用程序的路径
QString app_path = QCoreApplication::applicationDirPath();
// 拼接文件名
QString fileName = "config.ini";
QString config_path = QDir::toNativeSeparators(app_path +
QDir::separator() + fileName);

QSettings settings(config_path, QSettings::IniFormat);
QString gate_host = settings.value("GateServer/host").toString();
QString gate_port = settings.value("GateServer/port").toString();
gate_url_prefix = "http://"+gate_host+":"+gate_port;

将RegisterDialog发送post请求修改为

1
2
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/get_varifycode"),
json_obj, ReqId::ID_GET_VARIFY_CODE,Modules::REGISTERMOD);

再次测试仍旧可以收到服务器回馈的http包。

这么做的好处就是客户端增加了配置,而且以后修改参数也方便。

C++ 全栈聊天项目(7) 客户端实现Post验证码请求

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

属性管理器

推荐一种可复制配置的方式,视图里选择其他窗口,再选择属性管理器

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

我们选择要配置的类型,我选择Debug 64位的配置,添加新项目属性表

https://cdn.llfc.club/2789d4d0598e69bff5f0452159d3c14.png

选择创建属性的名字

https://cdn.llfc.club/7675ab8ac46308693eec2ea4ec0f708.png

接下来双击我们创建好的属性文件,将之前配置的boost和jsoncpp库属性移动到这里,把之前在项目中配置的删除。

包含目录

https://cdn.llfc.club/3e98a4ba407416e8a433a7b6254c3a6.png

库目录

https://cdn.llfc.club/56a894eca5a6b3888ba07f29678b291.png

链接库

https://cdn.llfc.club/43aba5606318b56dc56ba1a884c18b3.png

接下来配置grpc头文件包含目录,C++ 常规-> 附加包含目录添加如下

1
2
3
4
5
D:\cppsoft\grpc\third_party\re2
D:\cppsoft\grpc\third_party\address_sorting\include
D:\cppsoft\grpc\third_party\abseil-cpp
D:\cppsoft\grpc\third_party\protobuf\src
D:\cppsoft\grpc\include

https://cdn.llfc.club/375f8c4b21f643408b73a19e415fcd5.png

接下来配置库路径, 在链接器常规选项下,点击附加库目录,添加我们需要的库目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\cppsoft\grpc\visualpro\third_party\re2\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\types\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\synchronization\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\status\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\random\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\flags\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\debugging\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\container\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\hash\Debug
D:\cppsoft\grpc\visualpro\third_party\boringssl-with-bazel\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\numeric\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\time\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\base\Debug
D:\cppsoft\grpc\visualpro\third_party\abseil-cpp\absl\strings\Debug
D:\cppsoft\grpc\visualpro\third_party\protobuf\Debug
D:\cppsoft\grpc\visualpro\third_party\zlib\Debug
D:\cppsoft\grpc\visualpro\Debug
D:\cppsoft\grpc\visualpro\third_party\cares\cares\lib\Debug

https://cdn.llfc.club/89fcb7a4afef6721c893187fffcfecf.png

在链接器->输入->附加依赖项中添加

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
libprotobufd.lib
gpr.lib
grpc.lib
grpc++.lib
grpc++_reflection.lib
address_sorting.lib
ws2_32.lib
cares.lib
zlibstaticd.lib
upb.lib
ssl.lib
crypto.lib
absl_bad_any_cast_impl.lib
absl_bad_optional_access.lib
absl_bad_variant_access.lib
absl_base.lib
absl_city.lib
absl_civil_time.lib
absl_cord.lib
absl_debugging_internal.lib
absl_demangle_internal.lib
absl_examine_stack.lib
absl_exponential_biased.lib
absl_failure_signal_handler.lib
absl_flags.lib
absl_flags_config.lib
absl_flags_internal.lib
absl_flags_marshalling.lib
absl_flags_parse.lib
absl_flags_program_name.lib
absl_flags_usage.lib
absl_flags_usage_internal.lib
absl_graphcycles_internal.lib
absl_hash.lib
absl_hashtablez_sampler.lib
absl_int128.lib
absl_leak_check.lib
absl_leak_check_disable.lib
absl_log_severity.lib
absl_malloc_internal.lib
absl_periodic_sampler.lib
absl_random_distributions.lib
absl_random_internal_distribution_test_util.lib
absl_random_internal_pool_urbg.lib
absl_random_internal_randen.lib
absl_random_internal_randen_hwaes.lib
absl_random_internal_randen_hwaes_impl.lib
absl_random_internal_randen_slow.lib
absl_random_internal_seed_material.lib
absl_random_seed_gen_exception.lib
absl_random_seed_sequences.lib
absl_raw_hash_set.lib
absl_raw_logging_internal.lib
absl_scoped_set_env.lib
absl_spinlock_wait.lib
absl_stacktrace.lib
absl_status.lib
absl_strings.lib
absl_strings_internal.lib
absl_str_format_internal.lib
absl_symbolize.lib
absl_synchronization.lib
absl_throw_delegate.lib
absl_time.lib
absl_time_zone.lib
absl_statusor.lib
re2.lib

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

之后点击保存会看到项目目录下生成了PropertySheet.props文件

proto文件编写

在项目的根目录下创建一个proto名字为message.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

接下来我们利用grpc编译后生成的proc.exe生成proto的grpc的头文件和源文件

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"

上述命令会生成message.grpc.pb.h和message.grpc.pb.cc文件。

接下来我们生成用于序列化和反序列化的pb文件

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

上述命令会生成message.pb.h和message.pb.cc文件

接下来我们将这些pb.h和pb.cc放入项目中

我们新建一个VarifyGrpcClient类,vs帮我们自动生成头文件和源文件,我们在头文件添加Grpc客户端类

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
#include <grpcpp/grpcpp.h>
#include "message.grpc.pb.h"
#include "const.h"
#include "Singleton.h"
using grpc::Channel;
using grpc::Status;
using grpc::ClientContext;

using message::GetVarifyReq;
using message::GetVarifyRsp;
using message::VarifyService;

class VerifyGrpcClient:public Singleton<VerifyGrpcClient>
{
friend class Singleton<VerifyGrpcClient>;
public:

GetVarifyRsp GetVarifyCode(std::string email) {
ClientContext context;
GetVarifyRsp reply;
GetVarifyReq request;
request.set_email(email);

Status status = stub_->GetVarifyCode(&context, request, &reply);

if (status.ok()) {

return reply;
}
else {
reply.set_error(ErrorCodes::RPCFailed);
return reply;
}
}

private:
VerifyGrpcClient() {
std::shared_ptr<Channel> channel = grpc::CreateChannel("127.0.0.1:50051", grpc::InsecureChannelCredentials());
stub_ = VarifyService::NewStub(channel);
}

std::unique_ptr<VarifyService::Stub> stub_;
};

我们在之前收到post请求获取验证码的逻辑里添加处理

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
RegPost("/get_varifycode", [](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();
GetVarifyRsp rsp = VerifyGrpcClient::GetInstance()->GetVarifyCode(email);
cout << "email is " << email << endl;
root["error"] = rsp.error();
root["email"] = src_root["email"];
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

服务器读取配置

我们很多参数都是写死的,现通过配置文件读取以方便以后修改
在项目中添加config.ini文件

1
2
3
4
[GateServer]
Port = 8080
[VarifyServer]
Port = 50051

添加ConfigMgr类用来读取和管理配置, 定义一个SectionInfo类管理key和value

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
struct SectionInfo {
SectionInfo(){}
~SectionInfo(){
_section_datas.clear();
}

SectionInfo(const SectionInfo& src) {
_section_datas = src._section_datas;
}

SectionInfo& operator = (const SectionInfo& src) {
if (&src == this) {
return *this;
}

this->_section_datas = src._section_datas;
}

std::map<std::string, std::string> _section_datas;
std::string operator[](const std::string &key) {
if (_section_datas.find(key) == _section_datas.end()) {
return "";
}
// 这里可以添加一些边界检查
return _section_datas[key];
}
};

定义ComigMgr管理section和其包含的key与value

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
class ConfigMgr
{
public:
~ConfigMgr() {
_config_map.clear();
}
SectionInfo operator[](const std::string& section) {
if (_config_map.find(section) == _config_map.end()) {
return SectionInfo();
}
return _config_map[section];
}


ConfigMgr& operator=(const ConfigMgr& src) {
if (&src == this) {
return *this;
}

this->_config_map = src._config_map;
};

ConfigMgr(const ConfigMgr& src) {
this->_config_map = src._config_map;
}

ConfigMgr();
private:

// 存储section和key-value对的map
std::map<std::string, SectionInfo> _config_map;
};

构造函数里实现config读取

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
ConfigMgr::ConfigMgr(){
// 获取当前工作目录
boost::filesystem::path current_path = boost::filesystem::current_path();
// 构建config.ini文件的完整路径
boost::filesystem::path config_path = current_path / "config.ini";
std::cout << "Config path: " << config_path << std::endl;

// 使用Boost.PropertyTree来读取INI文件
boost::property_tree::ptree pt;
boost::property_tree::read_ini(config_path.string(), pt);


// 遍历INI文件中的所有section
for (const auto& section_pair : pt) {
const std::string& section_name = section_pair.first;
const boost::property_tree::ptree& section_tree = section_pair.second;

// 对于每个section,遍历其所有的key-value对
std::map<std::string, std::string> section_config;
for (const auto& key_value_pair : section_tree) {
const std::string& key = key_value_pair.first;
const std::string& value = key_value_pair.second.get_value<std::string>();
section_config[key] = value;
}
SectionInfo sectionInfo;
sectionInfo._section_datas = section_config;
// 将section的key-value对保存到config_map中
_config_map[section_name] = sectionInfo;
}

// 输出所有的section和key-value对
for (const auto& section_entry : _config_map) {
const std::string& section_name = section_entry.first;
SectionInfo section_config = section_entry.second;
std::cout << "[" << section_name << "]" << std::endl;
for (const auto& key_value_pair : section_config._section_datas) {
std::cout << key_value_pair.first << "=" << key_value_pair.second << std::endl;
}
}

}

在const.h里声明一个全局变量

1
2
class ConfigMgr;
extern ConfigMgr gCfgMgr;

接下来在main函数中将8080端口改为从配置读取

1
2
3
ConfigMgr gCfgMgr;
std::string gate_port_str = gCfgMgr["GateServer"]["Port"];
unsigned short gate_port = atoi(gate_port_str.c_str());

其他地方想要获取配置信息就不需要定义了,直接包含const.h并且使用gCfgMgr即可。

总结

本节基于visual studio配置grpc,并实现了grpc客户端发送请求的逻辑。下一节实现 grpc server

C++ 全栈聊天项目(8) windows配置和使用grpc

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

认证服务

我们的认证服务要给邮箱发送验证码,所以用nodejs较为合适,nodejs是一门IO效率很高而且生态完善的语言,用到发送邮件的库也方便。

nodejs可以去官网下载https://nodejs.org/en,一路安装就可以了

我们新建VarifyServer文件夹,在文件夹内部初始化server要用到的nodejs库的配置文件

1
npm init

根据提示同意会创建一个package.json文件

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

接下来安装grpc-js包,也可以安装grpc,grpc是C++版本,grpc-js是js版本,C++版本停止维护了。所以用grpc-js版本。

安装过程出现了错误,因为淘宝镜像地址过期了

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

清除之前npm镜像地址

1
npm cache clean --force

重新设置新的淘宝镜像

1
npm config set registry https://registry.npmmirror.com

接着下载grpc-js就成功了

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

接着安装proto-loader用来动态解析proto文件

1
npm install @grpc/proto-loader

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

我们再安装email处理的库

1
npm install nodemailer

我们将proto文件放入VarifyServer文件夹,并且新建一个proto.js用来解析proto文件

1
2
3
4
5
6
7
8
9
10
11
const path = require('path')
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')

const PROTO_PATH = path.join(__dirname, 'message.proto')
const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, enums: String, defaults: true, oneofs: true })
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition)

const message_proto = protoDescriptor.message

module.exports = message_proto

keepCase: 如果为 true,则保留字段名的原始大小写。如果为 false,则将所有字段名转换为驼峰命名法。

longs: 控制如何表示 Protocol Buffers 中的 long 类型。如果设置为 String,则长整数会被转换为字符串,以避免 JavaScript 中的整数溢出问题。

enums: 控制如何表示 Protocol Buffers 中的枚举类型。如果设置为 String,则枚举值会被转换为字符串。

defaults: 如果为 true,则为未明确设置的字段提供默认值。

oneofs: 如果为 true,则支持 Protocol Buffers 中的 oneof 特性。

在写代码发送邮件之前,我们先去邮箱开启smtp服务。我用的163邮箱,在邮箱设置中查找smtp服务器地址,需要开启smtp服务。这个是固定的,不需要修改。

网易163邮箱的 SMTP 服务器地址为: smtp.163.com

发送邮件,建议使用授权码(有的邮箱叫 独立密码),确保邮箱密码的安全性。授权码在邮箱设置中进行设置。如果开启了授权码,发送邮件的时候,必须使用授权码。

这里设置开启smtp服务和授权码。我这里已经是设置好的。

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

新增一个授权码用于发邮件

https://cdn.llfc.club/20210625165014232%20%282%29.png

读取配置

因为我们要实现参数可配置,所以要读取配置,先在文件夹内创建一个config.json文件

1
2
3
4
5
6
{
"email": {
"user": "secondtonone1@163.com",
"pass": "CRWTAZOSNCWDDQQTllfc"
},
}

user是我们得邮箱地址,pass是邮箱得授权码,只有有了授权码才能用代码发邮件。大家记得把授权码改为你们自己的,否则用我的无法发送成功。

另外我们也要用到一些常量和全局得变量,所以我们定义一个const.js

1
2
3
4
5
6
7
8
9
10
let code_prefix = "code_";

const Errors = {
Success : 0,
RedisErr : 1,
Exception : 2,
};


module.exports = {code_prefix,Errors}

新建config.js用来读取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');

let config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
let email_user = config.email.user;
let email_pass = config.email.pass;
let mysql_host = config.mysql.host;
let mysql_port = config.mysql.port;
let redis_host = config.redis.host;
let redis_port = config.redis.port;
let redis_passwd = config.redis.passwd;
let code_prefix = "code_";


module.exports = {email_pass, email_user, mysql_host, mysql_port,redis_host, redis_port, redis_passwd, code_prefix}

接下来封装发邮件的模块,新建一个email.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const nodemailer = require('nodemailer');
const config_module = require("./config")

/**
* 创建发送邮件的代理
*/
let transport = nodemailer.createTransport({
host: 'smtp.163.com',
port: 465,
secure: true,
auth: {
user: config_module.email_user, // 发送方邮箱地址
pass: config_module.email_pass // 邮箱授权码或者密码
}
});

接下来实现发邮件函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 发送邮件的函数
* @param {*} mailOptions_ 发送邮件的参数
* @returns
*/
function SendMail(mailOptions_){
return new Promise(function(resolve, reject){
transport.sendMail(mailOptions_, function(error, info){
if (error) {
console.log(error);
reject(error);
} else {
console.log('邮件已成功发送:' + info.response);
resolve(info.response)
}
});
})

}

module.exports.SendMail = SendMail

因为transport.SendMail相当于一个异步函数,调用该函数后发送的结果是通过回调函数通知的,所以我们没办法同步使用,需要用Promise封装这个调用,抛出Promise给外部,那么外部就可以通过await或者then catch的方式处理了。

我们新建server.js,用来启动grpc server

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
async function GetVarifyCode(call, callback) {
console.log("email is ", call.request.email)
try{
uniqueId = uuidv4();
console.log("uniqueId is ", uniqueId)
let text_str = '您的验证码为'+ uniqueId +'请三分钟内完成注册'
//发送邮件
let mailOptions = {
from: 'secondtonone1@163.com',
to: call.request.email,
subject: '验证码',
text: text_str,
};

let send_res = await emailModule.SendMail(mailOptions);
console.log("send res is ", send_res)

callback(null, { email: call.request.email,
error:const_module.Errors.Success
});


}catch(error){
console.log("catch error is ", error)

callback(null, { email: call.request.email,
error:const_module.Errors.Exception
});
}

}

function main() {
var server = new grpc.Server()
server.addService(message_proto.VarifyService.service, { GetVarifyCode: GetVarifyCode })
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
server.start()
console.log('grpc server started')
})
}

main()

GetVarifyCode声明为async是为了能在内部调用await。

提升GateServer并发

添加ASIO IOContext Pool 结构,让多个iocontext跑在不同的线程中

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 = 2/*std::thread::hardware_concurrency()*/);
std::vector<IOService> _ioServices;
std::vector<WorkPtr> _works;
std::vector<std::thread> _threads;
std::size_t _nextIOService;
};

实现

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
#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() {
Stop();
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处Start逻辑, 改为每次从IOServicePool连接池中获取连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void CServer::Start()
{
auto self = shared_from_this();
auto& io_context = AsioIOServicePool::GetInstance()->GetIOService();
std::shared_ptr<HttpConnection> new_con = std::make_shared<HttpConnection>(io_context);
_acceptor.async_accept(new_con->GetSocket(), [self, new_con](beast::error_code ec) {
try {
//出错则放弃这个连接,继续监听新链接
if (ec) {
self->Start();
return;
}

//处理新链接,创建HpptConnection类管理新连接
new_con->Start();
//继续监听
self->Start();
}
catch (std::exception& exp) {
std::cout << "exception is " << exp.what() << std::endl;
self->Start();
}
});
}

为了方便读取配置文件,将ConfigMgr改为单例, 将构造函数变成私有,添加Inst函数

1
2
3
4
static ConfigMgr& Inst() {
static ConfigMgr cfg_mgr;
return cfg_mgr;
}

VerifyGrpcClient.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
class RPConPool {
public:
RPConPool(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(VarifyService::NewStub(channel));
}
}

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

std::unique_ptr<VarifyService::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<VarifyService::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<VarifyService::Stub>> connections_;
std::mutex mutex_;
std::condition_variable cond_;
};

我们在VerifyGrpcClient类中添加成员

1
std::unique_ptr<RPConPool> pool_;

修改构造函数

1
2
3
4
5
6
VerifyGrpcClient::VerifyGrpcClient() {
auto& gCfgMgr = ConfigMgr::Inst();
std::string host = gCfgMgr["VarifyServer"]["Host"];
std::string port = gCfgMgr["VarifyServer"]["Port"];
pool_.reset(new RPConPool(5, host, port));
}

当我们想连接grpc server端时,可以通过池子获取连接,用完之后再返回连接给池子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GetVarifyRsp GetVarifyCode(std::string email) {
ClientContext context;
GetVarifyRsp reply;
GetVarifyReq request;
request.set_email(email);
auto stub = pool_->getConnection();
Status status = stub->GetVarifyCode(&context, request, &reply);

if (status.ok()) {
pool_->returnConnection(std::move(stub));
return reply;
}
else {
pool_->returnConnection(std::move(stub));
reply.set_error(ErrorCodes::RPCFailed);
return reply;
}
}

总结

到本节为止我们完成nodejs搭建的grpc server, 修改package.json中的脚本

1
2
3
"scripts": {
"serve": "node server.js"
},

接着命令行执行 npm run serve即可启动grpc 服务。

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++ 全栈聊天项目(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

聊天项目(25) 实现好友申请界面

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

简介

本文介绍如何实现好友申请界面, 其效果如下图所示

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

在此之前我们需要先定义一个ClickedOnceLabel类,支持点击一次的label功能。

接着新增一个ClickedOnceLabel类

1
2
3
4
5
6
7
8
9
class ClickedOnceLabel : public QLabel
{
Q_OBJECT
public:
ClickedOnceLabel(QWidget *parent=nullptr);
virtual void mouseReleaseEvent(QMouseEvent *ev) override;
signals:
void clicked(QString);
};

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ClickedOnceLabel::ClickedOnceLabel(QWidget *parent):QLabel(parent)
{
setCursor(Qt::PointingHandCursor);
}


void ClickedOnceLabel::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
emit clicked(this->text());
return;
}
// 调用基类的mousePressEvent以保证正常的事件处理
QLabel::mousePressEvent(event);
}

完善ClickedLabel

我们之前实现了ClickedLabel类,接下来修改下clicked信号,使其携带参数

1
void clicked(QString, ClickLbState);

然后在其实现的鼠标释放事件的逻辑中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ClickedLabel::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
if(_curstate == ClickLbState::Normal){
// qDebug()<<"ReleaseEvent , change to normal hover: "<< _normal_hover;
setProperty("state",_normal_hover);
repolish(this);
update();

}else{
// qDebug()<<"ReleaseEvent , change to select hover: "<< _selected_hover;
setProperty("state",_selected_hover);
repolish(this);
update();
}
emit clicked(this->text(), _curstate);
return;
}
// 调用基类的mousePressEvent以保证正常的事件处理
QLabel::mousePressEvent(event);
}

好友申请

好友申请界面和逻辑,我们可以创建一个设计师界面类叫做ApplyFriend类,我们在类的声明中添加如下成员。

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
class ApplyFriend : public QDialog
{
Q_OBJECT

public:
explicit ApplyFriend(QWidget *parent = nullptr);
~ApplyFriend();
void InitTipLbs();
void AddTipLbs(ClickedLabel*, QPoint cur_point, QPoint &next_point, int text_width, int text_height);
bool eventFilter(QObject *obj, QEvent *event);
void SetSearchInfo(std::shared_ptr<SearchInfo> si);

private:
Ui::ApplyFriend *ui;
void resetLabels();

//已经创建好的标签
QMap<QString, ClickedLabel*> _add_labels;
std::vector<QString> _add_label_keys;
QPoint _label_point;
//用来在输入框显示添加新好友的标签
QMap<QString, FriendLabel*> _friend_labels;
std::vector<QString> _friend_label_keys;
void addLabel(QString name);
std::vector<QString> _tip_data;
QPoint _tip_cur_point;
std::shared_ptr<SearchInfo> _si;
public slots:
//显示更多label标签
void ShowMoreLabel();
//输入label按下回车触发将标签加入展示栏
void SlotLabelEnter();
//点击关闭,移除展示栏好友便签
void SlotRemoveFriendLabel(QString);
//通过点击tip实现增加和减少好友便签
void SlotChangeFriendLabelByTip(QString, ClickLbState);
//输入框文本变化显示不同提示
void SlotLabelTextChange(const QString& text);
//输入框输入完成
void SlotLabelEditFinished();
//输入标签显示提示框,点击提示框内容后添加好友便签
void SlotAddFirendLabelByClickTip(QString text);
//处理确认回调
void SlotApplySure();
//处理取消回调
void SlotApplyCancel();
};

接下来我们修改ui使其变成如下布局

https://cdn.llfc.club/1720423721312.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
37
38
39
40
41
42
43
44
45
46
47
48
ApplyFriend::ApplyFriend(QWidget *parent) :
QDialog(parent),
ui(new Ui::ApplyFriend),_label_point(2,6)
{
ui->setupUi(this);
// 隐藏对话框标题栏
setWindowFlags(windowFlags() | Qt::FramelessWindowHint);
this->setObjectName("ApplyFriend");
this->setModal(true);
ui->name_ed->setPlaceholderText(tr("恋恋风辰"));
ui->lb_ed->setPlaceholderText("搜索、添加标签");
ui->back_ed->setPlaceholderText("燃烧的胸毛");

ui->lb_ed->SetMaxLength(21);
ui->lb_ed->move(2, 2);
ui->lb_ed->setFixedHeight(20);
ui->lb_ed->setMaxLength(10);
ui->input_tip_wid->hide();

_tip_cur_point = QPoint(5, 5);

_tip_data = { "同学","家人","菜鸟教程","C++ Primer","Rust 程序设计",
"父与子学Python","nodejs开发指南","go 语言开发指南",
"游戏伙伴","金融投资","微信读书","拼多多拼友" };

connect(ui->more_lb, &ClickedOnceLabel::clicked, this, &ApplyFriend::ShowMoreLabel);
InitTipLbs();
//链接输入标签回车事件
connect(ui->lb_ed, &CustomizeEdit::returnPressed, this, &ApplyFriend::SlotLabelEnter);
connect(ui->lb_ed, &CustomizeEdit::textChanged, this, &ApplyFriend::SlotLabelTextChange);
connect(ui->lb_ed, &CustomizeEdit::editingFinished, this, &ApplyFriend::SlotLabelEditFinished);
connect(ui->tip_lb, &ClickedOnceLabel::clicked, this, &ApplyFriend::SlotAddFirendLabelByClickTip);

ui->scrollArea->horizontalScrollBar()->setHidden(true);
ui->scrollArea->verticalScrollBar()->setHidden(true);
ui->scrollArea->installEventFilter(this);
ui->sure_btn->SetState("normal","hover","press");
ui->cancel_btn->SetState("normal","hover","press");
//连接确认和取消按钮的槽函数
connect(ui->cancel_btn, &QPushButton::clicked, this, &ApplyFriend::SlotApplyCancel);
connect(ui->sure_btn, &QPushButton::clicked, this, &ApplyFriend::SlotApplySure);
}

ApplyFriend::~ApplyFriend()
{
qDebug()<< "ApplyFriend destruct";
delete ui;
}

因为此时还未与服务器联调数据,此时我们写一个InitLabel的函数模拟创建多个标签展示

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 ApplyFriend::InitTipLbs()
{
int lines = 1;
for(int i = 0; i < _tip_data.size(); i++){

auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(_tip_data[i]);
connect(lb, &ClickedLabel::clicked, this, &ApplyFriend::SlotChangeFriendLabelByTip);

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度

if (_tip_cur_point.x() + textWidth + tip_offset > ui->lb_list->width()) {
lines++;
if (lines > 2) {
delete lb;
return;
}

_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

auto next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point,next_point, textWidth, textHeight);

_tip_cur_point = next_point;
}

}

下面这个函数是将标签添加到展示区

1
2
3
4
5
6
7
8
9
void ApplyFriend::AddTipLbs(ClickedLabel* lb, QPoint cur_point, QPoint& next_point, int text_width, int text_height)
{
lb->move(cur_point);
lb->show();
_add_labels.insert(lb->text(), lb);
_add_label_keys.push_back(lb->text());
next_point.setX(lb->pos().x() + text_width + 15);
next_point.setY(lb->pos().y());
}

重写事件过滤器展示滑动条

1
2
3
4
5
6
7
8
9
10
11
12
bool ApplyFriend::eventFilter(QObject *obj, QEvent *event)
{
if (obj == ui->scrollArea && event->type() == QEvent::Enter)
{
ui->scrollArea->verticalScrollBar()->setHidden(false);
}
else if (obj == ui->scrollArea && event->type() == QEvent::Leave)
{
ui->scrollArea->verticalScrollBar()->setHidden(true);
}
return QObject::eventFilter(obj, event);
}

后期搜索用户功能用户数据会从服务器传回来,所以写了下面的接口

1
2
3
4
5
6
7
8
void ApplyFriend::SetSearchInfo(std::shared_ptr<SearchInfo> si)
{
_si = si;
auto applyname = UserMgr::GetInstance()->GetName();
auto bakname = si->_name;
ui->name_ed->setText(applyname);
ui->back_ed->setText(bakname);
}

当点击按钮,可展示更多标签的功能。

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
void ApplyFriend::ShowMoreLabel()
{
qDebug()<< "receive more label clicked";
ui->more_lb_wid->hide();

ui->lb_list->setFixedWidth(325);
_tip_cur_point = QPoint(5, 5);
auto next_point = _tip_cur_point;
int textWidth;
int textHeight;
//重拍现有的label
for(auto & added_key : _add_label_keys){
auto added_lb = _add_labels[added_key];

QFontMetrics fontMetrics(added_lb->font()); // 获取QLabel控件的字体信息
textWidth = fontMetrics.width(added_lb->text()); // 获取文本的宽度
textHeight = fontMetrics.height(); // 获取文本的高度

if(_tip_cur_point.x() +textWidth + tip_offset > ui->lb_list->width()){
_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y()+textHeight+15);
}
added_lb->move(_tip_cur_point);

next_point.setX(added_lb->pos().x() + textWidth + 15);
next_point.setY(_tip_cur_point.y());

_tip_cur_point = next_point;

}

//添加未添加的
for(int i = 0; i < _tip_data.size(); i++){
auto iter = _add_labels.find(_tip_data[i]);
if(iter != _add_labels.end()){
continue;
}

auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(_tip_data[i]);
connect(lb, &ClickedLabel::clicked, this, &ApplyFriend::SlotChangeFriendLabelByTip);

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度

if (_tip_cur_point.x() + textWidth + tip_offset > ui->lb_list->width()) {

_tip_cur_point.setX(tip_offset);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point, next_point, textWidth, textHeight);

_tip_cur_point = next_point;

}

int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);

//qDebug()<<"after resize ui->lb_list size is " << ui->lb_list->size();
ui->scrollcontent->setFixedHeight(ui->scrollcontent->height()+diff_height);
}

重排好友标签编辑栏的标签

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
void ApplyFriend::resetLabels()
{
auto max_width = ui->gridWidget->width();
auto label_height = 0;
for(auto iter = _friend_labels.begin(); iter != _friend_labels.end(); iter++){
//todo... 添加宽度统计
if( _label_point.x() + iter.value()->width() > max_width) {
_label_point.setY(_label_point.y()+iter.value()->height()+6);
_label_point.setX(2);
}

iter.value()->move(_label_point);
iter.value()->show();

_label_point.setX(_label_point.x()+iter.value()->width()+2);
_label_point.setY(_label_point.y());
label_height = iter.value()->height();
}

if(_friend_labels.isEmpty()){
ui->lb_ed->move(_label_point);
return;
}

if(_label_point.x() + MIN_APPLY_LABEL_ED_LEN > ui->gridWidget->width()){
ui->lb_ed->move(2,_label_point.y()+label_height+6);
}else{
ui->lb_ed->move(_label_point);
}
}

添加好友标签编辑栏的标签

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
void ApplyFriend::addLabel(QString name)
{
if (_friend_labels.find(name) != _friend_labels.end()) {
return;
}

auto tmplabel = new FriendLabel(ui->gridWidget);
tmplabel->SetText(name);
tmplabel->setObjectName("FriendLabel");

auto max_width = ui->gridWidget->width();
//todo... 添加宽度统计
if (_label_point.x() + tmplabel->width() > max_width) {
_label_point.setY(_label_point.y() + tmplabel->height() + 6);
_label_point.setX(2);
}
else {

}


tmplabel->move(_label_point);
tmplabel->show();
_friend_labels[tmplabel->Text()] = tmplabel;
_friend_label_keys.push_back(tmplabel->Text());

connect(tmplabel, &FriendLabel::sig_close, this, &ApplyFriend::SlotRemoveFriendLabel);

_label_point.setX(_label_point.x() + tmplabel->width() + 2);

if (_label_point.x() + MIN_APPLY_LABEL_ED_LEN > ui->gridWidget->width()) {
ui->lb_ed->move(2, _label_point.y() + tmplabel->height() + 2);
}
else {
ui->lb_ed->move(_label_point);
}

ui->lb_ed->clear();

if (ui->gridWidget->height() < _label_point.y() + tmplabel->height() + 2) {
ui->gridWidget->setFixedHeight(_label_point.y() + tmplabel->height() * 2 + 2);
}
}

点击回车后,在好友标签编辑栏添加标签,在标签展示栏添加标签

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
void ApplyFriend::SlotLabelEnter()
{
if(ui->lb_ed->text().isEmpty()){
return;
}

auto text = ui->lb_ed->text();
addLabel(ui->lb_ed->text());

ui->input_tip_wid->hide();
auto find_it = std::find(_tip_data.begin(), _tip_data.end(), text);
//找到了就只需设置状态为选中即可
if (find_it == _tip_data.end()) {
_tip_data.push_back(text);
}

//判断标签展示栏是否有该标签
auto find_add = _add_labels.find(text);
if (find_add != _add_labels.end()) {
find_add.value()->SetCurState(ClickLbState::Selected);
return;
}

//标签展示栏也增加一个标签, 并设置绿色选中
auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(text);
connect(lb, &ClickedLabel::clicked, this, &ApplyFriend::SlotChangeFriendLabelByTip);
qDebug() << "ui->lb_list->width() is " << ui->lb_list->width();
qDebug() << "_tip_cur_point.x() is " << _tip_cur_point.x();

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度
qDebug() << "textWidth is " << textWidth;

if (_tip_cur_point.x() + textWidth + tip_offset + 3 > ui->lb_list->width()) {

_tip_cur_point.setX(5);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

auto next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point, next_point, textWidth, textHeight);
_tip_cur_point = next_point;

int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);

lb->SetCurState(ClickLbState::Selected);

ui->scrollcontent->setFixedHeight(ui->scrollcontent->height() + diff_height);
}

当我们点击好友标签编辑栏的标签的关闭按钮时会调用下面的槽函数

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
void ApplyFriend::SlotRemoveFriendLabel(QString name)
{
qDebug() << "receive close signal";

_label_point.setX(2);
_label_point.setY(6);

auto find_iter = _friend_labels.find(name);

if(find_iter == _friend_labels.end()){
return;
}

auto find_key = _friend_label_keys.end();
for(auto iter = _friend_label_keys.begin(); iter != _friend_label_keys.end();
iter++){
if(*iter == name){
find_key = iter;
break;
}
}

if(find_key != _friend_label_keys.end()){
_friend_label_keys.erase(find_key);
}


delete find_iter.value();

_friend_labels.erase(find_iter);

resetLabels();

auto find_add = _add_labels.find(name);
if(find_add == _add_labels.end()){
return;
}

find_add.value()->ResetNormalState();
}

当我们点击标签展示栏的标签,可以实现标签添加和删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//点击标已有签添加或删除新联系人的标签
void ApplyFriend::SlotChangeFriendLabelByTip(QString lbtext, ClickLbState state)
{
auto find_iter = _add_labels.find(lbtext);
if(find_iter == _add_labels.end()){
return;
}

if(state == ClickLbState::Selected){
//编写添加逻辑
addLabel(lbtext);
return;
}

if(state == ClickLbState::Normal){
//编写删除逻辑
SlotRemoveFriendLabel(lbtext);
return;
}

}

当标签文本变化时,下面提示框的文本跟随变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ApplyFriend::SlotLabelTextChange(const QString& text)
{
if (text.isEmpty()) {
ui->tip_lb->setText("");
ui->input_tip_wid->hide();
return;
}

auto iter = std::find(_tip_data.begin(), _tip_data.end(), text);
if (iter == _tip_data.end()) {
auto new_text = add_prefix + text;
ui->tip_lb->setText(new_text);
ui->input_tip_wid->show();
return;
}
ui->tip_lb->setText(text);
ui->input_tip_wid->show();
}

如果编辑完成,则隐藏编辑框

1
2
3
4
void ApplyFriend::SlotLabelEditFinished()
{
ui->input_tip_wid->hide();
}

点击提示框,也会添加标签,功能如下

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
void ApplyFriend::SlotAddFirendLabelByClickTip(QString text)
{
int index = text.indexOf(add_prefix);
if (index != -1) {
text = text.mid(index + add_prefix.length());
}
addLabel(text);

auto find_it = std::find(_tip_data.begin(), _tip_data.end(), text);
//找到了就只需设置状态为选中即可
if (find_it == _tip_data.end()) {
_tip_data.push_back(text);
}

//判断标签展示栏是否有该标签
auto find_add = _add_labels.find(text);
if (find_add != _add_labels.end()) {
find_add.value()->SetCurState(ClickLbState::Selected);
return;
}

//标签展示栏也增加一个标签, 并设置绿色选中
auto* lb = new ClickedLabel(ui->lb_list);
lb->SetState("normal", "hover", "pressed", "selected_normal",
"selected_hover", "selected_pressed");
lb->setObjectName("tipslb");
lb->setText(text);
connect(lb, &ClickedLabel::clicked, this, &ApplyFriend::SlotChangeFriendLabelByTip);
qDebug() << "ui->lb_list->width() is " << ui->lb_list->width();
qDebug() << "_tip_cur_point.x() is " << _tip_cur_point.x();

QFontMetrics fontMetrics(lb->font()); // 获取QLabel控件的字体信息
int textWidth = fontMetrics.width(lb->text()); // 获取文本的宽度
int textHeight = fontMetrics.height(); // 获取文本的高度
qDebug() << "textWidth is " << textWidth;

if (_tip_cur_point.x() + textWidth+ tip_offset+3 > ui->lb_list->width()) {

_tip_cur_point.setX(5);
_tip_cur_point.setY(_tip_cur_point.y() + textHeight + 15);

}

auto next_point = _tip_cur_point;

AddTipLbs(lb, _tip_cur_point, next_point, textWidth,textHeight);
_tip_cur_point = next_point;

int diff_height = next_point.y() + textHeight + tip_offset - ui->lb_list->height();
ui->lb_list->setFixedHeight(next_point.y() + textHeight + tip_offset);

lb->SetCurState(ClickLbState::Selected);

ui->scrollcontent->setFixedHeight(ui->scrollcontent->height()+ diff_height );
}

确认申请和取消申请只是打印了对应信息,并且回收界面

1
2
3
4
5
6
7
8
9
10
11
12
13
void ApplyFriend::SlotApplyCancel()
{
qDebug() << "Slot Apply Cancel";
this->hide();
deleteLater();
}

void ApplyFriend::SlotApplySure()
{
qDebug()<<"Slot Apply Sure called" ;
this->hide();
deleteLater();
}

美化界面

添加如下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
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#ApplyFriend{
border: 2px solid #f1f1f1;
font-size: 14px;
background: #f7f7f8;
}

#scrollArea{
background: #f7f7f8;
border: none;
}

#scrollcontent{
background: #f7f7f8;
}

#scrollcontent #apply_lb{
font-family: "Microsoft YaHei";
font-size: 16px;
font-weight: normal;
}

#apply_wid QLabel{
color:rgb(140,140,140);
font-size: 14px;
font-family: "Microsoft YaHei";
height: 25px;
}

#apply_wid #name_ed, #apply_wid #back_ed{
border: 1px solid #f7f7f8;
font-size: 14px;
font-family: "Microsoft YaHei";
}

#apply_wid #lb_ed {
border: none;
font-size: 14px;
font-family: "Microsoft YaHei";
}

#apply_wid #more_lb{
border-image: url(:/res/arowdown.png);
}

#apply_wid #tipslb[state='normal'] {
padding: 2px;
background: #e1e1e1;
color: #1e1e1e;
border-radius: 10px;
}

#apply_wid #tipslb[state='hover'] {
padding: 2px;
background: #e1e1e1;
color: #1e1e1e;
border-radius: 10px;
}

#apply_wid #tipslb[state='pressed'] {
padding: 2px;
background: #e1e1e1;
color: #48bf56;
border-radius: 10px;
}

#apply_wid #tipslb[state='selected_normal'] {
padding: 2px;
background: #e1e1e1;
color: #48bf56;
border-radius: 10px;
}

#apply_wid #tipslb[state='selected_hover'] {
padding: 2px;
background: #e1e1e1;
color: #48bf56;
border-radius: 10px;
}

#apply_wid #tipslb[state='selected_pressed'] {
padding: 2px;
background: #e1e1e1;
color: #1e1e1e;
border-radius: 10px;
}

#input_tip_wid {
background: #d3eaf8;
}

#apply_wid #FriendLabel {
background: #daf6e7;
color: #48bf56;
border-radius: 10px;
}

#apply_wid #tip_lb {
padding-left: 2px;
color:rgb(153,153,153);
font-size: 14px;
font-family: "Microsoft YaHei";
}

#gridWidget {
background: #fdfdfd;
}

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

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

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

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

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

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

#apply_sure_wid #sure_btn[state='normal'] {
background: #f0f0f0;
color: #2cb46e;
font-size: 16px; /* 设置字体大小 */
font-family: "Microsoft YaHei"; /* 设置字体 */
border-radius: 20px; /* 设置圆角 */
}

#apply_sure_wid #sure_btn[state='hover'] {
background: #d2d2d2;
color: #2cb46e;
font-size: 16px; /* 设置字体大小 */
font-family: "Microsoft YaHei"; /* 设置字体 */
border-radius: 20px; /* 设置圆角 */
}

#apply_sure_wid #sure_btn[state='press'] {
background: #c6c6c6;
color: #2cb46e;
font-size: 16px; /* 设置字体大小 */
font-family: "Microsoft YaHei"; /* 设置字体 */
border-radius: 20px; /* 设置圆角 */
}

#apply_sure_wid #cancel_btn[state='normal'] {
background: #f0f0f0;
color: #2e2f30;
font-size: 16px; /* 设置字体大小 */
font-family: "Microsoft YaHei"; /* 设置字体 */
border-radius: 20px; /* 设置圆角 */
}

#apply_sure_wid #cancel_btn[state='hover'] {
background: #d2d2d2;
color: #2e2f30;
font-size: 16px; /* 设置字体大小 */
font-family: "Microsoft YaHei"; /* 设置字体 */
border-radius: 20px; /* 设置圆角 */
}

#apply_sure_wid #cancel_btn[state='press'] {
background: #c6c6c6;
color: #2e2f30;
font-size: 16px; /* 设置字体大小 */
font-family: "Microsoft YaHei"; /* 设置字体 */
border-radius: 20px; /* 设置圆角 */
}

视频

https://www.bilibili.com/video/BV1ZM4m127z8/?vd_source=8be9e83424c2ed2c9b2a3ed1d01385e9

源码链接

https://gitee.com/secondtonone1/llfcchat

<1…567…37>

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