Chap11-LoginAndStatus

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;

    // 发送json
    QJsonObject 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>();

        // 验证密码得到uid
        UserInfo 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_context
    boost::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_context
    std::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

posted @ 2025-12-24 23:16  大胖熊哈  阅读(2)  评论(0)    收藏  举报