文章目录
- 添加好友业务
-
- 实现的功能-简单实现
- 功能不完善
- 表设计-每个表对应一个单独的处理文件
- 业务逻辑:-显然不好, 可以改进
- 为什么功能少
- 优化
- SQL联合查询语句
- 代码结构
- 测试
- 问题
- 群组业务
-
- 主要功能
- 表设计
- 多表查询:
- `Group` 类
- `GroupUser` 类
- `GroupModel`(数据访问层)
- 添加群聊业务
- 群组阶段面试问题
-
-
-
- 1. **项目介绍怎么讲**
- 2. **面试官常问点**
- 3. **容易翻车的地方**
- 4. **加分点**
- 5. **别忘了这些细节**
-
-
- 对于c++, 业务不重要!!!
-
-
- **为什么 C++ 更适合做底层,不是业务逻辑?**
-
- 至此, 服务器业务代码完毕
添加好友业务
实现的功能-简单实现
基于 控制台的 好友显示
功能不完善
本项目并没有非常的严格的, 必须是好友才能聊天, 只需要知道用户 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++ 更适合做底层,不是业务逻辑?
至此, 服务器业务代码完毕
转移以下代码文件, 把数据层 头文件 的 代码, 放到model文件夹里—–分开数据层与业务层代码
这时就要改cmake, 头文件搜索路径——对应的cpp 同理, 这样完成并修改 cmake
自行修改—-不会修改的 等于白看了
评论前必须登录!
注册