当前位置: 首页 > news >正文

【项目】微服务及时通讯系统:编写核心类

文章目录

  • 前言
  • 1. 核心数据结构
    • 1.1 用户信息
    • 1.2 会话信息
    • 1.3 消息信息
  • 2. 建立目录
  • 3. 编写代码
    • 3.1 用户信息
    • 3.2 会话信息
    • 3.3 消息信息
    • 3.4 工具函数
  • 4. data.h 完整代码
  • 总结

前言

在构建现代微服务架构的即时通讯系统时,核心数据结构的设计是至关重要的。它们不仅决定了系统的性能和可扩展性,而且也影响着用户交互的直观性和便捷性。本文将深入探讨即时通讯系统中的三个核心数据结构:用户信息、会话信息和消息信息,以及它们是如何在C++和Qt框架下实现的。通过详细解析这些数据结构的设计和实现,我们希望能够为开发者提供一个清晰的指导,帮助他们构建高效、稳定且用户友好的即时通讯应用。

1. 核心数据结构

1.1 用户信息

1.2 会话信息

用户和用户之间,聊天会话:
例如:

用户A:有2个好友:B、C
此时A可以和B单聊,也能和C单聊(2个聊天会话)
此时这个群组,也对应一个聊天会话

聊天程序中,会话的生命周期,是比较的;一直持续到把对方好友删除/退出群组才会随之销毁。

1.3 消息信息

  1. 文本消息
  2. 图片消息
  3. 文件消息
  4. 语音消息

2. 建立目录

在文件中显示
在这里插入图片描述
创建一个文件夹存放目录
在这里插入图片描述
新建一个文件
在这里插入图片描述
添加进来
在这里插入图片描述
在这里插入图片描述
此时cmake 就自动添加上了
在这里插入图片描述

qt_add_executable:指定编译qt需要依赖哪些文件(源代码,肯定是要依赖的)

关于命名空间的约定(这种约定,仅限于当前项目):
如果代码所在的文件,就是在项目的顶层目录中,此时就直接使用全局命名空间(不手动指定)
在这里插入图片描述
如果代码所在的文件,在某个子目录中;此时,就指定一个和目录名字相同的命名空间。
在这里插入图片描述

使代码中的命名空间的结构和文件在目录中结构一致。(尤其是在目录结构更复杂,嵌套多层)

3. 编写代码

3.1 用户信息


/// 用户信息
class UserInfo {
public:QString userId = "";         // 用户编号QString nickname = "";       // 用户昵称QString description = "";    // 用户签名QString phone = "";          // 手机号码QIcon avatar;                // 用户头像
};
QString userId;

使用字符串的方式来作为id,可以有更灵活的方式来生成。(也为了能够适应分布式后端)

mysql 数据库,支持自增主键:
如果是单个节点的 mysql,用上述方式没有任何问题
但是,如果是多个节点的分布式
mysql,就无法使用整数自增组件了 分布式 mysql 下,很可能需要针对用户信息“分库分表”

后续中可以通过例如:uuid 这样的方式,或者是 雪花算法这样的方式 来生成分布式系统中唯一id

3.2 会话信息


/// 会话信息
class ChatSessionInfo {QString chatSessionId = "";      // 会话编号QString chatSessionName = "";    // 会话名字,如果是会话是单聊,名字就是对方的昵称;如果是群聊,名字就是群聊的名称Message lastMessage;             // 表示最新的消息QIcon avatar;                    // 会话头像,如果会话是单聊,头像就是对方的头像;如果是群聊,头像群聊的头像QString userId = "";             // 对于单聊来说,表示对方的用户 id,对于群聊设置为 ""
};
ChatSessionInfo

前面谈到的“会话”都是针对 聊天过程中的,组织消息的 会话。
后面还会涉及到,客户端连接到服务器之后,也有一个“登录”用到的会话

在这里插入图片描述

Message lastMessage;

这个内容就是为了在会话列表中,能够起到“显示-提示”这样的效果。
在这里插入图片描述
针对会话信息来说:

一个会话,里面其实是可以包含多个用户的
这个会话里具体有哪些用户,
后续会通过单独的方式进行组织管理。

此处列出 QString userId;表示的含义是:

  1. 如果会话是单聊会话,此时 userId 表示“对方”的用户 id
  2. 如果会话是群聊会话,此时 userId 设为 “”,后续通过其他的方式来吧完整的用户id列表拿到

会话 - 消息 是 “一对多” 这样的关系。
消息:会话id。

 QByteArray content;         // 消息发送的正文内容

如果是 文本消息,正文就是一个字符串
如果是 图片,文件,语音消息,正文就是一个“二进制序列”

| 在C/C++ 中没有 byte 这样的类型,表示 byte 都是拿 char / unsigned char 凑合一下

| char / unsigned char
正常来说,一个char(字符) 不一定是一个字节

  • 对于 应用 ascii 来说,一个字符就是一个字节
  • 对于 中文 gbk 编码来说,一个字符就是2个字节
  • 对于 中文 utf8 编码来说,一个字符就是3个字节

由于 C/C++ 有点太老了,对于这一块的支持,比较有限
C++ 中,一个 char 就是固定的一个字节了。

| 比方说,C++中:std::string name = "张三";
C++ 中,通过代码取出“三”这个汉字,老麻烦了!

  1. 先判定是那种编码方式
  2. 计算第二个汉字 所属的范围
  3. 取字符串子串

| 像其他主流语言,Java/Python 之类的
string s = "张三";
s.charAt(1) => "三"

| 相比之下,Qt做了更好的处理;QString 就对上述情况处理的更好了。 Qt 也明确区分了 “字节” 和 “字符”

QString fileId;             // 文件的身份标识,当类型为 文件,图片,语言 的时候,才有效;当消息为文本,则为""

文件/图片/语言 这些消息,体积可能是比较大的!(网络带宽)

  • 一旦一个聊天会话中,包含多个上述这样的消息,就会使从服务器消息列表这样的操作,变得非常低效。
    一般的做法,都是“获取消息列表”,只是拿到文件/图片/语言 消息的 filed 等到客户端得到“消息列表”之后,再更具拿到的filed,给服务器发送额外的请求,获取文件内容。(化整为零)
QString fileName;           // 文件名称,只是当消息类型为 文件消息时, 才有效,其他消息均为 ""

虽然图片/语音,这两个也是“文件”但是文件名不需要显示到界面上。
对于文件消息,希望界面上显示“文件名”,点击之后可以进行“另存为”这样的操作。

3.3 消息信息


/// 消息信息
enum MessageType {TEXT_TYPE,      // 文本消息IMAGE_TYPE,     // 图片消息FILE_TYPE,      // 文件消息SPEECH_TYPE,    // 语音消息
};class Message {
public:QString messageId = "";          // 消息的编号QString chatSessionId = "";      // 消息所属会话的编号QString time = "";               // 消息时间,通过“格式化”时间的方式来表示, 形如:06-07 12:00:00MessageType messageType = TEXT_TYPE; // 消息类型UserInfo sender;                 // 发送者的信息QByteArray content = "";         // 消息发送的正文内容QString fileId = "";             // 文件的身份标识,当类型为 文件,图片,语言 的时候,才有效;当消息为文本,则为""QString fileName = "";           // 文件名称,只是当消息类型为 文件消息时, 才有效,其他消息均为 ""// 此处 extraInfo 目前只是再消息类型为文件消息时,作为“文件名”补充。static Message makeMessage(MessageType messageType, const QString& chatSessionId, const UserInfo& sender, const QByteArray& content, const QString& extraInfo){if (messageType == TEXT_TYPE) {return makeTextMessage(chatSessionId, sender, content);} else if (messageType == IMAGE_TYPE) {return makeImageMessage(chatSessionId, sender, content);} else if (messageType == FILE_TYPE) {return makeFileMessage(chatSessionId, sender, content, extraInfo);} else if (messageType == SPEECH_TYPE) {return makeSpeechMessage(chatSessionId, sender, content);} else {// 触发了未知消息类型return Message();}}private:// 通过这个方法生成一个唯一的 messageIdstatic QString makeId() {return "M" + QUuid::createUuid().toString().sliced(25, 12);}static Message makeTextMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content) {Message message;// 此处需要确保,设置的 messageId 是 “唯一” 的message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime()); // 生成一个格式化时间message.content = content;message.messageType = TEXT_TYPE;// 对于文本消息来说,这两个属性不使用,设为""message.fileId = "";message.fileName = "";return message;}static Message makeImageMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content) {Message message;// 此处确保,设置的 messageId 是“唯一”的message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime());message.content = content;message.messageType = IMAGE_TYPE;// fileId 后续使用的时候进一步设置message.fileId = "";// fileName 不使用,直接设为“”message.fileName = "";return message;}static Message makeFileMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content, const QString& fileName) {Message message;message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime());message.content = content;message.messageType = FILE_TYPE;// fileId 后续使用的时候进一步设置message.fileId = "";message.fileName = fileName;return message;}static Message makeSpeechMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content) {Message message;message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime());message.content = content;message.messageType = SPEECH_TYPE;// fileId 后续使用的时候进一步设置message.fileId = "";// fileName 不使用,直接设为“”message.fileName = "";return message;}
};
  • 工厂方法:(工厂模式)
    解决 C++/Java 等语言中,构造函数,不够用的问题。

虽然平时构建对象,都是通过对 构造函数 来完成。
如果有不同的方式来构造对象,此时构造函数就不太够用了。

比方说 “点”

class Point {
public:Point(double x, double y);  // 直角坐标的方式构造Point(double r, double a);	// 极坐标的构造方式
}

够高函数想要提供不同版本,必须要 “重载” 要求 函数名 相同(都是一样)
参数的个数/类型不同

使用普通的函数来实现不同的构造方式;普通函数,函数名可以随便起,也就不再受到“重载”的约束了。

这里说“普通”,一般都要使用 static 修饰的 静态函数。(调用已有的构造函数,根据不同的需求,进一步的实现初始化的细节)

相比于 UserInfoChatSessionInfo, Message 是更需要“工厂模式”的,Message 需要支持多种构造方式;

  1. 文本消息
  2. 图片消息
  3. 语音消息
  4. 文件消息

messageId 是一个“唯一”这样的内容
UUID 这个东西,背后是一套算法,通过这个算法,就能生成“全球唯一的身份标识”,Qt对这个算法也是有封装的。
在这里插入图片描述
这一串,其实是16进制的整数,实际开发中,为了提高“可读性”也可以截取uuid中一部分来进行使用。

file.write(content);
file.flush();
file.close();

flush: 刷新缓冲区

3.4 工具函数


/// 工具函数,后续很多模块可能都要用到
static inline QString getFileName(const QString& path) {QFileInfo fileInfo(path);return fileInfo.fileName();
}// 封装一个“宏”作为打印日志的方式
#define TAG QString("[%1:%2]").arg(model::getFileName(__FILE__), QString::number(__LINE__))// qDebug 打印字符串的时候,就会自动加上" "
#define LOG() qDebug().noquote() << TAG// 要求函数的定义如果写在 .h 中,必须加 static 或者 inline(当然两个都加也可以),避链接阶段出现“函数重定义”的问题
static inline QString formatTime(int64_t timestamp) { // 为了防止在2038年溢出,用64位整数// 先把时间戳,转换成QDateTime 对象QDateTime dateTime =  QDateTime::fromSecsSinceEpoch(timestamp);// 把 QDateTime 对象转换成“格式时间”return dateTime.toString("MM-dd HH:mm:ss");
}// 通过这个函数得到 秒级 的时间
static inline int64_t getTime() {return QDateTime::currentMSecsSinceEpoch();
}// 根据 QByteArray, 转成 QIcon
static inline QIcon makeIcon(const QByteArray& byteArray) {QPixmap pixmap;pixmap.loadFromData(byteArray);QIcon icon(pixmap);return icon;
}// 读写文件操作
// 从读取文件中,读取所有的二进制内容,得到一个 QByteArray
static inline QByteArray loadFileToByteArray(const QString& path) {QFile file(path);bool ok = file.open(QFile::ReadOnly);if (!ok) {qDebug() << "文件打开失败";return QByteArray();}QByteArray content = file.readAll();file.close();return content;
}// 把 QByteArray 中的内容,写入到某个指定的文件夹里
static inline void writeByteArryToFile(const QString& path, const QByteArray& content) {QFile file(path);bool ok = file.open(QFile::WriteOnly);if (!ok) {qDebug() << "文件打开失败";return;}file.write(content);file.flush();   // 刷新缓冲区file.close();
}

4. data.h 完整代码

#pragma once
#include <QString>
#include <QIcon>
#include <QUuid>
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QDebug>// 创建命名空间
namespace model {
/// 工具函数,后续很多模块可能都要用到
static inline QString getFileName(const QString& path) {QFileInfo fileInfo(path);return fileInfo.fileName();
}// 封装一个“宏”作为打印日志的方式
#define TAG QString("[%1:%2]").arg(model::getFileName(__FILE__), QString::number(__LINE__))// qDebug 打印字符串的时候,就会自动加上" "
#define LOG() qDebug().noquote() << TAG// 要求函数的定义如果写在 .h 中,必须加 static 或者 inline(当然两个都加也可以),避链接阶段出现“函数重定义”的问题
static inline QString formatTime(int64_t timestamp) { // 为了防止在2038年溢出,用64位整数// 先把时间戳,转换成QDateTime 对象QDateTime dateTime =  QDateTime::fromSecsSinceEpoch(timestamp);// 把 QDateTime 对象转换成“格式时间”return dateTime.toString("MM-dd HH:mm:ss");
}// 通过这个函数得到 秒级 的时间
static inline int64_t getTime() {return QDateTime::currentMSecsSinceEpoch();
}// 根据 QByteArray, 转成 QIcon
static inline QIcon makeIcon(const QByteArray& byteArray) {QPixmap pixmap;pixmap.loadFromData(byteArray);QIcon icon(pixmap);return icon;
}// 读写文件操作
// 从读取文件中,读取所有的二进制内容,得到一个 QByteArray
static inline QByteArray loadFileToByteArray(const QString& path) {QFile file(path);bool ok = file.open(QFile::ReadOnly);if (!ok) {qDebug() << "文件打开失败";return QByteArray();}QByteArray content = file.readAll();file.close();return content;
}// 把 QByteArray 中的内容,写入到某个指定的文件夹里
static inline void writeByteArryToFile(const QString& path, const QByteArray& content) {QFile file(path);bool ok = file.open(QFile::WriteOnly);if (!ok) {qDebug() << "文件打开失败";return;}file.write(content);file.flush();   // 刷新缓冲区file.close();
}
/// 用户信息
class UserInfo {
public:QString userId = "";         // 用户编号QString nickname = "";       // 用户昵称QString description = "";    // 用户签名QString phone = "";          // 手机号码QIcon avatar;                // 用户头像
};
/// 消息信息
enum MessageType {TEXT_TYPE,      // 文本消息IMAGE_TYPE,     // 图片消息FILE_TYPE,      // 文件消息SPEECH_TYPE,    // 语音消息
};class Message {
public:QString messageId = "";          // 消息的编号QString chatSessionId = "";      // 消息所属会话的编号QString time = "";               // 消息时间,通过“格式化”时间的方式来表示, 形如:06-07 12:00:00MessageType messageType = TEXT_TYPE; // 消息类型UserInfo sender;                 // 发送者的信息QByteArray content = "";         // 消息发送的正文内容QString fileId = "";             // 文件的身份标识,当类型为 文件,图片,语言 的时候,才有效;当消息为文本,则为""QString fileName = "";           // 文件名称,只是当消息类型为 文件消息时, 才有效,其他消息均为 ""// 此处 extraInfo 目前只是再消息类型为文件消息时,作为“文件名”补充。static Message makeMessage(MessageType messageType, const QString& chatSessionId, const UserInfo& sender, const QByteArray& content, const QString& extraInfo){if (messageType == TEXT_TYPE) {return makeTextMessage(chatSessionId, sender, content);} else if (messageType == IMAGE_TYPE) {return makeImageMessage(chatSessionId, sender, content);} else if (messageType == FILE_TYPE) {return makeFileMessage(chatSessionId, sender, content, extraInfo);} else if (messageType == SPEECH_TYPE) {return makeSpeechMessage(chatSessionId, sender, content);} else {// 触发了未知消息类型return Message();}}private:// 通过这个方法生成一个唯一的 messageIdstatic QString makeId() {return "M" + QUuid::createUuid().toString().sliced(25, 12);}static Message makeTextMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content) {Message message;// 此处需要确保,设置的 messageId 是 “唯一” 的message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime()); // 生成一个格式化时间message.content = content;message.messageType = TEXT_TYPE;// 对于文本消息来说,这两个属性不使用,设为""message.fileId = "";message.fileName = "";return message;}static Message makeImageMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content) {Message message;// 此处确保,设置的 messageId 是“唯一”的message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime());message.content = content;message.messageType = IMAGE_TYPE;// fileId 后续使用的时候进一步设置message.fileId = "";// fileName 不使用,直接设为“”message.fileName = "";return message;}static Message makeFileMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content, const QString& fileName) {Message message;message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime());message.content = content;message.messageType = FILE_TYPE;// fileId 后续使用的时候进一步设置message.fileId = "";message.fileName = fileName;return message;}static Message makeSpeechMessage(const QString& chatSessionId, const UserInfo& sender, const QByteArray& content) {Message message;message.messageId = makeId();message.chatSessionId = chatSessionId;message.sender = sender;message.time = formatTime(getTime());message.content = content;message.messageType = SPEECH_TYPE;// fileId 后续使用的时候进一步设置message.fileId = "";// fileName 不使用,直接设为“”message.fileName = "";return message;}
};
/// 会话信息
class ChatSessionInfo {QString chatSessionId = "";      // 会话编号QString chatSessionName = "";    // 会话名字,如果是会话是单聊,名字就是对方的昵称;如果是群聊,名字就是群聊的名称Message lastMessage;             // 表示最新的消息QIcon avatar;                    // 会话头像,如果会话是单聊,头像就是对方的头像;如果是群聊,头像群聊的头像QString userId = "";             // 对于单聊来说,表示对方的用户 id,对于群聊设置为 ""
};} // end model

总结

本文详细介绍了微服务即时通讯系统中的核心数据结构,包括用户信息、会话信息和消息信息,以及它们在C++和Qt环境下的具体实现。我们首先对每个数据结构的功能和属性进行了概述,然后通过代码示例展示了如何定义这些结构和相关的工具函数。特别地,我们使用了工厂模式来简化消息对象的创建过程,以支持不同类型的消息构造。此外,文中还探讨了UUID生成机制,确保了消息ID的唯一性,这对于分布式系统尤为重要。

通过本文的阅读,开发者应该能够理解并实现一个高效的消息处理系统,它不仅能够处理文本消息,还能够处理图片、文件和语音等多种类型的消息。此外,通过合理组织代码和使用适当的设计模式,可以提高代码的可维护性和可扩展性,为未来的功能扩展打下坚实的基础。最后,本文提供的代码示例和设计思路可以作为构建即时通讯系统的一个参考起点,帮助开发者快速进入项目开发阶段。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 拼车系统开发方案
  • Android about event log
  • 《通义千问AI落地—中》:前端实现
  • C语言程序设计(初识C语言后部分)
  • Compose(7)交互和动画
  • 第二节:Nodify 添加节点到编辑器中
  • 【GH】【EXCEL】P3: Set Conditional Formatting To Excel Data By Gh
  • Vue.js学习笔记(七)使用sortablejs或el-table-draggable拖拽ElementUI的el-table表格组件
  • “太猴看了”车衣引发热议:深蓝要和“黑悟空”跨界互动?
  • 【解压即玩】最终幻想7 重制版中文+预购特典+全DLC,难忘的一作
  • Facebook的区块链战略:如何在社交媒体中实现去中心化
  • Tomcat类加载机制详解
  • java多线程(七)AQS(AbstractQueuedSynchronizer)技术解析:以赛跑起跑场景为例
  • 【Python机器学习】NLP的部分实际应用
  • 企业数字化转型管控平台探索 ---基于流程的企业经络管理框架DEM
  • JavaScript 如何正确处理 Unicode 编码问题!
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • 11111111
  • Bootstrap JS插件Alert源码分析
  • CoolViewPager:即刻刷新,自定义边缘效果颜色,双向自动循环,内置垂直切换效果,想要的都在这里...
  • Docker下部署自己的LNMP工作环境
  • ECS应用管理最佳实践
  • Git的一些常用操作
  • Go 语言编译器的 //go: 详解
  • JavaScript创建对象的四种方式
  • Java编程基础24——递归练习
  • JAVA多线程机制解析-volatilesynchronized
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • JWT究竟是什么呢?
  • laravel with 查询列表限制条数
  • NLPIR语义挖掘平台推动行业大数据应用服务
  • PHP 的 SAPI 是个什么东西
  • python 学习笔记 - Queue Pipes,进程间通讯
  • Python利用正则抓取网页内容保存到本地
  • Ruby 2.x 源代码分析:扩展 概述
  • session共享问题解决方案
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • 蓝海存储开关机注意事项总结
  • 悄悄地说一个bug
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • $$$$GB2312-80区位编码表$$$$
  • (6) 深入探索Python-Pandas库的核心数据结构:DataFrame全面解析
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (翻译)terry crowley: 写给程序员
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (附源码)spring boot球鞋文化交流论坛 毕业设计 141436
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133
  • (免费领源码)python#django#mysql公交线路查询系统85021- 计算机毕业设计项目选题推荐
  • (七)Java对象在Hibernate持久化层的状态
  • (四) Graphivz 颜色选择
  • (四)opengl函数加载和错误处理
  • (学习总结16)C++模版2
  • (一)Spring Cloud 直击微服务作用、架构应用、hystrix降级
  • (转)为C# Windows服务添加安装程序