当前位置: 首页 > article >正文

CPP集群聊天服务器开发实践(一):用户注册与登录

目录

1 客户端用户注册与登录

1.1 主要思想

1.2 网络层

1.3 业务层

1.4 数据层

1.5 测试结果


1 客户端用户注册与登录

1.1 主要思想

实现网络层、业务层、数据层的解耦,提高系统的可维护性。

网络层:主要实现对客户端连接、客户端读写请求的捕获与回调,将其分发到多个线程中执行。

业务层:主要实现客户端读写请求回调的具体操作,当前阶段主要包含:登录业务、注册业务、用户异常退出业务

数据层:主要实现数据库中表的CUAD操作(增删改查)

1.2 网络层

利用muduo网络库实现epoll+线程池模式的网络模式。此模式具有模板化的特性,使用时可以原封不动的照搬。其原理可以查看前面关于muduo网络库以及epoll原理讲解的文章。其主要分为以下几个部分:

(1)组合Tcpserver对象以及Eventloop对象

(2)public下定义构造函数,实现上述对象的初始化以及处理连接的onConnection和处理读写的onMessage回调函数的注册

(3)private下定义onConnection和onMessage回调函数的具体实现

其中,在onConnection中:主要实现用户的连接以及用户异常关闭的情况,用户的连接直接调用muduo库的connected()方法;用户异常关闭则调用业务层定义的方法。

在onMessage中,为了实现网络层和业务层的解耦,通过定义哈希map _msgHandlerMap 将用户操作(msgid表征)和对应的回调处理进行一对一映射。即根据用户发来的msgid来获取对应的handler,获取到对应的handler后执行相应的业务。

整体来说由于muduo库强大的功能,实现比较简单,具体源码如下:

#include"chatserver.hpp"
#include"json.hpp"
#include<functional>
#include<string>
#include"chatservice.hpp"
using namespace std;
using namespace placeholders;
using json=nlohmann::json;

ChatServer::ChatServer(EventLoop *loop,
                       const InetAddress &listenAddr,
                       const string &nameArg) : _server(loop, listenAddr, nameArg), _loop(loop)
{
    //注册连接回调
    _server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));
    
    //注册读写回调
    _server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));

    //设置线程数量
    _server.setThreadNum(4);
}

void ChatServer::start()
{
    _server.start();
}

// 上报连接相关信息的回调函数
void ChatServer::onConnection(const TcpConnectionPtr &conn)
{
    //客户端断开连接
    if (!conn->connected())
    {
        //客户端异常关闭
        ChatService::instance()->clientCloseException(conn);
        conn->shutdown();
    }
}
// 上报读写事件相关信息的回调函数
void ChatServer::onMessage(const TcpConnectionPtr &conn,
                           Buffer *buffer,
                           Timestamp time)
{
    string buf=buffer->retrieveAllAsString();
    //数据的反序列化
    json js=json::parse(buf);
    //实现网络模块和业务模块的解耦:通过hs["msgid"]调取对应业务handler(回调业务代码),可以将conn,js,time传给handler,执行回调
    //解耦操作一般两种:1.面向基类2.回调操作
    //js["msgid"].get<int>():利用get方法将json对象转换为int类型,get方法提供的是模板
    auto msgHanndler=ChatService::instance()->getHandler(js["msgid"].get<int>());
    //回调消息绑定好的事件处理器,来执行相应的业务处理
    msgHanndler(conn,js,time);
}

1.3 业务层

上文提到,网络层和业务层解耦的关键就是回调操作。网络层获取到用户发来的msgid后,根据_msgHandlerMap找到对应的handler,每个handler都和对应的login或者register操作进行绑定,找到对应的handler后即执行相应的操作。其主要分为以下几个部分:

(1)构造函数中注册消息(msgid)以及对应的handler回调操作

(2)处理登录业务:根据id找到对应的user对象,验证用户是否已经登录,之后验证password是否正确,登录成功修改用户状态并进行响应

(3)处理注册业务:根据用户name 和 password进行注册,将自增的id作为账号返回给用户

(4)处理用户异常退出业务:定义关于用户id和状态信息的映射_userConnMap,从哈希表中获取异常关闭的用户id,修改数据库的状态,并删除哈希表中对应条目

业务层的实现主要靠回调操作,同时涉及单例模式、互斥锁等一些知识,具体可以查看专栏前面分享的文章。

具体源码如下:

#include"chatservice.hpp"
#include"public.hpp"
//利用muduo库的封装好的日志输出
#include<muduo/base/Logging.h>
using namespace muduo;

//获取单例对象的接口函数
ChatService* ChatService::instance()
{
    static ChatService service;
    return &service;
}
// 注册消息以及对应的Handler回调操作
ChatService::ChatService()
{
    _msgHandlerMap.insert({LOGIN_MSG,std::bind(&ChatService::login,this,_1,_2,_3)});
    _msgHandlerMap.insert({REG_MSG,std::bind(&ChatService::reg,this,_1,_2,_3)});
}

MsgHandler ChatService::getHandler(int msgid)
{
    //记录错误日志,msgid没有对应的事件处理回调
    auto it = _msgHandlerMap.find(msgid);
    if (it == _msgHandlerMap.end())
    {
        return [=](const TcpConnectionPtr &conn, json &js, Timestamp time)
        {
            LOG_ERROR << "msgid: " << msgid << "can not find handler";
        };
    }
    else
    {
        return _msgHandlerMap[msgid];
    }
}

// 处理登录业务 id+pwd 验证pwd
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    LOG_INFO<<"DO LOGIN SERVICE!";
    int id=js["id"];
    string pwd=js["password"];

    //查询id对应的user对象
    User user = _userModel.query(id);
    if(user.getId()==id&&user.getPwd()==pwd){
        if(user.getState()=="online"){
            //该用户已经登录,不允许重复登录
            json response;
            response["msgid"]=LOGIN_MSG_ACK;
            //响应error number如果为0,表示业务成功
            response["errno"] = 2;
            response["errmsg"] = "该账号已经登录,请重新输入新账号";
            //组装好json通过网络返回给客户端
            conn->send(response.dump());//数据序列化
        }
        else{
            //登录成功,记录用户连接信息
            /*群组聊天时,onMessage会被多线程调用,同时这个记录用户连接的map也会被多线程调用
            并且这个map会不断发生变化,需要考虑 线程安全 的问题*/
            {
                lock_guard<mutex> lock(_connMutex);
                _userConnMap.insert({id,conn});
            }
            //登录成功,更新用户state->online
            user.setState("online");
            _userModel.updateState(user);
            json response;
            response["msgid"]=LOGIN_MSG_ACK;
            //响应error number如果为0,表示业务成功
            response["errno"] = 0;
            response["id"]=user.getId();
            response["name"]=user.getName();

            //组装好json通过网络返回给客户端
            conn->send(response.dump());//数据序列化
        }

    }
    else{
        //用户不存在或者用户存在密码错误,登录失败
        json response;
        response["msgid"]=LOGIN_MSG_ACK;
        //响应error number如果为0,表示业务成功
        response["errno"] = 1;
        response["errmsg"] = "用户名或者密码错误";
        //组装好json通过网络返回给客户端
        conn->send(response.dump());//数据序列化
    }
}
// 处理注册业务 
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    LOG_INFO << "DO REGISTER SERVICE!";
    string name = js["name"];
    string pwd = js["password"];

    User user;
    user.setName(name);
    user.setPwd(pwd);
    bool state = _userModel.insert(user);
    if (state)
    {
        // 注册成功,返回客户端json消息,并把自增的id返回给用户当作账号
        json response;
        response["msgid"]=REG_MSG_ACK;
        //响应error number如果为0,表示业务成功
        response["errno"] = 0;
        response["id"]=user.getId();

        //组装好json通过网络返回给客户端
        conn->send(response.dump());//数据序列化
    }
    else
    {
        // 注册失败
        json response;
        response["msgid"]=REG_MSG_ACK;
        //响应error number如果为1,表示业务不成功
        response["errno"] = 1;
        //组装好json通过网络返回给客户端
        conn->send(response.dump());//数据序列化
    }
}

//处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr &conn)
{
    User user;
    //做两件事:1.用户数据库state->offline 2._userConnMap中用户信息删除
    {
        lock_guard<mutex> lock(_connMutex);
        //遍历_userConnMap
        for(auto it=_userConnMap.begin();it!=_userConnMap.end();++it){
            if(it->second==conn){
                user.setId(it->first);
                //从map表删除用户的连接信息
                _userConnMap.erase(it);
                break;
            }
        }
    }
    //更新用户的状态信息
    if(user.getId()!=-1){
        user.setState("offline");
        _userModel.updateState(user);
    }
}

1.4 数据层

数据层与业务层解耦的关键在于ORM架构的实现,可以有效防止操作数据库过程中重复的sql代码,通过将数据库的信息封装为对象进行操作,其核心主要包含dp.cpp(数据库连接、数据库更新、数据库查询功能);user.hpp(数据表的对象封装);usermodel.cpp(对user对象的增删改查操作)。主要包含以下内容:

(1)dp.cpp:可以理解为数据库的底层操作,主要依赖于mysql库提供的方法,实现数据库的连接、更新、查询操作

(2)user.hpp:主要是对user表的对象实例化封装,根据表的字段定义变量(私有),并提供get、set的公有化方法

(3)usermodel.cpp:这一层与业务层紧密相关,根据业务需要对user对象提供插入、查询、更新状态的方法,并调用底层的mysql库方法(dp.cpp中的内容)。具体步骤包含:a. 组装sql语句 b. 连接数据库 c.调用底层操作。

具体源码如下:

dp.cpp:

#include"db.h"
#include<muduo/base/Logging.h>
//数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";

//初始化数据库连接,为连接开辟资源
MySQL::MySQL()
{
    _conn = mysql_init(nullptr);
}

//释放数据库连接资源
MySQL::~MySQL()
{
    if(_conn!=nullptr){
        mysql_close(_conn);
    }
}

//连接数据库
bool MySQL::connect()
{
    MYSQL *p = mysql_real_connect(_conn, server.c_str(), user.c_str(), password.c_str(),
                                    dbname.c_str(), 3306, nullptr, 0);
    if (p != nullptr)
    {
        // C和C++代码默认的编码字符是ASCII,如果不设置,从Mysql上拉取的数据不支持汉字
        mysql_query(_conn, "set names gbk");
        LOG_INFO << "connect mysql success!";
    }
    else
    {
        LOG_INFO << "connect mysql fail!";
    }
    return p;
}

//更新操作
bool MySQL::update(string sql)
{
    if (mysql_query(_conn, sql.c_str()))
    {
        LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
                    << sql << "更新失败!";
        return false;
    }
    return true;
}

//查询操作
MYSQL_RES* MySQL::query(string sql)
{
    if (mysql_query(_conn, sql.c_str()))
    {
        LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
                    << sql << "查询失败!";
        return nullptr;
    }
    return mysql_use_result(_conn);
}

//获取连接
MYSQL *MySQL::getConnection()
{
    return _conn;
}

user.hpp:

#ifndef USER_H
#define USER_H

#include<string>
using namespace std;
/*
mysql> SHOW COLUMNS FROM user;
+----------+--------------------------+------+-----+---------+----------------+
| Field    | Type                     | Null | Key | Default | Extra          |
+----------+--------------------------+------+-----+---------+----------------+
| id       | int                      | NO   | PRI | NULL    | auto_increment |
| username | varchar(50)              | NO   | UNI | NULL    |                |
| password | varchar(50)              | NO   |     | NULL    |                |
| state    | enum('online','offline') | YES  |     | offline |                |
+----------+--------------------------+------+-----+---------+----------------+
*/
//定义数据库对象,将数据库信息整合为一个对象提交给业务层
//匹配User表的ORM类
class User{
public:
    User(int id = -1, string name = "", string pwd = "", string state = "offline")
    {
        this->id = id;
        this->name = name;
        this->password = pwd;
        this->state = state;
    }

    void setId(int id)
    {
        this->id = id;
    }

    void setName(string name)
    {
        this->name = name;
    }

    void setPwd(string pwd)
    {
        this->password = pwd;
    }

    void setState(string state)
    {
        this->state = state;
    }

    int getId()
    {
        return this->id;
    }

    string getName()
    {
        return this->name;
    }

    string getPwd()
    {
        return this->password;
    }

    string getState()
    {
        return this->state;
    }
private:
    int id;
    string name;
    string password;
    string state;
};
#endif

usermodel.cpp:

#include"usermodel.hpp"
#include"db.h"
#include<iostream>
#include<muduo/base/Logging.h>
using namespace std;
//User表的增加方法
bool UserModel::insert(User &user)
{
    //1. 组成sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into user(username,password,state) values('%s', '%s', '%s')",
            user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());
    LOG_INFO<<sql;
    //2.连接数据库
    MySQL mysql;
    if (mysql.connect())
    {
        if(mysql.update(sql)){
            //获取插入成功的用户数据生成的主键id
            user.setId(mysql_insert_id(mysql.getConnection()));
            return true;
        }
    }
    return false;
}

//根据用户号码查询用户信息
User UserModel::query(int id)
{
    //1. 组成sql语句
    char sql[1024] = {0};
    sprintf(sql, "select * from user where id = %d",id);
    //2.连接数据库
    MySQL mysql;
    if (mysql.connect())
    {
        MYSQL_RES* res= mysql.query(sql);
        //res不为空,查询成功
        if(res!=nullptr){
            //返回查到的行,得到的是字符串,可以用[ ]取值
            MYSQL_ROW row = mysql_fetch_row(res);
            if(row!=nullptr){
                User user;
                //Convert a string to an integer.
                user.setId(atoi(row[0]));
                user.setName(row[1]);
                user.setPwd(row[2]);
                user.setState(row[3]);
                //res使用指针动态分配资源,需要释放,防止内存泄漏
                mysql_free_result(res);
                return user;
            }
        }
    }
    //返回默认user,匿名对象
    return User();
}

//更新用户的状态信息
bool UserModel::updateState(User user)
{
    //1. 组成sql语句
    char sql[1024] = {0};
    sprintf(sql, "update user set state='%s' where id = %d",
            user.getState().c_str(), user.getId());
    MySQL mysql;
    if(mysql.connect()){
        if(mysql.update(sql)){
            return true;
        }
    }
    return false;
}

1.5 测试结果

主要针对用户注册、登录、用户异常退出业务的测试。

可以看到,客户端发送json字符串可以实现响应的业务,同时用户异常退出时数据库相应的状态也会变为offline。


http://www.kler.cn/a/537684.html

相关文章:

  • webGL
  • docker多个容器的相互通信
  • go语言中的反射
  • 时序数据库:Influxdb详解
  • 中国城商行信贷业务数仓建设白皮书(第五期:智能决策体系构建)
  • PHP的filter_var函数的安全问题
  • Chrome谷歌多开教程:实用方法与工具
  • 使用Python和`moviepy`库从输入的图片、动图和音频生成幻灯片式视频的示例代码
  • 盘姬工具箱:完全免费的电脑工具箱
  • DeepSeek从入门到精通:全面掌握AI大模型的核心能力
  • 【Outlook】如何将特定邮件显示在Outlook的重点收件箱中
  • 机器学习数学基础:19.线性相关与线性无关
  • TaskBuilder项目实战:创建项目
  • 为AI聊天工具添加一个知识系统 之90 详细设计之31 Derivation 之5-- 神经元变元用它衍生神经网络
  • 动手写ORM框架 - GeeORM第一天 database/sql 基础
  • IDEA查看项目依赖包及其版本
  • AIGC-微头条爆款文案创作智能体完整指令(DeepSeek,豆包,千问,Kimi,GPT)
  • 2025.2.8总结
  • 使用Postman创建Mock Server
  • .NET周刊【1月第4期 2025-01-26】
  • Matplotlib基础01( 基本绘图函数/多图布局/图形嵌套/绘图属性)
  • [渗透测试]热门搜索引擎推荐— — shodan篇
  • 本地缓存 Caffeine 中的时间轮(TimeWheel)是什么?
  • 机器学习之心的创作纪念日
  • move_base全局路径规划震荡之参数调优
  • Spring Boot常见面试题总结