云计算百科
云计算领域专业知识百科平台

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

文章目录

  • 业务模块ChatService
    • 网络模块-连接回调
    • 实现解耦
    • 业务头文件
    • 公共头文件
    • 业务函数定义文件
    • 补充网路模块onMessgae()
    • 测试
    • 至此
  • mysql数据库代码封装
    • ORM (对象关系映射) 框架
    • DAO (Data Access Object) 数据访问对象模式
    • 分离数据层与业务层
    • 数据库读取头文件
    • 数据库类函数源文件
  • 使用ORM, 实现注册业务
    • **第一步:**
    • **第二步:**
    • **第三步:**
    • 错误1:
    • 小技巧–红色波浪线问题
    • 补充并测试注册业务
    • mysql连接失败解决
  • 阶段总结-1

业务模块ChatService

网络模块-连接回调

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

先考虑一下:

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

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

实现解耦

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

解耦

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

怎么实现解耦呢?

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

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

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

    在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 润色:

  • 数据库基础层
    • db.h:声明数据库基本连接、查询等接口
    • db.cpp:实现数据库核心功能的具体细节
  • 数据模型层
    • user.hpp:定义与数据库表对应的模型类(采用头文件实现模式)
  • 业务逻辑层
    • usermodel.hpp:声明面向业务的操作接口
    • usermodel.cpp:实现具体的业务逻辑操作
  • 服务层
    • chatservice.hpp/cpp:通过接口调用业务方法,完全隔离具体实现
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 施磊老师基于muduo网络库的集群聊天服务器(三)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!