Chatroom 记录

4.1k words

Future 聊天室

功能

账号管理

  • 实现登录、注册、注销
  • 实现找回密码(提高)

好友管理

  • 实现好友的添加、删除、查询操作
  • 实现显示好友在线状态
  • 禁止不存在好友关系的用户间的私聊
  • 实现屏蔽好友消息
  • 实现好友间聊天

群管理

  • 实现群组的创建、解散
  • 实现用户申请加入群组
  • 实现用户查看已加入的群组
  • 实现群组成员退出已加入的群组
  • 实现群组成员查看群组成员列表
  • 实现群主对群组管理员的添加和删除
  • 实现群组管理员批准用户加入群组
  • 实现群组管理员/群主从群组中移除用户
  • 实现群组内聊天功能

聊天功能

  • 实现查看历史消息记录
  • 实现用户间在线聊天
  • 实现在线用户对离线用户发送消息,离线用户上线后获得通知
  • 实现在线发送文件
  • 实现在线用户对离线用户发送文件,离线用户上线后获得通知/接收
  • 实现后台发送文件
  • 实现用户在线时,消息的实时通知
    • 收到好友请求
    • 收到私聊
    • 收到加群申请

其他

  • 使用 C++编程语言
  • 使用 I/O 多路复用完成本项目
    • C++:Epoll ET 模式
  • 使用数据库完成数据存储
    • Redis 和 mysql
    • 历史消息采用redis做告诉缓存,mysql来存储大量历史消息
  • 数据库中数据的存储和取用使用序列化和反序列化完成(Json)
  • 支持大量客户端同时访问
  • 实现服务器日志,记录服务器的状态信息
  • C/S 双端均支持在 CLI/Web 自行指定 IP:Port
  • 实现具有高稳定性的客户端和服务器,防止在用户非法输入时崩溃或异常
    • 实现 TCP 心跳检测

遇到的问题

  • 在客户端因某些原因异常退出时,服务端将无法正常处理其他请求.

    • 开始时服务端没有将新连接的描述符设置为非阻塞模式,导致客户端异常退出时服务端的接收recv一直在阻塞着读取,且无法处理其他请求
  • 如何往线程池中传入成员函数.

    • auto task = []()
      { 
          类的成员函数 
      } 
      m_pool.submit(task);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30

      * `Tcp`传输为字节流传输,采用在每条消息前面加上`长度`来正确接收每条请求.
      * 在代码中往`redis`数据库中添加好友申请数据时,没有正确插入.
      * 由于好友申请当中包含时间,姓名等信息,中间含有**空格**,在redis的哈希表中无法插入.
      * 经过查阅在redis哈希表中,**空格**可以用`+`代替,在遇到`+`时会被转义为**空格**,或者存储为**十六进制**,但是这种方法在终端中的**空格**为灰色.

      * 在聊天界面如果输入带有`%`,服务器直接挂掉.

      * ```c++
      int redisAsyncContext::Lpush(const std::string &key, const std::string &value)
      {
      //std::string cmd = "lpush " + key + " " + value;
      this->m_reply = (redisReply *)redisCommand(this->m_connettion, "LPUSH %s %s", key.c_str(), value.c_str());
      // 检查 m_reply 是否为 NULL
      if (this->m_reply == NULL) {
      std::cerr << "Error: redisCommand returned NULL" << std::endl;
      return -1; // 或其他合适的错误值
      }

      // 检查 m_reply 类型
      if (this->m_reply->type != REDIS_REPLY_INTEGER) {
      std::cerr << "Error: Expected integer reply" << std::endl;
      freeReplyObject(this->m_reply);
      return -1; // 或其他合适的错误值
      }
      int num = this->m_reply->integer;
      freeReplyObject(this->m_reply);
      std::cout << "G" << std::endl;
      return num;
      }
    • 在执行下面两句是,%会被替换为cmd.c_str(),导致无法将消息插入历史记录表,导致出现错误.

      1
      2
      std::string cmd = "lpush " + key + " " + value;
      this->m_reply = (redisReply *)redisCommand(this->m_connettion, cmd.c_str());
    • 只要更改为下面即没有问题

      1
      this->m_reply = (redisReply *)redisCommand(this->m_connettion, "LPUSH %s %s", key.c_str(), value.c_str());
  • 在某次测试时,新注册账号id为0614897828,导致与该账号聊天的账号进入聊天界面时,0614897828发送的消息会在他人界面显示为通知消息.

    • 由于与人聊天时会在数据库中创建有排序集合来记录某人正在聊天的人的id.运行时通过查看发现,其余账号与0614897828聊天时,数据库中记录账号为614897828.
    • 更换为首位非0的id就不会出现该问题.
    • redis会将有序集合中的score字段字符串中的0去除.将从字符串的首位非0开始存储.
  • 在进入好友私聊界面时,客户端新建了来接收消息,同时主线程发消息,主线程通过判断用户是否键入Esc来退出聊天界面,在输入Esc时,接收消息线程无法正确回收,无法退出聊天界面.

    • 在接收消息线程中来判断标志位来结束该线程,主线程在键入Esc时,将标志位设置为false,但由于客户端的recv为阻塞,导致无法及时获取到标志位的更改.
    • 在键入Esc时,服务器向客户端发送退出信号,这样就可以正确及时的获取到标志位的更改,从而正确退出.
  • 在进行发送文件时,服务器会先将文件存储在本地,但是服务器存储的文件会多写入一些信息,导致接收文件大小偏大.

    • 由于在用户登陆进入之后,会有一个线程每5秒向服务器送送刷新请求,导致服务器会将刷新请求当作文件内容写入文件,导致接收文件不一致
    • 在进入文件收发菜单后,关闭实时刷新,在文件结束后,再将实时刷新打开.
  • 实现后台发送文件,由于会有实时刷新的存在,会导致文件发送不准确.

    • 在发送文件时,重新创建一个socket连接到服务器,在将发送这个文件交给另外一个线程
    • 初始时在用户选择发送文件时开始线程,但会导致主线程与发送线程会同时运行菜单.主线程继续循环菜单,而发送线程运行获取发送文件的信息。
      • 更改线程开始时间,在用户选择发送文件时,获取到发送文件信息后,在启动线程.
      • 新连接的socket发送文件后,服务器不用手动断开连接,服务器有心跳检测,会在一段时间后断开.

eventfd

详解

是一个Linux系统调用,也是一种进程间通信(IPC)机制,主要通过使用文件描述符生成和使用事件通知.

提供了一种在不同进程之间或同一进程内的线程之间的同步事件的方法.

异常处理

异常是程序在执行期间产生的问题。c++异常是指在程序运行时发生的特殊情况.

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

关键字:try、catch、throw

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常

  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块

1
2
3
4
5
6
7
8
9
10
11
12
13
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}

nlohmann::json

链接

SIGPIPE

SIGPIPE 是一种信号,当一个进程尝试向一个已经关闭的或不可写的管道或套接字写数据时,会触发这个信号。默认情况下,接收到 SIGPIPE 信号的进程会终止。这在网络编程中会导致一些问题,因为如果客户端断开连接,服务器进程在写入数据时会因为这个信号而意外退出。

在网络编程中,特别是使用套接字进行通信时,忽略 SIGPIPE 信号是一个常见的做法。这样可以防止进程因为 SIGPIPE 信号而意外终止。相反,程序可以通过检测 sendwrite 操作的返回值来处理错误,从而使程序更加健壮。

后续问题

  1. 初始客户端构思方面的缺陷,导致实时消息没有线程及时接收。

    当前处理方式为服务端将通知消息放入数据库当中,客户端在登陆之后,增加一个线程来定时执行刷新函数,刷新数据库中的通知消息

    可以优化为在客户端登陆之后,采用多线程的方式来处理相关操作.

  2. 目前只有在历史消息这一操作涉及到redis+mysql的处理方式.

    可以将一些重要信息也采用redis+mysql的处理方式(涉及到redis的存储方式,redis存储于内存当中,会造成数据丢失的情况)

    可以考虑将redis全部做为缓存的形式,将重要信息与历史消息类似,达到一定情况下放入mysql,使数据更加的持久化.

后续学习

  1. 继续学习epoll的相关内容.

  2. 了解其余网络框架,使得该项目的服务端更加的健壮.

  3. 了解消息队列的信息同步的问题.

  4. 熟悉零拷贝的过程,以及零拷贝的具体实现.

  5. 序列化的相关协议,json与其他序列化的区别、json的优点,以及为什么不用其他的序列化

  6. 数据库的深入学习,了解redis的缓存穿透、缓存雪崩、缓存击穿。加强对mysql数据库的学习.

  7. 加强对网络协议相关知识的学习.