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

文章目录

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

添加好友业务

实现的功能-简单实现

基于 控制台的 好友显示

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

功能不完善

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

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

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

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

用户可以直接添加好友,不需要对方同意。添加好友时,user_idfriend_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 

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

    // 返回好友列表 要显示好友的信息
    // 两个表的 联合查询
    vector 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 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 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();  //比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 uservec = _friendmodel.query(id);
if(!uservec.empty())
{
    // response["friends"] = uservec; // 这是不行的, 因为是自定义类型
    // map也不行, 因为map的value 不确定
    vector 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 friendid = js["friendid"].get();
    // 添加好友   显示好友信息 在登陆成功那里
    _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 IDuser ID,确保每个用户在同一群组中只有一条记录。

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

功能目标:

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

设计前提:

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

表设计

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

字段:

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

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

字段:

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

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

多表查询:

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

所以使用一个 vector 存储 groupuser

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

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

Group

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

include/server/group.hpp

// group的ORM类

#ifndef GROUP_HPP
#define GROUP_HPP

#include 
#include 
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 &getUsers()
    {
        return this->users;
    }


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

};



#endif

GroupUser

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

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

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

include/server/groupuser.hpp

#ifndef GROUPUSER_H
#define GROUPUSER_H

#include 
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 包含 vector 成员。

查询优化思路:

  • 利用 多表联合查询 一次性获取结构化数据,减少数据库连接次数。
  • 避免“查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 queryGroups(int userid);

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

src/server/groupmodel.cpp

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

#include "groupmodel.hpp"
#include 


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, role) 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 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 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);
            return groupVec;
        }
    }

    // 2.查询每个群组的其他用户信息---群组用户id列表
    for(auto &group : groupVec)  // 注意这里是引用, 不能用auto group : groupVec
    {
        sprintf(sql, "select a.id,a.name, a.state,b.role 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 vector();

}

// 根据指定群组id查询群组用户id列表, 除了自己
// 群聊转发业务!!!, 通过群组id查询群组用户id列表
vector 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 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();
}

添加群聊业务

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();  // 这是 哪个用户要创建群组,  不是群组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 groupid = js["groupid"].get();

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

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

    // 群组聊天, 需要将消息转发给群组中的所有用户
    lock_guard 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++ 更适合做底层,不是业务逻辑?

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

至此, 服务器业务代码完毕

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

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

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

你可能感兴趣的:(施磊老师集群聊天,网络,服务器,php)