CPP集群聊天服务器开发实践(三):群组聊天业务
目录
1 总体架构设计
2 数据层
3 业务层
在之前业务的基础上继续进行完善,主要针对群组相关的业务增加了创建群组、加入群组、以及发送群聊消息的功能。
开始之前,需要在数据库中创建两个表:allgroup 和 groupuser。allgroup主要用于保存所有群组信息,包括群组id、群组名称、群组描述;groupuser主要用于保存用户与群组的关系以及用户的角色,包括用户id、群组id、用户角色。
mysql> desc allgroup;
+-----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| groupname | varchar(50) | NO | UNI | NULL | |
| groupdesc | varchar(200) | YES | | | |
+-----------+--------------+------+-----+---------+----------------+
mysql> desc groupuser;
+-----------+--------------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------------------+------+-----+---------+-------+
| groupid | int | NO | PRI | NULL | |
| userid | int | NO | PRI | NULL | |
| grouprole | enum('creator','normal') | YES | | normal | |
+-----------+--------------------------+------+-----+---------+-------+
1 总体架构设计
沿用之前的设计思路,将数据层和业务层进行区分开:
数据层主要负责对象的封装以及数据库的交互,包括群组对象的封装、群组成员的封装、创建/加入/查询群组;
业务层则负责与客户端的交互,包括创建群组业务、加入群组业务、群组聊天业务。
2 数据层
- 对象的封装
主要封装群组对象{群组id 群组名称 群组描述 组内成员}以及相关接口、组内成员对象{成员信息 角色}。
群组对象的封装在group.hpp中实现:
#ifndef GROUP_H
#define GROUP_H
#include <string>
#include <vector>
#include "groupuser.hpp"
using namespace std;
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:
//群组id
int id;
//群组名称
string name;
//群组描述
string desc;
//获取组内成员
vector<GroupUser> users;
};
#endif
组内成员的封装继承user类,私有属性只包含一个role,注意要把user类中的属性设置为protected:
#ifndef GROUPUSER_H
#define GROUPUSER_H
//群组用户,相比user的信息多了一个role(角色)信息,直接继承user,复用user的其他信息
#include "user.hpp"
class GroupUser : public User
{
public:
void setRole(string role){
this->role = role;
}
string getRole(){
return this->role;
}
private:
//角色
string role;
};
#endif
- 数据库操作
数据层面的操作包含:
(1)创建群组:数据库插入
(2)用户加入群组: 数据库插入
(3)查询用户所在群组消息{群组id 群组名称 群组描述 群内成员信息}:表的联合查询,根据userid在groupuser表中查询到所有的groupid,再根据groupid在allgroup表中查询到群组的名称、描述等信息,根据groupid在groupuser表中查询到所有的userid,再根据userid在user表中查询到用户的名称、状态等信息
(4)查询群组中所有用户的信息:数据表查询,根据groupid在groupuser表中查询userid
创建群组的源代码如下:
//创建群组,引用传参
bool GroupModel::createGroup(Group &group)
{
//1.组装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;
}
加入群组的源代码如下:
//加入群组
void GroupModel::addGroup(int userid, int groupid, string role)
{
//1.组装sql语句
char sql[1024] = {0};
sprintf(sql, "insert into groupuser values(%d, %d, '%s')", groupid, userid, role.c_str());
MySQL mysql;
if(mysql.connect())
{
mysql.update(sql);
}
}
查询用户所在群组消息如下:
//查询用户所在群组信息
vector<Group> GroupModel::queryGroups(int userid)
{
//返回用户所在的组id 组名称 组描述 组内成员信息
//先根据userid在groupuser表中查询该用户所属的群组信息,再更具群组信息查询属于该群组的所有用户信息
//需要对allgroup表和groupuser表进行联合查询,利用userid在groupuser表中查询groupid,再利用groupid在allgroup表中查询group信息
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);
vector<Group> groupVec;
MySQL mysql;
if(mysql.connect())
{
MYSQL_RES *res = mysql.query(sql);
if(res != nullptr)
{
MYSQL_ROW row;
//mysql_fetch_row函数会一行一行的获取查询结果
while((row = mysql_fetch_row(res)) != nullptr)
{
Group group;
group.setId(atoi(row[0]));
group.setName(row[1]);
group.setDesc(row[2]);
groupVec.push_back(group);
}
mysql_free_result(res);
}
}
//查询群组的用户信息,利用groupid在groupuser表中查询userid,再利用userid在user表中查询用户信息
for(Group &group : groupVec)
{
sprintf(sql, "select a.id, a.username, a.state,b.grouprole from user a inner join groupuser b on b.userid = a.id where b.groupid = %d", group.getId());
MYSQL_RES *res = mysql.query(sql);
if(res != nullptr)
{
MYSQL_ROW row;
while((row = mysql_fetch_row(res)) != nullptr)
{
GroupUser user;
user.setId(atoi(row[0]));
user.setName(row[1]);
user.setState(row[2]);
group.getUsers().push_back(user);
}
mysql_free_result(res);
}
}
return groupVec;
}
查询群组中所有用户的信息源代码如下:
//根据指定的groupid查询群组用户id列表,除userid外,主要用于群聊业务给群组其他成员群发消息
vector<int> GroupModel::queryGroupUsers(int userid, int groupid)
{
char sql[1024] = {0};
sprintf(sql, "select userid from groupuser where groupid = %d and userid != %d", groupid, userid);
vector<int> idVec;
MySQL mysql;
if(mysql.connect())
{
MYSQL_RES *res = mysql.query(sql);
if(res != nullptr)
{
MYSQL_ROW row;
while((row = mysql_fetch_row(res)) != nullptr)
{
idVec.push_back(atoi(row[0]));
}
mysql_free_result(res);
}
}
return idVec;
}
3 业务层
经典套路之在chatservice构造函数中进行回调操作的注册,在chatservice.cpp中定义具体的操作,当接受到相应的msgid后会调取相应的操作。
业务层主要包含三类业务:创建群组、加入群组、群聊。
(1)在创建群组中需要将创建人的信息加入到groupuser表中
(2)加入群组则将用户角色设置为"nomal"即可
(3)群聊业务主要是先获取群组中其他用户的id,通过查询_userConnMap查看是否在线,在线则利用服务器转发消息,不在线则存储离线消息到数据表
源代码如下:
//创建群组业务
void ChatService::createGroup(const TcpConnectionPtr &conn,json &js,Timestamp time)
{
int userid = js["id"].get<int>();
string name = js["groupname"];
string desc = js["groupdesc"];
//存储新创建的群组信息
Group group(-1,name,desc);
if(_groupModel.createGroup(group)){
//存储群组创建人信息
_groupModel.addGroup(userid,group.getId(),"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(userid,groupid,"normal");
}
//群组聊天业务
void ChatService::groupChat(const TcpConnectionPtr &conn,json &js,Timestamp time)
{
int userid = js["id"].get<int>();
int groupid = js["groupid"].get<int>();
//获取群组中所有其他用户id
vector<int> useridVec = _groupModel.queryGroupUsers(userid,groupid);
//加锁,防止_userConnMap中的用户在发送消息时候上线或者下线,C++中map的操作本身是无法保证线程安全的
lock_guard<mutex> lock(_connMutex);
for(int id:useridVec){
auto it = _userConnMap.find(id);
//用户在线
if(it!=_userConnMap.end()){
it->second->send(js.dump());
}
else{
//存储离线消息
_offlineMsgModel.insert(id,js.dump());
}
}
}
由于群组的业务流程相对复杂一些,因此测试将在客户端开发之后进行功能测试。。。