项目第三弹:基础工具类实现
一、工具类的介绍
1.生活例子
生活当中有很多工具,比如打火机…
没有工具类,那么生火就要自己钻木取火。有了工具类,就可以用打火机点火了,这个打火机就是工具类,只不过需要我们自己完成它
它所起到的作用就是让我们需要使用它的时候非常方便,调用一下接口即可
2.专业术语
在软件开发当中,工具类:Utility Classes 或 Helper Classes
使得开发者在进行开发时能够更加高效、便捷地完成一些基础但必要的任务,而无需每次都从头开始编写相同的代码,且易于维护和扩展
工具类封装了那些与业务逻辑不直接相关,但频繁使用且功能相对独立的代码片段
好处:
代码复用(函数的好处,凸显的是高效)
解耦合(类的好处,凸显的是易于维护和扩展)
简化开发(凸显的是便捷)
二、FileHelper
Helper基础工具类中要完成的是一些零碎代码的实现,其中我们的项目包括文件操作,UUID生成操作,字符串截取操作,还有我们之前封装的SqliteHelper
下面就让我们造出这4个版本的打火机吧
1. 判断文件是否存在
2. 获取文件大小
3. 创建/删除文件
4. 创建/删除目录
5. 文件的读写操作(包括在指定偏移量【读取/写入】指定大小的数据)
6. 获取文件所在父级目录
7. 文件的重命名
下面我们一一实现
1.判断文件是否存在
1.C++ IO流
最直接能想到的方法就是打开一下,能打开,就是存在,否则就是不存在(不过注意:如果你用写方式打开,不要给它trunc)
Talk is cheap,Show me the code:
#include <fstream>
#include <iostream>
using namespace std;
int main()
{
fstream fs("hello", ios::in | ios::out);//默认不会截断文件
ofstream ofs1("hello");//默认会截断文件
ofstream ofs2("hello",ios::app);//app: append追加写 : 不会截断文件
ifstream ifs("hello");//默认不会截断文件
if (ifs.is_open())
{
ifs.close();
cout << "文件存在\n";
}
else
cout << "文件不存在\n";
return 0;
}
综上:
只要别用ofstream的默认打开方式即可
好处:
- 简单直观
- C++库函数的跨平台优势
不足:
- 性能开销较大,效率低
- 资源浪费:查询文件是否存在还需频繁打开关闭文件
- 权限受限:因为文件系统是具有权限约束的, 有些文件是我们打不开的,此时尽管对应文件的确存在,但是我们也依然打不开。此时就会发生误判,需要我们额外处理
(C++的IO流更注重文件操作,并不很关注底层OS上的相应问题)
刚才的代码没有判断权限问题,展示一番:
sudo touch advance_file
sudo chmod 640 advance_file
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/mqhelper$ ll advance_file
-rw-r----- 1 root root 0 Jul 17 10:07 advance_file
此时我wzs作为other,运行刚才的程序:
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/mqhelper$ ./mycmd
文件不存在
误判了
2.stat :Linux系统调用
介绍一下stat这个系统调用
int stat(const char *pathname, struct stat *statbuf);
pathname:文件名
statbuf是一个输出型参数,函数内部必定会对齐解引用,所以不要传入nullptr
不要相信他会判断,以最坏情况来准备,这是为了代码的健壮性
因此获取文件大小也可以用和这个函数
成功返回0,失败返回-1(且错误码被设置)
static bool exist(const std::string &filename)
{
struct stat st;
return stat(filename.c_str(), &st) == 0;
}
2.获取文件大小
static size_t size(const std::string &filename)
{
struct stat st;
if (stat(filename.c_str(), &st) == -1)
{
default_error("获取文件大小失败,errno: %d strerror: %s",errno,std::strerror(errno));
return 0;//不要返回-1,因为会被转为整形最大值
}
return st.st_size;
}
3.创建/删除文件
static bool createFile(const std::string &filename)
{
// 1. 先看该文件是否存在
if (exist(filename))
return true;
// 2. 写方式打开(即创建)
// 这里以追加写打开,防止多线程重入该函数导致意想不到的bug,提高代码健壮性
std::ofstream ofs(filename.c_str(), std::ios::app);
if (!ofs.is_open())
{
default_error("创建文件失败,filename: %s",filename.c_str());
return false;
}
ofs.close();
return true;
}
static bool removeFile(const std::string &filename)
{
// Linux系统调用的unlink 或者 C语言提供的remove(具有跨平台性)
// 直接调用remove函数:成功返回0,失败返回-1,错误码被设置
// 注意: 如果是因为文件不存在,则errno会被设置为2: No such file or directory
bool ret = remove(filename.c_str()) == 0 || errno == 2;
if (!ret)
{
default_error("删除文件失败,filename: %s",filename.c_str());
}
return ret;
}
4.创建/删除目录
注意:我们这里的删除目录是删除该目录以及该目录下面的所有文件,类似于rm -rf 目录名
创建目录时支持自动创建缺失的父目录,类似于:
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/mqhelper$ mkdir ./a/b/c/d -p
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/mqhelper$ tree a
a
└── b
└── c
└── d
3 directories, 0 files
static bool createDir(const std::string &dirpath, int dirmode = defaultDirMode)
{
// ./a/b/c/d -> 依次创建./a ./a/b ./a/b/c ./a/b/c/d
// 从前往后找/
size_t prev = 0, pos = dirpath.find('/', prev);
umask(0);
while (prev != std::string::npos) // 最后一个没有/,但是也要创建
{
std::string dir = dirpath.substr(0, pos);
bool ok = mkdir(dir.c_str(), dirmode) == 0 || errno == EEXIST;
if (!ok)
{
default_error("创建目录失败,errno: %d strerror: %s 总目录: %s 该目录: [%s]",errno,std::strerror(errno),dirpath.c_str(),dir.c_str());
return false;
}
prev = dirpath.find_first_not_of('/', pos);
pos = dirpath.find('/', prev);
}
return true;
}
static bool removeDir(const std::string &dirpath)
{
// 因为rmdir只能删空目录,所以我们需要后序遍历地递归式把该目录当中的所有文件全部删掉之后再删该目录,麻烦
// 所以直接用stdlib.h当中的system函数,专门执行系统指令: 例如 rm -rf
// 但是我们想搞一个黑名单,防止一些重要文件被删
// 黑名单目录集合 : 黑名单目录万万(此时省略一亿个万万)不敢直接测试(把system注释之后 make clean之后,在测试)
static std::unordered_set<std::string> blacklist = {
"/", // 根目录
"/bin", // 系统二进制文件目录
"/boot", // 启动文件目录
"/dev", // 设备文件目录
"/etc", // 配置文件目录
"/home/wzs", // 自己的家目录
"/root", // root的目录
// ... 其他不想删除的目录
};
if (blacklist.count(dirpath))
{
default_fatal("不允许您删除此目录, %s",dirpath.c_str());
return false;
}
bool ret = system(std::string("rm -rf " + dirpath).c_str()) != -1;
if (!ret)
{
default_fatal("删除目录失败, 目录名: %s",dirpath.c_str());
}
return ret;
}
5.read
因为我们不仅会写入字符串类型,也需要写入整形,所以统一都用指针,而不是string
// 因为不知道传入的指针指向的空间是栈区的还是堆区的,所以要求外界自行扩容
// 因为我们的文件当中可能会存放各种类型的数据,而文本的IO操作会对特殊字符进行特殊处理,比如转义字符等等...
// 因此我们二进制写,也要二进制读
static bool read(const std::string &filename, char* return_str)
{
size_t filesz = size(filename);
return read(filename, return_str, 0, filesz);
}
static bool read(const std::string &filename, char* return_str, size_t offset, size_t len)
{
// 1. 检查文件是否存在
if (!exist(filename))
{
return false;
}
// 2. 二进制读方式打开文件
std::ifstream ifs(filename, std::ios::in | std::ios::binary);
if (!ifs.is_open())
{
default_error("读取文件失败,因为打开文件失败,文件名:%s",filename.c_str());
return false;
}
// 3. 偏移文件指针
ifs.seekg(offset, ifs.beg); // beg : begin, 相对于文件起始位置偏移offset个字节
// 4. 读取
ifs.read(return_str, len);
if (!ifs.good())
{
default_error("文件读取失败... %s",filename.c_str());
ifs.close();
return false;
}
// 4. 关闭文件
ifs.close();
// 5. 返回
return true;
}
6.write
static bool write(const std::string &filename, const std::string &str)
{
return write(filename, str.c_str(), size(filename), str.size());
}
static bool write(const std::string &filename,const char* str, size_t offset, size_t len)
{
// 注意: 必须要用fstream,因为ofstream对文件没有读权限,无法偏移文件指针
// 1. 打开文件(二进制)
std::fstream fs(filename, std::ios::in | std::ios::out | std::ios::binary); // 这样是不会trunc的哦
// 2. 判断是否成功打开
if (!fs.is_open())
{
default_error("写入文件失败,因为打开文件失败,文件名:%s",filename.c_str());
return false;
}
// 3. 文件指针偏移
fs.seekp(offset, fs.beg);
// 4. 写入文件
// write 方法会读取从这个地址开始的 len 个字节,并将这些数据写入文件
// 这里并没有将指针本身写入文件,而是将指针所指向的数据写入文件
fs.write(str, len);
// 5. 判断是否成功写入
if (!fs.good())
{
default_error("写入文件失败,因为写入失败,文件名:%s",filename.c_str());
fs.close();
return false;
}
fs.close();
return true;
}
7.获取文件父级目录
static std::string parentDir(const std::string &dirpath)
{
// ./a/b/c/hello.txt -> ./a/b/c
// 从后往前找'/' 然后直接返回 [0~pos-1]
// 如果没有'/' 那就是.(当前目录)
size_t pos = dirpath.rfind('/');
if (pos == std::string::npos)
{
return ".";
}
return dirpath.substr(0, pos);
}
8.文件的重命名
static bool rename(const std::string &oldname, const std::string &newname)
{
bool ret = ::rename(oldname.c_str(), newname.c_str());
if (ret == -1)
{
default_error("文件重命名失败,oldname: %s newname: %s",oldname.c_str(),newname.c_str());
return false;
}
return true;
}
9.FileHelper完整代码
const int defaultDirMode = 0775;
class FileHelper
{
public:
static bool exist(const std::string &filename)
{
struct stat st;
return stat(filename.c_str(), &st) == 0;
}
static size_t size(const std::string &filename)
{
struct stat st;
if (stat(filename.c_str(), &st) == -1)
{
default_error("获取文件大小失败,errno: %d strerror: %s",errno,std::strerror(errno));
return 0; // 不要返回-1,因为会被转为整形最大值
}
return st.st_size;
}
static bool createFile(const std::string &filename)
{
// 1. 先看该文件是否存在
if (exist(filename))
return true;
// 2. 写方式打开(即创建)
// 这里以追加写打开,防止多线程重入该函数导致意想不到的bug,提高代码健壮性
std::ofstream ofs(filename.c_str(), std::ios::app);
if (!ofs.is_open())
{
default_error("创建文件失败,filename: %s",filename.c_str());
return false;
}
ofs.close();
return true;
}
static bool removeFile(const std::string &filename)
{
// Linux系统调用的unlink 或者 C语言提供的remove(具有跨平台性)
// 直接调用remove函数:成功返回0,失败返回-1,错误码被设置
// 注意: 如果是因为文件不存在,则errno会被设置为2: No such file or directory
// remove内部调用unlink删文件,rmdir删空目录(只能删空目录)
bool ret = remove(filename.c_str()) == 0 || errno == 2;
if (!ret)
{
default_error("删除文件失败,filename: %s",filename.c_str());
}
return ret;
}
static bool createDir(const std::string &dirpath, int dirmode = defaultDirMode)
{
// ./a/b/c/d -> 依次创建./a ./a/b ./a/b/c ./a/b/c/d
// 从前往后找/
size_t prev = 0, pos = dirpath.find('/', prev);
umask(0);
while (prev != std::string::npos) // 最后一个没有/,但是也要创建
{
std::string dir = dirpath.substr(0, pos);
bool ok = mkdir(dir.c_str(), dirmode) == 0 || errno == EEXIST;
if (!ok)
{
default_error("创建目录失败,errno: %d strerror: %s 总目录: %s 该目录: [%s]",errno,std::strerror(errno),dirpath,dir);
return false;
}
prev = dirpath.find_first_not_of('/', pos);
pos = dirpath.find('/', prev);
}
return true;
}
static bool removeDir(const std::string &dirpath)
{
// 因为rmdir只能删空目录,所以我们需要后序遍历地递归式把该目录当中的所有文件全部删掉之后再删该目录,麻烦
// 所以直接用stdlib.h当中的system函数,专门执行系统指令: 例如 rm -rf
// 但是我们想搞一个黑名单,防止一些重要文件被删
// 黑名单目录集合 : 黑名单目录万万(此时省略一亿个万万)不敢直接测试(把system注释之后 make clean之后,在测试)
static std::unordered_set<std::string> blacklist = {
"/", // 根目录
"/bin", // 系统二进制文件目录
"/boot", // 启动文件目录
"/dev", // 设备文件目录
"/etc", // 配置文件目录
"/home/wzs", // 自己的家目录
"/root", // root的目录
// ... 其他不想删除的目录
};
if (blacklist.count(dirpath))
{
default_fatal("不允许您删除此目录, %s",dirpath.c_str());
return false;
}
bool ret = system(std::string("rm -rf " + dirpath).c_str()) != -1;
if (!ret)
{
default_fatal("删除目录失败, 目录名: %s",dirpath.c_str());
}
return ret;
}
// 因为我们的文件当中可能会存放各种类型的数据,而文本的IO操作会对特殊字符进行特殊处理,比如转义字符等等...
// 因为我们二进制写,也要二进制读
static bool read(const std::string &filename, string *return_str)
{
size_t filesz = size(filename);
return read(filename, return_str, 0, filesz);
}
static bool read(const std::string &filename, std::string *return_str, size_t offset, size_t len)
{
// 1. 检查文件是否存在
if (!exist(filename))
{
return false;
}
// 2. 二进制读方式打开文件
std::ifstream ifs(filename, std::ios::in | std::ios::binary);
return_str->resize(len, '\0');
if (!ifs.is_open())
{
default_error("读取文件失败,因为打开文件失败,文件名:%s",filename.c_str());
return false;
}
// 3. 偏移文件指针
ifs.seekg(offset, ifs.beg); // beg : begin, 相对于文件起始位置偏移offset个字节
// 4. 读取
ifs.read(&(*return_str)[0], len);
// ifs.read(const_cast<char *>(return_str->c_str()), len);
// 这样不好,强制类型转换能少用最好少用,迫不得已再用; 避免不必要的、可能引发问题的操作
if (!ifs.good())
{
default_error("文件读取失败... %s",filename.c_str());
ifs.close();
return false;
}
// 4. 关闭文件
ifs.close();
// 5. 返回
return true;
}
static bool write(const std::string &filename, const std::string &str)
{
return write(filename, str, size(filename), str.size());
}
static bool write(const std::string &filename, const std::string &str, size_t offset, size_t len)
{
// 注意: 必须要用fstream,因为ofstream对文件没有读权限,无法偏移文件指针
// 1. 打开文件(二进制)
std::fstream fs(filename, std::ios::in | std::ios::out | std::ios::binary); // 这样是不会trunc的哦
// 2. 判断是否成功打开
if (!fs.is_open())
{
default_error("写入文件失败,因为打开文件失败,文件名:%s",filename.c_str());
return false;
}
// 3. 文件指针偏移
fs.seekp(offset, fs.beg);
// 4. 写入文件
// write 方法会读取从这个地址开始的 len 个字节,并将这些数据写入文件
// 这里并没有将指针本身写入文件,而是将指针所指向的数据写入文件
fs.write(str.c_str(), len);
// 5. 判断是否成功写入
if (!fs.good())
{
default_error("写入文件失败,因为写入失败,文件名:%s",filename.c_str());
fs.close();
return false;
}
fs.close();
return true;
}
static std::string parentDir(const std::string &dirpath)
{
// ./a/b/c/hello.txt -> ./a/b/c
// 从后往前找'/' 然后直接返回 [0~pos-1]
// 如果没有'/' 那就是.(当前目录)
size_t pos = dirpath.rfind('/');
if (pos == std::string::npos)
{
return ".";
}
return dirpath.substr(0, pos);
}
static bool rename(const std::string &oldname, const std::string &newname)
{
bool ret = ::rename(oldname.c_str(), newname.c_str());
if (ret == -1)
{
default_error("文件重命名失败,oldname: %s newname: %s",oldname.c_str(),newname.c_str());
return false;
}
return true;
}
};
三、UUIDHelper
UUID(Universally Unique Identifier), 也叫通⽤唯⼀识别码,通常由32位16进制数字字符组成
UUID的标准型式包含32个16进制数字字符,以连字号分为五段,形式为8-4-4-4-12的32个字符
如:550e8400-e29b-41d4-a716-446655440000
在这⾥,uuid⽣成,我们采⽤⽣成8个随机数字,加上8字节序号,共16字节数组⽣成32位16进制字符的组合形式来确保全局唯⼀的同时能够根据序号来分辨数据(随机数⾁眼分辨起来真是太难了…)
因此,思想:
- 生成8个0~255之间的随机数
- 生成一个8字节的序号
- 通过以上数据,组成一个16字节的数据,转换为16进制字符,共32位
1.C++生成随机数
1.random_device:随机数生成器
int main()
{
std::random_device rd;// 随机数生成器
std::cout << rd() << "\n"; // 1775612373
return 0;
}
这个random_device对象有一个operator()的重载,他通常基于底层硬件来生成随机数,因此它生成的随机数的随机性很高
但是由于它会直接访问底层硬件/通过系统调用接口来访问底层硬件,因此这个operator()比较慢
在一些对随机性要求非常高的场景中常用,否则不太建议使用,而是使用下面的mt19937_64梅森旋转算法
2. mt19937_64梅森旋转算法
它是一种伪随机数算法,能够快速产生高质量的伪随机数
伪随机数生成器是基于算法和初始值(种子)来生成随机数序列的,这意味着给定相同的种子,每次运行都会生成相同的随机数序列
然而,在实际应用中,我们通常会选择一个足够随机且难以预测的种子值(例如,使用系统时间、用户输入或其他随机源作为种子的一部分),以增加随机数序列的不可预测性
那这个19937是什么东西???
mt19937_64的周期长度是(2^19937)-1
,是一个天文数字
周期长度是伪随机数生成器能够生成的不重复随机数序列的最大长度
也就是说只有当我一次性要求生成的随机数数量大于周期长度,才会出现重复数据
64是指生成的随机数是64位
std::mt19937_64 generator(rd());
std::cout << generator() << "\n";// 14605549512862169153
// 1460 5549 5128 6216 9153
我们想让它生成0~255之间的一个随机数,因此需要用到
3.uniform_int_distribution
uniform_int_distribution用于从【a,b】之间生成均匀分布的随机数
std::uniform_int_distribution<int> distribute(0, 255);
std::cout << distribute(generator) << "\n";// 193
2.Code时间到
class UUIDHelper
{
public:
/*
UUID生成: 8-4-4-4-12 格式的字符串
思想:
1. 生成8个0~255之间的随机数
2. 生成一个8字节的序号
通过以上数据,组成一个16字节的数据,转换为16进制字符,共32个
*/
// 550e8400-e29b-41d4-a716-446655440000
static std::string uuid()
{
// 1.创建机器随机数对象
std::random_device rd;
// 2.创建梅森随机数对象
std::mt19937_64 generator(rd());
// 3.创建均匀分布对象
std::uniform_int_distribution<int> distribute(0, 255);
// 4.生成8个随机数并写入ostringstream对象
std::ostringstream oss;
for (int i = 0; i < 8; i++)
{
oss << std::setw(2) << std::setfill('0') << std::hex << distribute(generator);
if (i == 3 || i == 5 || i == 7)
{
oss << "-";
}
}
static atomic<uint64_t> seq(1);
// 5.生成8位序号
uint64_t num = seq.fetch_add(1, std::memory_order::memory_order_seq_cst); // 遵循代码健壮性原则,使用最强内存顺序
// 6.组成答案字符串并返回
// 我们想让低位放到字符串末尾,更符合我们人类的观察方式 00 00 00 00 00 00 00 0a
// 因此从后往前依次取一个字节转成16进制进行填充
for (int i = 7; i >= 0; i--)
{
oss << std::setw(2) << std::setfill('0') << std::hex << ((num >> (i * 8)) & 0xff); // 0xff : 一个字节全1
if (i == 6)
{
oss << "-";
}
}
return oss.str();
}
};
3.多线程测试
int main()
{
auto func = [](const std::string s)
{
while (true)
{
std::cout << s << UUIDHelper::uuid() << "\n";
std::this_thread::sleep_for(chrono::seconds(1));
}
};
thread t1(func, "t1: ");
thread t2(func, "t2: ");
thread t3(func, "t3: ");
thread t4(func, "t4: ");
thread t5(func, "t5: ");
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog/mqhelper$ ./mycmd
t1: 74aa161a-3a48-babd-0000-000000000001
t2: fd68071f-727f-c9c7-0000-000000000002
t3: t4: 1ec5bb82-fb4f-b4ec-0000-000000000003
aa686931-c47b-405e-0000-000000000004
t5: 948e7a91-e862-56af-0000-000000000005
t1: 874f7ee8-475a-5eda-0000-000000000006
t2: 6bdce81f-4d63-e5b9-0000-000000000007
t3: 9e5fe3a6-6edb-f235-0000-000000000008
t4: f9bc9a1d-95d1-e679-0000-000000000009
t5: 30772b37-48e3-73d9-0000-00000000000a
t2: t1: 8d468a78-57e6-98ee-0000-00000000000b
d08a3957-4c5d-9e53-0000-00000000000c
t5: t3: 4d582e55-0868-6960-0000-00000000000d
t4: 405cb8e1-e10e-d0ea-0000-00000000000f
6b31edc9-4286-d2f2-0000-00000000000e
t2: t1: 9d9f9182-d1a1-4f4d-0000-000000000010c2ed2a94-70eb-d7a6-0000-000000000011
t3: t5: e87e4ce9-be1c-9fdb-0000-000000000012
478ad984-6262-8027-0000-000000000013
t4: 4691b735-eb09-fe79-0000-000000000014
四、StringHelper
我们要实现一个字符串辅助类,主要用于实现字符串切割的
class StringHelper
{
public:
// 把src_str按照sep进行分割,将分割后的字符串放到out_vec当中
// ...news....music....pop...#.. 按照.分隔 -> news music pop # 放到out_vec当中
static void split(const std::string &src_str, const std::string &sep, std::vector<std::string> *out_vec)
{
size_t start = src_str.find_first_not_of(sep, 0), pos = src_str.find(sep, start);
while (start != std::string::npos)
{
out_vec->push_back(src_str.substr(start, pos - start));
start = src_str.find_first_not_of(sep, pos);
pos = src_str.find(sep, start);
}
}
};
五、SqliteHelper
SqliteHelper我们之前写过,直接拿过来就行,把cout换成日志就OK了
class SqliteHelper
{
public:
using SqliteCallback = int (*)(void *, int, char **, char **);
SqliteHelper(const string &dbfile)
: _dbfile(dbfile), _handler(nullptr) {
}
bool open()
{
if (sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, nullptr) != SQLITE_OK)
{
default_error("打开数据库失败 %s",errmsg().c_str());
return false;
}
return true;
}
void close()
{
if (_handler != nullptr)
sqlite3_close_v2(_handler);
}
bool exec(const string &sql, SqliteCallback cb, void *arg)
{
if (sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr) != SQLITE_OK)
{
default_error("执行sql语句:%s 失败,%s",sql.c_str(),errmsg().c_str());
return false;
}
return true;
}
string errmsg()
{
if (_handler != nullptr)
return sqlite3_errmsg(_handler);
else
return "sqlite3句柄为空";
}
private:
string _dbfile;
sqlite3 *_handler;
};
六、消息的proto文件
1.举例介绍RabbitMQ消息的投递过程
交通枢纽:
RabbitMQ服务器就像是城市交通中的一个大型交通枢纽,如火车站或机场。
生产者就像是来自不同地区的旅客,他们携带行李(消息)到达交通枢纽。
交换机就像是交通枢纽中的调度中心,负责将旅客(消息)引导到正确的候车区(队列)。
队列就像是候车区,旅客(消息)在这里等待下一班列车(消费者)的到来。
消费者就像是列车或飞机,它们定期到达候车区接载旅客(消息),并将其运送到目的地。
2.消息的proto文件定义
在正式开始项目代码编写之前,我们要定义消息的proto文件
- 交换机类型:
UNKNOWNTYPE
DIRECT
FANOUT
TOPIC - 消息的投递模式
UNKNOWNMODE
DURABLE
UNDURABLE - 消息的基本属性
消息ID
消息的投递模式
routing_key
因为我们的消息是基于发布确认模式的,只有当消息被确认之后才会删除该消息
而如何在文件当中找到对应消息呢?
我们可以借助于指针的思想:
每个消息结构体当中存放一个偏移量和一个长度,即可完全找到对应的消息
而消息删除时也需要将该消息在文件当中删除掉,如果直接删除,那就跟删除顺序表的中间数据一样,需要将后面的数据往前挪动覆盖
而这一成本在频繁的删除情况下太大了,尤其是文件IO上面的挪动覆盖
是需要一个字节一个字节的往前挪动覆盖,尽管此时寻址可能容易一点,但是还是需要重新寻址的,这一过程不正是磁盘IO慢的原因嘛,因为是非顺序的机械运动
- 所以我们采用标记(伪)删除
- 删除时直接读取数据,将有效标记位改成false,然后原样写回对应位置,即删除完毕
-
消息的有效载荷(持久化存储时消息需要往文件当中写的部分)
- 消息属性
- 消息具体内容
- 有效标记位
-
消息结构体
- 消息的有效载荷
- 消息有效载荷在文件当中的偏移量
- 消息有效载荷序列化后的长度
3. proto文件编写
1.为何有效标记不用bool?
实践是检验真理的唯一标准:
syntax = "proto3";
message A
{
bool my_bool = 1;
string body = 2;
}
static size_t size(const std::string &filename)
{
struct stat st;
if (stat(filename.c_str(), &st) == -1)
{
default_error("获取文件大小失败,errno: %d strerror: %s",errno,std::strerror(errno));
return 0; // 不要返回-1,因为会被转为整形最大值
}
return st.st_size;
}
int main()
{
A a;
a.set_my_bool(false);
a.set_body("hello");
ofstream ofs("heihei", ios::binary | ios::trunc);
string s = a.SerializeAsString();
ofs.write(s.c_str(), s.size());
ofs.close();
cout << FileHelper::size("heihei") << "\n";// my_bool为true时,文件大小是9 my_bool为false时,文件大小是7
return 0;
}
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog$ make
g++ -o cmd test.cc test.pb.cc -std=c++11 -lprotobuf -pthread
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog$ ./cmd
9
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog$ make
g++ -o cmd test.cc test.pb.cc -std=c++11 -lprotobuf -pthread
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog$ ./cmd
7
wzs@iZ2ze5xfmy1filkylv86zbZ:~/RabbitMQ/blog$
my_bool为true时,文件大小是9 my_bool为false时,文件大小是7
所以我们不能用bool,这为了代码健壮性考虑
2.proto文件
syntax = "proto3";
package ns_proto;
// 1. 交换机类型
enum ExchangeType
{
UNKNOWNTYPE = 0;
DIRECT = 1;
FANOUT = 2;
TOPIC = 3;
}
// 2. 消息的投递模式
enum DeliveryMode
{
UNKNOWNMODE = 0;
DURABLE = 1;
UNDURABLE = 2;
}
// 3. 消息的基本属性
message BasicProperities
{
string msg_id = 1;
DeliveryMode mode = 2;
string routing_key = 3;
}
// 4. 消息结构体
//为了便于管理消息:
// 1. 有效载荷(持久化在文件当中的)
// 属性
// 消息内容
// 2. 管理字段
// 是否有效
// 偏移量
// 消息长度
message Message
{
message ValidLoad
{
string body = 1;
BasicProperities properities = 2;
string valid = 3;// 因为bool的true/false在protobuf当中持久化后的长度不同,因此我们不用bool,而是用"0"代表无效,"1"代表有效
}
ValidLoad valid = 1;
uint64 offset = 2;
uint64 len = 3;
}
七、序列化与反序列化应用场景
序列化就是将结构化字段转为非结构化字段
反序列化就是将非结构化字段转为结构化字段
它的应用场景:
- 文件IO
- 网络IO
- 数据库IO:
当把某些结构化字段往数据库当中存储时需要序列化
比如:把<key,value>的键值对向数据库当中存储时 - 跨平台/跨语言的数据传输
使得不同平台,不同语言间实现轻松,高效的数据交换 - 组件间通信
通过将数据对象转换为统一的格式(JSON,protobuf等等),实现组件间的无缝通信,降低组件间的耦合度,增强系统的灵活性和可扩展性 - 在分布式系统当中
不同计算机之间的方法调用,数据共享和协作,都可以通过序列化与反序列化实现高效与解耦合 - 缓存存储:
将对象序列化后保存在缓存中(如Redis等等),可以提高数据的访问速度并减少数据库的访问压力。当需要访问数据时,可以直接从缓存中读取并反序列化
缓存存储这个就是利用了将结构化字段转为非结构化字段时,将结构体内存对齐所浪费的空间压缩没了,就像是一块有空隙的面包,我压一下,它就扁了,是通过将那些空隙压没了,也就是将结构体内存对齐所浪费的空间压缩没了。然后在放到Redis等高性能缓存系统当中,从而减少数据空间
又因为结构体取数据时需要频繁移动指针【多次内存跳转】(CPU缓存命中率低),而非结构化数据直接顺序读即可(CPU缓存命中率高),因此访问速度更高,访问压力更小
而分布式系统当中的数据传输,方法的远程调用,通常就采用RabbitMQ等消息中间件来完成
以上就是项目第三弹:基础工具类实现的全部内容