基于 控制台的 好友显示
friend
表。本项目并没有非常的严格的, 必须是好友才能聊天, 只需要知道用户 id 和 name, 就能聊天----有能力可以进行改进
friend
表只包含两个字段:user_id
和 friend_id
。通过联合主键确保同一好友关系不会重复。
用户可以直接添加好友,不需要对方同意。添加好友时,user_id
和friend_id
会被插入到数据库中。查询好友时,通过数据库的联合查询返回好友的详细信息,包括ID、名字和在线状态(不返回密码)。
C和C++并不像Java或PHP那样内置很多方便的框架和工具来快速处理复杂的业务逻辑。C/C++更偏向底层操作,开发人员需要自己管理更多的细节(如数据库连接、查询等),这会影响功能的扩展。
考虑到客户端登录后好友列表一般不会变化,服务器可以在用户登录时返回好友列表,并将该列表保存在客户端,避免每次登录都从服务器获取。—降低服务器压力
如果有修改, 在下次上线 进行修改
通过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
表。Group
对象。groupuser
表,角色为 creator
。2. 加入群组 addGroup
groupuser
表,角色为 normal
。3. 查询用户所有群组及成员信息 queryGroups
groupuser
+ allgroup
)。groupuser
+ user
)。vector
,其中每个 Group
包含 vector
成员。查询优化思路:
4. 查询某个群内除自己外的所有成员 ID(用于群聊转发)
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());
}
}
}
业务是很灵活的,
转移以下代码文件, 把数据层 头文件 的 代码, 放到model文件夹里-----分开数据层与业务层代码
这时就要改cmake, 头文件搜索路径------对应的cpp 同理, 这样完成并修改 cmake
自行修改----不会修改的 等于白看了