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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/966750.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ARM嵌入式学习--第十四天(SPI)

SPI -介绍 SPI&#xff08;Serial Peripheral Interface&#xff09;串行外围设备接口。是由Motorola公司开发&#xff0c;用来在微控制器和外围设备芯片之间提供一个低成本&#xff0c;易使用的接口。这样接口可以用来连接存储器、AD转换器、DA转换器、实时时钟、LCD驱动器、…

在大型语言模型(LLM)框架内Transformer架构与混合专家(MoE)策略的概念整合

文章目录 传统的神经网络框架存在的问题一. Transformer架构综述1.1 transformer的输入1.1.1 词向量1.1.2 位置编码&#xff08;Positional Encoding&#xff09;1.1.3 编码器与解码器结构1.1.4 多头自注意力机制 二.Transformer分步详解2.1 传统词向量存在的问题2.2 详解编解码…

GRU 和 LSTM 公式推导与矩阵变换过程图解

GRU 和 LSTM 公式推导与矩阵变换过程图解 GRULSTM 本文的前置篇链接: 单向/双向&#xff0c;单层/多层RNN输入输出维度问题一次性解决 GRU GRU&#xff08;Gate Recurrent Unit&#xff09;是循环神经网络&#xff08;RNN&#xff09;的一种&#xff0c;可以解决RNN中不能长期…

Redis存储⑤Redis五大数据类型之 List 和 Set。

目录 1. List 列表 1.1 List 列表常见命令 1.2 阻塞版本命令 1.3 List命令总结和内部编码 1.4 List典型使用场景 1.4.1 消息队列 1.4.2 分频道的消息队列 1.4.3 微博 Timeline 2. Set 集合 2.1 Set 集合常见命令 2.2 Set 集合间命令 2.3 Set命令小结和内部编码 2.…

JS实现灯光闪烁效果

在 JS中&#xff0c;我们可以实现灯光闪烁效果&#xff0c;这里主要用 setInterval 和 clearInterval 两个重要方法。 效果图 源代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>灯闪烁效果<…

RuoYi-Vue-Oracle的oracle driver驱动配置问题ojdbc8-12.2.0.1.jar的解决

RuoYi-Vue-Oracle的oracle driver驱动配置问题ojdbc8-12.2.0.1.jar的解决 1、报错情况 下载&#xff1a;https://gitcode.com/yangzongzhuan/RuoYi-Vue-Oracle 用idea打开&#xff0c;启动&#xff1a; 日志有报错&#xff1a; 点右侧m图标&#xff0c;maven有以下报误 &…

存储异常导致的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、高斯…

【论文翻译】DeepSeek-V3论文翻译——DeepSeek-V3 Technical Report——第一部分:引言与模型架构

论文原文链接&#xff1a;DeepSeek-V3/DeepSeek_V3.pdf at main deepseek-ai/DeepSeek-V3 GitHub 特别声明&#xff0c;本文不做任何商业用途&#xff0c;仅作为个人学习相关论文的翻译记录。本文对原文内容直译&#xff0c;一切以论文原文内容为准&#xff0c;对原文作者表示…

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

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

【大模型】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 响应效率…

20.<Spring图书管理系统①(登录+添加图书)>

PS&#xff1a;关于接口定义 接口定义&#xff0c;通常由服务器提供方来定义。 1.路径&#xff1a;自己定义 2.参数&#xff1a;根据需求考虑&#xff0c;我们这个接口功能完成需要哪些信息。 3.返回结果&#xff1a;考虑我们能为对方提供什么。站在对方角度考虑。 我们使用到的…

C# Winform怎么设计串口,客户端和相机控件界面显示

首先我们必须把这个类创建好 INIAPI using System; using System.Text; using System.Runtime.InteropServices;namespace Ini {public class IniAPI{#region INI文件操作/** 针对INI文件的API操作方法&#xff0c;其中的节点&#xff08;Section)、键&#xff08;KEY&#x…

stm32点灯 GPIO的输出模式

目录 1.选择RCC时钟 2.SYS 选择调试模式 SW 3.GPIO 配置 4.时钟树配置&#xff08; 默认不变&#xff09;HSI 高速内部时钟8Mhz 5.项目配置 6.代码 延时1s循环LED亮灭 1.选择RCC时钟 2.SYS 选择调试模式 SW 3.GPIO 配置 4.时钟树配置&#xff08; 默认不变&#xff09…

Linux常见命令——用户权限类

文章目录 useradd 添加新用户passwd 设置用户密码su 切换用户id 查看用户是否存在cat /etc/passwd 查看创建了哪些用户su 切换用户who 查看登录用户信息sudo 设置普通用户具有root 权限userdel 删除用户用户组管理命令groupadd 新增组usermod 修改用户groupmod 修改组groupdel …

深度剖析 Redis:缓存穿透、击穿与雪崩问题及实战解决方案

一、缓存基本使用逻辑 在应用程序中&#xff0c;为了提高数据访问效率&#xff0c;常常会使用缓存。一般的缓存使用逻辑是&#xff1a;根据 key 去 Redis 查询是否有数据&#xff0c;如果命中就直接返回缓存中的数据&#xff1b;如果缓存不存在&#xff0c;则查询数据库&#…

基于深度学习的人工智能量化衰老模型构建与全流程应用研究

一、引言 1.1 研究背景与意义 1.1.1 人口老龄化现状与挑战 人口老龄化是当今全球面临的重要社会趋势之一,其发展态势迅猛且影响深远。根据联合国的相关数据,1980 年,全球 65 岁及以上人口数量仅为 2.6 亿,到 2021 年,这一数字已翻番,达到 7.61 亿,而预计到 2050 年,…

模型 冗余系统(系统科学)

系列文章分享模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。为防故障、保运行的备份机制。 1 冗余系统的应用 1.1 冗余系统在企业管理中的应用-金融行业信息安全的二倍冗余技术 在金融行业&#xff0c;信息安全是保障业务连续性和客户资产安全的关键。随着数字化…

Java 如何覆盖第三方 jar 包中的类

目录 一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理 背景&#xff1a; 在我们日常的开发中&#xff0c;经常需要使用第三方的 jar 包&#xff0c;有时候我们会发现第三方的 jar 包中的某一个类有问题&#xff0c;或者我们需要定制化修改其中的逻辑&#xff0c…

重塑生产制造企业项目管理新范式:项目模板在Tita中的卓越实践

在竞争激烈的生产制造领域&#xff0c;每一个项目的成功执行都是企业稳健前行的重要基石。然而&#xff0c;面对复杂多变的生产流程、严格的交货期限以及不断变化的客户需求&#xff0c;如何确保项目高效、有序地进行&#xff0c;成为了众多企业面临的共同挑战。此时&#xff0…

脚手架开发【实战教程】prompts + fs-extra

创建项目 新建文件夹 mycli_demo 在文件夹 mycli_demo 内新建文件 package.json {"name": "mycli_demo","version": "1.0.0","bin": {"mycli": "index.js"},"author": "","l…