施磊老师基于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
// 维护好友信息的操作接口方法
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 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
:群内成员列表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, 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 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);
}
// 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();
}
// 根据指定群组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++ 更适合做底层,不是业务逻辑?
- C++ 更适合系统开发:擅长高性能、高并发、底层控制(如网络通信、内存管理、线程控制)。
- 业务逻辑适合用其他语言:像 Java、Go 这样的语言,开发效率更高,适合快速实现业务需求。
- C++ 做核心模块:像集群聊天中的长连接、消息转发引擎等,由 C++ 提供高性能支持。
- Go / Java 处理业务:这些语言适合做用户管理、消息记录等业务层面工作。
- 核心关注点:网络通信、协议设计、多线程、IO 模型、内存管理;
- 业务逻辑在 C++ 项目里只是“壳子”,重点是“底层系统能力”;
- 聊天系统这种项目里,真正难的是搞定高效、稳定、可扩展的通信框架,不是谁跟谁发了个消息。
至此, 服务器业务代码完毕
转移以下代码文件, 把数据层 头文件 的 代码, 放到model文件夹里-----分开数据层与业务层代码
这时就要改cmake, 头文件搜索路径------对应的cpp 同理, 这样完成并修改 cmake
自行修改----不会修改的 等于白看了