QT聊天项目DAY14

1. 客户端登录

1.1 初始化玩家头像

将头像的大小固定在250 * 250

cpp 复制代码
void InitHeadImage();																			// 初始化头像


/* 初始化头像 */
void LoginWidget::InitHeadImage()
{
	// 加载头像
	QPixmap OriginalPixmap(":/Chat/Images/head_5.jpg");
	OriginalPixmap = OriginalPixmap.scaled(ui.Head_Label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);

	// 绘制圆形头像
	QPixmap RoundPixmap(ui.Head_Label->size());
	RoundPixmap.fill(Qt::transparent);														// 用透明色填充

	QPainter painter(&RoundPixmap);
	painter.setRenderHint(QPainter::Antialiasing);											// 抗锯齿
	painter.setRenderHint(QPainter::SmoothPixmapTransform);									// 平滑缩放

	// 使用QPainterPath设置圆角
	QPainterPath path;
	path.addRoundedRect(0, 0, ui.Head_Label->width(), ui.Head_Label->height(), 10, 10);
	painter.setClipPath(path);

	// 将原始图片绘制到圆形头像上
	painter.drawPixmap(0, 0, OriginalPixmap);

	// 设置头像
	ui.Head_Label->setPixmap(RoundPixmap);
}

1.2 登录界面新增err_Tip

设置成水平居中,并且最大最小高度为25

添加文本刷新Function

cpp 复制代码
void ShowTipLabel(const QString& tip, const QString& State);									// 显示提示信息
void AddTipErr(TipErr err, const QString& tips);												// 添加错误提示
void DelTipErr(TipErr err);																		// 清除错误提示

QMap<TipErr, QString> _TipErrs;																	// 错误提示记录
cpp 复制代码
#include "Global.h"
ui.Tip_Label->clear();

/* 刷新文本提示标签的样式 */
void LoginWidget::ShowTipLabel(const QString& tip, const QString& State)
{
	ui.Tip_Label->setText(tip);
	ui.Tip_Label->setProperty("state", State);
	repolish(ui.Tip_Label);
}


/* 添加错误提示 */
void LoginWidget::AddTipErr(TipErr err, const QString& tips)
{
	_TipErrs[err] = tips;
	ShowTipLabel(tips, "error");
}

/* 删除错误提示 */
void LoginWidget::DelTipErr(TipErr err)
{
	_TipErrs.remove(err);
	if (_TipErrs.isEmpty())
	{
		ui.Tip_Label->clear();
		return;
	}

	ShowTipLabel(_TipErrs.first(), "error");
}

1.3 添加控制密码是否可见的标签

添加标签控件,将该标签提升为clickedLabel,重定义名称为PassWord_Visible并设置固定大小为20 * 20;

为标签设置样式

cpp 复制代码
/* 初始化可视化控件 */
ui.PassWord_Visible->SetState("unvisible", "unvisible_hover", "", "visible",
	"visible_hover", "");


/* 密码可见性 */
connect(ui.PassWord_Visible, &ClickedLabel::clicked, this, [this]()
	{
		auto state = ui.PassWord_Visible->GetState();
		if (state == ClickLabelState::Normal)
		{
			ui.PassWord_Edit->setEchoMode(QLineEdit::Password);
		}
		else if (state == ClickLabelState::Selected)
		{
			ui.PassWord_Edit->setEchoMode(QLineEdit::Normal);
		}
	});

1.4 登陆验证

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

1.4.1 添加登录按钮的槽函数

1. 检查用户名输入以及密码输入是否有问题

cpp 复制代码
bool CheckUserValid();																			// 检查用户名是否合法
bool CheckPasswordValid();																		// 检查密码是否合法
cpp 复制代码
/* 检查用户名是否合法 */
bool LoginWidget::CheckUserValid()
{
	if (ui.User_Edit->text().isEmpty())
	{
		AddTipErr(TipErr::TIP_USER_ERR, QString::fromLocal8Bit("用户名不能为空"));
		return false;
	}

	/* 一切正常则删除错误提示 */
	DelTipErr(TipErr::TIP_USER_ERR);
	return true;
}

/* 检验密码是否合法 */
bool LoginWidget::CheckPasswordValid()
{
	QString passText = ui.PassWord_Edit->text();
	if (passText.length() < 6 || passText.length() > 15)
	{
		AddTipErr(TipErr::TIP_PWD_ERR, QString::fromLocal8Bit("密码长度必须在6-15位之间"));
		return false;
	}

	/* 密码长度至少6位 可以是字母、数字、特定的特殊字符 */
	QRegularExpression regExp("^[a-zA-Z0-9!@#$%^&*]{6,15}$");
	bool match = regExp.match(passText).hasMatch();
	if (!match)
	{
		AddTipErr(TipErr::TIP_PWD_ERR, QString::fromLocal8Bit("不能包含非法字符"));
		return false;
	}

	/* 一切正常则删除错误提示 */
	DelTipErr(TipErr::TIP_PWD_ERR);
	return true;
}

2. 实现槽函数逻辑

cpp 复制代码
enum ReqID
{
	ID_GET_VARIFY_CODE = 1001,			// 获取验证码
	ID_REG_USER		   = 1002,			// 注册用户
    ID_RESET_PWD       = 1003,          // 重置密码
    ID_LOGIN_USER      = 1004,          // 登录用户
};

enum Modules
{
	REGISTERMOD		   = 0,				// 注册模块
	LOGINMOD		   = 1,				// 登录模块
};


[GateServer]
host=localhost
port=8080
getVerifycode=getVarifycode
RegisterUser=RegisterUser
ResetUserPassword=ResetUserPassword
LoginUser=LoginUser



/* 登录按钮点击时 */
void LoginWidget::OnLoginButtonClicked()
{
	if (!CheckUserValid())
		return;

	if (!CheckPasswordValid())
		return;

	QString user = ui.User_Edit->text();
	QString pass = ui.PassWord_Edit->text();

	// 发送http请求
	QJsonObject json;
	json["user"] = user;
	json["password"] = xorString(pass);

	// 创建URL
	QString urlStr = "http://" +
		ConfigSettings->value("GateServer/host").toString() + ":"
		+ ConfigSettings->value("GateServer/port").toString() + "//"
		+ ConfigSettings->value("GateServer/LoginUser").toString();

	QUrl url(urlStr);

	HttpManager::Instance()->PostHttpReq(url, json, ReqID::ID_LOGIN_USER, Modules::LOGINMOD);
}

1.4.2 处理客户端发来的响应

1. 添加对应ReqId的回调

Struct.h

cpp 复制代码
#ifndef STRUCT_H
#define STRUCT_H

#include <QString>

/* 服务器信息 */
struct ServerInfo {
	QString Host;
	QString Port;
	QString Token;
	int Uid;
};

#endif // STRUCT_H
cpp 复制代码
void sig_connect_tcp(ServerInfo serverInfo);													// 连接聊天服务器

void InitHttpHandlers();																		// 设置网络回包的回调函数

QMap<ReqID, std::function<void(const QJsonObject&)>> _handlers;									// 不同类型的回调函数

/* 网络回包的处理函数 */
void LoginWidget::InitHttpHandlers()
{
	// 注册获取验证码回包的逻辑
	_handlers.insert(ReqID::ID_LOGIN_USER, [this](const QJsonObject& jsonObj)
		{
			int error = jsonObj["error"].toInt();
			if (error != ErrorCodes::SUCCESS)
			{
				ShowTipLabel(QString::fromLocal8Bit("参数错误"), "error");
				return;
			}

			auto email = jsonObj["email"].toString();

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

			_Uid = serverInfo.Uid;
			_Token = serverInfo.Token;

			emit sig_connect_tcp(serverInfo);

			ShowTipLabel(QString::fromLocal8Bit("登陆成功"), "normal");
			qDebug() << QString::fromLocal8Bit("登陆成功 user: ") << email;
		});
}

2. 实现处理服务器响应的槽函数

cpp 复制代码
/* 服务器处理完登录请求回复的响应 */
void LoginWidget::slot_login_mod_finished(ReqID reqId, QString res, ErrorCodes errCode)
{
	if (errCode != ErrorCodes::SUCCESS) {
		ShowTipLabel(QString::fromLocal8Bit("网络请求错误"), "error");
		return;
	}

	QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());								// .json文件
	if (jsonDoc.isNull())
	{
		ShowTipLabel(QString::fromLocal8Bit("json 是空的"), "error");
		return;
	}

	// 解析json数据
	if (!jsonDoc.isObject())
	{
		ShowTipLabel(QString::fromLocal8Bit("json 格式不正确"), "error");
		return;
	}

	// 回调函数
	_handlers[reqId](jsonDoc.object());
}

客户端登录请求发送的模块封装完毕

2. 服务器处理登录请求

2.1 使用RAII思想来归还池式组件的连接

cpp 复制代码
// RAII 归还连接
class ConnectionRAII
{
public:
	ConnectionRAII(function<void()> RetunConnection)
		: m_RetunConnection(RetunConnection)
	{
	}

	~ConnectionRAII()
	{
		m_RetunConnection();
	}

private:
	function<void()> m_RetunConnection;
};

2.2 重新定义GRPC服务并编译

message.proto

javascript 复制代码
syntax = "proto3";

package message;

service VerifyService {
	rpc GetVerifyCode (GetVerifyRequest) returns (GetVerifyRsponse) {}
}

message GetVerifyRequest {
	string email = 1;
}

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

message GetChatServerRequest {
	int32 uid = 1;
}

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

message LoginRequest {
	int32 uid = 1;
	string token = 2;
}

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

service StatusService {
	rpc GetChatServer (GetChatServerRequest) returns (GetChatServerResponse) {}
	rpc Login (LoginRequest) returns (LoginResponse) {}
}

编译生成新的通信文件

cpp 复制代码
H:\BoostNetLib\grpc\visualpro\third_party\protobuf\Debug\protoc.exe  -I="." --grpc_out="." --plugin=protoc-gen-grpc="H:\BoostNetLib\grpc\visualpro\Debug\grpc_cpp_plugin.exe" "message.proto"

编译生成新的序列化和反序列化文件

cpp 复制代码
H:\BoostNetLib\grpc\visualpro\third_party\protobuf\Debug\protoc.exe --cpp_out=. "message.proto"

2.3 创建StatusGrpcClient

cpp 复制代码
#ifndef STATUSGRPCCLIENT_H
#define STATUSGRPCCLIENT_H
#include "Singletion.h"
#include "GlobalHead.h"
#include <grpcpp/grpcpp.h>
#include "message.grpc.pb.h"

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

using message::GetChatServerRequest;
using message::GetChatServerResponse;
using message::StatusService;

class StatusConPool
{
public:
	StatusConPool(int _PoolSize, string IP)
		:PoolSize(_PoolSize), bStop(false)
	{
		for (int i = 0; i < PoolSize; i++)
		{
			StatusService::Stub* stub = new StatusService::Stub(grpc::CreateChannel(IP, grpc::InsecureChannelCredentials()));
			conPool.push(stub);
		}
	}

	~StatusConPool() 
	{
		lock_guard<mutex> lock(mtx);
		bStop = true;
		cv.notify_all();
		for (int i = 0; i < PoolSize; i++)
		{
			delete conPool.front();
			conPool.pop();
		}
	}

	StatusService::Stub* GetCon()
	{
		unique_lock<mutex> lock(mtx);
		cv.wait(lock, [this]() {return !conPool.empty() || bStop; });
		if(bStop)
			return nullptr;

		StatusService::Stub* stub = conPool.front();
		conPool.pop();
		return stub;
	}

	void ReturnCon(StatusService::Stub* _con)
	{
		lock_guard<mutex> lock(mtx);
		if(bStop)
			return;
		conPool.push(_con);
		cv.notify_one();
	}

private:
	int PoolSize;

	/* 锁相关 */
	atomic<bool> bStop;
	mutex mtx;
	condition_variable cv;

	/* 连接池 */
	queue<StatusService::Stub*> conPool;
};

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

	GetChatServerResponse GetChatServer(int uid);

private:
	StatusGrpcClient();

private:
	StatusConPool* conPool = nullptr;
};

#endif // STATUSGRPCCLIENT_H
cpp 复制代码
#include "StatusGrpcClient.h"
#include "ServerStatic.h"

StatusGrpcClient::StatusGrpcClient()
{
    int Port = get<int>(ServerStatic::ParseConfig("StatusServer", "Port"));
    string Ip = "localhost:" + to_string(Port);
    conPool = new StatusConPool(5, Ip);
}

GetChatServerResponse StatusGrpcClient::GetChatServer(int uid)
{
    /* 向GRPC服务端发送请求,获取聊天服务器信息 */
    ClientContext context;
    GetChatServerResponse response;
    GetChatServerRequest request;
    request.set_uid(uid);
    auto stub = conPool->GetCon();

    Status status = stub->GetChatServer(&context, request, &response);

    ConnectionRAII ConRAII([this,&stub]()
        {
            conPool->ReturnCon(stub);
        });

    if (!status.ok())
        response.set_error(ErrorCodes::RPC_FAILED);
    return response;
}

修改Config.json

cpp 复制代码
{
    "GateServer": {
        "Port": 8080
    },
    "VerifyServer": {
        "Port": 50051
    },
    "StatusServer": {
        "Port" : 50052
    },
    "Redis": {
        "Host": "127.0.0.1",
        "Port": 6380,
        "Password": "123456"
    },
    "Mysql": {
        "Host": "127.0.0.1",
        "Port": 3306,
        "Username": "root",
        "Password": "123456",
        "Database": "YjjChat"
    }
}

2.4 处理登陆请求

cpp 复制代码
/* 用户登录 */
RegisterPost("/LoginUser", [](HttpConnection* connection)
	{
		if (connection)
		{
			auto bodyStr = boost::beast::buffers_to_string(connection->_request.body().data());	// 获取 Http请求体中的内容
			cout << "receive body is \n" << bodyStr << endl;
			connection->_response.set(http::field::content_type, "text/json");					// 设置 Http响应头中的 content-type
			Json::Value jsonResonse;															// 响应用的Json
			Json::Value jsonResult;																// 请求体解析出来的Json
			Json::Reader reader;																// Json解析器

			bool parseSuccess = reader.parse(bodyStr, jsonResult);								// 将请求体解析为Json
			if (!parseSuccess)
			{
				cout << "parse json failed" << endl;
				jsonResonse["error"] = ErrorCodes::ERROR_JSON;									// 设置响应的错误码
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;						// 向 Http响应体中写入错误码内容
				return;
			}

			UserInfo userInfo;

			/* 访问Mysql检查密码是否匹配 */
			bool bUpdatePassword = MySqlManage::GetInstance()->CheckPassword(jsonResult["user"].asString(), jsonResult["password"].asString(), userInfo);
			if (!bUpdatePassword)
			{
				cout << "update password failed\n";
				jsonResonse["error"] = ErrorCodes::Update_Password_Failed;
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;
				return;
			}

			/* 查询StatusServer匹配对应的服务器 */
			GetChatServerResponse response = StatusGrpcClient::GetInstance()->GetChatServer(userInfo.uid);
			if (response.error())
			{
				cout << "grpc get chat server failed: error is " << response.error() << endl;
				jsonResonse["error"] = response.error();
				string jsonStr = jsonResonse.toStyledString();
				beast::ostream(connection->_response.body()) << jsonStr;
				return;
			}

			/* 返回响应报文给客户端 */
			cout << "succeed to load userinfo uid is " << userInfo.uid << endl;
			jsonResonse["error"] = 0;
			jsonResonse["user"] = jsonResult["user"];
			jsonResonse["password"] = jsonResult["password"];
			string jsonStr = jsonResonse.toStyledString();
			beast::ostream(connection->_response.body()) << jsonStr;							// 向 Http响应体中写入Json内容
			return;
		}
		else
		{
			std::cout << "connection is null" << std::endl;
		}
	});

3. 创建状态服务器

3.1 创建属性页

视图->其他窗口->属性管理器 之前的项目怎么配置的现在怎么配置就行了

将pdb文件添加到静态库文件夹下

此时还是有,这是因为原本的redis库中Win32库与别的库冲突,重新修改了一份redis

3.2 填充服务器

3.2.1. 将这些文件全都添加到项目中

3.2.2. 创建新的StatusServiceImpl类

cpp 复制代码
#ifndef STATUS_SERVICE_IMPL_H
#define STATUS_SERVICE_IMPL_H
#include <string>
#include <grpcpp/grpcpp.h>
#include "message.grpc.pb.h"
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using message::GetChatServerRequest;
using message::GetChatServerResponse;
using message::StatusService;
using namespace std;

struct ChatServer
{
    string Host;
    int Port;
};

class StatusServiceImpl final : public StatusService::Service
{
public:
    StatusServiceImpl();
    Status GetChatServer(ServerContext* context, const GetChatServerRequest* request,
        GetChatServerResponse* response) override;

private:
    string generate_unique_string();

public:
    std::vector<ChatServer> _servers;
    int ServerIndex = 0;
};
#endif // STATUS_SERVICE_IMPL_H
cpp 复制代码
#include "StatusServiceImpl.h"

#include "ServerStatic.h"
#include <boost/beast/http.hpp>
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>


StatusServiceImpl::StatusServiceImpl()
    :ServerIndex(0)
{
    ChatServer server1;
    string Host1 = get<string>(ServerStatic::ParseConfig("ChatServer1", "Host"));
    int Port1 = get<int>(ServerStatic::ParseConfig("ChatServer1", "Port"));
    server1.Host = Host1;
    server1.Port = Port1;

    ChatServer server2;
    string Host2 = get<string>(ServerStatic::ParseConfig("ChatServer2", "Host"));
    int Port2 = get<int>(ServerStatic::ParseConfig("ChatServer2", "Port"));
    server2.Host = Host2;
    server2.Port = Port2;

    _servers.push_back(server1);
    _servers.push_back(server2);
}

Status StatusServiceImpl::GetChatServer(ServerContext* context, const GetChatServerRequest* request, GetChatServerResponse* response)
{
    ServerIndex = (ServerIndex + 1) % _servers.size();
    ChatServer server = _servers[ServerIndex];
    
    response->set_host(server.Host);
    response->set_port(to_string(server.Port));
    response->set_error(ErrorCodes::SUCCESS);

    string token = generate_unique_string();
    response->set_token(token);

	return Status::OK;
}

string StatusServiceImpl::generate_unique_string()
{
    // 创建UUID对象
    boost::uuids::uuid uuid = boost::uuids::random_generator()();
    // 将UUID转换为字符串
    std::string unique_string = to_string(uuid);
    return unique_string;
}

4. C++中线程的使用方法

4.1 线程创建

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;

void Function()
{
    std::cout << "This is a thread function.\n";
}

class HelloFunction
{
public:
    void operator()()
    {
        std::cout << "Hello, world!\n";
    }
};

int main()
{
    //1. 普通函数
    thread t1(Function);
    t1.join();

    // 2.函数对象
    HelloFunction hello;
    thread t2(hello);
    t2.join();

    // 3.lambda表达式
    thread t3([]() {
        std::cout << "Hello, lambda!\n";
    });
    t3.join();

    return 0;
}

4.2 等待线程完成

使用join()成员函数来等待线程完成。如果不等待,则线程可能在主线程结束后仍然运行,导致未定义行为(除非将线程分离)

4.3 分离线程

使用detach()成员函数将线程与thread对象分离,这样线程将在后台独立运行,不再收到thread对象的控制;

4.4 传递参数

线程函数可以接受参数。参数默认以值传递方式传递,如果需要传递引用必须使用ref进行包装

cpp 复制代码
#include <iostream>
#include <thread>
using namespace std;

void Function(int a)
{
    a++;
    std::cout << "a = " << a << "\n";
}

void RefFunction(int& a, int b)
{
    a += b;
    std::cout << "a = " << a << "\n";
}

int main()
{
    // 1.值传递
    int a = 10;
    thread t1(Function, 10);
    t1.join();

    cout << "a = " << a << "\n";

    // 2.引用传递
    thread t2(RefFunction, std::ref(a), 5);
    t2.join();

    cout << "a = " << a << "\n";

    return 0;
}

5. 测试

5.1 逻辑梳理

1. 客户端登录

登录按钮点击时,发送Post请求,等待服务器的响应报文

2. 服务器监听客户端发来的Http

只关注Post请求,

处理登录请求时,先拆解出用户发来的用户名和密码,将用户信息(用户名,邮件,密码,uid)读取出来,利用用户的uid向StatusServer发送请求

3. StatusServer处理请求

监听端口号并添加服务(客户端发来对应请求的回调函数)

再创建一个事件循环对象来监听Crtl + C用来优雅退出,再后台线程中进行监听

服务(返回聊天服务器的IP地址和端口号)

登陆成功

添加打印日志查看StatusServer是否监听到发来的请求

测试成功!!!

6. Git管理

在项目中建立分支将所有的项目通过分支的形式去提交,不提交Master

拉取分支,创建空项目,复制URL,按照下面操作就能得到想要的分支的项目了

相关推荐
委婉待续34 分钟前
Qt的学习(二)
c++·qt
重生之后端学习37 分钟前
苍穹外卖-day03
java·开发语言·数据库·spring boot·mysql·spring·tomcat
超大力王1 小时前
DAY 45 超大力王爱学Python
开发语言·python
林-梦璃1 小时前
Python开发基础手语识别(基础框架版)
开发语言·python·手语识别
追风赶月、1 小时前
【QT】信号和槽
开发语言·qt
wodownload21 小时前
CS003-2-2-perfermance
java·开发语言·jvm
清风~徐~来1 小时前
【Qt】控件 QWidget
前端·数据库·qt
随意0232 小时前
STL 1 容器
开发语言·c++
南瓜胖胖3 小时前
【R语言编程——数据调用】
开发语言·r语言
henreash3 小时前
C# dll版本冲突解决方案
开发语言·c#