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

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

文章目录

  • 添加好友业务
    • 实现的功能-简单实现
    • 功能不完善
    • 表设计-每个表对应一个单独的处理文件
    • 业务逻辑:-显然不好, 可以改进
    • 为什么功能少
    • 优化
    • SQL联合查询语句
    • 代码结构
    • 测试
    • 问题
  • 群组业务
    • 主要功能
    • 表设计
    • 多表查询:
    • `Group` 类
    • `GroupUser` 类
    • `GroupModel`(数据访问层)
    • 添加群聊业务
  • 群组阶段面试问题
        • 1. **项目介绍怎么讲**
        • 2. **面试官常问点**
        • 3. **容易翻车的地方**
        • 4. **加分点**
        • 5. **别忘了这些细节**
  • 对于c++, 业务不重要!!!
      • **为什么 C++ 更适合做底层,不是业务逻辑?**
  • 至此, 服务器业务代码完毕

添加好友业务

实现的功能-简单实现

基于 控制台的 好友显示

  • 用户登录后,服务器返回好友列表信息,用户可以与好友聊天。
  • 添加好友操作通过客户端发送请求到服务器,服务器将用户关系写入数据库的friend表。
  • 功能不完善

    本项目并没有非常的严格的, 必须是好友才能聊天, 只需要知道用户 id 和 name, 就能聊天—-有能力可以进行改进

    表设计-每个表对应一个单独的处理文件

    friend表只包含两个字段:user_id 和 friend_id。通过联合主键确保同一好友关系不会重复。

    业务逻辑:-显然不好, 可以改进

    用户可以直接添加好友,不需要对方同意。添加好友时,user_id和friend_id会被插入到数据库中。查询好友时,通过数据库的联合查询返回好友的详细信息,包括ID、名字和在线状态(不返回密码)。

    为什么功能少

    C和C++并不像Java或PHP那样内置很多方便的框架和工具来快速处理复杂的业务逻辑。C/C++更偏向底层操作,开发人员需要自己管理更多的细节(如数据库连接、查询等),这会影响功能的扩展。

    优化

    考虑到客户端登录后好友列表一般不会变化,服务器可以在用户登录时返回好友列表,并将该列表保存在客户端,避免每次登录都从服务器获取。—降低服务器压力

    如果有修改, 在下次上线 进行修改

    SQL联合查询语句

    通过SQL联合查询来获取用户的好友信息,避免重复查询。

    内连接查询类型:

    • 只返回两个表中匹配的记录。
    • 如果某一表中没有匹配的记录,则不会出现在结果中。

    select a.id a.name a.state from user a inner join friend b on b.friendid = a.id where b.userid=%d

    LEFT JOIN(左连接):

    • 返回左表中的所有记录,即使右表中没有匹配的记录。

    • 如果右表中没有匹配的记录,结果中对应的字段为 NULL。

    • SELECT u.id, u.name, f.friendid
      FROM user u
      LEFT JOIN friend f ON u.id = f.friendid;

    代码结构

    include/public.hpp

    ADD_FRIEND_MSG // 添加好友

    include/server/chatservice.hpp

    // 处理添加好友业务
    void addFriend(const TcpConnectionPtr &conn, json &js, Timestamp time); // conn用来维护用户与其网络连接之间的映射关系 , 快速找到某个用户的连接

    include/server/friendmodel.hpp

    #ifndef ADD_FRIEND_H
    #define ADD_FRIEND_H
    #include "user.hpp"
    #include <vector>

    // 维护好友信息的操作接口方法
    class FriendModel
    {
    public:
    // 添加好友
    void insert(int userid, int friendid);

    // 返回好友列表 要显示好友的信息
    // 两个表的 联合查询
    vector<User> query(int userid);

    };

    #endif

    src/server/friendmodel.cpp

    #include "friendmodel.hpp"
    #include "db.h"

    // 添加好友
    void FriendModel::insert(int userid, int friendid)
    {
    // 1. 创建sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into friend (userid, friendid) values (%d, %d)", userid, friendid);

    // 2. 执行sql语句
    MySQL mysql;
    if (mysql.connect())
    {
    mysql.update(sql);
    }
    }

    // 返回好友列表 要显示好友的信息
    // 两个表的 联合查询
    vector<User> FriendModel::query(int userid)
    {
    // 1. 创建sql语句
    char sql[1024] = {0};
    sprintf(sql, "select a.id, a.name, a.state from user a inner join friend b on b.friendid = a.id where b.userid = %d", userid);

    // 2. 执行sql语句
    MySQL mysql;
    if (mysql.connect())
    {
    MYSQL_RES *res = mysql.query(sql);
    if (res != nullptr)
    {
    // 3. 解析结果
    MYSQL_ROW row;
    vector<User> vec;
    while ((row = mysql_fetch_row(res)) != nullptr)
    {
    User user;
    user.setId(atoi(row[0]));
    user.setName(row[1]);
    user.setState(row[2]);
    vec.push_back(user);
    }
    mysql_free_result(res);
    return vec;
    }
    }
    return vector<User>(); //比vec好点
    }

    include/server/chatservice.hpp

    #include "friendmodel.hpp"

    // 好友操作对象
    FriendModel _friendModel;

    src/server/chatservice.cpp

    //绑定业务
    _msghandlermap.insert({ADD_FRIEND_MSG, std::bind(&ChatService::addFriend, this, _1, _2, _3)});

    //登陆成功里加
    // 查询好友列表并返回
    vector<User> uservec = _friendmodel.query(id);
    if(!uservec.empty())
    {
    // response["friends"] = uservec; // 这是不行的, 因为是自定义类型
    // map也不行, 因为map的value 不确定
    vector<string> vec;
    for(auto &user:uservec)
    {
    json js;
    js["id"] = user.getId();
    js["name"] = user.getName();
    js["state"] = user.getState();
    vec.push_back(js.dump());
    }
    response["friends"] = vec; // 好友列表
    }

    // 添加好友业务
    // 处理添加好友业务 带msgid
    void ChatService::addFriend(const TcpConnectionPtr &conn, json &js, Timestamp time)
    {
    int id = js["id"].get<int>();
    int friendid = js["friendid"].get<int>();
    // 添加好友 显示好友信息 在登陆成功那里
    _friendmodel.insert(id, friendid);

    }

    测试

    {"msgid":1,"id":22,"password":"101010"}
    {"msgid":6,"id":22,"friendid":23}
    {"msgid":6,"id":22,"friendid":25}

    //客户端正常退出
    {"msgid":1,"id":22,"password":"101010"}

    问题

    代码现在有个问题, 服务器异常退出, 用户状态可能没有改变, 还是online, 当再次连接时, 用户无法重复登录, 而且 记录在线用户信息的 map 也没有这个用户, 就会导致 即使此时 客户端断开连接, 也无法 下线

    群组业务

    主要功能

    创建群组:群组管理员创建一个新的群组,群名唯一,描述可选。每次创建群组时,数据库中会插入新的记录。群组的 ID 会自动生成,插入时会返回并更新到相应的群组对象。

    加入群组:用户加入群组时,会向 group user 表中插入一条记录,记录该用户在某个群组中的身份(如管理员或普通成员)。group user 表的联合主键是 group ID 和 user ID,确保每个用户在同一群组中只有一条记录。

    群聊功能:通过查询群组中的其他成员,将消息转发给他们。这里使用数据库的联合查询,查询指定群组内所有成员的 ID 和角色,来确定要转发消息的对象。

    功能目标:

    • 支持用户之间进行群聊交流。
    • 用户可以创建群组、加入群组、群聊通信。

    设计前提:

    • 一个用户可以属于多个群组。
    • 一个群组可以包含多个用户。
    • 群组内成员可能有不同的角色(如管理员)

    表设计

    allgroup 表:存储群组信息(id、name、desc(群描述))。

    字段:

    • id: 群组主键,自增。
    • groupname: 群名(唯一)。
    • groupdesc: 群描述。

    groupuser 表:记录用户和群组的关系,包含 groupid、userid 和 grouprole(成员身份)。

    字段:

    • groupid: 所属群组ID。
    • userid: 成员用户ID。
    • grouprole: 在群内的角色(如 creator、normal)。

    主键为联合主键(groupid, userid)防止重复加群。

    多表查询:

    为了提高效率,尽量在单次数据库查询中完成所有相关数据的获取,而不是分多次查询。使用内连接(INNER JOIN)进行联合查询,获取用户所在群组的详细信息以及群组内成员的详细信息。

    所以使用一个 vector 存储 groupuser 表

    大型项目 会采用 数据库 连接池 提高效率

    避免“查group ID后,再查group info,再查group members”的多次循环查法

    Group 类

    • 表示一个群组,包含群信息及成员列表。
    • 成员变量:
      • id, groupname, groupdesc
      • vector<GroupUser> users:群内成员列表

    include/server/group.hpp

    // group的ORM类

    #ifndef GROUP_HPP
    #define GROUP_HPP

    #include <string>
    #include <vector>
    using namespace std;
    #include "groupuser.hpp"

    class Group
    {
    public:
    // 群组的构造函数
    Group(int id=-1, string name="", string desc="")
    {
    this->id = id;
    this->name = name;
    this->desc = desc;
    }

    void setId(int id)
    {
    this->id = id;
    }
    void setName(string name)
    {
    this->name = name;
    }
    void setDesc(string desc)
    {
    this->desc = desc;
    }
    int getId()
    {
    return this->id;
    }
    string getName()
    {
    return this->name;
    }
    string getDesc()
    {
    return this->desc;
    }
    // 群组的成员列表
    vector<GroupUser> &getUsers()
    {
    return this->users;
    }

    private:
    int id; // 群组id
    string name; // 群组名称
    string desc; // 群组描述
    vector<GroupUser> users; // 群组成员id列表

    };

    #endif

    GroupUser 类

    • 继承自 User 类,增加 grouprole 字段。
    • 表示“某个群内”的一个用户。
    • 方便在群成员列表中体现其角色信息。

    群成员不仅要有用户信息,还需知道其在群内身份。

    继承 + 扩展字段是一种清晰可维护的做法。

    include/server/groupuser.hpp

    #ifndef GROUPUSER_H
    #define GROUPUSER_H

    #include <string>
    using namespace std;
    #include "user.hpp"

    // 群组用户, 多了一个角色属性, 从User类继承
    class GroupUser: public User
    {
    public:
    void setRole(string role)
    {
    this->role = role;
    }
    string getRole()
    {
    return this->role;
    }

    private:
    string role; // 群组角色

    };

    #endif

    GroupModel(数据访问层)

    1. 创建群组 createGroup

    • 插入 allgroup 表。
    • 获取生成的自增ID,填回 Group 对象。
    • 默认将创建者添加到 groupuser 表,角色为 creator。

    2. 加入群组 addGroup

    • 插入 groupuser 表,角色为 normal。
    • 若联合主键存在,避免重复插入。

    3. 查询用户所有群组及成员信息 queryGroups

    • 第一步: 查询用户所在群的基本信息(联合查询 groupuser + allgroup)。
    • 第二步: 对每个群,再查询其所有成员(联合查询 groupuser + user)。
    • 构建完整的 vector<Group>,其中每个 Group 包含 vector<GroupUser> 成员。

    查询优化思路:

    • 利用 多表联合查询 一次性获取结构化数据,减少数据库连接次数。
    • 避免“查group ID后,再查group info,再查group members”的多次循环查法。

    4. 查询某个群内除自己外的所有成员 ID(用于群聊转发)

    • 用于群聊时,找出接收方用户ID。
    • SQL:select userid from groupuser where groupid = ? and userid != ?

    include/server/groupmodel.hpp

    #ifndef GROUPMODEL_HPP
    #define GROUPMODEL_HPP

    #include "group.hpp"

    class GroupModel
    {
    public:
    bool createGroup(Group &group); // 创建群组

    // 加入群组
    bool addGroup(int groupid, int userid, string role);

    // 查询用户所在群组
    vector<Group> queryGroups(int userid);

    // 根据指定群组id查询群组用户id列表, 除了自己, 主要用户群聊业务
    vector<int> queryGroupUsers(int userid, int groupid);
    };
    #endif

    src/server/groupmodel.cpp

    查询用户所在id 的 函数, 可以优化为 只进行一次 mysql查询, 使用三表联合查询

    #include "groupmodel.hpp"
    #include <db.h>

    bool GroupModel::createGroup(Group &group) // 创建群组
    {
    // sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into allgroup(groupname, groupdesc) values('%s', '%s')",
    group.getName().c_str(), group.getDesc().c_str());

    // 连接数据库
    MySQL mysql;
    if (mysql.connect())
    {
    if (mysql.update(sql))
    {
    // 获取插入的id
    group.setId(mysql_insert_id(mysql.getConnection()));
    return true;
    }
    }
    return false;
    }

    // 加入群组
    bool GroupModel::addGroup(int groupid, int userid, string role)
    {
    // sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into groupuser(groupid, userid, grouprole) values(%d, %d, '%s')",
    groupid, userid, role.c_str());

    // 连接数据库
    MySQL mysql;
    if (mysql.connect())
    {
    if (mysql.update(sql))
    {
    return true;
    }
    }
    return false;
    }

    // 查询用户所在群组—联合查询, 直接取出群组的 全部信息
    // 根据用户id查询群组id, 再根据群组id查询群组信息
    vector<Group> GroupModel::queryGroups(int userid)
    {
    // 1.先查询用户所在的所有群组的 群组信息
    // sql语句
    char sql[1024] = {0};
    sprintf(sql, "select a.id, a.groupname, a.groupdesc from allgroup a inner join groupuser b on a.id = b.groupid where b.userid = %d", userid);

    // 连接数据库
    MySQL mysql;
    vector<Group> groupVec; // 存储群组信息以及群组用户信息
    if (mysql.connect())
    {
    MYSQL_RES *res = mysql.query(sql);
    if (res != nullptr)
    {

    while (MYSQL_ROW row = mysql_fetch_row(res))
    {
    Group group;
    group.setId(atoi(row[0]));
    group.setName(row[1]);
    group.setDesc(row[2]);
    groupVec.push_back(group);
    }
    mysql_free_result(res);
    }
    // 2.查询每个群组的其他用户信息—群组用户id列表
    for (auto &group : groupVec) // 注意这里是引用, 不能用auto group : groupVec
    {
    sprintf(sql, "select a.id,a.name, a.state,b.grouprole from user a inner join groupuser b on a.id = b.userid where b.groupid = %d", group.getId());
    MYSQL_RES *res = mysql.query(sql);
    if (res != nullptr)
    {
    while (MYSQL_ROW row = mysql_fetch_row(res))
    {
    GroupUser user;
    user.setId(atoi(row[0]));
    user.setName(row[1]);
    user.setState(row[2]);
    user.setRole(row[3]); // 群组角色
    // 将用户添加到群组对象中
    group.getUsers().push_back(user);
    }
    mysql_free_result(res);
    }
    }

    return groupVec;
    }

    return vector<Group>();
    }

    // 根据指定群组id查询群组用户id列表, 除了自己
    // 群聊转发业务!!!, 通过群组id查询群组用户id列表
    vector<int> GroupModel::queryGroupUsers(int userid, int groupid)
    {
    // sql语句
    char sql[1024] = {0};
    // 经过上面的查询用户所在群组的函数, 每个群组的用户id都已经存储在了数据库中
    sprintf(sql, "select userid from groupuser where groupid = %d and userid != %d", groupid, userid);

    // 连接数据库
    MySQL mysql;
    vector<int> userVec; // 存储群组用户id列表
    if (mysql.connect())
    {
    MYSQL_RES *res = mysql.query(sql);
    if (res != nullptr)
    {
    while (MYSQL_ROW row = mysql_fetch_row(res))
    {
    userVec.push_back(atoi(row[0]));
    }
    mysql_free_result(res);
    return userVec;
    }
    }
    return vector<int>();
    }

    添加群聊业务

    include/public.hpp

    CREATE_GROUP_MSG, // 创建群组
    ADD_GROUP_MSG, // 添加群组
    GROUP_CHAT_MSG, // 群聊

    include/server/chatservice.hpp

    // 处理群组业务
    GroupModel _groupModel;

    // 处理创建群组业务
    void createGroup(const TcpConnectionPtr &conn, json &js, Timestamp time);

    // 处理添加群组业务
    void addGroup(const TcpConnectionPtr &conn, json &js, Timestamp time);

    // 处理群组聊天业务
    void groupChat(const TcpConnectionPtr &conn, json &js, Timestamp time);

    src/server/chatservice.cpp

    // 处理创建群组业务
    void ChatService::createGroup(const TcpConnectionPtr &conn, json &js, Timestamp time)
    {
    int uerid = js["id"].get<int>(); // 这是 哪个用户要创建群组, 不是群组id
    string groupname = js["groupname"];
    string groupdesc = js["groupdesc"];

    // 存储新创建的群组信息—–此时还未添加到 数据库, 群id还未知
    Group group(-1, groupname, groupdesc);

    if(_groupModel.createGroup(group))
    {
    // 创建群后, 存储群组创建人 信息
    _groupModel.addGroup(group.getId(), uerid, "creator");
    // 服务器响应 可以自行添加
    }

    }

    // 处理添加群组业务
    void ChatService::addGroup(const TcpConnectionPtr &conn, json &js, Timestamp time)
    {
    int userid = js["id"].get<int>();
    int groupid = js["groupid"].get<int>();

    // 添加群组
    _groupModel.addGroup(groupid, userid, "normal");
    }

    // 处理群组聊天业务
    void ChatService::groupChat(const TcpConnectionPtr &conn, json &js, Timestamp time)
    {
    int userid = js["id"].get<int>();
    int groupid = js["groupid"].get<int>();
    vector<int> userVec = _groupModel.queryGroupUsers(userid, groupid); // 群组用户id列表

    // 群组聊天, 需要将消息转发给群组中的所有用户
    lock_guard<mutex> lock(_connMutex);
    for(int id : userVec)
    {
    // 用户在线, 就直接转发
    auto it = _userConnMap.find(id);
    if(it != _userConnMap.end())
    {
    // 在线, 转发消息
    it->second->send(js.dump());
    }
    else
    {
    // 不在线, 存储离线消息
    _offlineMsg.insert(id, js.dump());
    }
    }

    }

    群组阶段面试问题

    1. 项目介绍怎么讲
    • 先说整体架构(客户端 + 服务端 + 数据库)。
    • 强调使用了多线程、网络库(如 muduo)、多表联合查询、离线消息处理等。
    • 群聊业务涵盖:建群、加群、群聊消息转发,突出线程安全处理、connection map、model 层封装等设计。
    2. 面试官常问点
    • 你这个项目数据库有哪些表?
      • 答:user、friend、group、group_user、offline_message 等。
    • 数据量多少?
      • 别说 100 万、500 万级别,会被追问“你怎么优化?表怎么拆?”。
      • 建议说“万级”,比如 1~2 万行,合理且真实。
    3. 容易翻车的地方
    • 别一张嘴就说“我表里 100 万数据”,会被问爆:
      • 表的索引怎么设计?
      • 是否做了 水平/垂直拆表?
      • 有没有用 分库分表工具(如 ShardingSphere)?
    • 数据量吹太大,面试官会质疑你是否真的做过项目。
    4. 加分点
    • 主动提到:
      • 使用了线程池+IO线程模型。
      • 使用 STL map 做连接管理,但注意了线程不安全问题,加锁处理。
      • 将代码结构分层(model 层抽象数据逻辑,service 层处理业务,server 层处理网络通信)。
    5. 别忘了这些细节
    • CMake 项目结构是否清晰,有没有考虑 include 路径、模块拆分。
    • 聊群组业务时要提到 联合查询(从 group_user 表查成员,转发消息)。
    • 如何做离线消息存储(存到 offline_message 表)。

    对于c++, 业务不重要!!!

    业务是很灵活的,

    为什么 C++ 更适合做底层,不是业务逻辑?

  • C++ 更适合系统开发:擅长高性能、高并发、底层控制(如网络通信、内存管理、线程控制)。
  • 业务逻辑适合用其他语言:像 Java、Go 这样的语言,开发效率更高,适合快速实现业务需求。
  • C++ 做核心模块:像集群聊天中的长连接、消息转发引擎等,由 C++ 提供高性能支持。
  • Go / Java 处理业务:这些语言适合做用户管理、消息记录等业务层面工作。
  • 核心关注点:网络通信、协议设计、多线程、IO 模型、内存管理;
  • 业务逻辑在 C++ 项目里只是“壳子”,重点是“底层系统能力”;
  • 聊天系统这种项目里,真正难的是搞定高效、稳定、可扩展的通信框架,不是谁跟谁发了个消息。
  • 至此, 服务器业务代码完毕

    转移以下代码文件, 把数据层 头文件 的 代码, 放到model文件夹里—–分开数据层与业务层代码

    这时就要改cmake, 头文件搜索路径——对应的cpp 同理, 这样完成并修改 cmake

    自行修改—-不会修改的 等于白看了

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 施磊老师基于muduo网络库的集群聊天服务器(五)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!