C++集群聊天服务器 数据模块+业务模块+CMake构建项目 笔记 (上)
跟着施磊老师做C++项目,施磊老师_腾讯课堂 (qq.com)
本文在此篇博客的基础上继续实现数据模块和业务模块代码:
C++集群聊天服务器 网络模块+业务模块+CMake构建项目 笔记 (上)-CSDN博客
https://blog.csdn.net/weixin_41987016/article/details/135991635?spm=1001.2014.3001.5501一、mysql 项目数据库和表的设计
myql 项目数据库和表的设计-CSDN博客
https://blog.csdn.net/weixin_41987016/article/details/135981407?spm=1001.2014.3001.5501服务器数据模块:将数据库数据与业务模块代码区分开来,符合ORM(对象关系映射)框架设计,业务层操作的都是数据层的对象,数据层封装数据库SQL相应的操作
二、mysql数据库代码封装
确保系统上存在mysql.h和libmysqlclient.so库,

- include/public.hpp
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
REG_MSG, // 注册消息
REG_MSG_ACK // 注册响应消息
};
#endif // PUBLIC_H
- include/server/db/db.h
#ifndef DB_H
#define DB_H
#include
#include
using namespace std;
// 数据库操作类
class Mysql {
public:
// 初始化数据库连接
Mysql();
// 释放数据库连接资源
~Mysql();
// 连接数据库
bool connect();
// 更新操作
bool update(string sql);
// 查询操作
MYSQL_RES *query(string sql);
// 获取连接
MYSQL *getConnection();
private:
MYSQL *m_conn;
};
#endif // DB_H
src/server/db/db.cpp
#include "db.h"
#include
// 数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";
// 初始化数据库连接
Mysql::Mysql() {
m_conn = mysql_init(nullptr);
// 这里相当于只是给它开辟了一块存储连接数据的资源空间
}
// 释放数据库连接资源
Mysql::~Mysql() {
if(m_conn != nullptr) {
mysql_close(m_conn);
}
// 析构的时候把这块资源空间用mysql_close掉
}
// 连接数据库
bool Mysql::connect() {
MYSQL *p = mysql_real_connect(m_conn,server.c_str(),user.c_str(),
password.c_str(),dbname.c_str(),3306,nullptr,0);
if(p!=nullptr) {
// C和C++代码默认的编码字符是ASCII,如果不设置,
// 从MYSQL上拉下来的中文显示?
mysql_query(m_conn, "set names gbk");
LOG_INFO << "connect mysql success!!!";
} else{
LOG_INFO << "connect mysql failed!!!";
}
return p;
}
// 更新操作
bool Mysql::update(string sql) {
if(mysql_query(m_conn, sql.c_str())) {
LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
<< sql <<"更新失败!";
return false;
}
return true;
}
// 查询操作
MYSQL_RES* Mysql::query(string sql) {
if(mysql_query(m_conn, sql.c_str())) {
LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
<< sql <<"查询失败!";
return nullptr;
}
return mysql_use_result(m_conn);
}
// 获取连接
MYSQL* Mysql::getConnection() {
return m_conn;
}
三、Model数据层代码框架设计
数据库操作与业务代码进行分离,业务代码处理的都为对象,数据库层操作具体SQL语句。故定义相应的类,每一个类对应数据库中一张表,将数据库读出来的字段提交给业务使用。
1.表与类的映射user.hpp,为创建的User表,专门实现一个映射类User,对外提供公有接口访问私有成员变量

- include/server/user.hpp
#ifndef USER_H
#define USER_H
#include
using namespace std;
// 匹配User表的ORM类
class User {
public:
User(int id=-1, string name="", string password="", string state="offline") {
m_id = id;
m_name = name;
m_password = password;
m_state = state;
}
void setId(int id) { m_id = id; }
void setName(string name) { m_name = name; }
void setPwd(string pwd) { m_password = pwd; }
void setState(string state) { m_state = state; }
int getId() const { return m_id; }
string getName() const { return m_name; }
string getPwd() const { return m_password; }
string getState() const { return m_state; }
private:
int m_id;
string m_name;
string m_password;
string m_state;
};
#endif // USER_H
- include/server/usermodel.hpp,针对User表数据的增删改查
#ifndef USERMODEL_H
#define USERMODEL_H
#include "user.hpp"
// User表的数据操作类
class UserModel {
public:
// user表的增加方法
bool insert(User& user);
// 根据用户号码查询用户信息
User query(int id);
// 更新用户的状态信息
bool updateState(User user);
};
#endif // USERMODEL_H
- src/server/usermodel.cpp 相应方法的实现
#include "usermodel.hpp"
#include "db.h"
#include
// User表的增加方法
bool UserModel::insert(User &user) {
// 1.组装sql语句
char sql[1024] = {0};
std::sprintf(sql,"insert into user(name,password,state) values('%s','%s', '%s')",
user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
if(mysql.update(sql)) {
// 获取插入成功的用户数据生成的主键id
user.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
}
return false;
}
// 根据用户号码查询用户信息
User UserModel::query(int id) {
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql,"select * from user where id = %d", id);
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
MYSQL_RES* res = mysql.query(sql);
if(res != nullptr) {
MYSQL_ROW row = mysql_fetch_row(res);
if(row != nullptr) {
User user;
user.setId(atoi(row[0]));
user.setName(row[1]);
user.setPwd(row[2]);
user.setState(row[3]);
// 释放资源
mysql_free_result(res);
return user;
}
}
}
return User();
}
// 更新用户的状态信息
bool UserModel::updateState(User user) {
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql,"update user set state = '%s' where id = %d",
user.getState().c_str(), user.getId());
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
if(mysql.update(sql)) {
return true;
}
}
return false;
}
服务器业务模块ChatService
服务器业务模块: 客户端发送的业务数据,先到达服务器端网络模块,网络模块进行事件分发到业务模块相应的业务处理器,最终通过数据层访问底层数据模块。
>>用户注册业务
用户注册: 服务器将客户端收到的json反序列化后存储至数据库中,依据是否注册成功给客户端返回响应消息。
我们业务层与数据层分离,需要操作数据层数据对象即可,因此需要在ChatService类中实例化一个数据操作类对象进行业务开发。
UserModel m_userModel; // 存储在线用户的通信连接map表
需要在消息类型EnMsgType中增加一个注册响应消息,给客户端返回是否注册成功标识:
enum EnMsgType{
LOGIN_MSG = 1, //登录消息
REG_MSG, //注册消息
REG_MSG_ACK //注册响应消息
};
服务器注册业务流程:
(1)网络模块将json数据反序列化后上报到注册业务中,因为User表中id字段为自增的,state字段为默认的,因此注册业务只需要获取name与password字段即可。
(2)实例化User表对应的对象user,将获取到的name与password设置进去,再向UserModel数据操作类对象进行新用户user的注册。
(3)注册完成后,服务器返回相应json数据给客户端:若注册成功,返回注册响应消息REG_MSG_ACK、错误标识errno(0:成功,1:失败)、用户id等组装好的json数据;若注册失败,返回注册响应消息REG_MSG_ACK、错误标识。
- ChatService.cpp
// 处理注册业务 user表:name password
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time) {
// 1.获取name,password字段
string name = js["name"];
string pwd = js["password"];
// 处理业务,操作的都是数据对象
// 2.创建User对象,进行注册
User user;
user.setName(name);
user.setPwd(pwd);
// 新用户的插入
bool state = m_userModel.insert(user);
if(state) { // 注册成功
json response;
response["msgid"] = REG_MSG_ACK; // 注册响应消息
response["errno"] = 0; // 错误标识 0:成功 1:失败
response["id"] = user.getId();
conn->send(response.dump());
}
else { // 注册失败
json response;
response["msgid"] = REG_MSG_ACK;
response["errno"] = 1;
conn->send(response.dump());
}
}
服务器登录业务流程:
1、服务器获取输入用户id、密码字段。
2、查询id对应的数据,判断用户id与密码是否正确,分为以下三种情况返回相应json数据给客户端:
①若用户名、密码正确且未重复登录,及时更新登录状态为在线,返回登录响应消息LOGIN_MSG_ACK、错误标识errno(0:成功,1:失败,2:重复登录)、用户id、用户名等信息;
②若用户名、密码正确但重复登录,返回登录响应消息、错误标识、错误提示信息;
③若用户不存在或密码错误,返回登录响应消息、错误标识、错误提示信息;
- ChatService.cpp
// 处理登录业务 user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int id = js["id"].get();
string pwd = js["password"];
User user = m_userModel.query(id);
if(user.getId() == id && user.getPwd() == pwd) {
if(user.getState() == "online") {
//该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "该账号已经登录,请重新输入新账号";
conn->send(response.dump());
}
else{
// 登录成功,更新用户状态信息 state: offline => online
user.setState("online");
m_userModel.updateState(user);
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
conn->send(response.dump());
}
}
else {
// 该用户不存在/用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
conn->send(response.dump());
}
}
- ChatService.cpp
ChatService::ChatService() {
m_msgHandlerMap.insert({LOGIN_MSG,std::bind(&ChatService::login, this, _1, _2, _3)});
m_msgHandlerMap.insert({REG_MSG,std::bind(&ChatService::reg, this, _1, _2, _3)});
}
四、CMake 构建项目
- src/server/CMakeLists.txt
# 定义了一个SRC_LIST变量 包含了该目录下所有的源文件
aux_source_directory(. SRC_LIST)
aux_source_directory(./db DB_LIST)
# 指定生成可执行文件
add_executable(ChatServer ${SRC_LIST} ${DB_LIST})
# 指定可执行文件链接时需要依赖的库文件
target_link_libraries(ChatServer muduo_net muduo_base mysqlclient pthread)
- src/CMakeLists.txt
add_subdirectory(server)
- 和src,include,thirdparty同级目录的CMakeLists.txt
cmake_minimum_required(VERSION 3.28.0)
project(chat)
# 配置编译选项
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -g)
# 配置可执行文件生成路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
# 配置头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/include/server)
include_directories(${PROJECT_SOURCE_DIR}/include/server/db)
include_directories(${PROJECT_SOURCE_DIR}/thirdparty)
# 加载子目录
add_subdirectory(src)


cmake -B build cmake --build build
1.测试注册:
telnet 127.0.0.1 6000
{"msgid":3,"name":"heheda","password":"1024"} // 注册

{"msgid":3,"name":"Tom","password":"520"} // 注册

{"msgid":3,"name":"Jerry","password":"1314"} // 注册


2.测试登录:
(1)未登录

(2) 已经登录
telnet 127.0.0.1 6000
{"msgid":1,"id":4,"password":"1024"}

telnet 127.0.0.1 6000
{"msgid":1,"id":4,"password":"1024"}

(3)登录失败

3.gdb排错练习
比如输入以下这句,其实”id”:5才对,但是如果误输入的会引起核心中断,如何排查错误呢?
{"msgid":1,"id":"5","password":"520"}

>>gdb调试,比如我们怀疑可能是chatservice.cpp的20行出错了

heheda@linux:~/Linux/Server$ gdb ./bin/ChatServer (gdb) break chatservice.cpp 20 (gdb) run
telnet 127.0.0.1 6000

输入:
{"msgid":1,"id":"5","password":"520"}

检查出错误了:
reason: [json.exception.type_error.302] type must be number, but is string
故我们把
{"msgid":1,"id":"5","password":"520"}
修改为以下:
{"msgid":1,"id":5,"password":"520"}
总结:客户端发送过来一个注册的业务,先从最开始的网络,再通过事件的分发,到业务层的相关的handler处理注册,接着访问底层的model。其中在业务类设计,这里看到的都是对象,方便你把底层的数据模块改成你想要的,例如mysql,sql,oracle,mongoDB等都行。实现了网络模块,业务模块以及数据模块的低耦合。
五、记录用户的连接信息以及线程安全问题
用户连接信息处理: 假设此时用户1向用户2发送消息(源id、目的id、消息内容),此时服务器收到用户1的数据了,要主动向用户2推送该条消息,那么如何知道用户2是那条连接呢。因此我们需要专门处理下,用户一旦登录成功,就会建立一条连接,我们便要将该条连接存储下来,方便后续消息收发的处理。
- 在ChatService.hpp文件中,private处添加
private:
// 存储在线用户的通信连接
unordered_map m_userConnMap;
// 定义互斥锁,保证m_userConnMap的线程安全
mutex m_connMutex;
- 修改ChatService.cpp中的login函数,在登录成功,记录用户连接信息,将id和conn数据信息插入m_userConnMap,使用lock_guard使得线程安全
// 处理登录业务 user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int id = js["id"].get();
string pwd = js["password"];
User user = m_userModel.query(id);
if(user.getId() == id && user.getPwd() == pwd) {
if(user.getState() == "online") {
//该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "该账号已经登录,请重新输入新账号";
conn->send(response.dump());
}
else{
// 登录成功,记录用户连接信息
{
lock_guard lock(m_connMutex);
m_userConnMap.insert({id, conn});
}
// 登录成功,更新用户状态信息 state: offline => online
user.setState("online");
m_userModel.updateState(user);
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
conn->send(response.dump());
}
}
else {
// 该用户不存在/用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
conn->send(response.dump());
}
}
六、客户端异常退出业务代码和测试
客户端异常退出处理: 假设用户客户端直接通过Ctrl + C中断,并没有给服务器发送合法的json过来,我们必须及时修改用户登录状态,否则后续再想登录时为”online”状态,便无法登录了。
客户端异常退出处理流程:
- 通过conn连接去_userConnMap表中查找,删除conn键值对记录;
- 将conn连接对应用户数据库的状态从”online”改为”offline”;
- ChatServer.cpp中onConncetion方法处理
// 上报链接相关信息的回调函数:参数为连接信息
void ChatServer::onConnection(const TcpConnectionPtr &conn) {
// 客户端断开连接,释放连接资源 muduo库会打印相应日志
if(!conn->connected()) {
ChatService::getInstance()->clientCloseException(conn);// 处理客户端异常关闭
conn->shutdown();// 释放socket fd资源
}
}
- 在ChatService.hpp中添加处理客户端异常退出的函数声明
public:
// 处理客户端异常退出
void clientCloseException(const TcpConnectionPtr& conn);
- 在ChatService.cpp中编写处理客户端异常退出的函数
// 处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr &conn) {
User user;
{
lock_guard lock(m_connMutex);
for(auto it = m_userConnMap.begin();it!=m_userConnMap.end();++it) {
if(it->second == conn) {
// 从map表删除用户的链接信息
user.setId(it->first);
m_userConnMap.erase(it);
break;
}
}
}
// 更新用户的状态信息
if(user.getId() != -1) {
user.setState("offline");
m_userModel.updateState(user);
}
}

表里原先有Tom登录用户的信息,然后我们登录了该账号,就从offline状态更新为online状态


按下ctrl+],切换到telnet>,输入quit,此时客户端异常退出,也就执行了从online更新为offline


七、离线消息业务代码实现和测试
离线消息业务: 当用户一旦登录成功,我们查询用户是否有离线消息要发送,若有则发送相应数据,发送完后删除本次存储的离线数据,防止数据重复发送。
- 如果用户登录成功的话,查询该用户是否有离线消息,desc offlinemessage

- offlinemessagemodel.hpp
#ifndef OFFLINEMESSAGEMODEL_H
#define OFFLINEMESSAGEMODEL_H
#include
#include
using namespace std;
// 提供离线消息表的操作接口方法
class OfflineMsgModel {
public:
// 存储用户的离线消息
void insert(int userid, string msg);
// 删除用户的离线消息
void remove(int userid);
// 查询用户的离线消息
vector query(int userid);
};
#endif // OFFLINEMESSAGEMODEL_H
- offlinemessagemodel.cpp
在进行点对点聊天业务处理前,我们需要提前处理好以下几点:
1、建立与离线消息表的映射OfflineMsgModel类:我们数据库中有创建的OfflineMessage离线消息表,因为我们数据层与业务层要分离开来,所以这里与前面一样提供离线消息表的数据操作类,提供给业务层对应的操作接口。
#include "offlinemessagemodel.hpp"
#include "db.h"
// 存储用户的离线消息
void OfflineMsgModel::insert(int userid, string msg) {
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql, "insert into offlinemessage values(%d, '%s')", userid, msg.c_str());
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
mysql.update(sql);
}
}
// 删除用户的离线消息
void OfflineMsgModel::remove(int userid) {
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql, "delete from offlinemessage where userid = %d", userid);
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
mysql.update(sql);
}
}
// 查询用户的离线消息
vector OfflineMsgModel::query(int userid) {
// 1.组装sql语句
char sql[1024] = {0};
sprintf(sql, "select message from offlinemessage where userid = %d", userid);
// 2.执行sql语句
Mysql mysql;
vector vec;
if(mysql.connect()) {
MYSQL_RES *res = mysql.query(sql);
if(res != nullptr) {
// 把userid用户的所有离线消息放入vec中返回
MYSQL_ROW row;
while((row = mysql_fetch_row(res)) != nullptr) {
vec.push_back(row[0]);
}
mysql_free_result(res);
return vec;
}
}
return vec;
}
- 在chatservice.hpp中添加
2、业务层chatservice.hpp中增加离线消息表的数据操作类对象,方面我们后续对数据库进行操作。
#include "offlinemessagemodel.hpp"
// 聊天服务器业务类
class ChatService {
private:
OfflineMsgModel m_offlineMsgModel;
}
- chatservice.cpp
服务器离线消息业务流程:
1、无论是一对一聊天、还是群聊,若接收方用户不在线,则将发送方消息先存储至离线消息表里。
2、一旦接收方用户登录成功,检查该用户是否有离线消息(可能有多条),若有则服务器将离线消息发送给接收方用户。
3、服务器发送完成后删除本次存储的离线消息,保证接收方不会每次登录都收到重复的离线消息。
// 处理登录业务 user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int id = js["id"].get();
string pwd = js["password"];
User user = m_userModel.query(id);
if(user.getId() == id && user.getPwd() == pwd) {
if(user.getState() == "online") {
//该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "该账号已经登录,请重新输入新账号";
conn->send(response.dump());
}
else{
// 登录成功,记录用户连接信息
{
lock_guard lock(m_connMutex);
m_userConnMap.insert({id, conn});
}
// 登录成功,更新用户状态信息 state: offline => online
user.setState("online");
m_userModel.updateState(user);
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
// 查询该用户是否有离线消息
vector vec = m_offlineMsgModel.query(id);
if(!vec.empty()) {
response["offlinemsg"] = vec;
// 读取该用户的离线消息后,把该用户的所有离线消息删除掉
m_offlineMsgModel.remove(id);
}
conn->send(response.dump());
}
}
else {
// 该用户不存在/用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
conn->send(response.dump());
}
}
// 一对一聊天业务
void ChatService::oneChat(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int toid = js["toid"].get();
{
lock_guard lock(m_connMutex);
auto it = m_userConnMap.find(toid);
if(it != m_userConnMap.end()) {
// toid在线,转发消息 服务器主动推送消息给toid用户
it->second->send(js.dump());
return;
}
}
// toid不在线,存储离线消息
m_offlineMsgModel.insert(toid, js.dump());
}



八、服务器异常退出处理代码和测试
服务器异常退出处理: 假设用户服务器端直接通过Ctrl + C中断,并没有给客户端发送合法的json过去,我们必须及时修改所有用户登录状态为”offline”,否则后续再想登录时为”online”状态,便无法登录了。
服务器异常退出处理流程: 主动截获Ctrl + c信号(SGINIT),在信号处理函数中将数据库中用户状态重置为”offline”。
- main.cpp
#include "chatserver.hpp"
#include "chatservice.hpp"
#include
#include
using namespace std;
// 处理服务器ctrl+c结束后,重置user的状态信息
void resetHandler(int) {
ChatService::getInstance()->reset();
exit(0);
}
int main() {
signal(SIGINT,resetHandler);
...
}
- 在chatservice.hpp添加reset()方法声明,服务器异常,业务重置方法
// 服务器异常,业务重置方法 void reset();
- 在chatservice.cpp中编写reset()方法
// 服务器异常,业务重置方法
void ChatService::reset() {
// 把online状态的用户,设置成offline
m_userModel.resetState();
}
- 在usermodel.hpp中添加重置用户的状态信息resetState方法声明
// 重置用户的状态信息 void resetState();
- 在usermodel.cpp中编写resetState()方法
// 重置用户的状态信息
void UserModel::resetState() {
// 1.组装sql语句
char sql[1024] = "update user set state = 'offline' where state = 'online'";
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
mysql.update(sql);
}
}


- ctrl+c终止服务

九、添加好友业务代码和测试
服务器离线消息业务流程:
1、无论是一对一聊天、还是群聊,若接收方用户不在线,则将发送方消息先存储至离线消息表里。
2、一旦接收方用户登录成功,检查该用户是否有离线消息(可能有多条),若有则服务器将离线消息发送给接收方用户。
3、服务器发送完成后删除本次存储的离线消息,保证接收方不会每次登录都收到重复的离线消息。

- public.hpp
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
ADD_FRIEND_MSG, // 添加好友消息
};
#endif // PUBLIC_H
- friendmodel.hpp
#ifndef FRIENDMODEL_H
#define FRIENDMODEL_H
#include "user.hpp"
#include
using namespace std;
// 维护好友信息的操作接口方法
class FriendModel {
public:
// 添加好友关系
void insert(int userid, int friendid);
// 返回用户好友列表 friendid
vector query(int userid);
};
#endif // FRIENDMODEL_H
- 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 values (%d, %d)", userid, friendid);
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
mysql.update(sql);
}
}
// 返回用户好友列表 friendid
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);
vector vec;
Mysql mysql;
if(mysql.connect()) {
MYSQL_RES * res = mysql.query(sql);
if(res != nullptr) {
// 把userid用户的所有离线消息放入vec中返回
MYSQL_ROW row;
while((row = mysql_fetch_row(res)) != nullptr) {
User user;
user.setId(atoi(row[0])); // id
user.setName(row[1]); // name
user.setState(row[2]); // state
vec.push_back(user);
}
mysql_free_result(res); // 释放资源
return vec;
}
}
return vec;
}
// select a.id,a.name,a.state from user a inner join
// friend b on b.friendid = a.id
// where b.userid = %d
- chatservice.hpp
// 聊天服务器业务类
class ChatService {
public:
// 添加好友业务
void addFriend(const TcpConnectionPtr& conn,json& js,Timestamp time);
private:
FriendModel m_friendModel;
}
- chatservice.cpp
服务器添加好友业务流程:
1、服务器获取当前用户id、要添加好友的id;
2、业务层调用数据层接口往数据库中添加相应好友信息;用户登录成功时,查询该用户的好友信息并返回。
// 注册消息以及对应的Handler回调操作
ChatService::ChatService() {
m_msgHandlerMap.insert({LOGIN_MSG,std::bind(&ChatService::login, this, _1, _2, _3)});
m_msgHandlerMap.insert({REG_MSG,std::bind(&ChatService::reg, this, _1, _2, _3)});
m_msgHandlerMap.insert({ONE_CHAT_MSG,std::bind(&ChatService::oneChat, this, _1, _2, _3)});
m_msgHandlerMap.insert({ADD_FRIEND_MSG,std::bind(&ChatService::addFriend, this, _1, _2, _3)});
}
// 处理登录业务 user表:id password
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int id = js["id"].get();
string pwd = js["password"];
User user = m_userModel.query(id);
if(user.getId() == id && user.getPwd() == pwd) {
if(user.getState() == "online") {
//该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "该账号已经登录,请重新输入新账号";
conn->send(response.dump());
}
else{
// 登录成功,记录用户连接信息
{
lock_guard lock(m_connMutex);
m_userConnMap.insert({id, conn});
}
// 登录成功,更新用户状态信息 state: offline => online
user.setState("online");
m_userModel.updateState(user);
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
// 查询该用户是否有离线消息
vector vec = m_offlineMsgModel.query(id);
if(!vec.empty()) {
response["offlinemsg"] = vec;
// 读取该用户的离线消息后,把该用户的所有离线消息删除掉
m_offlineMsgModel.remove(id);
}
// 查询该用户的好友信息并返回
vectoruserVec = m_friendModel.query(id);
if(!userVec.empty()) {
vector vec2;
for(User &user : userVec) {
json js;
js["id"] = user.getId();
js["name"] = user.getName();
js["state"] = user.getState();
vec2.push_back(js.dump());
}
response["friends"] = vec2;
}
conn->send(response.dump());
}
}
else {
// 该用户不存在/用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
conn->send(response.dump());
}
}
// 添加好友业务 msgid id friendid
void ChatService::addFriend(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int userid = js["id"].get();
int friendid = js["friendid"].get();
// 存储好友信息
m_friendModel.insert(userid, friendid);
}



十、模拟QQ好友添加(呵呵哒改造)
- public.hpp
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
ADD_FRIEND_REQ_MSG, // 添加好友请求消息
ADD_FRIEND_MSG_ACK, // 添加好友响应消息
};
#endif // PUBLIC_H
- chatservice.hpp
// 聊天服务器业务类
class ChatService {
public:
// 添加好友业务请求
void addFriendRequest(const TcpConnectionPtr& conn,json& js,Timestamp time);
// 添加好友业务响应
void addFriendResponse(const TcpConnectionPtr& conn,json& js,Timestamp time);
}
- chatservice.cpp
// 注册消息以及对应的Handler回调操作
ChatService::ChatService() {
m_msgHandlerMap.insert({LOGIN_MSG,std::bind(&ChatService::login, this, _1, _2, _3)});
m_msgHandlerMap.insert({REG_MSG,std::bind(&ChatService::reg, this, _1, _2, _3)});
m_msgHandlerMap.insert({ONE_CHAT_MSG,std::bind(&ChatService::oneChat, this, _1, _2, _3)});
m_msgHandlerMap.insert({ADD_FRIEND_REQ_MSG,std::bind(&ChatService::addFriendRequest, this, _1, _2, _3)});
m_msgHandlerMap.insert({ADD_FRIEND_MSG_ACK,std::bind(&ChatService::addFriendResponse, this, _1, _2, _3)});
}
// 添加好友业务请求
void ChatService::addFriendRequest(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int userid = js["id"].get();
int friendid = js["friendid"].get();
json response;
response["msgid"] = ADD_FRIEND_REQ_MSG;
response["msg"] = "Please add me as a friend, thank you!";
response["from"] = userid;
response["toid"] = friendid;
// std::cout<<"来到这里了"<<std::endl;
oneChat(conn,response,time);
}
// 添加好友业务 msgid id friendid
void ChatService::addFriendResponse(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int userid = js["id"].get();
int friendid = js["friendid"].get();
bool flag = js["flag"].get();
json response;
response["msgid"] = ADD_FRIEND_MSG_ACK;
response["from"] = userid;
response["toid"] = friendid;
if(flag) {
response["msg"] = "I very happy to make friends with you!!!";
m_friendModel.insert(userid, friendid);
}
else{
response["msg"] = "I am very sorry, you are not my friend!!!";
}
oneChat(conn,response,time);
}
情景一:id=1的用户 想要和 id=2的在线用户交个朋友,向其发送好友请求,id=2的在线用户响应同意互为好友
先登录两个账号:
{"msgid":1,"id":1,"password":"1024"} // 登录
{"msgid":1,"id":2,"password":"520"} // 登录

(1)id=1的用户,发送好友请求
{"msgid":6,"id":1,"friendid":2} // 发送好友请求

(2)响应好友请求(允许)
{"msgid":7,"id":2,"friendid":1,"flag":true} // 响应好友请求(允许)


情景二:id=1的用户 想要和 id=3的离线用户交个朋友,向其发送好友请求,id=2的离线用户登录后,在线时看到离线消息,响应不同意互为好友
(1)发送好友请求
先登录一个账号,id=1的用户:
{"msgid":1,"id":1,"password":"1024"} // 登录

(1)发送好友请求
{"msgid":6,"id":1,"friendid":3} // 发送好友请求
(2)响应好友请求(拒绝)
再登录id=3的用户账号,查看到来自id=1用户发来的离线消息
{"msgid":1,"id":3,"password":"1314"} // 登录

id=3的用户,响应好友请求(拒绝)
{"msgid":7,"id":3,"friendid":1,"flag":false} // 响应好友请求(拒绝)

十一、群组业务
群组业务: 群组业务分为三块,群管理员创建群组、组员加入群组与群组聊天功能。
在进行群组业务处理前,我们需要提前处理好以下几点:
1、我们需要在消息类型EnMsgType中增加不同的消息类型,创建群组、加入群组、群组聊天三种类型消息,给客户端标识此时要做什么事情:
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
ADD_FRIEND_MSG, // 添加好友消息
CREATE_GROUP_MSG, // 创建群组消息
ADD_GROUP_MSG, // 加入群组消息
GROUP_CHAT_MSG, // 群聊天消息
};
#endif // PUBLIC_H
2、将群组业务的消息id分别与对应的事件处理器提前在聊天服务器业务类的构造函数里绑定好。
m_msgHandlerMap.insert({CREATE_GROUP_MSG, std::bind(&ChatService::createGroup, this, _1, _2, _3)});
m_msgHandlerMap.insert({ADD_GROUP_MSG, std::bind(&ChatService::joinGroup, this, _1, _2, _3)});
m_msgHandlerMap.insert({GROUP_CHAT_MSG, std::bind(&ChatService::groupChat, this, _1, _2, _3)});
3、建立群组表与类的映射Group类与群组表的数据操作类GroupModel:提供给业务层对应操作接口。
- group.hpp
#ifndef GROUP_H
#define GROUP_H
#include
#include
using namespace std;
#include "groupuser.hpp"
// User表的ORM类
// Group群组表的映射类:映射表的相应字段
class Group{
public:
Group(int id=-1,string name="",string desc="")
: m_id(id)
,m_name(name)
,m_desc(desc) {
}
void setId(int id) { m_id = id; }
void setName(string name) { m_name = name; }
void setDesc(string desc) { m_desc = desc; }
int getId() const { return m_id; }
string getName() const { return m_name; }
string getDesc() const { return m_desc; }
vector &getUsers() { return m_users; }
private:
int m_id; // 群组id
string m_name; // 群组名称
string m_desc; // 群组功能描述
vector m_users;// 存储组成员
};
#endif // GROUP_H
groupmodel.hpp:
#ifndef GROUPMODEL_H
#define GROUPMODEL_H
#include "group.hpp"
#include
#include
using namespace std;
// 群组表的数据操作类:维护数组信息的操作接口方法
class GroupModel {
public:
// 创建数组
bool createGroup(Group &group);
// 加入群组
void joinGroup(int userid, int groupid, string role);
// 查询用户所在群组信息
vector queryGroups(int userid);
// 根据指定的groupid查询群组用户id列表,除userid自己,主要用户群聊业务给群组其他成员群发消息
vector queryGroupUsers(int userid, int groupid);
};
#endif // GROUPMODEL_H
groupmodel.cpp:
#include "groupmodel.hpp"
#include "db.h"
// 创建群组
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());
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
if(mysql.update(sql)) {
// 获取到自增id
group.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
}
return false;
}
// 加入群组:即给群组员groupuser表添加一组信息
void GroupModel::joinGroup(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());
// 2.执行sqls语句
Mysql mysql;
if(mysql.connect()) {
mysql.update(sql);
}
}
// 查询用户所在群组信息:群信息以及组员信息
vector GroupModel::queryGroups(int userid) {
/*
1.先根据userid在groupuser表中查询出该用户所属的群组信息
2.在根据群组信息,查询属于该群组的所有用户的userid,并且和user表
进行多表联合查询,查出用户的详细信息
*/
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 groupVec;
Mysql mysql;
if(mysql.connect()) {
MYSQL_RES *res = mysql.query(sql);
if(res != nullptr) {
MYSQL_ROW row;
// 查出userid所有的群组信息
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);
}
}
// 查询群组的用户信息
for(Group& group:groupVec) {
sprintf(sql,"select a.id,a.name,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]);
user.setRole(row[3]);
group.getUsers().push_back(user);
}
mysql_free_result(res);
}
}
return groupVec;
}
// 根据指定的groupid查询群组用户id列表,除userid自己,主要用户群聊业务给群组其他成员群发消息
vector 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 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;
}
4、建立群组员表的映射GroupUser类:群组和组员是多对多关系,需要这张中间表体现他们的关系,同时封装该类提供给业务层对应操作接口
- groupuser.hpp
#ifndef GROUPUSER_H
#define GROUPUSER_H
#include
#include "user.hpp"
using namespace std;
// 群组用户,多了一个role角色信息,从User类直接继承,复用User的其他信息
// GroupUser群组员表的映射类:映射表的相应字段
class GroupUser : public User {
public:
void setRole(string role) { m_role = role; }
string getRole() { return m_role; }
private:
string m_role;
};
#endif // GROUPUSER_H
5、业务层chatservice.hpp中增加群组相关的数据操作类对象,方面我们后续对数据库进行操作。
GroupModel m_groupModel; //群组相关的数据操作类对象
加入群组
服务器组员加入群组业务流程:
1、服务器获取要加入群用户的id、要加入的群组id;
2、业务层调用数据层方法将普通用户加入;
// 加入群组业务
void ChatService::joinGroup(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int userid = js["id"].get();
int groupid = js["groupid"].get();
// 存储用户加入的群组信息
m_groupModel.joinGroup(userid,groupid,"normal");
}
群组聊天
服务器群组聊天业务流程:
1、获取要发送消息的用户id、要发送的群组id;
2、查询该群组其它用户id;
3、查询同组用户id,若用户在线则发送消息;若用户不在线则存储离线消息;
// 群组聊天业务
void ChatService::groupChat(const TcpConnectionPtr &conn, json &js, Timestamp time) {
// 1.获取要发送消息的用户id,要发送的群组id
int userid = js["id"].get();
int groupid = js["groupid"].get();
// 2.查询该群组其他的用户id
vector useridVec = m_groupModel.queryGroupUsers(userid, groupid);
// 3.进行用户查找
lock_guard lock(m_connMutex);
for(int id : useridVec) {
auto it = m_userConnMap.find(id);
// 用户在线,转发群消息
if(it != m_userConnMap.end()) {
// 转发群消息
it->second->send(js.dump());
}
else {
// 存储离线群消息
m_offlineMsgModel.insert(id, js.dump());
}
}
}
十二、注销业务
注销业务: 客户端用户正常退出,更新其在线状态。
在进行注销业务处理前,我们需要提前处理好以下几点:
1、我们需要在消息类型EnMsgType中增加一个注销业务类型,给客户端标识此时是一个注销业务消息:
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
LOGIN_OUT_MSG, //注销消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
ADD_FRIEND_MSG, // 添加好友消息
CREATE_GROUP_MSG, // 创建群组消息
ADD_GROUP_MSG, // 加入群组消息
GROUP_CHAT_MSG, // 群聊天消息
};
#endif // PUBLIC_H
2、将注销业务的消息id与对应的事件处理器提前在聊天服务器业务类的构造函数里绑定好。
m_msgHandlerMap.insert({GROUP_CHAT_MSG, std::bind(&ChatService::groupChat, this, _1, _2, _3)});
服务器注销业务业务流程:
1、服务器获取要注销用户的id,删除其对应的连接。
2、更新用户状态信息,从在线更新为离线。
// 注销:int接收sockfd,string接收用户发送的数据
void loginout(int clientfd, string str)
{
json js;
js["msgid"] = LOGIN_OUT_MSG;
js["id"] = g_currentUser.getId();
string buffer = js.dump();
// std::cout<<"注销:int接收sockfd,string接收用户发送的数据 buffer: "<<buffer<<std::endl;
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str()) + 1, 0);
if (-1 == len)
{
cerr < " << buffer << endl;
}
else
{
isMainMenuRunning = false;
}
}
十二、 群组业务
群组业务: 群组业务分为三块,群管理员创建群组员加入群组与群组聊天功能。
在进行群组业务处理前,需要提前处理好以下几点:
1、需要在消息类型EnMsgType中增加不同的消息类型,创建群组、加入群组、群组聊天三种类型消息,给客户端标识此时要做什么事情
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
LOGIN_OUT_MSG, //注销消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
// ADD_FRIEND_MSG, // 添加好友消息
ADD_FRIEND_REQ_MSG, // 添加好友请求消息
ADD_FRIEND_MSG_ACK, // 添加好友响应消息
CREATE_GROUP_MSG, // 创建群组消息
ADD_GROUP_MSG, // 加入群组消息
GROUP_CHAT_MSG, // 群聊天消息
};
#endif // PUBLIC_H
2、将群组业务的消息id分别与对应的事件处理器提前在聊天服务器业务类的构造函数里绑定好
// 构造函数:注册消息以及对应的Handler回调操作 实现网络模块与业务模块解耦的核心
// 将群组业务的消息id分别与对应的事件处理器提前在聊天服务器业务类的构造函数里绑定好
ChatService::ChatService() {
...
...
...
m_msgHandlerMap.insert({CREATE_GROUP_MSG, std::bind(&ChatService::createGroup, this, _1, _2, _3)});
m_msgHandlerMap.insert({ADD_GROUP_MSG, std::bind(&ChatService::joinGroup, this, _1, _2, _3)});
m_msgHandlerMap.insert({GROUP_CHAT_MSG, std::bind(&ChatService::groupChat, this, _1, _2, _3)});
}
3、建立群组表与类的映射Group类与群组表的数据操作类GroupModel:提供给业务层对应操作接口
群组表的映射类group.hpp:
#ifndef GROUP_H
#define GROUP_H
#include
#include
using namespace std;
#include "groupuser.hpp"
// User表的ORM类
// Group群组表的映射类:映射表的相应字段
class Group{
public:
Group(int id=-1,string name="",string desc="")
: m_id(id)
,m_name(name)
,m_desc(desc) {
}
void setId(int id) { m_id = id; }
void setName(string name) { m_name = name; }
void setDesc(string desc) { m_desc = desc; }
int getId() const { return m_id; }
string getName() const { return m_name; }
string getDesc() const { return m_desc; }
vector &getUsers() { return m_users; }
private:
int m_id; // 群组id
string m_name; // 群组名称
string m_desc; // 群组功能描述
vector m_users;// 存储组成员
};
#endif // GROUP_H
群组表的数据操作类groupmodel.hpp
#include "groupmodel.hpp"
#include "db.h"
// 创建群组
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());
// 2.执行sql语句
Mysql mysql;
if(mysql.connect()) {
if(mysql.update(sql)) {
// 获取到自增id
group.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
}
return false;
}
// 加入群组:即给群组员groupuser表添加一组信息
void GroupModel::joinGroup(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());
// 2.执行sqls语句
Mysql mysql;
if(mysql.connect()) {
mysql.update(sql);
}
}
// 查询用户所在群组信息:群信息以及组员信息
vector GroupModel::queryGroups(int userid) {
/*
1.先根据userid在groupuser表中查询出该用户所属的群组信息
2.在根据群组信息,查询属于该群组的所有用户的userid,并且和user表
进行多表联合查询,查出用户的详细信息
*/
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 groupVec;
Mysql mysql;
if(mysql.connect()) {
MYSQL_RES *res = mysql.query(sql);
if(res != nullptr) {
MYSQL_ROW row;
// 查出userid所有的群组信息
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);
}
}
// 查询群组的用户信息
for(Group& group:groupVec) {
sprintf(sql,"select a.id,a.name,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]);
user.setRole(row[3]);
group.getUsers().push_back(user);
}
mysql_free_result(res);
}
}
return groupVec;
}
// 根据指定的groupid查询群组用户id列表,除userid自己,主要用户群聊业务给群组其他成员群发消息
vector 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 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;
}
4、建立群组员表的映射GroupUser类:群组和组员是多对多关系,需要这张中间表体现他们的关系,同时封装该类提供给业务层对应操作接口。

群组员表的映射类groupuser.hpp
#ifndef GROUPUSER_H
#define GROUPUSER_H
#include
#include "user.hpp"
using namespace std;
// 群组用户,多了一个role角色信息,从User类直接继承,复用User的其他信息
// GroupUser群组员表的映射类:映射表的相应字段
class GroupUser : public User {
public:
void setRole(string role) { m_role = role; }
string getRole() { return m_role; }
private:
string m_role;
};
#endif // GROUPUSER_H
5、业务层chatservice.hpp中增加群组相关的数据操作类对象,方面我们后续对数据库进行操作。
GroupModel m_groupModel;
十三、群组业务
>>创建群组
服务器创建群组业务业务流程:
1、服务器获取创建群的用户id、要创建群名称、群功能等信息;
2、业务层创建数据层对象,调用数据层方法进行群组创建,创建成功保存群组创建人信息;
- chatservice.hpp
// 创建群组业务 void createGroup(const TcpConnectionPtr& conn,json& js,Timestamp time);
- chatservice.cpp
// 创建群组业务
void ChatService::createGroup(const TcpConnectionPtr &conn, json &js, Timestamp time) {
// 1.获取创建群的用户id,群名称,群功能
int userid = js["id"].get();
string name = js["groupname"];
string desc = js["groupdesc"];
// 2.存储新创建的群组信息
Group group(-1, name, desc);
if(m_groupModel.createGroup(group)) {
// 存储群组创建人信息
m_groupModel.joinGroup(userid,group.getId(),"creator");
}
}
>>加入群组
服务器组员加入群组业务流程:
1、服务器获取要加入群用户的id、要加入的群组id;
2、业务层调用数据层方法将普通用户加入;
- chatservice.hpp
// 加入群组业务 void joinGroup(const TcpConnectionPtr& conn,json& js,Timestamp time);
- chatservice.cpp
// 加入群组业务
void ChatService::joinGroup(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int userid = js["id"].get();
int groupid = js["groupid"].get();
// 存储用户加入的群组信息
m_groupModel.joinGroup(userid,groupid,"normal");
}
>>群组聊天
服务器群组聊天业务流程:
1、获取要发送消息的用户id、要发送的群组id;
2、查询该群组其它用户id;
3、查询同组用户id,若用户在线则发送消息;若用户不在线则存储离线消息;
- chatservice.hpp
// 群组聊天业务 void groupChat(const TcpConnectionPtr& conn,json& js,Timestamp time);
- chatservice.cpp
// 群组聊天业务
void ChatService::groupChat(const TcpConnectionPtr &conn, json &js, Timestamp time) {
// 1.获取要发送消息的用户id,要发送的群组id
int userid = js["id"].get();
int groupid = js["groupid"].get();
// 2.查询该群组其他的用户id
vector useridVec = m_groupModel.queryGroupUsers(userid, groupid);
// 3.进行用户查找
/*
* A向B说话,在map表中未找到B,B可能不在本台服务器上但通过数据库查找
* 在线,要发送的消息直接发送以B用户为id的通道上;也可能是离线状态,
* 发送离线消息
*/
lock_guard lock(m_connMutex);
for(int id : useridVec) {
auto it = m_userConnMap.find(id);
// 用户在线,转发群消息
if(it != m_userConnMap.end()) {
// 转发群消息
it->second->send(js.dump());
}
else { // 用户不在线,存储离线消息 或 在其它服务器上登录的
// 查询toid是否在线
User user = m_userModel.query(id);
if(user.getState() == "online") { // 在其他服务器上登录的
m_redis.publish(id,js.dump());
}else{
// 存储离线群消息
m_offlineMsgModel.insert(id, js.dump());
}
}
}
}
十四、注销业务
注销业务: 客户端用户正常退出,更新其在线状态。
在进行注销业务处理前,需要提前处理好以下几点:
1、需要在消息类型EnMsgType中增加一个注销业务类型,给客户端标识此时是一个注销业务消息:
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
LOGIN_OUT_MSG, //注销消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
// ADD_FRIEND_MSG, // 添加好友消息
ADD_FRIEND_REQ_MSG, // 添加好友请求消息
ADD_FRIEND_MSG_ACK, // 添加好友响应消息
CREATE_GROUP_MSG, // 创建群组消息
ADD_GROUP_MSG, // 加入群组消息
GROUP_CHAT_MSG, // 群聊天消息
};
#endif // PUBLIC_H
2、将注销业务的消息id与对应的事件处理器提前在聊天服务器业务类的构造函数里绑定好。
// 构造函数:注册消息以及对应的Handler回调操作 实现网络模块与业务模块解耦的核心
// 将群组业务的消息id分别与对应的事件处理器提前在聊天服务器业务类的构造函数里绑定好
ChatService::ChatService() {
m_msgHandlerMap.insert({ONE_CHAT_MSG,std::bind(&ChatService::oneChat, this, _1, _2, _3)});
...
...
...
}
服务器注销业务业务流程:
1、服务器获取要注销用户的id,删除其对应的连接。
2、更新用户状态信息,从在线更新为离线。
//处理注销业务
void ChatService::loginOut(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
//1、获取要注销用户的id,删除对应连接
int userid = js["id"].get();
// std::cout<<"获取要注销用户的id,删除对应连接: userid: "<<userid<<std::endl;
{
lock_guard lock(m_connMutex);
auto it = m_userConnMap.find(userid);
if (it != m_userConnMap.end())
{
m_userConnMap.erase(it);
}
}
// 用户注销,相当于就是下线,在redis中取消订阅通道
m_redis.unsubscribe(userid);
//2、更新用户状态信息
User user(userid, "", "", "offline");
m_userModel.updateState(user);
}
十五、客户端开发
- chatservice.cpp
// 处理登录业务 user表:id password字段
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
// 1.获取ids,password字段
int id = js["id"].get();
string pwd = js["password"];
// 传入用户id,返回相应数据
User user = m_userModel.query(id);
if(user.getId() == id && user.getPwd() == pwd) { // 登录成功
if(user.getState() == "online") {
//该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2; // 重复登录
// response["errmsg"] = "该账号已经登录,请重新输入新账号";
response["errmsg"] = "this account has logined, please input a new account";
conn->send(response.dump());
}
else{ // 用户未登录,此时登录成功
// 登录成功,记录用户连接信息
/*
在用户登录成功时便将用户id与连接信息记录在一个map映射表里,方便后续查找与使用
线程安全问题:上述我们虽然建立了用户id与连接的映射,但是在多线程环境下,不同的用户
可能会在不同的工作线程中调用同一个业务,可能同时有多个用户上线,下线操作,因此要
保证map表的线程安全
*/
{
lock_guard lock(m_connMutex);
m_userConnMap.insert({id, conn}); // 登录成功记录用户连接信息
}
// id用户登录成功后,向redis订阅channel(id)通道的事件
m_redis.subscribe(id);
// 登录成功,更新用户状态信息 state: offline => online
user.setState("online");
m_userModel.updateState(user); // 更新用户状态信息
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
// 查询该用户是否有离线消息
vector vec = m_offlineMsgModel.query(id);
if(!vec.empty()) {
response["offlinemsg"] = vec;// 查询到离线消息,发送给用户
// 读取该用户的离线消息后,把该用户的所有离线消息删除掉
m_offlineMsgModel.remove(id);
}
// 登录成功,查询该用户的好友信息并返回
vectoruserVec = m_friendModel.query(id);
if(!userVec.empty()) {
vector vec2;
for(User &user : userVec) {
json js;
js["id"] = user.getId();
js["name"] = user.getName();
js["state"] = user.getState();
vec2.push_back(js.dump());
}
response["friends"] = vec2;
}
vector groupVec = m_groupModel.queryGroups(id);
if(groupVec.size() > 0) {
// cout<<"................sdsdfasas................."<<endl;
vector vec3;
for(Group& group:groupVec) {
vector users = group.getUsers();
json js;
js["id"] = group.getId();
js["groupname"] = group.getName();
js["groupdesc"] = group.getDesc();
vector userVec;
for(GroupUser& user:users) {
json js_tmp;
js_tmp["id"] = user.getId();
js_tmp["name"] = user.getName();
js_tmp["state"] = user.getState();
js_tmp["role"] = user.getRole();
userVec.push_back(js_tmp.dump());
}
js["users"] = userVec;
vec3.push_back(js.dump());
// cout<<"js.dump() = "<<js.dump()<send(response.dump());
}
}
else {
// 该用户不存在/用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
// response["errmsg"] = "该用户不存在,您输入用户名或者密码可能错误!";
response["errmsg"] = "This user does not exist, or the password you entered may be incorrect!";
conn->send(response.dump());
}
}
- src/client/main.cpp
#include "json.hpp" #include #include #include #include #include #include #include
github完整项目:
heheda102410/chatServer01: C++集群聊天服务器 nginx+redis+muduo (github.com)
https://github.com/heheda102410/chatServer01参考和推荐文章,部分文字来自这篇文章:
集群聊天服务器:一、服务器代码实现_集群聊天服务器代码-CSDN博客
https://blog.csdn.net/qq_42441693/article/details/129013429?spm=1001.2014.3001.5501
本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/f91d66332f.html
