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

news2025/2/10 23:45:26

目录

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.coloradmin.cn/o/2295306.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学JDBC 第二日

数据库连接池 作用 使数据库连接达到重用的效果&#xff0c;较少的消耗资源 原理 在创建连接池对象时&#xff0c;创建好指定个数的连接对象 之后直接获取连接对象使用即可&#xff0c;不用每次都创建连接对象 从数据库连接池中获取的对象的close方法真的关闭连接对象了吗…

【Uniapp-Vue3】z-paging插件组件实现触底和下拉加载数据

一、下载z-paing插件 注意下载下载量最多的这个 进入Hbuilder以后点击“确定” 插件的官方文档地址&#xff1a; https://z-paging.zxlee.cn 二、z-paging插件的使用 在文档中向下滑动&#xff0c;会有使用方法。 使用z-paging标签将所有的内容包起来 配置标签中的属性 在s…

【C语言标准库函数】三角函数

目录 一、头文件 二、函数简介 2.1. 正弦函数&#xff1a;sin(double angle) 2.2. 余弦函数&#xff1a;cos(double angle) 2.3. 正切函数&#xff1a;tan(double angle) 2.4. 反正弦函数&#xff1a;asin(double value) 2.5. 反余弦函数&#xff1a;acos(double value)…

Redisson全面解析:从使用方法到工作原理的深度探索

文章目录 写在文章开头详解Redisson基本数据类型基础配置字符串操作列表操作映射集阻塞队列延迟队列更多关于Redisson详解Redisson 中的原子类详解redisson中的发布订阅模型小结参考写在文章开头 Redisson是基于原生redis操作指令上进一步的封装,屏蔽了redis数据结构的实现细…

声明式导航,编程式导航,导航传参,下拉刷新

1.页面导航 1.声明式导航 1.1跳转到tabBar页面 1.2跳转到非tabBar页面 1.2后退导航 、 2.编程式导航 2.1跳转到tabBar页面 2.1跳转到非tabBar页面 2.3后退导航 3.导航传参 3.1声名式导航传参 3.2编程式导航传参 3.3在onLoad中接受参数 4.下拉刷新 4.1回顾下拉刷新…

金和OA C6 DownLoadBgImage任意文件读取漏洞

金和OA C6 DownLoadBgImage任意文件读取漏洞 漏洞描述 金和C6数据库是一款针对企业信息化管理而设计的高级数据库管理系统&#xff0c;主要应用于企业资源规划&#xff08;ERP&#xff09;、客户关系管理&#xff08;CRM&#xff09;以及办公自动化&#xff08;OA&#xff09…

激活函数篇 03 —— ReLU、LeakyReLU、ELU

本篇文章收录于专栏【机器学习】 以下是激活函数系列的相关的所有内容: 一文搞懂激活函数在神经网络中的关键作用 逻辑回归&#xff1a;Sigmoid函数在分类问题中的应用 整流线性单位函数&#xff08;Rectified Linear Unit, ReLU&#xff09;&#xff0c;又称修正线性单元&a…

Kafka 入门与实战

一、Kafka 基础 1.1 创建topic kafka-topics.bat --bootstrap-server localhost:9092 --topic test --create 1.2 查看消费者偏移量位置 kafka-consumer-groups.bat --bootstrap-server localhost:9092 --describe --group test 1.3 消息的生产与发送 #生产者 kafka-cons…

5 计算机网络

5 计算机网络 5.1 OSI/RM七层模型 5.2 TCP/IP协议簇 5.2.1:常见协议基础 一、 TCP是可靠的&#xff0c;效率低的&#xff1b; 1.HTTP协议端口默认80&#xff0c;HTTPSSL之后成为HTTPS协议默认端口443。 2.对于0~1023一般是默认的公共端口不需要注册&#xff0c;1024以后的则需…

VMware虚拟机安装、创建Ubuntu虚拟机及汉化设置全流程详细教程

一、安装VMware Workstation 下载VMware 访问官网&#xff1a;https://www.vmware.com 选择适合的版本&#xff08;如 Workstation Pro 或 VMware Player&#xff0c;后者免费&#xff09;。完成下载后运行安装程序。 网盘下载&#xff1a; 链接: https://pan.baidu.com/s/1MQ…

21.2.7 综合示例

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 【例 21.7】【项目&#xff1a;code21-007】填充职员表并打印。 本例使用到的Excel文件为&#xff1a;职员信息登记表.xlsx&#x…

【大模型】DeepSeek与chatGPT的区别以及自身的优势

目录 一、前言二、核心技术对比2.1 模型架构设计2.1.1 ChatGPT的Transformer架构2.1.2 DeepSeek的混合架构 2.2 训练数据体系2.2.1 ChatGPT的数据特征2.2.2 DeepSeek的数据策略 三、应用场景对比3.1 通用场景表现3.1.1 ChatGPT的强项领域3.2.2 DeepSeek的专项突破 3.3 响应效率…

burpsuite抓取html登陆和上传数据包

一、burpsuite抓取html登陆数据包 1、先写一个html格式的登陆页面 <!doctype html> <html lang"en"> <head><meta charset"UTF-8"><title>这是标签</title></head> <body> <hr><!-- 登陆表单 …

Linux 安装 Ollama

1、下载地址 Download Ollama on Linux 2、有网络直接执行 curl -fsSL https://ollama.com/install.sh | sh 命令 3、下载慢的解决方法 1、curl -fsSL https://ollama.com/install.sh -o ollama_install.sh 2、sed -i s|https://ollama.com/download/ollama-linux|https://…

使用Ollama本地部署deepseek

1、下载安装Ollama 前往下载页面 https://ollama.com/download下载好安装包&#xff0c;如同安装软件一样&#xff0c;直接安装即可 win中默认为C盘&#xff0c;如果需要修改到其他盘&#xff0c;查找具体教程 运行list命令&#xff0c;检查是否安装成功 2、修改模型下载的…

如何在RTACAR中配置IP多播(IP Multicast)

一、什么是IP多播 IP多播&#xff08;IP Multicast&#xff09;是一种允许数据包从单一源地址发送到多个目标地址的技术&#xff0c;是一种高效的数据传输方式。 多播地址是专门用于多播通信的IP地址&#xff0c;范围从 224.0.0.0到239.255.255.255 与单播IP地址不同&#x…

2025年最新版武书连SCD期刊(中国科学引文数据库)来源期刊已更新,可下载PDF版!需要的作者进来了解~

2025年最新版武书连SCD期刊&#xff08;中国科学引文数据库&#xff09;来源期刊已更新&#xff01; 官网是不提供免费查询的。小编给大家两个路径&#xff0c;无需下载PDF&#xff0c;随时随地都能查25版SCD目录。 路径一&#xff1a;中州期刊联盟官网&#xff0c;25版SCD目…

存储异常导致的Oracle重大生产故障

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 作者&#xff1a;IT邦德 中国DBA联盟(ACDU)成员&#xff0c;10余年DBA工作经验 Oracle、PostgreSQL ACE CSDN博客专家及B站知名UP主&#xff0c;全网粉丝10万 擅长主流Oracle、MySQL、PG、高斯…

基于Java的远程视频会议系统(源码+系统+论文)

第一章 概述 1.1 本课题的研究背景 随着人们对视频和音频信息的需求愈来愈强烈&#xff0c;追求远距离的视音频的同步交互成为新的时尚。近些年来&#xff0c;依托计算机技术、通信技术和网络条件的发展&#xff0c;集音频、视频、图像、文字、数据为一体的多媒体信息&#xff…

C++ Primer 成员访问运算符

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…