代码链接:https://github.com/ynighter/chatserver.git
目录
1.前言
本项目在muduo网络库的基础上,实现基于nginx负载均衡的聊天服务器,可实现用户注册,登录,群聊,一对一聊天,注销登录等功能。其主要的流程如下图所示。
图中可见,不同的客户端由nginx使用负载均衡算法分布到相应的服务器,为克服不同服务器之通信的问题,使用redis的发布订阅的模式接收消息然后发送给对应的服务器,避免了服务器之间两两相互连接。
主要的业务逻辑如下图所示:
网络模块底层采用的muduo库,其采用的是 one loop per thread + nonblocking IO 的网络事件模型,其基于epoll的事件处理机制能够高效地处理网络事件,提供了高效的事件处理和内存管理方式。有一个较高muduo通过事件驱动的方式实现了异步I/O,能够支持上万的并发连接。用户只需要关注连接到来和socket消息到来时的业务处理。使用muduo库作为项目的核心网络模块,提供高并发以及高可用网络IO服务,解耦网络和业务模块的代码,提高了系统的可维护性和可扩展性。
本项目在vscode上开发进行,使用ssh远程连接linux,还需要安装redis,nginx,mysql,cmake环境还需要json头文件,boost与muduo网络开发库。下图为vscode开发界面,使用ssh远程连接。其余环境文件可自行安装。
2.mysql表格设计
本项目的用户信息都使用mysql来存储,其中包括用户信息,好友信息,群组消息,离线消息, 对于不同的表结构的设计,分别如下所示:
1.user表格用来存储对应的注册人员信息,包括密码,姓名,id,状态,当注册新成员时,将新成员的信息写入该表格,同时返回一个用户id用户登录。
2.friend表格用来存储两个用户,这两个用户的好友信息只会被存储一次。
3.offlinemessage表格用来存储在线用户给离线用户发送的信息,当离线用户登录之后,便会将离线消息发送给该用户。
4.groupuser表格用来存储某一群组的id与该群组之中的成员id,同时每一群组成员的身份信息也被列举。
5.最后一个表格为allgroups,用来保存已经创建的所有群组,包含群组的id,名称,与群聊描述。
3.json
json为一种轻量级的数据交换格式(也叫数据序列化方式),采用完全独立于编程语言的文本格式来存储和表示数据。的层次结构简洁和清晰,易于阅读和编写、机器解析和生成,且能有效地提升网络传输效率。
在网络中,常用的数据传输序列化格式有 XML、Json、ProtoBuf。在公司级别的项目中,大量的在使用 ProtoBuf 作为数据序列化的方式,以其数据压缩编码传输,占用带宽小,同样的数据信息,是 Json 的1/10,XML 的1/20,但使用起来比 Json 稍复杂一些。
本项目的网络中,因为简便的特性,需要使用json数据序列化方式来进行网络传输,对于json的使用需要包含以下内容:
#include "json.hpp"
using json = nlohmann::json;
json本质为一种键值对,在数据序列化时将数据对象直接处理成json字符串进行发送,在反序列化时根据相应的key值就可以获取到对应的信息,同时对于c++,可以直接进行容器序列化,使用非常简便。
json数据序列化:
#include "json.hpp"
#include <iostream>
int main()
{
using json = nlohmann::json;
json js;
// 序列化数组
js["array"] = {
{1,2,3},{4,5,6}};
std::cout << js << std::endl;
// 序列化键值对
js["key"] = "value";
std::cout << js << std::endl;
// 序列化对象
js["key1"] = {"value1_1", "value1_2"};
js["key2"] = {"value2_1", "value2_2"};
js["msg"]["key3_1"] = "value3_1";
js["msg"]["key3_2"] = "value3_2";
std::cout << js << std::endl;
return 0;
}
/*
{"array":[[1,2,3],[4,5,6]]}
{"array":[[1,2,3],[4,5,6]],"key":"value"}
{"array":[[1,2,3],[4,5,6]],"key":"value","key1":["value1_1","value1_2"],"key2":["value2_1","value2_2"],"msg":{"key3_1":"value3_1","key3_2":"value3_2"}}
*/
json容器序列化:
#include "json.hpp"
#include <iostream>
#include <vector>
#include <map>
int main()
{
using json = nlohmann::json;
// 容器序列化
std::vector<int> arr_vctor({1,2,3,4,5});
js["arr_vctor"] = arr_vctor;
std::cout << js << std::endl;
std::vector<std::vector<int>> arr2_vctor({
{1,2,3},{4,5,6}});
js["arr2_vctor"] = arr2_vctor;
std::cout << js << std::endl;
std::map<int, std::string> mp;
mp[1] = "mp1"; mp[2] = "mp2";
js["map"] = mp;
std::cout << js << std::endl;
return 0;
}
/*
{"arr_vctor":[1,2,3,4,5],"array":[[1,2,3],[4,5,6]],"key":"value","key1":["value1_1","value1_2"],"key2":["value2_1","value2_2"],"msg":{"key3_1":"value3_1","key3_2":"value3_2"}}
{"arr2_vctor":[[1,2,3],[4,5,6]],"arr_vctor":[1,2,3,4,5],"array":[[1,2,3],[4,5,6]],"key":"value","key1":["value1_1","value1_2"],"key2":["value2_1","value2_2"],"msg":{"key3_1":"value3_1","key3_2":"value3_2"}}
{"arr2_vctor":[[1,2,3],[4,5,6]],"arr_vctor":[1,2,3,4,5],"array":[[1,2,3],[4,5,6]],"key":"value","key1":["value1_1","value1_2"],"key2":["value2_1","value2_2"],"map":[[1,"mp1"],[2,"mp2"]],"msg":{"key3_1":"value3_1","key3_2":"value3_2"}}
*/
json反序列化:
#include "json.hpp"
#include <iostream>
#include <string>
#include <vector>
#include <map>
using json = nlohmann::json;
std::string func1()
{
json js;
// 序列化数组
js["array"] = {
{1,2,3},{4,5,6}};
std::cout << js << std::endl;
// 序列化键值对
js["key"] = "value";
std::cout << js << std::endl;
// 序列化对象
js["key1"] = {"value1_1", "value1_2"};
js["key2"] = {"value2_1", "value2_2"};
js["msg"]["key3_1"] = "value3_1";
js["msg"]["key3_2"] = "value3_2";
std::cout << js << std::endl;
std::string ret = js.dump(); // 将json对象序列化为字符串
return ret;
}
std::string func2()
{
json js;
// 容器序列化
std::vector<int> arr_vctor({1,2,3,4,5});
js["arr_vctor"] = arr_vctor;
std::cout << js << std::endl;
std::vector<std::vector<int>> arr2_vctor({
{1,2,3},{4,5,6}});
js["arr2_vctor"] = arr2_vctor;
std::cout << js << std::endl;
std::map<int, std::string> mp;
mp[1] = "mp1"; mp[2] = "mp2";
js["map"] = mp;
std::cout << js << std::endl;
std::string ret = js.dump(); // 将json对象序列化为字符串
return ret;
}
int main()
{
json js;
std::string revBuf1 = func1();
// 模拟从网络接收到json字符串,通过json::parse函数把json字符串转换为json对象
json jsBuf1 = json::parse(revBuf1);
auto ret1 = jsBuf1["array"];
std::cout << ret1 << std::endl;
auto ret2 = jsBuf1["msg"];
std::cout << ret2 << std::endl;
std::cout << "................." << std::endl;
std::string revBuf2 = func2();
json jsBuf2 = json::parse(revBuf2);
auto ret3 = jsBuf2["arr2_vctor"]; // auto ==> vector<vector<int>>
std::cout << ret3 << std::endl;
auto ret4 = jsBuf2["map"]; // auto ==> map<int, string>
std::cout << ret4 << std::endl;
return 0;
}
4.相关类函数的实现
本项目的类函数编写主要为服务器和类函数两部分内容,其余部分内容为其他接口对应,本文只对不同类的函数功能做简要分析,具体功能实现请阅读文章开头附上的代码链接。
Public
此类主要是为了区分不同种类的消息类型,进行通信时,消息种类可分为群聊消息,注册消息等,设计此类可根据不同的消息类型实现不同的回调函数。
#ifndef PUBLIC_H
#define PUBLIC_H
/* server和client的公共文件 */
enum EnMsgType
{
LOGIN_MSG = 1, // 登录消息 {"msgid":1,"id":*,"password":"***"}
LOGIN_MSG_ACK, // 登录响应消息
LOGINOUT_MSG, // 注销消息
SIGNUP_MSG, // 注册消息 {"msgid":3,"id":*,"name":"***","password":*}
SIGNUP_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息 {"msgid":5,"id":*,"from":"***","to":*,"msg":"***"}
ADD_FRIEND_MSG, // 添加好友消息
CREATE_GROUP_MSG, // 创建群组 CREATE_GROUP_MSG_ACK, // 创建群组响应消息
ADD_GROUP_MSG, // 加入群组
GROUP_CHAT_MSG, // 群聊天
};
#endif
MySQL
此类为了将发送的消息与mysql之间进行交互,实现数据库之中的查询,更新,连接等操作。
#ifndef MYSQL_H
#define MYSQL_H
#include <mysql/mysql.h>
#include <string>
#include <ctime>
using namespace std;
/* 实现MySQL数据库的操作 */
class MySQL
{
public:
// 初始化数据库连接
MySQL(string ip = "192.168.134.129", string username = "root", string password = "wzzclgsj111", string dbname = "chat");
// 释放数据库连接资源
~MySQL();
// 连接数据库
bool connect();
// 更新操作 insert、delete、update
bool update(string sql);
// 查询操作 select
MYSQL_RES* query(string sql);
void PrintQueryResult(MYSQL_RES* results);
MYSQL* getConnection() { return _conn; }
private:
MYSQL* _conn; // 与MySQL Server的一条连接
string _ip;
string _username;
string _password;
string _dbname;
};
#endif
User
此类主要是为注册的用户设计操作,初始化用户的状态,id名称与密码,同时设计一个usermodel类来进行mysql中数据表的相关操作。
#ifndef USER_H
#define USER_H
#include <string>
using namespace std;
// User表的ORM类(Object Relation Map对象关系映射)
class User
{
public:
User(int id = -1, string name = "", string pwd = "", string state = "offline")
: _id(id), _name(name), _pwd(pwd), _state(state)
{ }
void setID(int id) { _id = id; }
void setName(string name) { _name = name; }