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

2024-08-24 09:44

本文主要是介绍【项目】微服务及时通讯系统:编写核心类,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 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的唯一性,这对于分布式系统尤为重要。

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

这篇关于【项目】微服务及时通讯系统:编写核心类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1102127

相关文章

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

利用Python快速搭建Markdown笔记发布系统

《利用Python快速搭建Markdown笔记发布系统》这篇文章主要为大家详细介绍了使用Python生态的成熟工具,在30分钟内搭建一个支持Markdown渲染、分类标签、全文搜索的私有化知识发布系统... 目录引言:为什么要自建知识博客一、技术选型:极简主义开发栈二、系统架构设计三、核心代码实现(分步解析

Spring Boot项目中结合MyBatis实现MySQL的自动主从切换功能

《SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能》:本文主要介绍SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能,本文分步骤给大家介绍的... 目录原理解析1. mysql主从复制(Master-Slave Replication)2. 读写分离3.

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1

一文教你如何将maven项目转成web项目

《一文教你如何将maven项目转成web项目》在软件开发过程中,有时我们需要将一个普通的Maven项目转换为Web项目,以便能够部署到Web容器中运行,本文将详细介绍如何通过简单的步骤完成这一转换过程... 目录准备工作步骤一:修改​​pom.XML​​1.1 添加​​packaging​​标签1.2 添加

tomcat多实例部署的项目实践

《tomcat多实例部署的项目实践》Tomcat多实例是指在一台设备上运行多个Tomcat服务,这些Tomcat相互独立,本文主要介绍了tomcat多实例部署的项目实践,具有一定的参考价值,感兴趣的可... 目录1.创建项目目录,测试文China编程件2js.创建实例的安装目录3.准备实例的配置文件4.编辑实例的

springboot集成Deepseek4j的项目实践

《springboot集成Deepseek4j的项目实践》本文主要介绍了springboot集成Deepseek4j的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录Deepseek4j快速开始Maven 依js赖基础配置基础使用示例1. 流式返回示例2. 进阶

Linux上设置Ollama服务配置(常用环境变量)

《Linux上设置Ollama服务配置(常用环境变量)》本文主要介绍了Linux上设置Ollama服务配置(常用环境变量),Ollama提供了多种环境变量供配置,如调试模式、模型目录等,下面就来介绍一... 目录在 linux 上设置环境变量配置 OllamPOgxSRJfa手动安装安装特定版本查看日志在

Linux系统之主机网络配置方式

《Linux系统之主机网络配置方式》:本文主要介绍Linux系统之主机网络配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、查看主机的网络参数1、查看主机名2、查看IP地址3、查看网关4、查看DNS二、配置网卡1、修改网卡配置文件2、nmcli工具【通用