云备份
前言
书接上回:云备份(实用工具类)实现后,接下来会逐步实现不同模块的功能。会频繁用到工具类,如果不熟悉的老铁还是先看看前面的内容,对接下来要讲解的模块实现会有比较好理解。
配置文件加载模块
配置信息的设计
- 热点时间判断:设定一个合适的时间,决定在这个时间过后的文件是非热点文件。对于非热点文件进行压缩存储。
- 文件下载 URL 前缀路径:通常这个路径是用来判断用户的下载请求,可能是下载文件请求,也可能是查看备份文件列表的请求。
- 压缩文件的后缀名 :在这里默认将压缩后的文件后缀设置为
.lz
。 - 上传文件的存放路径:决定了用户上传文件的存储到服务器的哪里。
- 压缩包存放位置:对于非热点文件、压缩包 和 热点文件、普通文件要区分存储。决定了非热点文件、压缩文件的存储位置。
- 服务器备份信息存储到文件:记录备份文件信息,保证数据的持久化
- 服务器的监听IP、监听端口:当客户端运行到其他主机时,不再需要修改程序
单例文件配置类设计
这里的单例模式采用懒汉模式,对饿汉、懒汉单例模式的实现可以参考小编的这篇文章:特殊类设计
单例文件配置类是用来配置信息的,其中主要实现以下几个功能:
GetHotTime
:提供热点判断时间GetServerPort
:提供服务器监听端口GetServerIP
:提供服务器监听IPGetDownloadPrefix
:提供下载的 URL 前缀路径GetPackFileSuffix
:提供压缩包后缀名GetBackDir
:提供备份文件的存放目录GetPackDir
:提供压缩包存放的目录GetBackupFile
:提供信息存储的文件
下面开始进行代码的编写。
- 首先创建
cloud.conf
配置文件,编写配置信息。使用的是Json格式进行编写:
bash
{
"hot_time" : 30,
"server_port" : 9090,
"server_ip" : "xxx.xxx.xxx.xxx",
"download_prefix" : "/download/",
"packfile_suffix" : ".lz",
"pack_dir" : "./packdir/",
"back_dir" : "./backdir/",
"backup_file" : "./cloud.dat"
}
这里的服务器 IP 设置按照个人需求进行改写,当然端口号也是。
- 单例文件配置类的实现,创建
config.hpp
文件进行代码的编写:
cpp
#ifndef CLOUD_HPP
#define CLOUD_HPP
#include <mutex>
#include "util.hpp"
namespace cloud
{
#define CONFIG_FILE "./cloud.conf"
class Config
{
public:
// 获取单例对象指针
static Config *GetInstance()
{
// 双检查加锁
if (_instance == nullptr)
{
// 加锁
_mutex.lock();
if (_instance == nullptr)
{
return _instance = new Config();
}
// 解锁
_mutex.unlock();
}
}
//提供热点判断时间
int GetHotTime() { return _hot_time; }
//提供服务器监听端口
int GetServerPort() { return _server_port; }
//提供服务器监听IP
std::string GetServerIP() { return _server_ip; }
//提供下载的URL前缀路径
std::string GetDownloadPrefix() { return _download_prefix; }
//提供压缩包后缀名
std::string GetPackFileSuffix() { return _packfile_suffix; }
//提供备份文件的存放目录
std::string GetBackDir() { return _back_dir; }
//提供压缩包存放的目录
std::string GetPackDir() { return _pack_dir; }
//提供信息存储的文件
std::string GetBackupFile() { return _backup_file; }
private:
// 获取cloud.conf文件信息,进行初始化
bool ReadConfigFile()
{
cloud::FileUtil fu(CONFIG_FILE);
std::string body;
// 提取文件信息
if (fu.GetContent(&body) == false)
{
std::cout << "load config file failed!\n";
return false;
}
// 将提取的内容进行反序列化操作
Json::Value root;
if (JsonUtil::UnSerialize(body, &root) == false)
{
std::cout << "parse config file failed!\n";
return false;
}
// 初始化:
_hot_time = root["hot_time"].asInt();
_server_port = root["server_port"].asInt();
_download_prefix = root["download_prefix"].asString();
_packfile_suffix = root["packfile_suffix"].asString();
_pack_dir = root["pack_dir"].asString();
_back_dir = root["back_dir"].asString();
_backup_file = root["backup_file"].asString();
return true;
}
int _hot_time; // 热点时间判断
int _server_port; // 服务器监听端口
std::string _server_ip; // 服务器监听IP
std::string _download_prefix; // 文件下载的URL前缀路径
std::string _packfile_suffix; // 压缩文件的后缀名
std::string _pack_dir; // 压缩包存放目录
std::string _back_dir; // 备份文件的存放目录
std::string _backup_file; // 提供信息存储的文件
private:
Config()
{
ReadConfigFile();
}
// 防止单例的拷贝与赋值
Config(const Config &config) = delete;
Config &operator=(const Config &config) = delete;
static Config *_instance;
static std::mutex _mutex;
};
// 静态成员的初始化:类外进行
Config *Config::_instance = nullptr;
std::mutex Config::_mutex;
}
#endif
测试代码:
cpp
#pragma once
#include "util.hpp"
#include "config.hpp"
void ConfigTest()
{
// 实例化单例
cloud::Config *config = cloud::Config::GetInstance();
std::cout << config->GetHotTime() << std::endl;
std::cout << config->GetServerPort() << std::endl;
std::cout << config->GetServerIP() << std::endl;
std::cout << config->GetDownloadPrefix() << std::endl;
std::cout << config->GetPackFileSuffix() << std::endl;
std::cout << config->GetBackDir() << std::endl;
std::cout << config->GetPackDir() << std::endl;
std::cout << config->GetBackupFile() << std::endl;
}
int main(int argc, char *argv[])
{
ConfigTest();
return 0;
}
实现效果:
可以看到,这里将 cloud.conf 文件内容进行了正常输出,初始化也没有问题。
数据管理模块
好的管理模式,在后期可以帮助我们去更好的处理大量的数据。
为了后期更好的管理数据,并且使用这些数据,首先要对文件的以下这些数据信息进行封装管理:
- 文件的实际存储路径:客户端要下载文件时,从这个文件中读取数据进行响应。
- 文件压缩包存放路径:对于非热点文件会被压缩处理,压缩包的路径也是需要管理的。当用户需要这个文件时,要先对压缩包进行解压,让后再读取响应的内容。
- 文件是否压缩的标志位:用于判断文件是否压缩
- 文件大小
- 文件最后一次访问时间
- 文件最后一次修改时间
- 文件访问URL中的资源路径path:/download/a.txt
数据信息类设计
在 src 文件下创建 data.hpp
文件,对 BackupInfo 数据信息类进行代码编写。
- 实现
BackupInfo
数据信息类
BackupInfo 主要功能是对数据信息进行封装,实现如下:
c++
#include "config.hpp"
namespace cloud
{
struct BackupInfo
{
bool pack_flag; // 压缩文件的标志位
size_t fsize; // 文件大小
time_t mtime; // 文件最后一次修改时间
time_t atime; // 文件最后一次访问时间
std::string real_path; // 文件的实际存储路径
std::string pack_path; // 文件压缩包存放路径
std::string url; // URL中的资源路径path
//初始化信息
bool NewBackupInfo(const std::string& realpath)
{
FileUtil fu(realpath);
if(fu.Exists() == false)
{
std::cout << "new backupbnfo : file is not exists!\n";
return false;
}
Config* config = Config::GetInstance();
//获取配置文件信息
std::string packdir = config->GetPackDir();
std::string packsuffix = config->GetPackFileSuffix(); //获取压缩文件后缀
std::string download_prefix = config->GetDownloadPrefix(); //文件下载路径
pack_flag = false; //初始设置为普通文件
//赋值处理
fsize = fu.FileSize();
mtime = fu.LastMTime();
atime = fu.LastATime();
real_path = realpath; //文件实际所在的路径
//./backdir/a.txt -> ./packdir/a.txt.lz
pack_path = packdir + fu.FileName() + packsuffix; //压缩包路径
//./backdir/a.txt -> /download/a.txt
url = download_prefix + fu.FileName(); //文件下载路径
return true;
}
};
}
测试代码:
c++
#include "data.hpp"
void DataTest(const std::string &filename)
{
cloud::BackupInfo info;
info.NewBackupInfo(filename);
//输出BackupInfo数据信息
std::cout << info.pack_flag << std::endl;
std::cout << info.fsize << std::endl;
std::cout << info.mtime << std::endl;
std::cout << info.atime << std::endl;
std::cout << info.real_path << std::endl;
std::cout << info.pack_path << std::endl;
std::cout << info.url << std::endl;
}
int main(int argc, char *argv[])
{
DataTest(argv[1]);
return 0;
}
这里直接拿 src 当前目录下的文件进行测试,实现效果:
数据管理类实现
为了更快的访问数据信息,实现一个数据管理类。
在这里采用 hash 表在内存中管理数据;以 URL 的 path 路径作为key值;拿数据信息类 BackupInfo 对象作为为 val值。
其中,DataManager 数据管理类主要实现以下几个功能:
Insert
:支持插入新的数据信息Update
:支持更新数据信息GetOneByUrl
:通过URL资源路径查找对应文件的数据信息GetOneByRealPath
:通过文件路径查找对应的数据信息GetAll
:获取所有的文件数据信息
- 实现
DataManager
数据管理类
在实现前要讲一点:由于数据可以被多人访问,但是对于写只能允许一个人进行操作。
由此要引入一个 pthread_rwlock_t
读写锁:
- 读写锁 允许多个线程同时读取共享资源,但只允许一个线程进行写操作
下面是 DataManager
类的实现代码:
cpp
#include "config.hpp"
#include <unordered_map>
#include <pthread.h>
class DataManager
{
public:
DataManager()
{
// 初始化读写锁
pthread_rwlock_init(&_rwlock, nullptr);
// 将拷贝文件的信息进行初始化
_backup_file = Config::GetInstance()->GetBackupFile();
}
~DataManager()
{
// 销毁读写锁
pthread_rwlock_destroy(&_rwlock);
}
// 插入数据信息
bool Insert(const BackupInfo &info)
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
_table[info.url] = info; // 数据不存在直接进行插入
pthread_rwlock_unlock(&_rwlock); // 解锁
return true;
}
// 更新数据信息
bool UpDate(const BackupInfo &info)
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
_table[info.url] = info; // 数据不存在直接进行插入,否则更新对应的内容
pthread_rwlock_unlock(&_rwlock); // 解锁
return true;
}
// 通过URL的文件资源路径,获取对应的备份文件的数据信息
bool GetOneByUrl(const std::string &url, BackupInfo *info)
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
auto it = _table.find(url);
if (it == _table.end())
{
// 没有找到
pthread_rwlock_unlock(&_rwlock); // 解锁
return false;
}
// 找到了
*info = it->second; // 迭代器的second就是val值
pthread_rwlock_unlock(&_rwlock); // 解锁
return true;
}
// 通过文件路径查找备份文件的数据信息
bool GetOneByRealPath(const std::string &realpath, BackupInfo *info)
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
// 遍历整个_table
auto it = _table.begin();
while (it != _table.end())
{
if (it->second.real_path == realpath)
{
// 文件数据信息匹配到了
*info = it->second;
pthread_rwlock_unlock(&_rwlock); // 解锁
return true;
}
++it;
}
// 没有找到
pthread_rwlock_unlock(&_rwlock); // 解锁
return false;
}
//获取整个数据信息
bool GetAll(std::vector<BackupInfo> *array) //输出型参数
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
//遍历整个_table
auto it = _table.begin();
while(it != _table.end())
{
//_table表中的val值都插入array中
array->push_back(it->second);
++it;
}
pthread_rwlock_unlock(&_rwlock); // 解锁
return true;
}
private:
pthread_rwlock_t _rwlock; // 读写锁
std::string _backup_file; // 备份文件
std::unordered_map<std::string, BackupInfo> _table; // 文件路径,数据信息对象
};
测试代码:
cpp
#include "data.hpp"
void PrintBackUpInfo(const cloud::BackupInfo &info)
{
std::cout << info.pack_flag << std::endl;
std::cout << info.fsize << std::endl;
std::cout << info.mtime << std::endl;
std::cout << info.atime << std::endl;
std::cout << info.real_path << std::endl;
std::cout << info.pack_path << std::endl;
std::cout << info.url << std::endl;
}
void DataTest(const std::string &filename)
{
cloud::BackupInfo info;
info.NewBackupInfo(filename); // 初始化
cloud::DataManager data;
std::cout << "------------------Insert------------------------\n";
data.Insert(info); // 将备份文件数据信息进行插入
cloud::BackupInfo tmp;
data.GetOneByUrl("/download/bundle.h", &tmp); // 查看下载目录下是否存在bundle.h文件
// 打印对应的数据信息
PrintBackUpInfo(tmp);
std::cout << "------------------UpDate------------------------\n";
// 修改刚刚插入数据的内容
info.pack_flag = true;
data.UpDate(info);
std::vector<cloud::BackupInfo> array;
data.GetAll(&array);
for (auto &e : array)
{
PrintBackUpInfo(e);
}
std::cout << "------------------GetOneByRealPath------------------------\n";
data.GetOneByRealPath(filename, &tmp);
PrintBackUpInfo(tmp);
}
int main(int argc, char *argv[])
{
DataTest(argv[1]);
return 0;
}
在这里还是以 src 目录下的 bundle.h 文件为测试案例,测试结果如下:
数据持久化存储
为了持久化存储管理,采用Json序列化将所有的数据保存在文件中。
完善 DataManager 类,实现 Storage
成员函数。实现步骤如下:
- 获取 _table 中所有数据
- 将数据添加到 Json::Value 对象中
- 对 Json::Value 对象进行序列化操作
- 将序列化数据填写到文件
下面只展示当前功能代码,如下:
cpp
class DataManager
{
public:
//...其他成员函数
// 数据持久化操作
bool Storage()
{
// 1.获取所有的数据信息
std::vector<BackupInfo> array;
GetAll(&array);
// 2.将获取的数据添加到Json::Value对象
Json::Value root;
for (int i = 0; i < array.size(); i++)
{
Json::Value tmp;
tmp["pack_flag"] = array[i].pack_flag;
// Json不认识 time_t、size_t类型,需要强制类型转换
tmp["fsize"] = (Json::Int64)array[i].fsize;
tmp["mtime"] = (Json::Int64)array[i].mtime;
tmp["atime"] = (Json::Int64)array[i].atime;
tmp["real_path"] = array[i].real_path;
tmp["pack_path"] = array[i].pack_path;
tmp["url"] = array[i].url;
// 整个数据不仅仅只有一份
root[i].append(tmp);
}
// 3.序列化操作
std::string body;
if(JsonUtil::Serialize(root, &body) == false)
{
std::cout << "Data write failed!\n";
return false;
}
// 4.将数据存储到文件
FileUtil fu(_backup_file); // 写入到备份文件
fu.SetContent(body);
return true;
}
private:
pthread_rwlock_t _rwlock; // 读写锁
std::string _backup_file; // 备份文件
std::unordered_map<std::string, BackupInfo> _table; // 文件路径,数据信息对象
};
数据持久化操作,常用于新的数据插入后、数据更新后。
对此,需要对先前的 插入数据、更新数据的函数增添数据持久化功能,下面是 Insert、UpDate 函数修改后的代码:
cpp
// 插入数据信息
bool Insert(const BackupInfo &info)
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
_table[info.url] = info; // 数据不存在直接进行插入
pthread_rwlock_unlock(&_rwlock); // 解锁
//数据持久化
Storage();
return true;
}
// 更新数据信息
bool UpDate(const BackupInfo &info)
{
pthread_rwlock_wrlock(&_rwlock); // 上锁
_table[info.url] = info; // 数据不存在直接进行插入,否则更新对应的内容
pthread_rwlock_unlock(&_rwlock); // 解锁
//数据持久化
Storage();
return true;
}
初始化加载实现
初始化程序运行时,从文件读取数据。
完善 DataManager 类,实现 InitLoad
成员函数。实现步骤如下:
- 将数据从文件中读取出来
- 反序列化操作
- 反序列化得到的数据添加到 _table 中
下面只展示当前功能代码,如下:
cpp
class DataManager
{
public:
//...其他成员函数
//数据初始化加载
bool InitLoad()
{
//1.将数据从文件中读取出来
FileUtil fu(_backup_file);
if(fu.Exists() == false)
{
//文件不存在
std::cout << "The back file does not exist\n";
return false;
}
std::string body;
fu.GetContent(&body);
//2.反序列化
Json::Value root;
if(JsonUtil::UnSerialize(body, &root) == false)
{
std::cout << "Deserialization failed\n";
return false;
}
//3.将反序列化的数据添加到_table
for(int i = 0; i < root.size(); i++)
{
BackupInfo info;
info.pack_flag = root[i]["pack_flag"].asBool();
info.fsize = root[i]["fsize"].asInt64();
info.mtime = root[i]["mtime"].asInt64();
info.atime = root[i]["atime"].asInt64();
info.pack_path = root[i]["pack_path"].asString();
info.real_path = root[i]["real_path"].asString();
info.url = root[i]["url"].asString();
//将info添加到_table中
Insert(info);
}
return true;
}
private:
pthread_rwlock_t _rwlock; // 读写锁
std::string _backup_file; // 备份文件
std::unordered_map<std::string, BackupInfo> _table; // 文件路径,数据信息对象
};
初始化一般都会在构造函数内部进行。实现初始化信息加载后,需要对构造函数进行功能添加:
cpp
DataManager()
{
// 初始化读写锁
pthread_rwlock_init(&_rwlock, nullptr);
// 将拷贝文件的信息进行初始化
_backup_file = Config::GetInstance()->GetBackupFile();
//初始化加载
InitLoad();
}
到这里,数据管理模块的功能基本实现。
该篇文章主要实现了云备份项目的两个功能模块,由于篇幅过长,剩下模块的内容会在后续博客中体现。喜欢的老铁可以点赞 +
收藏!你们的关注是我持续更新的动力,感谢大家的观看。