目录
项目介绍
将本地计算机上指定文件夹中需要备份的文件上传备份到服务器中。
能够随时通过浏览器进行查看并且下载,其中下载过程支持断点续传功能,
服务器也会对上传文件进行热点管理,将非热点文件进行压缩存储,节省磁盘空间。
这个云备份项目需要我们实现两端程序,其中包括部署在用户机的客户端程序,上传需要备份的文件,以及运行在服务器上的服务端程序,实现备份文件的存储和管理,两端合作实现总体的自动云备份功能。
服务端功能细分与模块划分
服务端功能细分:
- 支持客户端文件上传功能
- 支持客户端文件备份列表查看功能
- 支持客户端文件下载功能(断点续传)
- 对热点文件管理功能、对非热点文件进行压缩存储
服务端模块划分:
- 数据管理模块(负责服务器上备份文件的信息管理。)
- 网络通信模块(实现与客户端的网络通信)
- 业务处理模块(上传,列表,下载(断点续传))
- 热点管理模块(对长时间无访问文件进行压缩存储)
客户端功能细分与模块划分
客户端功能细分
- 指定目录的文件检测(获取一个文件夹中有什么文件)
- 判断指定的文件是否需要备份(新增文件、备份过但是修改了的文件)
- 将需要备份的文件上传备份到服务器
客户端模块划分:
- 数据管理模块(负责客户端备份的文件信息管理,通过这些数据可以确定一个文件是否需要备份。)
- 文件检测模块(监控指定文件夹)
- 文件备份模块/网络通信模块(搭建网络通信客户端,实现将文件数据备份上传到服务器。)
环境搭建
gcc升级到7.3版本
sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
gcc -v
查看版本号
安装jsoncpp库
sudo yum install epel-release
sudo yum install jsoncpp-devel
ls /usr/include/jsoncpp/json/
查看是否安装成功
下载bundle数据压缩库
git clone https://github.com/r-lyeh-archived/bundle.git
下载httplib库
git clone https://github.com/yhirose/cpp-httplib.git
第三方库的使用
第三方库的使用主要包括:
- jsoncpp库序列化和反序列化
- bundle文件压缩和解压
- httplib搭建服务器
在 C++项目】云备份项目(补充)——第三方库的使用
中详细介绍如何使用第三方库
服务端实现
文件操作工具类
先设计封装文件操作类,创建一个文件对象将磁盘上的一个文件关联起来,方便对该文件进行操作和获取文件的信息等操作,这样在任意模块中对文件进行操作时都将变的简单化。
文件实用类需要实现的功能:
- 获取文件大小
- 获取文件最后一次修改时间
- 获取文件最后一次访问时间
- 获取文件路径中的文件名
- 向文件写入数据、获取文件数据
- 获取文件指定位置,指定长度的数据(断点重传功能的实现需要该接口)
- 判断文件是否存在
- 创建文件目录、浏览文件目录
- 压缩文件、解压文件
- 删除文件
接口设计:
/*util.hpp*/
class FileUtil{
private:
std::string _filename;
public:
size_t FileSize();//获取文件大小
time_t LastModifyTime();//获取文件最后一次修改时间
time_t LastAcccessTime();//获取文件最后一次访问时间(热点管理)
std::string FileName();//获取文件路径名中的文件名
bool SetContent(const std::strint& body);//向文件写入数据
bool GetContent(std::string *body);//获取文件所有数据
bool GetPosLen(std::string *body, size_t pos, size_t len);//获取文件指定位置 指定长度的数据
bool Exists();//判断文件是否存在
bool CreateDirectory();//创建目录
bool GetDirectory(std::vector<std::string>* arry);//获取目录下所有文件名
bool Compress(const std::string& packname);//压缩文件
bool UnCompress(const std::string& filename);//解压文件
};
获取文件大小
stat接口:获取文件属性到struct stat结构体中
struct stat结构体:
int64_t FileSize() // 获取文件大小
{
struct stat st;
if(stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed\n";
return -1;
}
return st.st_size;
}
获取文件最后访问和修改时间
同样使用stat接口
time_t LastModifyTime() // 获取文件最后一次修改时间
{
struct stat st;
if(stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed\n";
return -1;
}
return st.st_mtime;
}
time_t LastAcccessTime()// 获取文件最后一次访问时间(热点管理)
{
struct stat st;
if(stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed\n";
return -1;
}
return st.st_atime;
}
获取文件名称
其实就是字符串解析
std::string FileName() // 获取文件路径名中的文件名
{
int pos = _filename.find_last_of("/");
if (pos <0)
{
std::cout << "filename error" << std::endl;
return nullptr;
}
if(pos=std::string::npos)
{
return _filename;
}
return _filename.substr(pos + 1);
}
文件数据的读写操作
// 获取文件指定位置 指定长度的数据
bool GetPosLen(std::string *body, size_t pos, size_t len)
{
size_t fsize=this->FileSize();
if(pos+len>fsize)
{
std::cout<<"error reading length\n";
return false;
}
std::ifstream ifs;
ifs.open(_filename,std::ios::binary);
if(ifs.is_open()==false)
{
std::cout<<"read open file failed\n";
return false;
}
ifs.seekg(pos,std::ios::beg);
body->resize(len);
ifs.read(&(*body)[0],len);
if(ifs.good()==false)
{
std::cout<<"get filecontent failed\n";
ifs.close();
return false;
}
ifs.close();
return true;
}
// 获取文件所有数据
bool GetContent(std::string *body)
{
size_t fsize=this->FileSize();
return GetPosLen(body,0,fsize);
}
// 向文件写入数据
bool SetContent(const std::string &body)
{
std::ofstream ofs;
ofs.open(_filename,std::ios::binary);
if(ofs.is_open()==false)
{
std::cout<<"write open file failed\n";
return false;
}
ofs.write(&body[0],body.size());
if(ofs.good()==false)
{
std::cout<<"write content failed\n";
ofs.close();
return false;
}
ofs.close();
return true;
}
文件压缩&解压
使用bundle库pack和unpack来完成压缩和解压缩
T pack( unsigned Q, T );//按照Q的压缩格式对T中的数据进行解压缩
T unpack( T ); //将T中的数据进行解压缩
// 压缩文件
bool Compress(const std::string &packname)
{
//1.获取原文件数据
std::string body;
if(this->GetContent(&body)==false)
{
std::cout<<"compress get content failed\n";
return false;
}
//2.对数据进行压缩bundle::pack
std::string packed=bundle::pack(bundle::LZIP,body);
//3.压缩后的数据存储到压缩文件中
FileUtil fu(packname);
if(fu.SetContent(packed)==false)
{
std::cout<<"compress write packed data failed\n";
return false;
}
return true;
}
// 解压文件
bool UnCompress(const std::string &filename)
{
//1.获取当前压缩包得数据
std::string body;
if(this->GetContent(&body)==false)
{
std::cout<<"uncompress get content failed\n";
return false;
}
//2.对数据进行解压bundle::unpack
std::string unpacked=bundle::unpack(body);
//3.将解压后的数据保存到新文件中
FileUtil fu(filename);
if(fu.SetContent(unpacked)==false)
{
std::cout<<"uncompress write packed data failed\n";
return false;
}
return true;
}
判断文件是否存在
需要使用c++17支持的filesystem library文件系统库。
C++17中filesystem手册:https://en.cppreference.com/w/cpp/experimental/fs
使用该库实现文件目录的创建, 检索文件是否存在, 获取不带路径的纯文件名, 浏览遍历指定目录下文件, 删除文件等操作。
filesystem library库exists接口判断文件是否存在,传入文件名即可:
namespace fs=std::experimental::filesystem;
bool Exists() // 判断文件是否存在
{
return fs::exists(_filename);
}
创建目录
使用filesystem library库create_directory接口
bool CreateDirectory()// 创建目录
{
if(this->Exists()) return true;
return fs::create_directories(_filename);
}
获取目录下所有文件的相对路径
需要使用c++17支持的filesystem library文件系统库中的目录迭代器directory_iterator。
- 遍历迭代器获取到的文件信息存放在directory_entry对象里面,
- 判断该文件是否是一个目录is_directory接口,如果是目录则不处理
- 不是目录,directory_entry类中包括一个path的类型的变量,path类里面有一个relative_path接口。这个接口是获取文件的相对路径。
- 添加文件相对路径到数组中。
bool ScanDirectory(std::vector<std::string> *arry)
{
for(auto& p:fs::directory_iterator(_filename))
{
if(fs::is_directory(p)==true) continue;
arry->push_back(fs::path(p).relative_path().string());
}
return 0;
}
使用c++17filesystem_library在编译时要链接库 -lstdc++fs
Json工具类
Json工具类的设计需要使用到Jsoncpp,主要是对Json序列化反序列化进行一个封装
/*util.hpp*/
class JsonUtil{
public:
static bool Serialize(const Json::Value &root, std::string *str)//静态函数
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(root,&ss);
*str=ss.str();
}
static bool UnSerialize(const std::string &str, Json::Value *root)
{
std::string err;
Json::CharReaderBuilder crb;
std::shared_ptr<Json::CharReader> cr(crb.newCharReader());
bool ret=cr->parse(str.c_str(),str.c_str()+str.size(),root,&err);
if(ret==false)
{
std::cout<<"parse error: "<<err<<std::endl;
return false;
}
return true;
}
};
服务端配置信息模块实现
系统配置信息
使用文件配置加载一些程序的运行关键信息可以让程序的运行更加灵活。
配置信息:
- 热点判断时间(多长时间没有访问的文件算非热点文件)
- 文件下载URL前缀路径(用于表示客户端请求是一个下载请求)
- 压缩包后缀名称(压缩包命名规则,就是在文件原名称后加上后缀)
- 上传文件存放路径(决定文件上传后实际存储在服务器的什么地方)
- 压缩文件存放路径(非热点文件压缩后存放的路径)
- 服务端备份信息存放文件(服务端所记录的备份文件信息的持久化存储)
- 服务器监听IP 地址
- 服务器的监听端口
配置文件的编写按照json的格式:
系统运行配置信息的单例类设计
该类用于加载配置信息,并且对外提供获取对应配置信息的接口。使用单例模式管理系统配置信息,能够让配置信息的管理控制更加统一灵活。
接口设计:
class Config{
private:
time_t _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 _manager_file;//备份信息的存放文件
private:
static std::mutex _mutex;
static Config *_instance;
Config();//构造函数内,加载配置文件,(读取配置文件数据进行解析,将数据存放到私有成员里面)
public:
bool ReadConfig(const std::string &filename);
int GetHotTime();
int GetServerPort();
std::string GetServerIp();
std::string GetDownloadPrefix();
std::string GetPackFileSuffix();
std::string GetPackDir();
std::string GetBackDir();
std::string GetManagerFile();
public:
static Config *GetInstance();
};
代码实现
#ifndef __MY_CONFIG_H__
#define __MY_CONFIG_H__
#include "util.hpp"
#include <mutex>
namespace vic_cloud
{
#define CONFIG_FILE "./cloud.conf"
class Config
{
private:
static std::mutex _mutex;
static Config *_instance;
// 构造函数内,加载配置文件,(读取配置文件数据进行解析,将数据存放到私有成员里面)
Config()
{
ReadConfigFile();
}
public:
bool ReadConfig(const std::string &filename);
int GetHotTime()
{
return _hot_time;
}
int GetServerPort()
{
return _server_port;
}
std::string GetServerIp()
{
return _server_ip;
}
std::string GetDownloadPrefix()
{
return _download_prefix;
}
std::string GetPackFileSuffix()
{
return _packfile_suffix;
}
std::string GetPackDir()
{
return _pack_dir;
}
std::string GetBackDir()
{
return _back_dir;
}
std::string GetBackupFile()
{
return _backup_file;
}
// 读取配置文件
bool ReadConfigFile()
{
FileUtil f(CONFIG_FILE);
std::string body;
if (f.GetContent(&body) == false)
{
std::cout << "load config failed\n";
return false;
}
Json::Value root;
if (JsonUtil::UnSerialize(body, &root) == false)
{
std::cout << "parse config failed\n";
return false;
}
_hot_time = root["hot_time"].asInt();
_server_port = root["server_port"].asInt();
_server_ip = root["server_ip"].asString();
_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;
}
public:
static Config *GetInstance()
{
if (_instance == NULL)
{
_mutex.lock();
if (_instance == NULL)
{
_instance = new Config();
}
_mutex.unlock();
}
return _instance;
}
private:
time_t _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; // 备份信息的存放文件
};
Config *Config::_instance = NULL;
std::mutex Config::_mutex;
}
#endif
测试:
服务端数据管理模块实现
数据管理模块用于服务端对上传的备份文件进行数据信息管理,更具后续的业务需求需要管理的信息如下:
- 文件的实际存储路径:当客户端要下载文件时,则从这个文件中读取数据进行响应
- 文件压缩包存放路径名:如果这个文件是一个非热点文件会被压缩,则这个就是压缩包路径名称如果客户端要下载文件,则需要先解压缩,然后读取解压后的文件数据。
- 文件是否压缩的标志位:判断文件是否已经被压缩了
- 文件大小
- 文件最后一次修改时间
- 文件最后一次访问时间
- 文件访问URL中资源路径path:/download/a.txt
如何管理数据:
- 用于数据信息访问:使用hash表在内存中管理数据,以url的path作为key值,数据信息结构为val—查询速度快O(1)
- 持久化存储管理:使用json序列化将所有数据信息保存在文件中
数据管理类的设计
接口设计:
/*data.hpp*/
//数据信息结构体
typedef struct BackupInfo_t{
bool _pack_flag;//是否压缩标志
time_t _mtime;//最后修改时间
time_t _atime;//最后访问时间
size_t _fsize;//文件大小
std::string _real_path;//文件实际存储路径名
std::string _url_path;//请求的资源路径
std::string _pack_path;//压缩报存储路径名
bool NewBackupInfo(const std::string &realpath);
}BackupInfo;
//数据管理类
class DataManager{
private:
std::string _backup_file;//持久化存储文件
pthread_rwlock_t _rwlock;//读写锁,读共享,写互斥
std::unordered_map<std::string, BackupInfo> _table;//内存中hash存储的文件信息管理表
public:
DataManager();
//初始化加载,每次系统重启都要加载以嵌的数据
bool InitLoad();
//每次有信息改变则需要持久化存储一次
bool Storage();
//新增
bool Insert(const BackupInfo &info);
//修改
bool Update(const BackupInfo &info);
//根据请求url获取对应文件信息(用户根据url请求下载文件)
bool GetOneByURL(const std::string &url, BackupInfo *info);
//根据真实路径获取文件信息,(服务器端测备份文件 热点文件判断)
bool GetOneByRealPath(const std::string &realpath, BackupInfo *info);
//获取所有文件信息
bool GetAll(std::vector<BackupInfo> *arry);
};
代码实现:
#ifndef __M_DATA_H__
#define __M_DATA_H__
#include<unordered_map>
#include<pthread.h>
#include"util.hpp"
#include"config.hpp"
namespace vicloud
{
//数据信息结构体
typedef struct BackupInfo_t{
bool _pack_flag;//是否压缩标志
time_t _mtime;//最后修改时间
time_t _atime;//最后访问时间
size_t _fsize;//文件大小
std::string _real_path;//文件实际存储路径名
std::string _url_path;//请求的资源路径
std::string _pack_path;//压缩报存储路径名
//数据填充(获取各项属性信息,存储到BackupInfo结构体)
bool NewBackupInfo(const std::string &realpath)
{
FileUtil f(realpath);
if(f.Exists()==false)
{
std::cout<<"new backupfile file not find\n";
return false;
}
Config* config=Config::GetInstance();
std::string packdir=config->GetPackDir();
std::string packsuffix=config->GetPackFileSuffix();
std::string downloadprefix=config->GetDownloadPrefix();
this->_pack_flag=false;
this->_fsize=f.FileSize();
this->_mtime=f.LastModifyTime();
this->_atime=f.LastAcccessTime();
this->_real_path=realpath;
// ./backdir/a.txt --> ./packdir/a.txt.lz
this->_pack_path=packdir+f.FileName()+packsuffix;
// ./backdir/a.txt --> /download/a.txt
this->_url_path=downloadprefix+f.FileName();
return true;
}
}BackupInfo;
//数据管理类
class DataManager{
private:
std::string _backup_file;//持久化存储文件
pthread_rwlock_t _rwlock;//读写锁,读共享,写互斥
std::unordered_map<std::string, BackupInfo> _table;//内存中hash存储的文件信息管理表
public:
DataManager()
{
_backup_file=Config::GetInstance()->GetBackupFile();
//读写锁初始化
pthread_rwlock_init(&_rwlock,NULL);
InitLoad();
}
~DataManager()
{
//销毁锁初始化
pthread_rwlock_destroy(&_rwlock);
}
//初始化加载,每次系统重启都要加载以嵌的数据
bool InitLoad()
{
//1.读取backup_file备份信息的存放文件中的数据
FileUtil f(_backup_file);
if(f.Exists()==false)
{
return true;
}
std::string body;
f.GetContent(&body);
//2.反序列化
Json::Value root;
JsonUtil::UnSerialize(body,&root);
//3.将反序列化得到的Json::Value中的数据添加到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._atime=root[i]["atime"].asInt64();
info._mtime=root[i]["mtime"].asInt64();
info._pack_path=root[i]["pack_path"].asString();
info._real_path=root[i]["real_path"].asString();
info._url_path=root[i]["url_path"].asString();
Insert(info);
}
return true;
}
//每次有信息改变则需要持久化存储一次
bool Storage()
{
//1.获取所有数据
std::vector<BackupInfo> arr;
this->GetAll(&arr);
//2.填充到Json::Value中
Json::Value root;
for(int i=0;i<arr.size();i++)
{
Json::Value val;
val["pack_flag"]=arr[i]._pack_flag;
val["fsize"]=(Json::Int64)arr[i]._fsize;
val["atime"]=(Json::Int64)arr[i]._atime;
val["real_path"]=arr[i]._real_path;
val["pack_path"]=arr[i]._pack_path;
val["url_path"]=arr[i]._url_path;
root.append(val);
}
//3.对Json::Value序列化
std::string body;
JsonUtil::Serialize(root,&body);
//4.写文件
FileUtil f(_backup_file);
f.SetContent(body);
return true;
}
//新增
bool Insert(const BackupInfo &info)
{
pthread_rwlock_wrlock(&_rwlock);//读写锁加锁
_table[info._url_path]=info;
pthread_rwlock_unlock(&_rwlock);
Storage();
return true;
}
//修改
bool Update(const BackupInfo &info)
{
pthread_rwlock_wrlock(&_rwlock);//读写锁加锁
_table[info._url_path]=info;
pthread_rwlock_unlock(&_rwlock);
Storage();
return true;
}
//根据请求url获取对应文件信息(用户根据url请求下载文件)
bool GetOneByURL(const std::string &url, BackupInfo *info)
{
pthread_rwlock_wrlock(&_rwlock);//读写锁加锁
auto it =_table.find(url);//url是key值直接find查找
if(it==_table.end())
{
pthread_rwlock_unlock(&_rwlock);
return false;
}
*info=it->second;
pthread_rwlock_unlock(&_rwlock);
return true;
}
//根据真实路径获取文件信息,(服务器端测备份文件 热点文件判断)
bool GetOneByRealPath(const std::string &realpath, BackupInfo *info)
{
pthread_rwlock_wrlock(&_rwlock);//读写锁加锁
auto it =_table.begin();
//真实路径需要遍历unordered_map 中second的real_path
for(;it!=_table.end();it++)
{
if(it->second._real_path==realpath)
{
*info=it->second;
pthread_rwlock_unlock(&_rwlock);
return true;
}
}
pthread_rwlock_unlock(&_rwlock);
return false;
}
//获取所有文件信息
bool GetAll(std::vector<BackupInfo> *arry)
{
pthread_rwlock_wrlock(&_rwlock);//读写锁加锁
//遍历
auto it =_table.begin();
for(;it!=_table.end();it++)
{
arry->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
};
}
#endif
测试:
服务端热点管理理模块实现
对服务器上备份的文件进行检测,长时间没有访问认为是非热点文件,进行压缩存储,节省磁盘空间
实现思路:
遍历所有文件,检测文件的最后一次访问时间,与当前时间相减得到插值,得到的插值大于设定好的值则认为是非热点文件,进行压缩存放到压缩路径中,删除原文件。
遍历所有文件的方法:
- 从数据管理模块中遍历所有的备份文件
- 遍历备份文件夹,对所有文件进行属性获取
选择第二种:遍历备份文件夹,这样每一次都可以获取到文件的最新数据,还可以解决数据信息缺漏的问题
热点管理流程:
- 遍历备份目录,获取所有文件路径名称
- 逐个文件获取最后一次访问时间与当前系统时间进行比较判断
- 对非热点文件进行压缩处理,删除源文件
- 修改数据管理模块对应的文件信息(压缩标志(_pack_flag)–>true)
热点管理类的设计
#ifndef __MY_HOT_H__
#define __MY_HOT_H__
#include "data.hpp"
#include<unistd.h>
extern vic_cloud::DataManager *_data; // 先声明一个数据管理类的全局变量,方便使用
namespace vic_cloud
{
class HotManager
{
private:
std::string _back_dir; // 备份文件的路径
std::string _pack_dir; // 压缩文件的路ing
std::string _pack_suffix; // 压缩文件后缀
int _hot_time; // 热点判断时间
public:
HotManager()
{
Config *config = Config::GetInstance();
_back_dir = config->GetBackDir();
_pack_dir = config->GetPackDir();
_pack_suffix = config->GetPackFileSuffix();
_hot_time = config->GetHotTime();
//如果目录不存在,创建目录
FileUtil tmp1(_back_dir);
FileUtil tmp2(_pack_dir);
tmp1.CreateDirectory();
tmp2.CreateDirectory();
}
// 热点文件压缩功能实现
bool RunModule()
{
while (1)
{
{
// 1. 遍历备份目录,获取所有文件路径名称
FileUtil f(_back_dir);
std::vector<std::string> arr;
f.ScanDirectory(&arr);
// 2. 判断是否位热点文件
for (auto &a : arr)
{
if (HotJudge(a) == false)
{
continue; // 热点文件不处理
}
// 3. 对非热点文件进行压缩处理
// 获取文件的备份信息
BackupInfo bi;
if (_data->GetOneByRealPath(a, &bi) == false)
{
// 如果文件存在,但是没有备份信息--设置一个新的备份信息
bi.NewBackupInfo(a);
}
FileUtil tmp(a);
tmp.Compress(bi._pack_path);
// 4. 删除源文件,修改数据管理模块对应的文件信息(压缩标志(_pack_flag)–>true)
tmp.Remove();
bi._pack_flag = true;
_data->Update(bi);
}
usleep(1000);
}
}
}
private:
// 私有接口判断是否是热点文件
bool HotJudge(const std::string &filename)
{
// 逐个文件获取最后一次访问时间与当前系统时间进行比较判断
FileUtil f(filename);
time_t last_atime = f.LastAcccessTime();
time_t cur_time = time(NULL);
if (cur_time - last_atime > _hot_time)
{
return true;
}
return false;
}
};
}
#endif
测试:
拷贝一个文件到 backdir中,等待30秒(热点事件设置的30秒)packdir中出现压缩文件
服务端业务处理模块实现
业务处理实现思路
云备份项目中 ,业务处理模块是针对客户端的业务请求进行处理,并最终给与响应。而整个过程中包含以下要实现的功能:
- 借助网络通信模块httplib库搭建http服务器与客户端进行网络通信
- 针对收到的请求进行对应的业务处理并进行响应。
- 文件上传请求,备份客户端上传的文件,响应上传成功
- 文件列表展示请求,客户端浏览器请求一个备份文件的展示页面,响应页面
- 文件下载请求,通过展示也买你,点击下载,响应客户端要下载的文件数据
网络通信接口设计
约定好,客户端发送什么样的请求,我们给与什么样的响应。
-
文件上传:
当服务器收到了一个POST方法的/upload情求,我们则认为这是一个文件上传请求解析请求,得到文件数据,将数据写入到文件中响应HTTP/1.1 200 OK
-
展示页面
当服务器收到了一个GET方法的/listshow情求,我们则认为这是一个文件列表展示请求。响应html页面数据
-
文件下载
当服务器收到了一个GET方法的/download情求,我们则认为这是一个文件下载请求。响应文件数据
服务端业务处理类的设计
结合httplib完成
接口设计&整体框架:
#ifndef __M_SERVICE_H__
#define __M_SERVICE_H__
#include "data.hpp"
#include "httplib.h"
// 因为业务处理的回调函数没有传入参数的地方,因此无法直接访问外部的数据管理模块数据
// 可以使用lamda表达式解决,但是所有的业务功能都要在一个函数内实现,于功能划分上模块不够清晰
// 因此将数据管理模块的对象定义为全局数据,在这里声明一下,就可以在任意位置访问了
extern vic_cloud::DataManager *_data;
namespace vic_cloud
{
class Service
{
public:
Service()
{
Config *config =Config :: GetInstance();
_server_port= config->GetServerPort();
_server_ip= config->GetServerIp();
_download_prefix= config->GetDownloadPrefix();
}
// 服务启动(httplib 绑定对应处理函数)
bool RunModule()
{
_server.Post("/upload",UpLoad);
_server.Get("/listshow",ListShow);
//std::string download_url=_download_prefix+"(.*)";
_server.Get("/download/(.*)",DownLoad);
//正则表达式 :
// . 匹配除\n和\r之外的任何单个字符
// .* 匹配任意一个字符任意次
_server.Get("/",ListShow);
_server.listen("0.0.0.0",_server_port);//启动服务器
}
private:
// 注意回调函数都需要设置成static,因为httplib库中函数要求的参数只有两个
// 如果不用static修饰,那么会多出来一个this指针
// 上传文件
static void UpLoad(const httplib::Request &req, httplib::Response &res);
// 文件列表展示
static void ListShow(const httplib::Request &req, httplib::Response &res);
// 文件下载
static void DownLoad(const httplib::Request &req, httplib::Response &res);
private:
int _server_port; // 端口号
std::string _server_ip; // 服务器ip
std::string _download_prefix; // 下载路径前缀
httplib::Server _server; // httplib库搭建服务器
};
}
#endif
文件上传
- 服务器收到上传请求后,需要先判断是否有文件字段名,如果有则提取该文件的信息。
- 然后使用FileUtil文件工具打开一个文件存放在backdir下,并将文件正文写入到该文件中,向数据管理模块添加对应的备份信息
- 重新返回一个文件页面
// 上传文件
static void UpLoad(const httplib::Request &req, httplib::Response &res)
{
//post/upload 文件数据在正文中(分区存储,正文并不是全是文件数据)
std::cout<<"uploading 。。。。"<<std::endl;
//判断是否有文件上传
auto ret=req.has_file("file");
if(ret==false)
{
res.status=400;
return;
}
const auto& file=req.get_file_value("file");
std::string back_dir=Config::GetInstance()->GetBackDir();
std::string realpath=back_dir+FileUtil(file.filename).FileName();
FileUtil f(realpath);
f.SetContent(file.content);//将数据写入文件
BackupInfo info;
info.NewBackupInfo(realpath);
_data->Insert(info);//向数据管理模块添加对应的备份信息
return ;
}
文件展示
用stringstream对象将前端页面的源代码组织起来,然后组织在http响应中,httplib服务器在底层会将http响应发送給浏览器。
// 文件列表展示
static void ListShow(const httplib::Request &req, httplib::Response &res)
{
//1.获取所有的文件备份信息
std::vector<BackupInfo> arr;
_data->GetAll(&arr);
//组织html文件数据
std::stringstream ss;
ss<<"<html><head><title>Download</title></head>";
ss<<"<body><h1>Download</h1><table>";
for(auto &a: arr)
{
ss<<"<tr>";
std::string filename=FileUtil(a._real_path).FileName();
ss<<"<tb><a href='"<<a._url_path<<"'>"<<filename<<"</a></td>";
ss<<"<td align='right'>"<<TimetoStr(a._mtime)<<"</td>";
ss<<"<td align='right'>"<<a._fsize/1024<<"k</tb>";
ss<<"</tr>";
}
ss<<"</table></body></html>";
res.body=ss.str();
res.set_header("Content_Type","text/html");
res.status=200;
return ;
}
static std::string TimetoStr(time_t t)
{
std::string tmp=std::ctime(&t);
return tmp;
}
文件下载
http的ETag头部字段:其中存储了一个资源的唯一标识
客户端第一次下载文件的时候,会收到这个响应信息
第二次下载,就会将这个信息发送给服务器,想要让服务器根据这个唯一标识判断这个资源有没有被修改过,如果没有被修改过,直接使用原先缓存的数据,不用重新下载,
http协议木身对于etag中是什么数据并不关心,只要服务端能够自己标识就行
因此etag=文件名-文件大小-最后一次修改时间
并且etag字段不仅仅是缓存用到,还有就是后边的断点续传的实现也会用到
因为断点续传也要保证文件没有被修改过
http协议的Accept-Ranges:bytes字段:用于告诉客户端其支持断点续传功能,并且数据单位以字节作为单位
// 文件下载
static std::string GetETag(const BackupInfo &info)
{
// etg:filename-fsize-mtime
FileUtil fu(info._real_path);
std::string etag=fu.FileName();
etag+="-";
etag+=std::to_string(info._fsize);
etag+="-";
etag+=std::to_string(info._mtime);
return etag;
}
static void DownLoad(const httplib::Request &req, httplib::Response &res)
{
//1. 获取客户端请求的资源路径path req.path
//2. 根据资源路径,获取文件的备份信息
BackupInfo info;
_data->GetOneByURL(req.path,&info);
//3.判断文件是否被压缩,如果被压缩,先进行解压
if(info._pack_flag==true)
{
FileUtil fu(info._pack_path);
fu.UnCompress(info._real_path);//将文件解压到备份目录下
// 删除压缩包,修改备份信息(文件已经没有被压缩)
fu.Remove();
info._pack_flag=false;
_data->Update(info);
}
//4. 读取文件数据,并将数据写入rsp.body中
FileUtil fu(info._real_path);
fu.GetContent(&res.body);
//5. 设置响应头部字段:ETag,Accept-Ranges:bytes
res.set_header("Accept-Ranges","bytes");
res.set_header("ETag",GetETag(info));
res.set_header("Content-Type","application/octet-stream");//application/octet-stream——表示响应的正文是一个二进制数据流(常用于文件下载)
res.status=200;
}
Content-Type字段的重要性:决定了浏览器如何处理响应正文
断点续传
功能:
当文件下载过程中,因为某种异常而中断,如果再次进行从头下载,效率较低,因为需要将之前已经传输过的数据再次传输一遍。因此断点续传就是从上次下载断开的位置,重新下载即可,之前已经传输过的数据将不需要在重新传输。
目的:
提高文件重新传输效率
实现思想:
客户端在下载文件的时候,要每次接收到数据写入文件后记录自己当前下载的数据量。
当异常下载中断时,下次断点续传的时候,将要重新下载的数据区间(下载起始位置,结束位置)发送给服务器,服务器收到后,仅仅回传客户端需要的区间数据即可。
需要考虑的问题:
如果上次下载文件之后。这个文件在服务器上被修改了,则这时候将不能重新断点续传,而是应该重新进行文件下载操作。
在http协议中断点续传的实现:
主要关键点:
- 在于能够告诉服务器下载区间范围,
- 服务器上要能够检测上一次下载之后这个文件是否被修改过
下载文件服务器給浏览器发送的http响应:Accept-Ranges和ETag字段
Accept-Ranges一般被设置为bytes ,表示这个字段表示服务器支持断点续传,以字节为单位传输数据.
ETag表示的是服务器上某一版本资源的唯一标识,如果资源被改动过,则ETag会改变,客户端收到后则会保证这个信息。下载中断客户端第二次的http请求:If-Range和Range字段
If-Range字段:“保存服务端的响应的ETag字段的信息”,用于判断与服务端中与上一次请求资源是否一致,如果一致则断点续传,如果不一致,则重新开始下载。
Range字段:bytes start-end,表示的是请求服务器资源从start个字节开始到end个字节结束的数据。断电续传服务端发送http响应:Content-Range和ETag字段:
Content-Range:start-end/文件大小,表示http响应包含文件数据从start开始到end结束的文件数据,文件大小表示文件总大小。
ETag:表示的是服务器上资源的唯一标识
文件下载包括断点续传代码:
// 文件下载
static std::string GetETag(const BackupInfo &info)
{
// etg:filename-fsize-mtime
FileUtil fu(info._real_path);
std::string etag = fu.FileName();
etag += "-";
etag += std::to_string(info._fsize);
etag += "-";
etag += std::to_string(info._mtime);
return etag;
}
static void DownLoad(const httplib::Request &req, httplib::Response &res)
{
// 1. 获取客户端请求的资源路径path req.path
// 2. 根据资源路径,获取文件的备份信息
BackupInfo info;
_data->GetOneByURL(req.path, &info);
// 3. a:判断文件是否被压缩,如果被压缩,先进行解压
if (info._pack_flag == true)
{
FileUtil fu(info._pack_path);
fu.UnCompress(info._real_path); // 将文件解压到备份目录下
// b:删除压缩包,修改备份信息(文件已经没有被压缩)
fu.Remove();
info._pack_flag = false;
_data->Update(info);
}
// 4. 读取文件数据,并将数据写入rsp.body中
FileUtil fu(info._real_path);
fu.GetContent(&res.body);
// 判断是否断点续传
bool retrans = false; // 符合断电续传的标志位
std::string old_etag;
if (req.has_file("If_Range"))
{
// 有if-range字段&&etag相同 :符合断点续传
old_etag = req.get_header_value("If_Range");
if (old_etag == GetETag(info))
{
retrans = true;
}
}
if (retrans == false)
{
// 没有If-Rang字段||etag值不一样 则是正常下载(重新返回全部数据)
// 5. 设置响应头部字段:ETag,Accept-Ranges:bytes
res.set_header("Accept-Ranges", "bytes");
res.set_header("ETag", GetETag(info));
res.set_header("Content-Type", "application/octet-stream"); // application/octet-stream——表示的是我响应的正文是一个二进制数据流(常用于文件下载)
res.status = 200;
}
else
{
// httplib已近支持力断点续传
//只需要我们用户将文件所有数据取到res.body中,它内部会自动根据请求区间,从body中取出指定区间数据进行响应
fu.GetContent(&res.body);
res.set_header("Accept-Ranges", "bytes");
res.set_header("ETag", GetETag(info));
res.status = 206;
}
}
客户端实现
客户端功能实现:自动将指定文件夹中的文件备份到服务器
模块划分:
- 数据管理模块:管理备份的文件信息
- 目录遍历模块:获取指定文件夹中的所有文件路径名
- 文件备份模块:将需要备份的文件上传备份到服务器
由于我们客户端大多是给windows用户使用的,因此客户端在Windows上用vs2019进行开发
文件操作实用类设计
相比于服务端客户端文件实用工具类功能要少一些。主要用于目录遍历 文件的检测。
#define _CRT_SECURE_NO_WARNINGS 1
#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING 1
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/stat.h>
#include <experimental/filesystem>
namespace vic_cloud
{
namespace fs = std::experimental::filesystem;
class FileUtil
{
private:
std::string _filename;
public:
FileUtil(const std::string& filename) : _filename(filename)
{
}
// 获取文件大小
size_t FileSize()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size failed\n";
return 0;
}
return st.st_size;
}
// 获取文件最后一次修改时间
time_t LastModifyTime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size failed\n";
return -1;
}
return st.st_mtime;
}
// 获取文件最后一次访问时间(热点管理)
time_t LastAcccessTime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size failed\n";
return -1;
}
return st.st_atime;
}
// 获取文件路径名中的文件名
std::string FileName()
{
int pos = _filename.find_last_of("/");
if (pos == std::string::npos)
{
return _filename;
}
return _filename.substr(pos + 1);
}
// 获取文件指定位置 指定长度的数据
bool GetPosLen(std::string* body, size_t pos, size_t len)
{
size_t fsize = this->FileSize();
if (pos + len > fsize)
{
std::cout << "error reading length\n";
return false;
}
std::ifstream ifs;
ifs.open(_filename, std::ios::binary);
if (ifs.is_open() == false)
{
std::cout << "read open file failed\n";
return false;
}
ifs.seekg(pos, std::ios::beg);
body->resize(len);
ifs.read(&(*body)[0], len);
if (ifs.good() == false)
{
std::cout << "get filecontent failed\n";
ifs.close();
return false;
}
ifs.close();
return true;
}
// 获取文件所有数据
bool GetContent(std::string* body)
{
size_t fsize = this->FileSize();
return GetPosLen(body, 0, fsize);
}
// 向文件写入数据
bool SetContent(const std::string& body)
{
// std::ofstream ofs;
// ofs.open(_filename, std::ios::binary);
// if (ofs.is_open() == false)
// {
// std::cout << "write open file failed\n";
// return false;
// }
// ofs.write(&body[0], body.size());
// if (ofs.good() == false)
// {
// std::cout << "write content failed\n";
// ofs.close();
// return false;
// }
// ofs.close();
// return true;
std::ofstream ofs(_filename, std::ios::binary);
if (ofs.is_open() == false)
{
std::cout << "SetContent open file failed" << std::endl;
return false;
}
ofs.write(&body[0], body.size());
if (ofs.good() == false)
{
std::cout << "write file failed" << std::endl;
return false;
}
ofs.close();
return true;
}
bool Remove()
{
if (this->Exists() == false) return true;
remove(_filename.c_str());
return true;
}
bool Exists() // 判断文件是否存在
{
return fs::exists(_filename);
}
bool CreateDirectory() // 创建目录
{
if (this->Exists())
return true;
return fs::create_directories(_filename);
}
// 获取目录下所有文件名
bool ScanDirectory(std::vector<std::string>* arry)
{
this->CreateDirectory();
for (auto& p : fs::directory_iterator(_filename))
{
if (fs::is_directory(p) == true)
continue;
arry->push_back(fs::path(p).relative_path().string());
}
return 0;
}
};
}
#endif
客户端数据管理模块实现
数据管理模块:其中的信息用于判断一个文件是否需要重新备份
判断条件有两个:
-
文件是否是新增的
-
不是新增的,则上次备份后有没有被修改过
管理的数据:
- 文件的路径名
- 文件的唯一标识(文件上传后后没有被修改过)
实现思想:
-
内存存储:高访问效率–使用的是hash表-unordered_map
-
持久化存储:文件存储
文件存储涉及到数据序列化:因为在vs中安装jsoncpp较为麻烦,直接自定义序列化格式
key val:key是文件路径名,val是文件唯一标识(用于判断没有被修改)
key val\nkey val\n
其实就是字符串拆分,先拆除一个一个的KV键值对,在将KV值分离
#define _CRT_SECURE_NO_WARNINGS 1
#ifndef __M_DATA_H__
#define __M_DATA_H__
#include"util.hpp"
#include<unordered_map>>
#include<sstream>
namespace vic_cloud
{
class DataManager {
private:
std::unordered_map<std::string, std::string> _table;
std::string _backup_file;//备份信息的持久化存储文件
private:
//字符串分割,对序列化字符串进行分割
// 按指定的分隔符sep进行分割,将分割后的每一跳数据放到数组中
//"key val key" -> "key" "val" "key"
int Split(const std::string& str, const std::string& seq, std::vector<std::string>* arry)
{
size_t count = 0;
size_t pos = 0, idx = 0;
while (idx < str.size())
{
pos = str.find(seq, idx);
if (pos == std::string::npos) break;
arry->push_back(str.substr(idx, pos - idx));
idx = pos + 1;
count++;
}
if (idx < str.size())
{
//说明str还有最后一截字符串
arry->push_back(str.substr(idx));
count++;
}
return count;//分割之后数据的个数
}
public:
DataManager(const std::string& backup_file)
:_backup_file(backup_file)
{
InitLoad();
}
//程序运行时加载以前的数据
bool InitLoad()
{
//1.从文件中读取所有数据
FileUtil f(_backup_file);
std::string body;
f.GetContent(&body);
//2.按照自定义格式进行数据解析,"key val\nkey val" ->"kay val" "key val"
//字符串分割函数得到每一项数据
std::vector<std::string> arr;
Split(body, "\n", &arr);
for (auto& a : arr)
{
//再字符串分割函数得到key 和 val
std::vector<std::string> tmp;
//"key val" -> "key" "val"
Split(a, " ", &tmp);
if (tmp.size() != 2) continue;
//添加到_table中
_table[tmp[0]] = tmp[1];
}
return true;
}
//持久化存储
bool Storage()
{
std::stringstream ss;
//1.获取所有备份信息
auto it = _table.begin();
for (; it != _table.end(); it++)
{
//2.自定义持久化存储格式组织 key val\nkey val\n
ss << it->first << " " << it->second << "\n";
}
//3.持久化存储
FileUtil f(_backup_file);
f.SetContent(ss.str());
return true;
}
bool Insert(const std::string& key, const std::string& val)
{
_table[key] = val;
Storage();
return true;
}
bool Update(const std::string& key, const std::string& val)
{
_table[key] = val;
Storage();
return true;
}
bool GetOneByKey(const std::string& key, std::string* val)
{
auto it = _table.find(key);
if (it == _table.end()) return false;
*val = it->second;
return true;
}
};
}
#endif
客户端文件备份模块实现
客户端功能实现:自动将指定文件夹中的文件备份到服务器
流程:
- 遍历指定文件夹,获取所有文件信息
- 逐一判断文件是否需要上传备份(创建文件的唯一标识)
- 需要备份的文件进行上传备份
文件备份类设计
#define _CRT_SECURE_NO_WARNINGS 1
#ifndef __M_CLOUD_H__
#define __M_CLOUD_H__
#include"data.hpp"
#include"httplib.h"
#include<Windows.h>
namespace vic_cloud
{
#define SERVER_IP "43.143.228.57"
#define SERVER_PORT 8080
class BackUp
{
private:
std::string _back_dir;
DataManager* _data;
public:
BackUp(const std::string& back_dir,const std::string& back_file)
:_back_dir(back_dir)
{
_data = new DataManager(back_file);
}
//获取文件的唯一标识
std::string GetFileIdentifier(const std::string& filename)
{
//a.txt-fsize-mtime
FileUtil f(filename);
std::stringstream ss;
ss << f.FileName() << "-" << f.FileSize() << "-" << f.LastModifyTime();
return ss.str();
}
bool Upload(const std::string& filename)
{
//1.获取文件数据
FileUtil f(filename);
std::string body;
f.GetContent(&body);
//2.搭建http客户端,上传文件
//httplib实例化一个client对象
httplib::Client client(SERVER_IP,SERVER_PORT);
httplib::MultipartFormData item;
item.content = body;//正文就是文件数据
item.filename = f.FileName();
item.name = "file";//字段表示
item.content_type = "application/octet-stream";//二进制流数据
httplib::MultipartFormDataItems items;
items.push_back(item);
auto res = client.Post("/upload", items);
if (!res || res->status != 200)
{
return false;
}
return true;
}
//判断文件是否需要上传
bool JudgeUpload(const std::string& filename)
{
//文件新增||被修改过 需要上传
//新增判断:有没有备份信息 修改判断:有备份信息 但是文件的唯一标识不一致
std::string id;
if (_data->GetOneByKey(filename, &id) != false)
{
//有历史信息判断是否修改过(文件唯一标识)
std::string new_id=GetFileIdentifier(filename);
if (new_id == id)//文件标识符一致,不用上传
{
return false;
}
}
//注意:这里有一种情况
//一个大文件正在被拷贝,拷贝需要一个过程,该文件的唯一标识时时刻刻都不一致
//如果文件唯一标识不一致就上传,该文件会被上传很多次
//因此我们对于被修改文件的上传应该再加一个条件
//一段时间没有被修改过 修改时间间隔大于3
FileUtil f(filename);
if (time(NULL) - f.LastModifyTime() < 3)
{
//修改时间间隔小于3 认为文件还在修改中,不上传
return false;
}
std::cout << filename << " need upload \n";
return true;
}
//客户端整体的逻辑合并 运行模块
bool RunModule()
{
while (1)
{
//1. 遍历指定文件夹,获取所有文件信息
FileUtil f(_back_dir);
std::vector<std::string> arr;
f.ScanDirectory(&arr);
//2. 逐一判断文件是否需要上传
for (auto& a : arr)
{
if (JudgeUpload(a) == false)
{
continue;
}
if (Upload(a) == true)//3. 需要备份的文件进行上传备份
{
//如果文件上传成功了 新增文件备份信息
//向数据管理模块插入文件名称和唯一标识
_data->Insert(a, GetFileIdentifier(a));
std::cout << a << " upload sucess\n";
}
}
Sleep(1);
}
}
};
}
#endif
项目功能演示
项目整体展示:
在客户端目录下生成backup文件夹,将httplib.h 和util.hpp两个文件拷贝到backup目录下
客户端检测backup文件夹下的文件是否需要上传,如果需要则进行上传
客户端备份文件信息backup.dat (存储的是文件名 和文件唯一标识)
热点文件管理:服务端收到客户端上传的文件,在超过热点时间后对文件进行压缩到packdir目录下
服务端备份文件信息cloud.dat:
文件上传:选择一个文件上传
文件列表展示:展示出httplib.h 、util.hpp和刚才上传的文件
文件下载:
**断点续传:**断点续传功能不太好演示,我们在下载时终止服务器,然后重新启动,观察下载情况
项目总结
项目名称:云备份系统
项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件进行热点管理,将长时间无访问
文件进行压缩存储。
开发环境: centos7.6/vscode、g++、gdb、makefile 以windows10/vs2019
技术特点: http 客户端/服务器搭建, json 序列化,文件压缩,热点管理,断点续传,线程池,读写锁,单例模式
项目模块:
服务端:
- 配置信息模块:实用单例模式来管理服务端配置文件数据
- 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
- 业务处理模块:搭建http 服务器与客户端进行通信处理客户端的上传,下载,查看请求,并支持断点续
传 - 热点管理模块:对备份的文件进行热点管理,将长时间无访问文件进行压缩存储,节省磁盘空间。
客户端:
- 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
- 文件检索模块:基于c++17 文件系统库,遍历获取指定文件夹下所有文件。
- 文件备份模块:搭建http 客户端上传备份文件。