IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 基于muduo,mysql,redis,nginx的集群聊天服务器 -> 正文阅读

[系统运维]基于muduo,mysql,redis,nginx的集群聊天服务器

集成聊天服务器

项目介绍

  • 项目名称:集成聊天服务器

  • 平台工具:vs code ,linux shell命令行,cmake

  • 编程语言:C++

  • 项目内容:

    1. 网络层基于muduo网络库的reactor模型,one loop per thread,使用方便;

    2. 使用优秀的第三方库json,用以实现数据的序列化和反序列化,进行数据传输;

    3. 数据库选择了关系型数据库MySQL;

    4. 负载均衡器方面,选择了nginx的tcp负载均衡模块;

    5. 实现多台服务器之间的通信功能,选择使用redis的订阅-发布功能

  • 收获:熟悉了C++程序中调用MySQL数据库的方法,掌握了json库的使用方法,了解了nginx的tcp均衡模块的配置方法,熟悉了redis的发布-订阅方法在程序中的适当的调用方式。

  • 问题及解决方案:在进行集成聊天服务项目时,代码时常会出现bug,解决方案为gdb调试,查看tcp连接情况以及定点打印方式进行debug。

所需配置及工具

  1. Ubuntu虚拟机
  2. VS code ,用以与虚拟机连接,在VS code上书写代码且调试,相对vim更为方便
  3. MySQL,建立表
  4. redis
  5. nginx负载均衡器
  6. muduo网络库
  7. json

数据库设计

表的设计

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pXq2NCKi-1646896273411)(C:\Users\chenzhengchang\AppData\Roaming\Typora\typora-user-images\image-20220310142159780.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4u7ka2Jc-1646896273412)(C:\Users\chenzhengchang\AppData\Roaming\Typora\typora-user-images\image-20220310142229964.png)]

OffiineMessage表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sj7dxEme-1646896273412)(C:\Users\chenzhengchang\AppData\Roaming\Typora\typora-user-images\image-20220310142314703.png)]

代码

服务端代码

chatserver

在这里实现了基础的ChatServer聊天服务器类,muduo库的调用以及最基本的回调函数的绑定等

#ifndef CHATSERVER_H
#define CHATSERVER_H

//聊天服务器主类
class ChatServer
{
public:
    //初始化聊天服务器对象
    ChatServer(EventLoop *loop,
               const InetAddress &listenAddr,
               const string &nameArg);
    //启动服务
    void start();

private:
    //上报链接相关信息的回调
    void onConnection(const TcpConnectionPtr &);

    //上报读写事件相关信息的回调函数
    void onMessage(const TcpConnectionPtr &,
                        Buffer*,
                        Timestamp);

    TcpServer server_; 
    EventLoop *loop_;
};

#endif
chatservice

这里包含了数种处理方法的类,以及登录,注册,聊天等方法。

#ifndef CHATSERVICE_H
#define CHATSERVICE_H

using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp)>;

//聊天服务业务类
class ChatService
{
public:
    //获取单例对象的接口函数
    static ChatService *instance();
    //处理登录业务
    void login(const TcpConnectionPtr &conn, json &js, Timestamp time);
    //处理注册业务
    void reg(const TcpConnectionPtr &conn, json &js, Timestamp time);
    //一对一聊天
    void oneChat(const TcpConnectionPtr &conn, json &js, Timestamp time);
    //添加好友
    void addFriend(const TcpConnectionPtr &conn, json &js, Timestamp time);
    //获取消息对应的处理器
    MsgHandler getHandler(int msgid);
    //处理用户异常退出
    void clientCloseException(const TcpConnectionPtr &conn);
    //用户注销操作
    void loginout(const TcpConnectionPtr &conn, json &js, Timestamp time);
    //处理服务器断开时修改用户登录状态
    void reset();
    //创建群组业务
    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);
    //从redis消息队列中获取订阅的消息
    void handleRedisSubscribeMessage(int userid, string msg);

private :
    //获取单例对象的接口函数
    ChatService();

    //存储消息id和其对应的业务处理方法
    unordered_map<int, MsgHandler> _msgHandlerMap;

    //存储在线用户的通信连接
    unordered_map<int, TcpConnectionPtr> _userConnMap;

    //定义互斥锁,保证_userConnMap的线程安全
    mutex _connMutex;

    UserModel _userModel;

    offlineMsgModel _offlineMsgModel;

    FriendModel _friendModel;

    GroupModel _groupModel;

    Redis _redis;
};

#endif
public
#ifndef PUBLIC_H
#define PUBLIC_H

/*
    server和client的公共文件
*/
enum EnMsgType
{
    LOGIN_MSG = 1,//登录消息
    LOGIN_MSG_ACK,
    LOGINOUT_MSG,
    REG_MSG, //注册消息
    REG_MSG_ACK,
    ONE_CHAT_MSG,//聊天信息
    ADD_FRIEND_MSG,//添加好友信息

    CREATE_GROUP_MSG,//创建群聊
    ADD_GROUP_MSG,//加入群聊
    GROUP_CHAT_MSG//群聊天
};


#endif
friendmodel
class FriendModel
{
public:
    //添加好友关系
    void insert(int userid, int friendid);
    //返回用户好友列表
    vector<User> query(int userid);
};
group
#ifndef GROUP_H
#define GROUP_H

#include "groupuser.hpp"
#include <vector>
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:
    int id;
    string name;
    string desc;
    vector<GroupUser> users;
};

#endif
groupmodel
#ifndef GROUPMODEL_H
#define GROUPMODEL_H

#include"group.hpp"
#include<string>
#include<vector>

class GroupModel
{
public:
    //创建群聊
    bool createGroup(Group &group);
    //加入群聊
    void addGroup(int userid, int groupid, string role);
    //查询用户所在群组信息
    vector<Group> queryGroups(int userid);
    //根据指定的groupid查询用户id列表,除了userid自己,主要用户群聊业务给群组其它成员群发消息
    vector<int> queryGroupUsers(int userid, int groupid);
};

#endif
group
#ifndef USER_H
#define USER_H

#include<string>
using namespace std;

class User
{
public:
    User(int id=-1,string name="",string password="",string state="offline")
    {
        this->id = id;
        this->name = name;
        this->password = password;
        this->state = state;
    }
    void setId(int id) { this->id = id; }
    void setName(string name) { this->name = name; }
    void setPassword(string password) { this->password = password; }
    void setState(string state) { this->state = state; }

    int getId() { return id; }
    string getName() { return name; }
    string getPassword() { return password; }
    string getState() { return state; }

private: 
    int id;
    string name;
    string password;
    string state;
};

#endif
groupuser
#ifndef GROUPUSER_H
#define GROUPUSER_H

#include"user.hpp"

class GroupUser:public User
{
public:
    void setRole(string role) { this->role = role; }

    string getRole() { return this->role; }

private:
    string role;//角色
};

#endif
usermodel
#ifndef USERMODEL_H
#define USERMODEL_H

#include"user.hpp"

class UserModel
{
public:
    //User表的增加方法
    bool insert(User &user);

    //根据用户号码查询id
    User query(int id);

    //更新用户状态
    bool updateState(User user);

    //服务器断开时,重置用户登录状态
    void resetState();

private:
};

#endif
offlinemessagemodel
#ifndef OFFLINEMESSAGEMODEL_H
#define OFFLINEMESSAGEMODEL_H

#include<string>
#include<vector>
using namespace std;

class offlineMsgModel
{
public:
    //存储用户离线消息
    void insert(int userid, string msg);

    //删除用户的离线消息
    void remove(int userid);

    //查询用户的离线消息
    vector<string> query(int userid);
};

#endif
db --MySQL调用接口方法
#ifndef DB_H
#define DB_H

#include <mysql/mysql.h>
#include <string>
using namespace std;


// 数据库操作类
class MySQL
{
public:
    // 初始化数据库连接
    MySQL();

    // 释放数据库连接资源
    ~MySQL();

    // 连接数据库
    bool connect();

    // 更新操作
    bool update(string sql);

    // 查询操作
    MYSQL_RES *query(string sql);

    //获取连接
    MYSQL *getConnection();

private:
    MYSQL *_conn;
};

#endif 

redis --redis调用方法
#ifndef REDIS_H
#define REDIS_H

#include<hiredis/hiredis.h>
#include<thread>
#include<functional>
using namespace std;

/*
redis作为集群服务器通信的基于发布-订阅消息队列时,会遇到两个难搞的bug问题,参考我的博客详细描述:
https://blog.csdn.net/QIANGWEIYUAN/article/details/97895611
*/
class Redis
{
public:
    Redis();
    ~Redis();

    //连接redis服务器
    bool connect();

    //向redis指定的通道channel发布消息
    bool publish(int channel, string message);

    //向redis指定的通道subscriber订阅消息
    bool subscribe(int channel);

    //向redis指定的通道unsubscriber取消订阅消息
    bool unsubscribe(int channel);

    //在独立线程中接收订阅通道中的消息
    void observer_channel_message();

    //初始化向业务层上报通道消息的回调对象
    void init_notify_handler(function<void(int, string)> fn);
    
private:
    //hiredis同步上下文对象,负责publish消息
    redisContext *_publish_context;

    //hiredis同步上下文对象,负责subscribe消息
    redisContext *_subscribe_context;

    //回调操作,收到订阅的消息,给service层上报
    function<void(int, string)> _notify_message_handler;
};

#endif

客户端启动流程

主函数开启子线程,子线程进入阻塞

选择登录业务,将id和password序列化成json,发送给服务器,此后main函数进入条件变量阻塞,需等待子线程唤醒。

服务器发现可读事件发送,调用onMessage,反序列化后得到msgid,由此得到对应的登录对调函数,并运行。(其中传入的json参数即主函数发送过来的json变量)

运行中会发送相关数据,子线程接收到数据,解除阻塞,识别json数据,得知是登录业务,处理后,释放条件变量,主线程继续运行,进入mainMenu函数。在该函数内,不断接收用户输入的指令,然后与登录业务一样,组合json语句,根据commanMap中的键值对来调用回调函数。期间自然是主线程给服务端发送数据,服务端给客户端子线程发送数据,子线程完成任务返回到mainMenu不断循环的过程了。当用户注销用户时便会退回mian函数。

nginx负载均衡器的tcp模块的配置方法

配置
//与HTTP处于同级位置

# nginx tcp loadbalance config
 stream {
     upstream MyServer {
         server 127.0.0.1:6000 weight=1 max_fails=3 fail_timeout=30s;
         server 127.0.0.1:6002 weight=1 max_fails=3 fail_timeout=30s;
      }
  
      server {
          proxy_connect_timeout 1s;
          listen 8000;
          proxy_pass MyServer;
          tcp_nodelay on;
      }
  }

注释:

  • listen 端口号 :nginx监听该端口,客户端连接的就是该端口

  • proxy_pass MyServer; 负载均衡分配到该内容里面的端口,我们可以看到上面还有个MyServer

  • server 127.0.0.1:6000 weight=1 max_fails=3 fail_timeout=30s;

    ? 一台服务器运行在6000端口,weight代表权重,nginx以轮询的方式进行分发客户端。

    ? max_fails=3 fail_timeout=30s 代表超时时间,和重复次数

启动方法

cd /usr/local/nginx/sbin

sudo ./nginx

sudo ./nginx -s reload 平滑启动

sudo ./nginx -s stop 关闭服务

项目不足之处以及思考

不足

  • 该项目中用redis的发布-订阅功能进行多台服务器间的通信功能。目前考虑的是所有服务器同时关闭的情况。如若是仅仅关闭一台服务器,那么mysql库的更新用户在线状态的功能是不合理的,如表的结构以及mysql语句所示,一台服务器关闭时会将mysql库中所有用户的在线重置为下线状态。

  • 还有一个就是在关闭一台服务器时,相关的代码并没有将在该台服务器上登录的用户的redis订阅通道给取消订阅。

解决思路

我的想法是:在user表中增加一列,该列记录的是该次登陆的服务器的ip及端口号,不在线的用户该列可以置为-1。这样当我们仅仅关闭一台服务器时,就可以在数据库中区分开登陆在该服务器中的用户然后选择性得更改其状态。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-03-11 22:36:33  更:2022-03-11 22:37:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 2:53:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码