1. 客户端登录
1.1 初始化玩家头像
将头像的大小固定在250 * 250
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
void ShowTipLabel(const QString& tip, const QString& State); // 显示提示信息
void AddTipErr(TipErr err, const QString& tips); // 添加错误提示
void DelTipErr(TipErr err); // 清除错误提示
QMap<TipErr, QString> _TipErrs; // 错误提示记录
#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;
为标签设置样式
/* 初始化可视化控件 */
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. 检查用户名输入以及密码输入是否有问题
bool CheckUserValid(); // 检查用户名是否合法
bool CheckPasswordValid(); // 检查密码是否合法
/* 检查用户名是否合法 */
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. 实现槽函数逻辑
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
#ifndef STRUCT_H
#define STRUCT_H
#include <QString>
/* 服务器信息 */
struct ServerInfo {
QString Host;
QString Port;
QString Token;
int Uid;
};
#endif // STRUCT_H
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. 实现处理服务器响应的槽函数
/* 服务器处理完登录请求回复的响应 */
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思想来归还池式组件的连接
// RAII 归还连接
class ConnectionRAII
{
public:
ConnectionRAII(function<void()> RetunConnection)
: m_RetunConnection(RetunConnection)
{
}
~ConnectionRAII()
{
m_RetunConnection();
}
private:
function<void()> m_RetunConnection;
};
2.2 重新定义GRPC服务并编译
message.proto
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) {}
}
编译生成新的通信文件
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"
编译生成新的序列化和反序列化文件
H:\BoostNetLib\grpc\visualpro\third_party\protobuf\Debug\protoc.exe --cpp_out=. "message.proto"
2.3 创建StatusGrpcClient
#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
#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
{
"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 处理登陆请求
/* 用户登录 */
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类
#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
#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 线程创建
#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进行包装
#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,按照下面操作就能得到想要的分支的项目了