项目第三弹:基础工具类实现

项目第三弹:基础工具类实现

  • 一、工具类的介绍
    • 1.生活例子
    • 2.专业术语
  • 二、FileHelper
    • 1.判断文件是否存在
      • 1.C++ IO流
      • 2.stat :Linux系统调用
    • 2.获取文件大小
    • 3.创建/删除文件
    • 4.创建/删除目录
    • 5.read
    • 6.write
    • 7.获取文件父级目录
    • 8.文件的重命名
    • 9.FileHelper完整代码
  • 三、UUIDHelper
    • 1.C++生成随机数
      • 1.random_device:随机数生成器
      • 2. mt19937_64梅森旋转算法
      • 3.uniform_int_distribution
    • 2.Code时间到
    • 3.多线程测试
  • 四、StringHelper
  • 五、SqliteHelper
  • 六、消息的proto文件
    • 1.举例介绍RabbitMQ消息的投递过程
    • 2.消息的proto文件定义
    • 3. proto文件编写
      • 1.为何有效标记不用bool?
      • 2.proto文件
  • 七、序列化与反序列化应用场景

一、工具类的介绍

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";}elsecout << "文件不存在\n";return 0;
}
综上:
只要别用ofstream的默认打开方式即可

好处:

  1. 简单直观
  2. C++库函数的跨平台优势

不足:

  1. 性能开销较大,效率低
  2. 资源浪费:查询文件是否存在还需频繁打开关闭文件
  3. 权限受限:因为文件系统是具有权限约束的, 有些文件是我们打不开的,此时尽管对应文件的确存在,但是我们也依然打不开。此时就会发生误判,需要我们额外处理
    (C++的IO流更注重文件操作,并不很关注底层OS上的相应问题)

刚才的代码没有判断权限问题,展示一番:

sudo touch advance_file
sudo chmod 640 advance_filewzs@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 directorybool 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└── d3 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进制字符的组合形式来确保全局唯⼀的同时能够根据序号来分辨数据(随机数⾁眼分辨起来真是太难了…)

因此,思想:

  1. 生成8个0~255之间的随机数
  2. 生成一个8字节的序号
  3. 通过以上数据,组成一个16字节的数据,转换为16进制字符,共32位

1.C++生成随机数

1.random_device:随机数生成器

int main()
{std::random_device rd;// 随机数生成器std::cout << rd() << "\n"; // 1775612373return 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-446655440000static 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 : 一个字节全1if (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-000000000013t4: 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);elsereturn "sqlite3句柄为空";}private:string _dbfile;sqlite3 *_handler;
};

六、消息的proto文件

1.举例介绍RabbitMQ消息的投递过程

交通枢纽:
RabbitMQ服务器就像是城市交通中的一个大型交通枢纽,如火车站或机场。
生产者就像是来自不同地区的旅客,他们携带行李(消息)到达交通枢纽。
交换机就像是交通枢纽中的调度中心,负责将旅客(消息)引导到正确的候车区(队列)。
队列就像是候车区,旅客(消息)在这里等待下一班列车(消费者)的到来。
消费者就像是列车或飞机,它们定期到达候车区接载旅客(消息),并将其运送到目的地。

2.消息的proto文件定义

在正式开始项目代码编写之前,我们要定义消息的proto文件

  1. 交换机类型:
    UNKNOWNTYPE
    DIRECT
    FANOUT
    TOPIC
  2. 消息的投递模式
    UNKNOWNMODE
    DURABLE
    UNDURABLE
  3. 消息的基本属性
    消息ID
    消息的投递模式
    routing_key
    在这里插入图片描述
    因为我们的消息是基于发布确认模式的,只有当消息被确认之后才会删除该消息
    而如何在文件当中找到对应消息呢?

我们可以借助于指针的思想:
每个消息结构体当中存放一个偏移量和一个长度,即可完全找到对应的消息

而消息删除时也需要将该消息在文件当中删除掉,如果直接删除,那就跟删除顺序表的中间数据一样,需要将后面的数据往前挪动覆盖
而这一成本在频繁的删除情况下太大了,尤其是文件IO上面的挪动覆盖

是需要一个字节一个字节的往前挪动覆盖,尽管此时寻址可能容易一点,但是还是需要重新寻址的,这一过程不正是磁盘IO慢的原因嘛,因为是非顺序的机械运动

  • 所以我们采用标记(伪)删除
  • 删除时直接读取数据,将有效标记位改成false,然后原样写回对应位置,即删除完毕
  1. 消息的有效载荷(持久化存储时消息需要往文件当中写的部分)

    1. 消息属性
    2. 消息具体内容
    3. 有效标记位
  2. 消息结构体

    1. 消息的有效载荷
    2. 消息有效载荷在文件当中的偏移量
    3. 消息有效载荷序列化后的长度

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;  
}

七、序列化与反序列化应用场景

序列化就是将结构化字段转为非结构化字段
反序列化就是将非结构化字段转为结构化字段

它的应用场景:

  1. 文件IO
  2. 网络IO
  3. 数据库IO:
    当把某些结构化字段往数据库当中存储时需要序列化
    比如:把<key,value>的键值对向数据库当中存储时
  4. 跨平台/跨语言的数据传输
    使得不同平台,不同语言间实现轻松,高效的数据交换
  5. 组件间通信
    通过将数据对象转换为统一的格式(JSON,protobuf等等),实现组件间的无缝通信,降低组件间的耦合度,增强系统的灵活性和可扩展性
  6. 在分布式系统当中
    不同计算机之间的方法调用,数据共享和协作,都可以通过序列化与反序列化实现高效与解耦合
  7. 缓存存储:
    将对象序列化后保存在缓存中(如Redis等等),可以提高数据的访问速度并减少数据库的访问压力。当需要访问数据时,可以直接从缓存中读取并反序列化

缓存存储这个就是利用了将结构化字段转为非结构化字段时,将结构体内存对齐所浪费的空间压缩没了,就像是一块有空隙的面包,我压一下,它就扁了,是通过将那些空隙压没了,也就是将结构体内存对齐所浪费的空间压缩没了。然后在放到Redis等高性能缓存系统当中,从而减少数据空间

又因为结构体取数据时需要频繁移动指针【多次内存跳转】(CPU缓存命中率低),而非结构化数据直接顺序读即可(CPU缓存命中率高),因此访问速度更高,访问压力更小

而分布式系统当中的数据传输,方法的远程调用,通常就采用RabbitMQ等消息中间件来完成

以上就是项目第三弹:基础工具类实现的全部内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/147028.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

华为摄像机/NVR主动注册协议接入SVMSP平台

华为摄像机/NVR主动注册协议接入SVMSPro平台 步骤一&#xff1a;进华为网页或者NVR界面进配置选项&#xff0c;左边选配置-网络-平台对接参数 勾选启用SDK注册开关&#xff1b;SDK主动注册 服务器地址&#xff1a;平台软件IP地址 端口&#xff1a;6060&#xff08;默认&#xf…

科研入门学习

学习视频链接 为什么要读论文 读哪些论文 论文的分类 论文质量 如何找论文 根据领域大牛的名字进行搜索查看高水平论文引用的论文&#xff0c;高水平论文引用的论文很大程度也是高水平的论文 如何整理论文 如何读论文 读论文的困境 不同人群阅读差异 读论文的方式 论文的结构…

【pyVista】在三维模型中的网格属性

一&#xff0c;什么是属性&#xff1f; 属性是存在于 一个网格。在 PyVista 中&#xff0c;我们同时使用点数据和单元数据&#xff0c;并且 允许轻松访问数据字典以保存属性数组 它们位于网格的所有点或所有单元上。 点数据 点数据是指值数组&#xff08;标量、向量等&#x…

mockito+junit搞定单元测试(2h)

一&#xff0c;简介 1.1 单元测试的特点 配合断言使用(杜绝 System.out )可重复执行不依赖环境不会对数据产生影响spring 的上下文环境不是必须的一般都需要配合 mock 类框架来实现 1.2 mock 类框架使用场景 要进行测试的方法存在外部依赖(如 db, redis, 第三方接口调用等)…

在Linux中运行flask项目

准备 这里我准备了一个GitHub上某个大佬写的留言板的Flask项目&#xff0c;就用这个来给大家做示范了。 查看留言板的目录结构 查看主程序所用的库函数 只有一个第三方库 Flask 安装pip sudo apt install python3-pip -y测试 pip 安装成功 修改pip镜像源 修改pip的默认下载…

Django学习实战之评论验证码功能(附A)

前言&#xff1a; 对于具有评论功能的博客来说&#xff0c;无论是否为知名博客&#xff0c;都会被恶意广告关注&#xff0c;上线没几天就会有人开始通过程序灌入广告评论&#xff0c;因此针对所有有用户输入的页面&#xff0c;验证码是必需品。 在Django系统中使用验证码非常简…

[Python数据可视化]探讨数据可视化的实际应用:三个案例分析

数据可视化是理解复杂数据集的重要工具&#xff0c;通过图形化的方法&#xff0c;可以直观地展示信息、趋势和模式。本文将深入探讨三个实际案例&#xff0c;包括健康数据分析、销售趋势分析、城市交通流量分析。每个案例将提供假设数据、详细注释的代码及分析结果。 案例 1: …

【每日刷题】Day128

【每日刷题】Day128 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 606. 根据二叉树创建字符串 - 力扣&#xff08;LeetCode&#xff09; 2. LCR 194. 二叉树的最近公…

Spring在不同类型之间也能相互拷贝?

场景还原 日常开发中&#xff0c;我们会定义非常多的实体&#xff0c;例如VO、DTO等&#xff0c;在涉及实体类的相互转换时&#xff0c;常使用Spring提供的BeanUtils.copyProperties&#xff0c;该类虽好&#xff0c;可不能贪用。 这不在使用过程中就遇到一个大坑&#xff0c…

逻辑分析仪看波形方法

一、串口波形讲解 异步串行数据的一般格式是&#xff1a;起始位数据位停止位&#xff0c;其中起始位1 位&#xff0c;数据位可以是5、6、7、8位&#xff0c;停止位可以是1、1.5、2位。 对于正逻辑的TTL电平&#xff0c; a.起始位是一个值为0的位&#xff0c;低电平&#xff…

leetcode练习 二叉树的最大深度

给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3提示&#xff1a; 树中节点的数量在 [0, 104] 区间内。-100 …

【图像检索】基于Gabor特征的图像检索,matlab实现

博主简介&#xff1a;matlab图像代码项目合作&#xff08;扣扣&#xff1a;3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于Gabor特征的图像检索&#xff0c;用matlab实现。 一、案例背景和算法介绍 这次博…

排序----快速排序(快排)(递归版)

首先讲一下单趟的思路&#xff1a; 在这一块数据中&#xff0c;记录第一个元素为key&#xff0c;然后设置L和R两个指针&#xff0c;L找比key处的元素大的&#xff0c;R找比key处元素小的&#xff0c;找到了就交换这两个位置的元素。当两个指针相遇时&#xff0c;若相遇点的元素…

20240921在友善之臂的NanoPC-T6开发板上确认宸芯的数传模块CX6602N的AT命令

console:/dev # cat ttyUSB1 & console:/dev # echo AT > ttyUSB1 20240921在友善之臂的NanoPC-T6开发板上确认宸芯的数传模块CX6602N的AT命令 2024/9/21 21:03 【必须】Android12/Linux&#xff08;Buildroot&#xff09;都必须要&#xff01; 4、【Android12默认打开U…

https的连接过程

根证书: 内置在操作系统和浏览器中,可手动添加,下级是中间证书或服务器证书,只有当中间证书或服务器证书关联到已存在的根证书时,中间证书或服务器证书才视为有效 中间证书: 位于根证书和服务器证书之间,他们之间也可以没有中间证书,作用是对根证书增加一个下级,方便管理,由根…

GAMES101(作业4~5)

作业四 题目&#xff1a; 由 4 个控制点表示的 Bzier 曲线&#xff0c; bezier&#xff1a;该函数实现绘制 Bzier 曲线的功能。它使用一个控制点序列和一个 OpenCV&#xff1a;&#xff1a;Mat 对象作为输入&#xff0c;没有返回值。它会使 t 在 0 到 1 的范围内进 行迭代&a…

【Linux】进程地址空间和进程调度队列

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/qinjh_/category_12625432.html 目录 问题现象 进程地址空间 进一步理解 地址空间 Linux2.6内核进程调度队列 …

RecyclerView的notifyDataSetChanged和notifyItemRemoved之间的区别

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 RecyclerView 提供了多种方法来通知适配器&#xff08;Adapter&#xff09;数据集发生变化&#xff0c;其中 notifyDataSetChanged() 和 notify…

数据库系统基础概述

文章目录 前言一、数据库基础概念 1.数据库系统的组成2.数据模型3.数据库的体系结构二、MySQL数据库 1.了解MySQL2.MySQL的特性3.MySQL的应用场景总结 前言 MySQL数据库是一款完全免费的产品&#xff0c;用户可以直接从网上下载使用&#xff0c;不用花费任何费用。这点对于初学…

proteus仿真学习(1)

一&#xff0c;创建工程 一般选择默认模式&#xff0c;不配置pcb文件 可以选用芯片型号也可以不选 不选则从零开始布局&#xff0c;没有初始最小系统。选用则有初始最小系统以及基础的main函数 本次学习使用从零开始&#xff0c;不配置固件 二&#xff0c;上手软件 1.在元件…