施磊老师基于muduo网络库的集群聊天服务器(三)

业务模块ChatService

网络模块-连接回调

先补充一下 这部分 ChatServer::onConnect 函数

先考虑一下:

不想在这里 写 ifelse等等, 调用登录业务啥的,注册服务啥的---- 这样不好----这样把 网络模块和 业务模块 放一起了, 强耦合性, 并没有解耦

强耦合(Tight Coupling)是指 模块(类、组件、服务)之间高度依赖彼此的实现细节

实现解耦

不要在 网络模块里 直接调用业务模块的 代码和方法, 我们希望 在网络模块里, 是看不到业务模块的 代码

解耦

  • 网络模块应避免直接依赖业务模块的代码或方法,保持模块间的隔离性。理想情况下,网络模块的代码中不应出现业务逻辑相关的引用。

怎么实现解耦呢?

  • 可以通过回调、协议抽象或事件通知等方式实现解耦,确保网络模块只处理通信相关逻辑,业务模块负责具体的数据处理和业务规则。

oop语言里边,要解偶模块之间的这个关系啊,一般有两种方法:

  1. 一种就是使用基于面向接口的编程

    在C++里边没有所谓的接口。

    实际上,在C++里边接口就是抽象类嘛,抽象基类,面向抽象基类的编程,或者就是基于这个。

英语: 在编程中,handler(中文常译为“处理器”或“处理程序”)通常指用于响应特定事件或管理特定任务的代码单元

业务头文件

include/server/chatservice.hpp

// 仅需要一个 实例即可 因此使用单例模式

#ifndef CHATSERVICE_H
#define CHATSERVICE_H

#include <muduo/net/TcpConnection.h>
#include <unordered_map>
#include <functional>
using namespace std;
using namespace muduo;
using namespace muduo::net;

#include "json.hpp"
using namespace nlohmann;

//表示处理消息的事件回调方法类型
using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp time)>; // #1

// 聊天服务器 业务类
class ChatService
{
public:
    // 获取单例对象的接口函数 #6
    static ChatService *instance();

    // 处理的登录业务 #2
    void login(const TcpConnectionPtr &conn, json &js, Timestamp time);
    // 处理注册业务 #3
    void reg(const TcpConnectionPtr &conn, json &js, Timestamp time);

    //获取消息对应的处理器 #7
    MsgHandler getHandler(int msgid);

private:
    // 单例模式----构造函数私有化,并写一个惟一的实例
    ChatService(); // #5

    // 存储消息id 和 其对应的 业务处理方法
    unordered_map<int, MsgHandler> _msghandlermap; // #4
};

#endif

公共头文件

include/public.hpp

#ifndef PUBLIC_H
#define PUBLIC_H
/*
server 和 client 的公共文件
*/

enum EnMsgType  // 枚举
{
    LOGIN_MSG=1,   // 与业务的login函数连接
    REG_MSG  //注册消息
};


#endif

业务函数定义文件

src/server/service.cpp

使用了 线程安全的 非互斥锁 懒汉式单例模式—详见c++笔记专栏

C++ 中,如果异常没有被捕获(uncaught exception),默认情况下会调用 std::terminate(),导致程序终止。这是 C++ 异常处理机制的核心行为之一。

“如果网络模块(如 muduo)调用了服务模块(如 ChatService),而服务模块抛出了异常,但网络模块没有捕获它,那整个进程就会崩溃。这种情况下,我们是不是必须在网络模块里处理服务模块抛出的异常?否则程序就完蛋了?”

答案是:是的! 如果异常没有被捕获,它会导致 程序终止(std::terminate 被调用),这在服务器程序中是 灾难性的(所有客户端连接都会断开,服务不可用)。

#include "chatservice.hpp"
#include "public.hpp"
#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())
    {
        // 使用 muduo 自带的日志系统
        // 不要endl, 已经封装了
        // LOG_ERROR<<"msgid: "<<msgid<<"can not find Handler!"; 

        return [=](const TcpConnectionPtr &conn, json &js, Timestamp time){
            LOG_ERROR<<"msgid: "<<msgid<<"can not find Handler!"; 
        };
    }

    else 
    {
        return _msghandlermap[msgid];
    }
    
}

// 处理登录业务
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    LOG_INFO<<"do login service"; //测试用
}

// 处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    LOG_INFO<<"do reg service"; 测试用
}

补充网路模块onMessgae()

void ChatServer::onMessage(const TcpConnectionPtr& conn,
    Buffer* buffer,
    Timestamp time)
{   
    string buf = buffer->retrieveAllAsString();
    // 数据反序列化
    json js = json::parse(buf);

     //达到的目的: 完全解耦网络模块的代码 和 业务模块的 代码
     //通过js["msgid"]获取=>业务handler=>传 conn, js, time等信息
    /*
        这样, 业务仅有 两行代码, 没有任何业务层的方法(login,reg等)调用
        仅需在 服务层内部, 做一个 业务相应的 回调
    */
     //获取msgid 对应的处理器
     auto msgHandler = ChatService::instance()->getHandler(js["msgid"].get<int>());  //json键是字符串, 要换成整型----get方法, 会用就行
     // 回调消息绑定好的 事件处理器, 来执行相应的业务处理
    msgHandler(conn, js, time);

}

测试

注意: 前面的 CMakeLists.txt 要添加 json 头文件搜索路径

//client
{"msgid":1}

//server 打印
20250418 03:41:52.258725Z 31993 INFO  do login service - chatservice.cpp:50

至此

每一个大的模块, 写完都要测试一下

网络 模块 完毕, 剩下的 将专注于 业务层 与 数据层!!!

mysql数据库代码封装

业务层处理 数据层----> 要低耦合, 不要在 业务层 写数据库代码

ORM (对象关系映射) 框架

ORM (Object-Relational Mapping) 是一种编程技术,用于在面向对象编程语言中实现不兼容类型系统之间的数据转换,即在关系型数据库和面向对象语言之间建立映射关系。

DAO (Data Access Object) 数据访问对象模式

DAO 是一种核心的J2EE设计模式,用于抽象和封装对数据源的访问,将底层数据访问逻辑与业务逻辑分离

1. 定义

DAO (Data Access Object) 是数据访问对象模式的简称,它:

  • 位于业务逻辑与持久化存储之间
  • 封装所有数据访问细节
  • 为上层提供统一的数据操作接口

2. 核心组件

  • DAO接口:定义数据操作的标准方法
  • DAO实现类:针对不同数据源的具体实现
  • 数据传输对象(DTO):在层间传输数据的载体

分离数据层与业务层

业务层操作的都是 对象 —> 使用ORM

数据层 封装了所有数据库操作 —> 使用DAO

数据库读取头文件

添加 数据层头文件搜索路径 以及 编译要加 mysqlclient

include_directories(${PROJECT_SOURCE_DIR}/include/server/db) #数据层头文件


target_link_libraries(Chatserver muduo_net muduo_base pthread mysqlclient)

一定要安装 libmysqlclient-dev, 要有这个库文件

sudo apt-get install libmysqlclient-devsudo apt-get install libmysqlclient-dev

include/db/db.h

#ifndef DB_H
#define DB_H

#include <mysql/mysql.h> //只有安装了 库, 才有这个头文件
#include <string>
using namespace std;



// 数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";
// 数据库操作类
class MySQL
{
public:
    // 初始化数据库连接
    MySQL();
   
    ~MySQL();
    // 连接数据库
    bool connect();
    
    // 更新操作
    bool update(string sql);
    
    // 查询操作
    MYSQL_RES *query(string sql);
    

private:
    MYSQL *_conn;
};

#endif

数据库类函数源文件

要注意: CMakeLists.txt 编译时 要包含这个cpp

# 所有源文件
aux_source_directory(. SRC_LIST)
# 数据层步骤 需要添加
aux_source_directory(./db DB_LIST)
# 生成可执行 
add_executable(Chatserver ${SRC_LIST} ${DB_LIST})

# 链接库
target_link_libraries(Chatserver muduo_net muduo_base pthread mysqlclient)

src/db/db.cpp

代码看不明白----需要补充mysql知识

#include "db.h"
#include <muduo/base/Logging.h>
using namespace muduo;

// 初始化数据库连接
MySQL::MySQL()
{
    _conn = mysql_init(nullptr);
}
// 释放数据库连接资源这里用UserModel示例,通过UserModel如何对业务层封装底层数据库的操作。代码示例如下:
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");
    }
    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);
}

使用ORM, 实现注册业务

第一步:

需要先定义类, 这个类 需要跟 数据库里的表 一一对应

sprintf 是一个用于格式化字符串的函数,它将格式化的数据写入一个字符串缓冲区,而不是像 printf 那样直接输出到标准输出(stdout)

以用户类为例:

include/server/user.hpp

// 映射类
// user的 ORM类, 用于映射字段

#ifndef USER_H
#define USER_H
#include <string>
using namespace std;



class User
{
public:
    User(int id=-1, string name="", string password = "", string state="offline")
    {
        this->id = id;
        this->name = name;
        this->password=password;
        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

第二步:

修改 用户类的 头文件

include/server/usermodel.hpp

// 实际操作 表 的 类----增删改查

#ifndef USERMODEL_H
#define USERMODEL_H

#include "user.hpp"

//User 表的 数据操作类
class UserModel
{
    public:
    //user表的 增加方法
    bool insert(User &user);
};

#endif;

第三步:

定义 修改 类 的 函数

db.h 与 db.cpp 添加 获取数据库对象的 方法

MYSQL *MySQL::getConnection()
{
    return _conn;
}

src/server/usermodel.cpp

#include "usermodel.hpp"
#include "db.h"

#include <iostream>
using namespace std;

//user表的 增加方法
bool UserModel::insert(User &user)
{
    //1. 组装 sql语句
    char sql[1024]={0};
    sprintf(sql, "insert into user(name, password, state) values('%s', '%s', '%s')", user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());  // 注意 %s, 每个要单独''起来, 整个''是连起来的一个字符串

    MySQL mysql;
    if(mysql.connect())
    {
        if(mysql.update(sql))
        {
            //获取插入成功的 用户数据生成的 主键id
            user.setId(mysql_insert_id(mysql.getConnection()));
            return true;
        }
    }
    return false;
}

错误1:

sprintf(sql, "insert into user(name, password, state) values('%s', '%s', '%s')", user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());
insert into user(name, password, state) values('%s', '%s', '%s');  // 这是mysql中的添加语句, user与表名必须一致!!

小技巧–红色波浪线问题

在vscode里, 有时候cmake能正常编译, 但是 编写代码 有的 会出现 红色波浪线, 尽管包含了头文件, 它也还在 , 这个 对于 有点强迫症的, 可忍受不了!!!

修改一下 .vscode的 json文件: c_cpp_properties.json

"includePath": [
                "${workspaceFolder}/**"
    }

// 改成 下面这个, 具体看自己的 头文件在哪

"includePath": [
                "${workspaceFolder}/**",
                "${workspaceFolder}/include/**",
                "${workspaceFolder}/thirdparty/**"
            ],

更简单的: 让 cmake 管理 路径

"configurationProvider": "ms-vscode.cmake-tools"  // 关键!让CMake接管配置

还有一种是 右下角 会有 IntelliSense 提示 未配置, 黄色感叹号, 可以点击 以下, 选择 cmake , 也能解决

一共三种 方法, 那种能用 用哪个

补充并测试注册业务

model 给 业务 提供的 都是 对象!!

select * from user;   //mysql里查看 user表的全部信息

第一步: 业务中添加 数据操作类对象

chatservice.hpp

private:
// 注册业务测试
    // 数据操作类对象
    UserModel _usermodel;

第二步:补充 注册业务代码

src/server/chatservice.cpp

// 处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    // LOG_INFO<<"do reg service"; 测试用

    // 注册仅需要 name password
    string name = js["name"];
    string password = js["password"];

    User user;
    user.setName(name);
    user.setPwd(password);
    bool state = _usermodel.insert(user);
    if(state)
    {
        //注册成功
        json response;
        response["msgid"] = REG_MSG_ACK;
        response["errno"] = 0;
        response["id"] = user.getId();
        conn->send(response.dump());
    }
    else
    {
        //注册失败
        json response;
        response["msgid"] = REG_MSG_ACK;
        response["errno"] = 1;
        conn->send(response.dump());
    }

}

mysql连接失败解决

  • auth_socket 插件只允许通过 Unix Socket 本地连接(不能用于 TCP/IP 或远程连接)
  • 如果你的代码使用 127.0.0.1(TCP/IP)连接,auth_socket 会拒绝认证
    (即使是在本机,127.0.0.1 也算 TCP/IP 连接,不是 Unix Socket)

查看当前认证插件:

SELECT user, plugin FROM mysql.user;

修改 root 认证方式(两种选择):

  • 改为 mysql_native_password(兼容旧客户端):

    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';
    FLUSH PRIVILEGES;
    

阶段总结-1

自己的 语言:

db.h—> 提供 数据库 基本连接,查询等的 声明

db.cpp—> 提供db.h 的具体实现

user.hpp —> 数据库的 模型类 , 由于内容少, 直接 包含了 具体实现

usermodel.hpp —> 提供 给 业务 的 对象, 仅包含 操作方法的 声明

usermodel.cpp —> 对 操作方法的 具体实现

在 业务模块中, 例如 chatservice.hpp 和 cpp, 都是 仅接收了 对象, 调用 对应方法, 没有设计 具体实现, 达到了 解耦性

ai 润色:

  1. 数据库基础层
    • db.h:声明数据库基本连接、查询等接口
    • db.cpp:实现数据库核心功能的具体细节
  2. 数据模型层
    • user.hpp:定义与数据库表对应的模型类(采用头文件实现模式)
  3. 业务逻辑层
    • usermodel.hpp:声明面向业务的操作接口
    • usermodel.cpp:实现具体的业务逻辑操作
  4. 服务层
    • chatservice.hpp/cpp:通过接口调用业务方法,完全隔离具体实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值