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 = ¶ms[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 = ¶ms[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 = ¶ms[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;
}
我们的服务的基本结构,主要是为了防止文件过多混乱重新组织了一下:

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"]);
结果如下:


浙公网安备 33010602011771号