那曲市网站建设_网站建设公司_H5网站_seo优化
2025/12/24 23:22:22 网站建设 项目流程

Chap11-LoginAndStatus

Login

同注册以及忘记密码同理,设计验证、点击后的槽函数、接受到返回信息的槽函数等。

下面是点击登陆后的槽函数

void LoginScreen::do_login_clicked()
{QString accountStr = accountEdit->text().trimmed();QString passwordStr = passwordEdit->text().trimmed();bool clicked = agreeCheck->isChecked();bool allFilled = true;// 账号框if (accountStr.isEmpty()){showToolTip(accountEdit,"此项不能为空");accountEdit->setStyleSheet("border: 1px solid red;");allFilled = false;}else{accountEdit->setStyleSheet("");}// 密码框if (passwordStr.isEmpty()){showToolTip(passwordEdit,"此项不能为空");passwordEdit->setStyleSheet("border: 1px solid red;");allFilled = false;}else if(passwordStr.size()<6 || passwordStr.size()>12){showToolTip(passwordEdit,"长度在6-12位");passwordEdit->setStyleSheet("border: 1px solid red;");allFilled = false;}else{passwordEdit->setStyleSheet("");}// 同意协议if (!clicked){showToolTip(agreeCheck,"请勾选同意协议");allFilled = false;}if (!allFilled)return;// 发送jsonQJsonObject json;json["user"] = accountStr;json["password"] = cryptoString(passwordStr);HttpManager::GetInstance()->PostHttp(QUrl(gate_url_prefix+"/userLogin"),json,RequestType::LOGIN_USER,Modules::LOGINMOD);
}

发送信息之后等待服务器回传信息,当HttpManager触发对应的信号时,触发on_login_finished,下面是处理的函数:

void LoginScreen::do_login_finished(RequestType requestType,const QString&res,ErrorCodes errorCode)
{if (errorCode != ErrorCodes::SUCCESS){showToolTip(loginBtn,"网络请求错误");return;}QJsonDocument doc = QJsonDocument::fromJson(res.toUtf8());if (doc.isNull()){showToolTip(loginBtn,"解析错误");return;}if (!doc.isObject()){showToolTip(loginBtn,"解析错误");return;}// _handlers[requestType](doc.object());// 安全检查:确保处理器存在auto it = _handlers.find(requestType);if (it == _handlers.end()) {showToolTip(loginBtn, "未知的请求类型");return;}it.value()(doc.object());
}void LoginScreen::initHandlers()
{_handlers[RequestType::LOGIN_USER] = [this](QJsonObject json){int error = json["error"].toInt();if(error != static_cast<int>(ErrorCodes::SUCCESS)){showToolTip(loginBtn,"参数错误");return;}showToolTip(loginBtn,"登陆成功");auto email = json["email"].toString();ServerInfo si;si.uid = json["uid"].toInt();si.host = json["host"].toString();si.port = json["port"].toString();si.token = json["token"].toString();si.email = json["email"].toString();emit on_tcp_connect(si);};
}

GateServer

首先在LogicSystem添加登陆的路由

RegistHandlers("/userLogin", RequestType::POST, [this](std::shared_ptr<Session> session) {auto body_str = beast::buffers_to_string(session->_request.body().data());SPDLOG_DEBUG("receive userLogin request, body: {}", body_str);session->_response.set(http::field::content_type, "text/json");json j = json::parse(body_str);if (j.is_null() || j.is_discarded()) {SPDLOG_WARN("Invalid json");j["error"] = ErrorCodes::ERROR_JSON;std::string str = j.dump(4);beast::ostream(session->_response.body()) << str;return true;}auto user = j["user"].get<std::string>();auto password = j["password"].get<std::string>();// 验证密码得到uidUserInfo userInfo;bool ok = MysqlManager::GetInstance()->CheckPwd(user, password, userInfo);if (!ok) {SPDLOG_WARN("Failed to login user: {}", user);j["error"] = ErrorCodes::ERROR_USER_OR_PASSWORD_INCORRECT;std::string str = j.dump(4);beast::ostream(session->_response.body()) << str;return true;}// 登录成功,根据uid寻找服务器auto reply = StatusClient::GetInstance()->GetChatServer(userInfo.uid);if (reply.error()) {SPDLOG_ERROR("Failed to load user chat server: {}", reply.error());j["error"] = ErrorCodes::RPCFAILED;std::string str = j.dump(4);beast::ostream(session->_response.body()) << str;return true;}SPDLOG_INFO("User {} login success.", userInfo.uid);j["token"] = reply.token();j["host"] = reply.host();j["port"] = reply.port();j["uid"] = userInfo.uid;j["name"] = userInfo.name;j["email"] = userInfo.email;j["error"] = ErrorCodes::SUCCESS;j["message"] = "登录成功";beast::ostream(session->_response.body()) << j.dump(4);return true;});

其中CheckPwd是用来验证账号(uid/邮箱)与密码是否存在且匹配。如果正确就返回数据存入UserInfo.之后调用grpc服务查询状态服务器获取一个服务器的信息,通过这个服务器进行以后的通信。

那么得到的UserInfo的数据和通过grpc的数据都要以json格式发送给前端。前端首先处理GateWayServer发送回去的这个json,然后通过解析json内的信息,通过tcp去连接聊天服务器(之后的内容)。

下面是CheckPwd的DAO层实现。

bool MysqlDao::CheckPwd(const std::string& user, const std::string& password, UserInfo& userInfo)
{auto conn = _pool->GetConnection();Defer defer([this, &conn]() {_pool->ReturnConnection(std::move(conn));});try {if (conn == nullptr) {return false;}MYSQL_STMT* stmt = mysql_stmt_init(conn.get());std::string query = "select uid,name,email from user where ( uid = ? OR email = ? ) AND password = ?";if (mysql_stmt_prepare(stmt, query.c_str(), query.size()) != 0) {SPDLOG_WARN("mysql_stmt_prepare failed: {}", mysql_stmt_error(stmt));mysql_stmt_close(stmt);return false;}MYSQL_BIND params[3];memset(params, 0, sizeof(params));params[0].buffer_type = MYSQL_TYPE_STRING;params[0].buffer = (char*)user.c_str();params[0].buffer_length = user.size();params[0].length = &params[0].buffer_length;params[1].buffer_type = MYSQL_TYPE_STRING;params[1].buffer = (char*)user.c_str();params[1].buffer_length = user.size();params[1].length = &params[1].buffer_length;params[2].buffer_type = MYSQL_TYPE_STRING;params[2].buffer = (char*)password.c_str();params[2].buffer_length = password.size();params[2].length = &params[2].buffer_length;if (mysql_stmt_bind_param(stmt, params) != 0) {SPDLOG_WARN("mysql_stmt_bind_param failed: {}", mysql_stmt_error(stmt));mysql_stmt_close(stmt);return false;}if (mysql_stmt_execute(stmt) != 0) {mysql_stmt_close(stmt);return false;}if (mysql_stmt_store_result(stmt) != 0) {mysql_stmt_close(stmt);return false;}int count = mysql_stmt_num_rows(stmt);if (count != 1) {mysql_stmt_close(stmt);return false;}// 绑定结果缓冲区MYSQL_BIND result_bind[3]; // 根据实际列数调整long uid;char name_buffer[70];char email_buffer[70];memset(result_bind, 0, sizeof(result_bind));// 绑定第一列(示例:email字段)result_bind[0].buffer_type = MYSQL_TYPE_LONG;result_bind[0].buffer = &uid;result_bind[1].buffer_type = MYSQL_TYPE_STRING;result_bind[1].buffer = name_buffer;result_bind[1].buffer_length = sizeof(name_buffer);result_bind[1].length = &result_bind[1].buffer_length;result_bind[2].buffer_type = MYSQL_TYPE_STRING;result_bind[2].buffer = email_buffer;result_bind[2].buffer_length = sizeof(email_buffer);result_bind[2].length = &result_bind[2].buffer_length;if (mysql_stmt_bind_result(stmt, result_bind) != 0) {SPDLOG_WARN("mysql_stmt_bind_result failed: {}", mysql_stmt_error(stmt));mysql_stmt_close(stmt);return false;}if (mysql_stmt_fetch(stmt) != 0) {mysql_stmt_close(stmt);return false;}userInfo.email = email_buffer;userInfo.name = name_buffer;userInfo.uid = uid;mysql_stmt_close(stmt);return true; // 返回1表示重置密码成功} catch (const std::exception& e) {SPDLOG_ERROR("MysqlDao::CheckPwd failed: {}", e.what());return false;}return false;
}

接下来我们看状态服务的实现(GetChatServer,简单就是获取一个可以通信的服务器信息)

首先是proto文件的改变

syntax = "proto3";
package message;service VarifyService{rpc GetSecurityCode(GetSecurityCodeRequest) returns (GetSecurityCodeResponse){}
}message GetSecurityCodeRequest{string email = 1;
}message GetSecurityCodeResponse{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){}
}

然后创建新的类StatusClient

// h
#ifndef STATUCCLIENT_H
#define STATUCCLIENT_H#include "../global/ConfigManager.h"
#include "../global/Singleton.h"
#include "../global/const.h"
#include "RPCPool.h"using message::GetChatServerRequest;
using message::GetChatServerResponse;
using message::StatusService;class StatusClient : public Singleton<StatusClient> {friend class Singleton<StatusClient>;public:~StatusClient() = default;GetChatServerResponse GetChatServer(int uid);private:StatusClient();std::unique_ptr<RPCPool<StatusService, StatusService::Stub>> _pool;
};#endif// cpp
#include "StatusClient.h"GetChatServerResponse StatusClient::GetChatServer(int uid)
{grpc::ClientContext context;GetChatServerResponse reply;GetChatServerRequest request;request.set_uid(uid);auto stub = _pool->GetConnection();grpc::Status status = stub->GetChatServer(&context, request, &reply);Defer defer([this, &stub]() mutable {_pool->ReturnConnection(std::move(stub));});if (!status.ok()) {reply.set_error(status.error_code());}return reply;
}StatusClient::StatusClient()
{auto& cfgMgr = ConfigManager::GetInstance();std::string host = cfgMgr["StatusServer"]["host"];std::string port = cfgMgr["StatusServer"]["port"];_pool = std::make_unique<RPCPool<StatusService, StatusService::Stub>>(10, host, port);
}

在这里我们调用了grpc服务,简单而言就是想要借状态服务器得到一个可以通信的服务器的地址,有了这个地址才能与其他人有沟通的路径。

那么很显然我们需要实现这个状态服务器的真正操作,新创建一个项目,命名为StatusServer.我们可以把GateWayServer的mysql/redis/config/grpc等的文件直接拷贝过来复用。其次最重要的就是编写grpc服务的处理。

// .h
#ifndef STATUSSERVICEIMPL_H
#define STATUSSERVICEIMPL_H#include "message.grpc.pb.h"
#include <grpc++/grpc++.h>
#include <queue>
#include <unordered_map>using grpc::Server;struct ChatServer {std::string host;std::string port;std::string name;int con_count;
};class StatusServiceImpl final : public message::StatusService::Service {
public:StatusServiceImpl();grpc::Status GetChatServer(grpc::ServerContext* context, const message::GetChatServerRequest* request, message::GetChatServerResponse* response) override;grpc::Status Login(grpc::ServerContext* context, const message::LoginRequest* request, message::LoginResponse* response) override;private:void insertToken(int uid, const std::string& token);ChatServer GetChatServer();private:struct CompareServers {bool operator()(const ChatServer& a, const ChatServer& b) const{return a.con_count < b.con_count;}};std::priority_queue<ChatServer, std::vector<ChatServer>, CompareServers> _servers;std::mutex _server_mutex;std::unordered_map<int, std::string> _tokens;std::mutex _token_mutex;
};#endif

_servers里面存放了许多聊天服务器的信息ChatServer,GetChatServer的目的就是从多个服务器中选取一个负载最小的进行使用。

因此我们使用了优先队列,并自定义比较函数CompareServers。这里讲一下token,token可以看作是一个场景的临时牌号。虽然不是你的身份证号(uid),但是在当前场景下,是可以作为唯一的标志符作为你的登陆信息。

#include "StatusServiceImpl.h"
#include "../global/ConfigManager.h"
#include "../global/const.h"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <mutex>std::string generate_unique_string()
{boost::uuids::random_generator gen;boost::uuids::uuid uuid = gen();return boost::uuids::to_string(uuid);
}grpc::Status StatusServiceImpl::GetChatServer(grpc::ServerContext* context, const message::GetChatServerRequest* request, message::GetChatServerResponse* response)
{std::string prefix("ChatServer received :");const auto& server = GetChatServer();response->set_host(server.host);response->set_port(server.port);response->set_error(static_cast<int>(ErrorCodes::SUCCESS));response->set_token(generate_unique_string());insertToken(request->uid(), response->token());return grpc::Status::OK;
}void StatusServiceImpl::insertToken(int uid, const std::string& token)
{std::lock_guard<std::mutex> lock(_token_mutex);_tokens[uid] = token;
}ChatServer StatusServiceImpl::GetChatServer()
{std::lock_guard<std::mutex> lock(_server_mutex);return _servers.top();
}grpc::Status StatusServiceImpl::Login(grpc::ServerContext* context, const message::LoginRequest* request, message::LoginResponse* response)
{
}StatusServiceImpl::StatusServiceImpl()
{auto& cfg = ConfigManager::GetInstance();ChatServer server;server.port = cfg["StatusServer"]["port"];server.host = cfg["StatusServer"]["host"];_servers.push(server);
}

可以看到,服务就是调用了私有函数GetChatServer()得到服务器信息,再设置给response,发送给网关服务器。

那么这个状态服务器如何启动:

#include "global/ConfigManager.h"
#include "grpc/StatusServiceImpl.h"
#include <boost/asio.hpp>
#include <boost/asio/signal_set.hpp>
#include <grpc++/grpc++.h>
#include <thread>
#include <spdlog/spdlog.h>void RunServer()
{auto& cfg = ConfigManager::GetInstance();// 路径std::string server_address(cfg["StatusServer"]["host"] + ":" + cfg["StatusServer"]["port"]);// 配置和构建 gRPC 服务器的核心类grpc::ServerBuilder builder;// 设置端口builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());// 注册服务StatusServiceImpl service;builder.RegisterService(&service);// 构建grpc服务器并启动std::unique_ptr<grpc::Server> server(builder.BuildAndStart());// 创建boost.asio的io_contextboost::asio::io_context io_context;// 捕获退出boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);// 设置异步等待退出信号signals.async_wait([&](const boost::system::error_code& error, int signal_number) {if (!error) {server->Shutdown();io_context.stop();}});// ddd单独的线程运行io_contextstd::jthread([&io_context]() {io_context.run();});// 等待服务器的关闭server->Wait();
}int main()
{try{SPDLOG_INFO("Starting StatusServer");RunServer();}catch(const std::exception& e){spdlog::error("Exception: {}", e.what());}return 0;
}

我们的服务的基本结构,主要是为了防止文件过多混乱重新组织了一下:

image-20251025085301950

Spdlog

同时为了使得信息更加清晰,更加规范,我们使用了spdlog打印日志。

spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%s:%#] %v");
spdlog::set_level(spdlog::level::debug);

像这样首先设置显示的格式(最好加上文件和行号%s %#)和显示的等级。

使用的时候不使用静态函数,而是使用spdlog的宏可以显示文件和行号:

SPDLOG_INFO("GateWayServer starting on port: {}", cfgMgr["GateWayServer"]["port"]);

结果如下:

image-20251025085705985

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询