负载均衡器的引入
在服务器集群的环境当中,我们引入负载均衡器帮我们统一接受客户端的请求,根据配置的负载算法来把客户端请求分发到业务服务器上。用户使用客户端时候不用选择去链接哪台服务器,客户端默认链接的都是负载均衡器。负载均衡器的角色就是一个中间的桥梁,连接客户端和服务器之间的通信。客户端的请求与服务端的响应都是要经过负载均衡器的。
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; }
最终测试:
开启俩个不同的服务器,俩个客户端分别登录在每一个服务器上,
群聊天,一对一聊天
服务器现在已经完成集群,不管用户在哪台服务器上登录,他们的聊天业务是没有任何功能的,对于客户端不需要了解是哪台服务器给它提供服务的。