二,核心类的编写
一,需求分析
-
用户信息
-
会话信息
用户A有两个好友,需要两个聊天会话,且持续存在。
(一直持续到删除好友/退出群组才会删除)
用户ABC有一个群组,这个群组存在一个聊天会话。
- 消息信息
文本消息
图片消息
文件消息
语音消息
二,核心类的编写
建立模型文件夹“model”
- 关于命名空间的约定(针对此项目):
a 项目所在的文件,就是项目的顶层目录,此时,就直接使用全局命名空间(不手动指定)。
b 如果项目所在的文件,在某个子目录中,此时就指定一个和目录名字相同的命名空间。
#pragma once //不被重复包含
#include<Qstring>
//创建命名空间
namespace model{
//
///用户信息///
/
class UseInof{
public:
QString userID;//int userId;使用字符串的方式来作为id,可以有更灵活的方式来进行生成
//并且更好的适用分布式的mysql
//后续可通过uuid或者雪花算法这样的方式来生成分布式系统中唯一标识的id
private:
};
}//end model描述结束命名空间
一.用户信息UserInfo类
class UserInfo{
public:
QString userID="";//用户的编号
//int userId;使用字符串的方式来作为id,可以有更灵活的方式来进行生成
//并且更好的适用分布式的mysql
//后续可通过uuid或者雪花算法这样的方式来生成分布式系统中唯一标识的id
QString nickname="";//用户的昵称
QString description="";//用户签名
QString phone="";//手机号码
QIcon avatar;//用户头像
};
二.ChatSessionInfo会话信息
class ChatSessionInfo{
public:
QString chatSessionID="";//会话编号
QString chatSessionName="";//会话名字(有群聊和单聊)
Message lastMessage;//在会话列表显示提示
QIcon avatar;//会话的头像(有单聊和群聊之分)
QString userId="";
};
三.会话信息ChatSessionInfo产生的Message类
1.什么是message的工厂方法?
是一种工厂设计模式。
解决构造函数不太够用的情况。
虽然平时构建对象,都是通过 构造函数 来完成。
但是如果有不同的方式来构建对象,此时构造函数就不太够用了。
class Point{
public:
Point(double x,double y);
Point(double r,double a);
//会发现大家了,构造函数,要提供不同的版本,必须要重载,要求函数名相同,但是参数个数/类型不同
}
运用工厂模式,也就是使用普通的函数来实现不同的构造方式。
普通函数,函数名可以随便起,也就不再受到重载的约束了。
虽然说是普通,但是一般要使用static修饰的静态函数
相比于ChatSessionInfo和UserInfo更需要工厂模式,是因为Message要支持四种消息
2.怎么生成唯一的messageid
https://baike.baidu.com/item/UUID/5921266?fr=ge_ala
使用uuid,可以生成“全球唯一的身份标识”
return QUuid::createUuid().toString();
3.类的书写
class Message{
public:
QString messageId="";//消息的编号
QString chatSessionId="";//消息所属会话编号
QString time="";//消息的时间,通过格式化时间的方式来表示 形如 06-07 12:00
MessageType messageType;//消息类型
UserInfo sender;//发送者的信息
QByteArray content;//消息的正文内容:文本就是字符串,图片、文件、语音就是二进制的序列
// C和cpp没有byte类型,都是拿char/unsigned char来凑合的
//例如在cpp中通过代码取出“string张三”的三很麻烦
//1.先判定哪种编码方式 2.计算第二个汉字所属出字节范围 3.取字符串子串
//但是QT相比之下做了更好的处理,QString就对上述情况处理的更好,QT明确区分了字节和字符,
//表示字符串必须使用QString(内置了对不同字符集进行处理),
//表示二进制数据必须使用QByteArray
QString fileId="";//表示一个文件的身份标识,当消息类型为文件,图片,语音时才有效
//一旦一个聊天会话中,包含上述多个这样的信息,就会从服务器获取消息列表,这样的操作,就变得非常低效
//所以一般的做法是获取此三种消息的id,当客户端拿到消息列表之后,再根据拿到的field,给服务器发送额外的请求,获取文件内容
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:
//通过这个方法生成唯一的messageId
static QString makeId(){
//return QUuid::createUuid().toString();
//取M做标识符字符串子串
return "M"+QUuid::createUuid().toString().sliced(25,12);//从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& extraInfo)
{
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="";
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;
}
};
4.MessageType
//会话消息是1对多
enum MessageType{
TEXT_TYPE,//文本消息
IMAGE_TYPE,//图像消息
FILE_TYPE,//文件消息
SPEECH_TYPE//语音消息
};
四.在头文件中定义函数
在 cpp 中,如果你在头文件中定义了函数,而不加 static
或 inline
,会导致链接阶段的重定义错误。这是因为头文件通常会被多个源文件(.cpp
文件)包含,如果没有特殊处理,每个源文件中都会生成一个相同的函数定义,最终在链接时会发生冲突。
1. 为什么会出现重定义问题?
假设我们有一个头文件 example.h
:
cpp复制代码// example.h
int add(int a, int b) {
return a + b;
}
以及两个源文件 file1.cpp
和 file2.cpp
都包含了 example.h
:
cpp复制代码// file1.cpp
#include "example.h"
// 其他代码...
// file2.cpp
#include "example.h"
// 其他代码...
当编译器编译这两个源文件时,它会在 file1.o
和 file2.o
中都生成 add
函数的定义。在链接阶段,链接器会发现有两个相同的 add
函数的定义,从而报出重定义错误。
2. 解决方案:使用 **static**
或 **inline**
**static**
将函数定义为 static
会使它的链接属性变为内部链接,即该函数的定义仅在包含它的源文件中可见,而不对其他源文件可见。这样即使在多个源文件中定义了相同的 static
函数,它们也是独立的,不会产生冲突。
cpp复制代码// example.h
static int add(int a, int b) {
return a + b;
}
**inline**
将函数定义为 inline
可以避免重定义错误,因为 inline
函数的特殊属性是它们的定义可以在多个翻译单元中重复,而不会产生冲突。编译器会尝试将 inline
函数的调用点用函数体替换,以避免多次定义。
cpp复制代码// example.h
inline int add(int a, int b) {
return a + b;
}
**static inline**
你也可以同时使用 static
和 inline
,表示该函数不仅仅是内部链接的(static
),还希望编译器对其进行内联优化(inline
)。这通常会用于一些特定场景,例如函数体非常小且希望减少调用开销的情况下。
cpp复制代码// example.h
static inline int add(int a, int b) {
return a + b;
}
3. 使用 **static**
和 **inline**
的区别
-
**static**
函数:函数在每个包含它的翻译单元(源文件)中都有一个独立的定义,各个源文件中的static
函数是互相独立的,不会冲突。 -
**inline**
函数:告诉编译器将函数定义内联化,允许在多个翻译单元中重复定义。在绝大多数现代编译器中,inline
函数的实际行为取决于编译器的优化策略,而不强制内联。
4. 实际开发中的应用建议
-
头文件中的小型工具函数:优先使用
inline
,因为它能减少函数调用开销,同时不会出现重定义问题。 -
静态库中的函数:如果只希望在当前文件中使用某个函数,可以选择
static
。这可以避免其他文件误用该函数。 -
头文件中重复出现的函数:可以同时使用
static
和inline
,以避免冲突,并可能获得内联优化。
综上所述,在头文件中定义函数时,应尽量加上 static
或 inline
关键字(或两者都加),以避免在链接阶段出现重定义问题,并可能获得编译器的优化。
五.解决了四,我们来写工具函数
1.通过时间戳转化为格式化的时间
static inline QString formatTime(int64_t timestamp){
//通过时间戳,转换成格式化时间
//时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数
//为什么使用int64因为使用int32会出现溢出情况
//先把时间戳,转换成QDateTime对象
QDateTime dateTime=QDateTime::fromSecsSinceEpoch(timestamp);
//fromSecsSinceEpoch(timestamp)将时间戳转化为QDateTime对象
//把对象转为“格式化时间”
return dateTime.toString("MM-dd HH:mm:ss");
}
2.获得秒级的时间
//获得秒级的时间
static inline int64_t getTime()
{
//用于获取自 "Epoch" 时间(即 1970 年 1 月 1 日 00:00:00 UTC)以来经过的毫秒数
return QDateTime::currentMSecsSinceEpoch();
}
3.根据QByteArray,转成QIcon
//根据QByteArray,转成QIcon
//qt中qicon是图标
//qpixmap提供很多的关于图片的处理方法
//通过上面两个的操作,完成通过字节数组构造qicon
//qimage
static inline QIcon makeIcon(const QByteArray& byteArray)
{
QPixmap pixmap;
pixmap.loadFromData(byteArray);
QIcon icon(pixmap);
return icon;
}
4.读写文件操作
//从指定文件中,读取所有的二进制内容,得到一个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 writeByteArrayToFile(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();
return;
}
5.打印日志的宏
1.关于#define TAG QString(“[%1]”).arg(QString::number(__LINE__));
-
QString("[%1]")
创建了一个格式字符串,%1
是一个占位符。 -
QString::number(__LINE__)
将当前行号(__LINE__
是一个预处理宏,表示当前代码的行号)转换为字符串。 -
.arg(...)
将行号替换到格式字符串中的占位符%1
。
.arg(...)
是 QString
类中的一个成员函数,用于格式化字符串。它允许你将一个或多个值插入到字符串中的占位符位置。
2.#define LOG() qDebug() << TAG
3.显示文件名的方法
使用内置函数
使用自己创造的函数
static inline QString getFileName(const QString& path)
{
QFileInfo fileInfo(path);
return fileInfo.fileName();
}
三,核心类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在打印字符串的会加上引号.noquote
#define LOG() qDebug().noquote() << TAG
//要求函数的定义如果在.h中,必须加static或者inline(两个都加也可以),避免链接阶段出现“函数”
static inline QString formatTime(int64_t timestamp){
//通过时间戳,转换成格式化时间
//时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数
//为什么使用int64因为使用int32会出现溢出情况
//先把时间戳,转换成QDateTime对象
QDateTime dateTime=QDateTime::fromSecsSinceEpoch(timestamp);
//fromSecsSinceEpoch(timestamp)将时间戳转化为QDateTime对象
//把对象转为“格式化时间”
return dateTime.toString("MM-dd HH:mm:ss");
}
//获得秒级的时间
static inline int64_t getTime()
{
//用于获取自 "Epoch" 时间(即 1970 年 1 月 1 日 00:00:00 UTC)以来经过的毫秒数
return QDateTime::currentMSecsSinceEpoch();
}
//根据QByteArray,转成QIcon
//qt中qicon是图标
//qpixmap提供很多的关于图片的处理方法
//通过上面两个的操作,完成通过字节数组构造qicon
//qimage
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 writeByteArrayToFile(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();
return;
}
//
///用户信息///
/
class UserInfo{
public:
QString userID="";//用户的编号
//int userId;使用字符串的方式来作为id,可以有更灵活的方式来进行生成
//并且更好的适用分布式的mysql
//后续可通过uuid或者雪花算法这样的方式来生成分布式系统中唯一标识的id
QString nickname="";//用户的昵称
QString description="";//用户签名
QString phone="";//手机号码
QIcon avatar;//用户头像
};
//
///消息信息
/
//会话消息是1对多
enum MessageType{
TEXT_TYPE,//文本消息
IMAGE_TYPE,//图像消息
FILE_TYPE,//文件消息
SPEECH_TYPE//语音消息
};
class Message{
public:
QString messageId="";//消息的编号
QString chatSessionId="";//消息所属会话编号
QString time="";//消息的时间,通过格式化时间的方式来表示 形如 06-07 12:00
MessageType messageType;//消息类型
UserInfo sender;//发送者的信息
QByteArray content;//消息的正文内容:文本就是字符串,图片、文件、语音就是二进制的序列
// C和cpp没有byte类型,都是拿char/unsigned char来凑合的
//例如在cpp中通过代码取出“string张三”的三很麻烦
//1.先判定哪种编码方式 2.计算第二个汉字所属出字节范围 3.取字符串子串
//但是QT相比之下做了更好的处理,QString就对上述情况处理的更好,QT明确区分了字节和字符,
//表示字符串必须使用QString(内置了对不同字符集进行处理),
//表示二进制数据必须使用QByteArray
QString fileId="";//表示一个文件的身份标识,当消息类型为文件,图片,语音时才有效
//一旦一个聊天会话中,包含上述多个这样的信息,就会从服务器获取消息列表,这样的操作,就变得非常低效
//所以一般的做法是获取此三种消息的id,当客户端拿到消息列表之后,再根据拿到的field,给服务器发送额外的请求,获取文件内容
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:
//通过这个方法生成唯一的messageId
static QString makeId(){
//return QUuid::createUuid().toString();
//取M做标识符字符串子串
return "M"+QUuid::createUuid().toString().sliced(25,12);//从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& extraInfo)
{
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="";
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;
}
};
//
///会话信息
/
class ChatSessionInfo{
public:
QString chatSessionID="";//会话编号
QString chatSessionName="";//会话名字(有群聊和单聊)
Message lastMessage;//在会话列表显示提示
QIcon avatar;//会话的头像(有单聊和群聊之分)
QString userId="";
};
}//end model描述结束命名空间