c++聊天集群服务器

项目地址:https://gitee.com/cai-jinxiang/chat-server

网络模块:采用muduo库完成,解耦了网络与业务模块
服务层:使用c++11技术,设计了消息id及回调函数的绑定,服务器和客户端
数据存储层:使用mysql存储消息,用户信息,离线消息,群聊消息等

负载均衡模块:Nginx的基于TCP的负载均衡模块,长连接
redis的发布订阅功能,作为消息队列,服务器中不同用户的通信。

c++聊天集群服务器_第1张图片

使用的技术

数据库连接池

  • MySQL数据库编程
  • 懒汉式单例模式
  • 智能指针,自定义删除器(lambda表达式)
  • 基于CAS的原子整形
    通过queue队列管理数据库的连接,当系统需要访问数据库时,向池中申请连接,对数据库进行操作
    开启一个线程负责创建连接(连接队列空时)
    一个线程负责销毁连接(空闲连接过多时)

Json

一种轻量级的数据交换格式,只包含一个json.hpp
使用Json格式作为客户端和服务端之间的消息传递格式

  1. 数据包装为一个Json对象,
  2. Json.dump()将Json对象序列化为一个string格式,传输给对方
  3. 对方接受到string格式,json::parse(recvBuf),将recvBuf反序列化为一个Json对象

muduo网络库

  • Tcp网络编程库,支持Reactor模型
  • 只需要简单组合好连接回调函数和消息回调函数,就可以提高一个高效的网络服务

CMake

一个项目通常的目录
bin:生成的可执行文件
lib:生成的库文件
include:头文件
src:源文件
build:编译产生的中间文件
example:示例文件
thridparty:第三方库的源码文件,比如json.hpp
CMakeLists.txt
autobuild.sh :一键编译,执行的cmake文件

在做项目时,在项目根目录创建 build文件,把cmake文件放进去,并在cmake设置文件中,设置可执行文件的路径为根目录下的bin

nginx

  • 配置tcp负载均衡
  • 多个服务器都被nginx代理
  • 所有的用户都连接一个nginx的端口,nginx负责均衡的分发给不同的服务器

redis的发布订阅功能

c++聊天集群服务器_第2张图片

使用redis的发布订阅功能,在多服务器中进行通信
通过hredis库进行c++的redis编码

  • 服务端连接成功redis后,开启线程监听redis发送的消息
// 在独立线程中接收订阅通道中的消息
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;
}
  • 登录成功后,服务器会向redis服务 订阅指定的消息,用户id的消息.
    redisAppendCommand将消息存入缓存
    redisBufferWrite 将缓存消息发送redis
    不直接使用redisCommand是因为,redisCommand会阻塞线程等待redis的回应,
// 向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服务器
// 向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指定的通道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;
}

优化方向

消息加密

  • 使用非对称加密 加密传输 对称加密密钥

保证消息按序显示

  • 本服务器只在局域网上测试,网络状况良好,一般不会出现消息到达顺序与发送顺序不一致,但在真实网络情况下,是可能出现的
  • 解决方案:
    • 通过给消息加上一个循环使用的序列号,类似tcp的seq。
    • 加上这个seq后还可以实现撤回消息的功能
  • 加时间戳是无法保证消息顺序到达的,比如到达了一个时分秒为15:14:02的消息,此时也许有个15:14:01的消息还未到达,但是客户端显然不会知道,也就无法保证消息按序显示

chatServer如何感知用户掉线

  • 目前来说只有client主动下线

  • 添加心跳机制,使用UDP协议绑定一个端口,负责心跳,让在线用户每隔1s发送一个心跳包,当超过ns没有心跳包时,可以认为用户下线。

  • tcp有个保活机制,

    1. 但是tcp在传输层,用户掉线在应用层会有很多的资源需要释放。
    2. 当用户的业务出现问题时,对于服务器来说相当于下线,但是如果使用tcp的保活机制,会认为用户仍然在线,因此使用tcp保活机制不合适。
      c++聊天集群服务器_第3张图片

确保消息发送成功(消息发送不成功,用户能知道)

问题:在应用层中,只是通过send(fd, buf, sizeof(buf), 0)的返回值,来判断是否发送成功,但是,send的成功只是将buf的内容拷贝到内核的TCP缓冲区,TCP虽然有超时重传机制,但是如果一直接收不到对方的ACK超过一定次数后,就不会再传了。
解决方案:

  • 在应用层中实现类似TCP的功能,发送的消息存储下来加入seq字段,接受到消息后发送ack确认,建立一个应用层的超时重传机制。可以与心跳机制建立交互,例如当心跳失败时直接发送失败。

历史消息存储

  • 本地存储:文件夹、SQLite
  • 云存储

你可能感兴趣的:(服务器)