C++实现集群聊天服务器(7)

负载均衡器的引入

 

在服务器集群的环境当中,我们引入负载均衡器帮我们统一接受客户端的请求,根据配置的负载算法来把客户端请求分发到业务服务器上。用户使用客户端时候不用选择去链接哪台服务器,客户端默认链接的都是负载均衡器。负载均衡器的角色就是一个中间的桥梁,连接客户端和服务器之间的通信。客户端的请求与服务端的响应都是要经过负载均衡器的。

2.能够和ChatServer保持心跳机制,监测ChatServer故障。时刻监测业务服务器是否在线,负载均衡器的职责就是把客户端的请求分发给服务器。

3.能够发现新添加的ChatServer设备,方便拓展服务器数量。(平滑加载配置文件启动)

解决跨服务器通信问题

每一台服务器上都有自己的_userConnMap。当client1给client2发送消息时,在client1的_userConnMap找不到client2的conn。现有的业务找不到conn会将其视为离线,存储成离线消息。在集群环境中,我们服务器代码需要修改,客户端是不需要改的,也不需要是在哪个服务器上完成的。我们应该继续判断,如果当前聊天的client2不在这台机器上,应_userConnMap上查找client2的id,找不到的话应该在数据库中判断client2在线状态,如果是offline确实不在线存储离线消息。如果是oline,那是因为client2登录在其他服务器上。

引入服务器中间件

集群部署的服务器之间进行通信,最好的方式就是引入中间件消息队列,解耦各个服务器,使整个系统 松耦合,提高服务器的响应能力,节省服务器的带宽资源,如下图所示:

nginx配置tcp负载均衡

nginx在1.9版本之前,只支持http协议web服务器的负载均衡,从1.9版本开始以后,nginx开始支持tcp的长连接负载均衡,但是 nginx默认并没有编译tcp负载均衡模块,编写它时,需要加入--with-stream参数来激活这个模块。

nginx编译加入--with-stream参数激活tcp负载均衡模块

 下面的make命令会向系统路径拷贝文件,需要在root用户下执行                                   tony@tony-virtual-machine:~/package/nginx-1.12.2# ./configure --with-stream              tony@tony-virtual-machine:~/package/nginx-1.12.2# make && make install                                 编译完成后,默认安装在了/usr/local/nginx目录。                                                                  tony@tony-virtual-machine:~/package/nginx-1.12.2$ cd /usr/local/nginx/                                 tony@tony-virtual-machine:/usr/local/nginx$ ls                                                                                   conf html logs sbin

cd conf进入

vim nginx.conf   编辑下面代码

 

# nginx tcp loadbalance config
stream{
   upstream MyServer{
           server 192.168.142.128:1900 weight=1 max_fails=3 fail_timeout=30s;
           server 192.168.142.128:1902 weight=1 max_fails=3 fail_timeout=30s;
   }

   server{
           proxy_connect_timeout 1s;
           #proxy_timeout 3s;
           listen 8000;
           proxy_pass MyServer;
           tcp_nodelay on;
   }
}

cd /usr/local/nginx/sbin j进入nginx sbin目录

./nginx  打开nginx

./nginx -s reload 重新加载配置文件启动

./nginx -s stop 停止nginx服务

测试:

先分别登录配置文件里的ip+端口的服务器,客户端不直接连服务器,连nginx的端口,客户端都登录8000端口。nginx确实是把客户端的请求通过相同权重比负载算法 转发给相应1900 1902端口,服务器依次收到相应。

 

 

 功能都依旧照常。

redis的发布-订阅

sudo apt-get install redis-server # ubuntu命令安装 redis服务

ps -ef | grep redis   ubuntu通过上面命令安装完redis,会自动启动redis服务,通过ps命令确认:

redis 2717 1 0 13:24 ? 00:00:00 /usr/bin/redis-server 127.0.0.1:6379

可以看到redis默认工作在本地主机的6379端口上。

消息的订阅:subscribe **

发布消息:publish ** msg

redis发布-订阅的客户端编程

redis支持多种不同的客户端编程语言,例如Java对应jedis、php对应phpredis、C++对应的则是
hiredis。下面是安装hiredis的步骤:

1. git clone https://github.com/redis/hiredis 从github上下载hiredis客户端,进行源码 编译安装       

tony@tony-virtual-machine:~/github$ git clone

https://github.com/redis/hiredis

正克隆到 'hiredis'... remote: Enumerating objects: 3261, done.

^C收对象中: 83% (2707/3261), 876.01 KiB | 59.00 KiB/s

2. cd hiredis

3. make 

tony@tony-virtual-machine:~/github/hiredis$ make

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb net.c

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb hiredis.c

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb sds.c

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb async.c

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb read.c

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb sockcompat.c

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb sslio.c

cc -shared -Wl,-soname,libhiredis.so.0.14 -o libhiredis.so net.o hiredis.o sds.o async.o read.o sockcompat.o sslio.o

ar rcs libhiredis.a net.o hiredis.o sds.o async.o read.o sockcompat.o sslio.o

cc -std=c99 -pedantic -c -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwritestrings -Wno-missing-field-initializers -g -ggdb test.c

cc -O3 -fPIC -Wall -W -Wstrict-prototypes -Wwrite-strings -Wno-missingfield-initializers -g -ggdb -o hiredis-test test.o libhiredis.a

Generating hiredis.pc for pkgconfig...

tony@tony-virtual-machine:~/github/hiredis$

 redis代码实现

redis.hpp 

#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指定的通道subscribe订阅消息
    bool subscribe(int channel);

    // 向redis指定的通道unsubscribe取消订阅消息
    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 *_subcribe_context;

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

#endif

redis.cpp 

 

#include "redis.hpp"
#include <iostream>
using namespace std;

Redis::Redis()
    : _publish_context(nullptr), _subcribe_context(nullptr)
{
}

Redis::~Redis()
{
    if (_publish_context != nullptr)
    {
        redisFree(_publish_context);
    }

    if (_subcribe_context != nullptr)
    {
        redisFree(_subcribe_context);
    }
}

bool Redis::connect()
{
    // 负责publish发布消息的上下文连接
    _publish_context = redisConnect("127.0.0.1", 6379);
    if (nullptr == _publish_context)
    {
        cerr << "connect redis failed!" << endl;
        return false;
    }

    // 负责subscribe订阅消息的上下文连接
    _subcribe_context = redisConnect("127.0.0.1", 6379);
    if (nullptr == _subcribe_context)
    {
        cerr << "connect redis failed!" << endl;
        return false;
    }

    // 在单独的线程中,监听通道上的事件,有消息给业务层进行上报
    thread t([&]() {
        observer_channel_message();
    });
    t.detach();

    cout << "connect redis-server success!" << endl;

    return true;
}

// 向redis指定的通道channel发布消息
bool Redis::publish(int channel, string message)
{
    redisReply *reply = (redisReply *)redisCommand(_publish_context, "PUBLISH %d %s", channel, message.c_str());
    if (nullptr == reply)
    {
        cerr << "publish command failed!" << endl;
        return false;
    }
    freeReplyObject(reply);
    return true;
}

// 向redis指定的通道subscribe订阅消息
bool Redis::subscribe(int channel)
{
    // SUBSCRIBE命令本身会造成线程阻塞等待通道里面发生消息,这里只做订阅通道,不接收通道消息
    // 通道消息的接收专门在observer_channel_message函数中的独立线程中进行
    // 只负责发送命令,不阻塞接收redis server响应消息,否则和notifyMsg线程抢占响应资源
    if (REDIS_ERR == redisAppendCommand(this->_subcribe_context, "SUBSCRIBE %d", channel))
    {
        cerr << "subscribe command failed!" << endl;
        return false;
    }
    // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
    int done = 0;
    while (!done)
    {
        if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done))
        {
            cerr << "subscribe command failed!" << endl;
            return false;
        }
    }
    // redisGetReply

    return true;
}

// 向redis指定的通道unsubscribe取消订阅消息
bool Redis::unsubscribe(int channel)
{
    if (REDIS_ERR == redisAppendCommand(this->_subcribe_context, "UNSUBSCRIBE %d", channel))
    {
        cerr << "unsubscribe command failed!" << endl;
        return false;
    }
    // redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
    int done = 0;
    while (!done)
    {
        if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done))
        {
            cerr << "unsubscribe command failed!" << endl;
            return false;
        }
    }
    return true;
}

// 在独立线程中接收订阅通道中的消息
void Redis::observer_channel_message()
{
    redisReply *reply = nullptr;
    while (REDIS_OK == redisGetReply(this->_subcribe_context, (void **)&reply))
    {
        // 订阅收到的消息是一个带三元素的数组
        if (reply != nullptr && reply->element[2] != nullptr && reply->element[2]->str != nullptr)
        {
            // 给业务层上报通道上发生的消息
            _notify_message_handler(atoi(reply->element[1]->str) , reply->element[2]->str);
        }

        freeReplyObject(reply);
    }

    cerr << ">>>>>>>>>>>>> observer_channel_message quit <<<<<<<<<<<<<" << endl;
}

void Redis::init_notify_handler(function<void(int,string)> fn)
{
    this->_notify_message_handler = fn;
}

chatservice.hpp

    // 连接redis服务器
    if (_redis.connect())
    {
        // 设置上报消息的回调
        _redis.init_notify_handler(std::bind(&ChatService::handleRedisSubscribeMessage, this, _1, _2));
    }

一对一聊天,群组消息

先在model表里查是否在线,在线的话通过redis publish发送消息,不在线储存离线消息

   // 查询toid是否在线 
    User user = _userModel.query(toid);
    if (user.getState() == "online")
    {
        _redis.publish(toid, js.dump());
        return;
    }

    // toid不在线,存储离线消息
    _offlineMsgModel.insert(toid, js.dump());
}


            // 查询toid是否在线 
            User user = _userModel.query(id);
            if (user.getState() == "online")
            {
                _redis.publish(id, js.dump());
            }
            else
            {
                // 存储离线群消息
                _offlineMsgModel.insert(id, js.dump());
            }

// 从redis消息队列中获取订阅的消息
void ChatService::handleRedisSubscribeMessage(int userid, string msg)
{
    lock_guard<mutex> lock(_connMutex);
    auto it = _userConnMap.find(userid);
    if (it != _userConnMap.end())
    {
        it->second->send(msg);
        return;
    }

最终测试:

开启俩个不同的服务器,俩个客户端分别登录在每一个服务器上, 

 群聊天,一对一聊天

服务器现在已经完成集群,不管用户在哪台服务器上登录,他们的聊天业务是没有任何功能的,对于客户端不需要了解是哪台服务器给它提供服务的。 

gitee链接

https://gitee.com/kung-pao-chicken2/chatserver.git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C集群聊天服务器视频下载是指使用C语言进行开发的集群聊天服务器程序,该程序具备视频下载功能。集群聊天服务器是一种用于群组通信的服务器程序,可以实现多个用户之间的文本、语音、视频等多种形式的交流。 在C语言中开发集群聊天服务器视频下载功能需要使用网络编程相关的API,如Socket编程。通过Socket编程,可以在服务器端建立一个监听端口,等待客户端的连接请求。一旦客户端连接成功,服务器可以和客户端进行双向的数据传输。 为了支持视频下载功能,服务器端需要提供相应的视频资源,并通过Socket将视频内容传输给客户端。客户端在接收到视频数据后,可以将数据保存成视频文件。视频文件可以通过常见的视频播放器进行播放。 在集群聊天服务器中,可以通过添加一些额外的功能来实现视频下载功能的完善。例如,可以提供视频分类和搜索功能,让用户可以更方便地找到自己需要的视频资源。还可以加入断点续传的功能,当下载过程中出现网络中断或其他异常情况时,可以在恢复连接后继续下载,避免重新下载整个视频文件。 总结起来,C集群聊天服务器视频下载是一种基于C语言开发的服务器程序,它可以实现多人聊天、视频传输和视频下载等功能。通过Socket编程,服务器和客户端之间可以进行双向的数据传输,从而实现视频数据的传输和保存。同时,可以通过添加一些额外的功能来提高视频下载的体验,如视频分类、搜索和断点续传等功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值