核心数据结构分析开发
前言
在上一集我们已经完成了对客户端环境的搭建。这一集我们将正式开始对客户端代码的编写。基于MVC软件设计模式中,我们先来考虑我们的M(Model),所以我们这一集就先分析并开发关键的类。
分析
在整一个即时通讯系统中,我们最关键的就是数据。其中包括以下三类数据:
- 用户信息
- 会话信息
- 消息信息
其中用户信息毋庸置疑,包括用户编号、用户昵称、用户签名、手机号码、用户头像等相关信息。
其次,会话信息是指两人之间以及群聊的会话的信息。最后,消息信息就是包括但不限于文本、语音、文件、图片等信息。
分析之后我们会发现非常的简单,那么我就开始着手开始完成这三大信息类的代码编写吧!
用户信息
UserInfo
/*** @brief 消息信息*/
class UserInfo{
public:QString userId = ""; //用户编号QString nickname = ""; //用户昵称QString description = "";//用户签名QString phone = ""; //手机号码QIcon avatar; //用户头像
};
这里的userId使用的是字符串类型,如果是单节点的MySQL数据库我们是可以选择使用int类型,并让这个参数在MySQL当中作为自增主键,但在分布式系统下,这个做法就可能欠缺考虑了。在分布式系统当中,事物的一致性和完整性是一个挑战,数据的一致性也是一个巨大的挑战,在分布式系统中,我们通常会选择使用分布式ID生成器来生成
userId
,而不是依赖于MySQL的自增主键。
会话信息
ChatSessionInfo
/*** @brief 会话信息*/
class ChatSessionInfo{
public:QString chatSessionId = ""; //会话编号QString chatSessionName = ""; //会话名字,如果是单聊,就是对方的昵称,如果是群聊,那就是群聊名Message lastMessage; //表示最新消息QIcon avatar; //会话头像,如果是单聊,就是对方的头像,如果是群聊,则是群聊头像QString userId = ""; //表示单聊,就是对方的昵称,如果是群聊,则先设置为"",后续通过其他方式把完整的用户id列表拿到
};
这里的Message是消息信息,所以大家不要感到沮丧,下一个就是他!
同样chatSessionId不选择使用int类型!
我们平时使用的微信与QQ,每一个会话都含有对方的昵称,对方的头像以及最后的一次消息。
消息信息
消息信息就是最复杂的,它包括四种不同的消息类型,我们这里会使用一个枚举去表示他的消息类型。
MessageType
/*** @brief 消息信息*/
enum MessageType{TEXT_TYPE, //文本消息IMAGE_TYPE, //图片消息FILE_TYPE, //文件消息SPEECH_TYPE //语音消息
};
Message
/*** @brief 消息信息*/
class Message{
public:QString messageId = ""; //消息编号QString chatSessionId = ""; //消息所属会话编号QString time = ""; //消息的时间,通过格式化时间的方式来表示,eg:10-27 10:02:00MessageType messageType; //消息类型UserInfo sender; //发送者信息QByteArray content; //消息的正文内容QString fileId = ""; //文件的身份标识,消息类型是文件,图片,语音的时候才有效,文本消息时为空QString fileName = ""; //文件名称,只有当消息类型是文件时才生效,其他时候为空
}
由于我们这个消息类型有四种,所以我们就需要四种不同的构造函数!
但是我们发现了一个问题,如果使用构造函数,我们貌似无法构成重载!
我们的构造函数的形参是几乎一样的!
所以我们就要引入一个新的设计思路------工厂模式
我们直接来看代码!
makeMessage
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();}}
我们这里判断完消息类型之后就可以进入各自的方法进行构造出一个自己的消息实例。
makeTextMessage
static Message makeTextMessage(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 = TEXT_TYPE;message.fileId = "";message.fileName = "";return message;}
makeImageMessage
static Message makeImageMessage(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 = IMAGE_TYPE;message.fileId = "";message.fileName = "";return message;}
makeFileMessage
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;message.fileId = "";message.fileName = fileName;return message;}
makeSpeechMessage
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;message.fileId = "";message.fileName = "";return message;}
这就是工厂模式!能完美的解决我们构造函数不能够重载的好方法。
但是这里也要说一下,他并不是完美的设计模式,缺点是,每增加一个产品类,就需要增加一个具体的工厂类,这可能会导致系统中类的个数急剧增加,在一定程度上增加了系统的复杂度。
工具方法
讲完了三个最重要的类,我们也要把相关的工具也落实到位。
/*** @brief 工具函数*/
//获取文件名称
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__))
#define LOG() qDebug().noquote() << TAG//格式化时间
static inline QString formatTime(int64_t timestamp){QDateTime dateTime = QDateTime::fromSecsSinceEpoch(timestamp);return dateTime.toString("YYYY-MM-dd HH:mm:ss");
}
//获取时间戳
static inline int64_t getTime(){return QDateTime::currentSecsSinceEpoch();
}
//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 success = file.open(QFile::ReadOnly);if(!success){LOG() << "文件打开失败";return QByteArray();}QByteArray content = file.readAll();file.close();return content;
}
//QByteArray=>文件
static inline void writeByteArrayToFile(const QString& path,const QByteArray& content){QFile file(path);bool success = file.open(QFile::WriteOnly);if(!success){LOG() << "文件打开失败";return;}file.write(content);file.flush();//刷新缓冲区file.close();
}
完整代码
#pragma once#include <QString>
#include <QIcon>
#include <QUuid>
#include <QDateTime>
#include <QFile>
#include <QDebug>
#include <QFileInfo>/*** 关于命名空间,约定:* 如果代码所在的文件,就是在项目的顶层目录中,直接使用全局命名空间,不手动指定。* 如果代码所在的文件,在某个子目录当中,指定一个和目录名字相同的命名空间。*/// 创建一个命名空间namespace model{
/*** @brief 工具函数*/
//获取文件名称
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__))
#define LOG() qDebug().noquote() << TAG//格式化时间
static inline QString formatTime(int64_t timestamp){QDateTime dateTime = QDateTime::fromSecsSinceEpoch(timestamp);return dateTime.toString("YYYY-MM-dd HH:mm:ss");
}
//获取时间戳
static inline int64_t getTime(){return QDateTime::currentSecsSinceEpoch();
}
//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 success = file.open(QFile::ReadOnly);if(!success){LOG() << "文件打开失败";return QByteArray();}QByteArray content = file.readAll();file.close();return content;
}
//QByteArray=>文件
static inline void writeByteArrayToFile(const QString& path,const QByteArray& content){QFile file(path);bool success = file.open(QFile::WriteOnly);if(!success){LOG() << "文件打开失败";return;}file.write(content);file.flush();//刷新缓冲区file.close();
}/*** @brief 用户信息*/
class UserInfo{
public:QString userId = ""; //用户编号QString nickname = ""; //用户昵称QString description = "";//用户签名QString phone = ""; //手机号码QIcon avatar; //用户头像
};
/*** @brief 消息信息*/
enum MessageType{TEXT_TYPE, //文本消息IMAGE_TYPE, //图片消息FILE_TYPE, //文件消息SPEECH_TYPE //语音消息
};/*** @brief 消息信息*/
class Message{
public:QString messageId = ""; //消息编号QString chatSessionId = ""; //消息所属会话编号QString time = ""; //消息的时间,通过格式化时间的方式来表示,eg:10-27 10:02:00MessageType messageType; //消息类型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 "Message" + QUuid::createUuid().toString().sliced(25,12);}static Message makeTextMessage(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 = TEXT_TYPE;message.fileId = "";message.fileName = "";return message;}static Message makeImageMessage(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 = IMAGE_TYPE;message.fileId = "";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;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;message.fileId = "";message.fileName = "";return message;}
};/*** @brief 会话信息*/
class ChatSessionInfo{
public:QString chatSessionId = ""; //会话编号QString chatSessionName = ""; //会话名字,如果是单聊,就是对方的昵称,如果是群聊,那就是群聊名Message lastMessage; //表示最新消息QIcon avatar; //会话头像,如果是单聊,就是对方的头像,如果是群聊,则是群聊头像QString userId = ""; //表示单聊,就是对方的昵称,如果是群聊,则先设置为"",后续通过其他方式把完整的用户id列表拿到
};}
那么这一集我们就完成了关键类的编写以及相关的工具方法的编写,后面,我们就要开始落实主窗口,那么期待我们的下一集。