1. 云备份概述
⾃动将本地计算机上指定⽂件夹中需要备份的⽂件上传备份到服务器中。并且能够随时通过浏览器进⾏查看并且下载,其中下载过程⽀持断点续传功能,⽽服务器也会对上传⽂件进⾏热点管理,将⾮热点⽂件进⾏压缩存储,节省磁盘空间。
该项目的核心技术是要了解掌握有关于http网络协议等知识,该项目采用C++语言进行开发,客户端的主要功能就是将目录下的文件通过http协议,上传到服务端,并且我们需要一个简单的可视化界面,能够看到已经上传的文件的部分基本信息,并且还可以通过该界面去将上传备份的文件进行本地的下载,客户端的开发环境采用Windows下的VStudio2022(要求至少支持C++17的版本),服务端的业务主要就是支持客户端上传和下载文件,并且将文件进行管理,其中还涉及到热点管理,也就是当判断某个文件长时间未被访问,则为了节省空间,我们会对文件进行压缩处理,服务端的开发选择在Linux环境下采用VSCode远程连接云服务器的环境。
本篇博客会从项目准备、环境搭建、第三方库的认识和简单使用、整个项目开发逻辑和各个板块的开发思路以及具体实现,一步步整理总结该项目的内容。

2. 实现目标
这个云备份项⽬需要我们实现两端程序,其中包括部署在⽤⼾机的客⼾端程序,上传需要备份的⽂件,以及运⾏在服务器上的服务端程序,实现备份⽂件的存储和管理,两端合作实现总体的⾃动云备份功能。
2.1 服务端功能细分
1.支持客户端文件的上传功能
2.支持客户端文件备份列表查看功能
3.支持客户端文件下载功能(断点续传)
4.热点文件管理功能(对长时间无法访问的文件进行压缩存储)
2.2 服务端功能模块划分
1.数据管理模块: 管理的备份的文件信息, 以便于随时获取
2.网络通信模块: 实现与客户端的网络通信
3.业务处理模块: 上传、列表、下载(断点续传)
4.热点管理模块: 对长时间无法访问的文件进项压缩存储
2.3 客户端功能细分
1.指定文件夹中的文件检测(获取文件夹中有什么文件)
2.判断指定的文件是否需要备份(备份条件: 新增、已经备份过但是又被修改
(时间间隔一段时间未被修改过如3秒、1分钟等))
3.将需要备份的文件上备份到服务器上。
2.4 客户端功能模块划分
1.数据管理模块: 备份的文件信息
2.文件检测模块: 监控指定的文件夹
3.文件备份模块: 上传需要备份的文件数据
3. 环境搭建
3.1 环境搭建-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
输入完成后检擦版本

3.2 环境搭建-安装jsoncpp库
在服务器上输入一下指令
sudo yum install epel-release
sudo yum install jsoncpp-devel
ls /usr/include/jsoncpp/json/ #检查json是否安装成功
#注意,centos版本不同有可能安装的jsoncpp版本不同,安装的头⽂件位置也就可能不同了。

3.3 环境搭建-下载bundle数据压缩库
在服务器上输入一下指令
sudo yum install git
git clone https://github.com/r-lyeh-archived/bundle.git
3.4 环境搭建-下载 httplib 库
在服务器上输入一下指令
git clone https://github.com/yhirose/cpp-httplib.git
4. 第三方库认识
4.1 第三方库认识- jsoncpp 认识
json 是⼀种数据交换格式,采⽤完全独⽴于编程语⾔的⽂本格式来存储和表示数据。
示例如下(小明同学的学生信息):
char name = "⼩明";
int age = 18;
float score[3] = {88.5, 99, 58};
则json这种数据交换格式是将这多种数据对象组织成为⼀个字符串:
[
{
"姓名" : "⼩明",
"年龄" : 18,
"成绩" : [88.5, 99, 58]
},
{
"姓名" : "⼩⿊",
"年龄" : 18,
"成绩" : [88.5, 99, 58]
}
]
json 数据类型:对象,数组,字符串,数字
对象:使⽤花括号 {} 括起来的表⽰⼀个对象。
数组:使⽤中括号 [] 括起来的表⽰⼀个数组。
字符串:使⽤常规双引号 "" 括起来的表⽰⼀个字符串
数字:包括整形和浮点型,直接使⽤。
1. 第三方库认识- jsoncpp 认识
jsoncpp 库⽤于实现 json 格式的序列化和反序列化,完成将多个数据对象组织成为 json 格式字 符串,以及将 json 格式字符串解析得到多个数据对象的功能。
这其中主要借助三个类以及其对应的少量成员函数完成:
//Json数据对象类
class Json::Value{
Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
Value& operator[](const std::string& key);//简单的⽅式完成 val["姓名"] = "⼩明";
Value& operator[](const char* key);
Value removeMember(const char* key);//移除元素
const Value& operator[](ArrayIndex index) const; //val["成绩"][0]
Value& append(const Value& value);//添加数组元素val["成绩"].append(88);
ArrayIndex size() const;//获取数组元素个数 val["成绩"].size();
std::string asString() const;//转string string name =
val["name"].asString();
const char* asCString() const;//转char* char *name =
val["name"].asCString();
Int asInt() const;//转int int age = val["age"].asInt();
float asFloat() const;//转float
bool asBool() const;//转 bool
};
//json序列化类,低版本⽤这个更简单
class JSON_API Writer {
virtual std::string write(const Value& root) = 0;
}
class JSON_API FastWriter : public Writer {
virtual std::string write(const Value& root);
}
class JSON_API StyledWriter : public Writer {
virtual std::string write(const Value& root);
}
//json序列化类,⾼版本推荐,如果⽤低版本的接⼝可能会有警告
class JSON_API StreamWriter {
virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
virtual StreamWriter* newStreamWriter() const;
}
//json反序列化类,低版本⽤起来更简单
class JSON_API Reader {
bool parse(const std::string& document, Value& root, bool collectComments
= true);
}
//json反序列化类,⾼版本更推荐
class JSON_API CharReader {
virtual bool parse(char const* beginDoc, char const* endDoc,
Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
virtual CharReader* newCharReader() const;
}
2. 第三方库认识- jsoncpp 实现序列化
#include <iostream>
#include <memory>
#include <string>
#include <sstream>
#include <jsoncpp/json/json.h> // 引入JsonCpp库用于JSON数据处理
int main()
{
// 定义原始数据
const char* name = "小明"; // 姓名
int age = 18; // 年龄
float score[] = {80.0, 75.5, 91.9}; // 成绩数组
// 创建Json::Value对象用于存储JSON数据
Json::Value val;
// 将数据添加到JSON对象中
val["姓名"] = name; // 添加姓名字段
val["年龄"] = age; // 添加年龄字段
val["成绩"].append(score[0]); // 向成绩数组添加第一个分数
val["成绩"].append(score[1]); // 向成绩数组添加第二个分数
val["成绩"].append(score[2]); // 向成绩数组添加第三个分数
// 配置JSON写入器
Json::StreamWriterBuilder swb; // 创建StreamWriterBuilder对象
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 创建StreamWriter指针
// 将JSON数据写入字符串流
std::stringstream ss; // 创建字符串流对象
sw->write(val, &ss); // 将JSON数据写入流
// 输出JSON字符串
std::cout << ss.str() << std::endl; // 打印生成的JSON字符串
return 0;
}
注意:编译时需链接第三方库
g++ json_example1.cpp -o json_example1 -ljsoncpp

3. 第三方库认识- jsoncpp 实现反序列化
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h> // 必需:JSON解析和生成库
int main()
{
// 定义一个JSON格式的字符串
std::string str = R"({"姓名":"小明","年龄":18, "成绩":[76.5, 55, 88]})";
// 创建Json::Value对象用于存储解析后的JSON数据
Json::Value root;
// 配置JSON解析器
Json::CharReaderBuilder crb; // 创建CharReaderBuilder对象
std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); // 创建CharReader指针
std::string err; // 存储解析错误信息
// 解析JSON字符串
bool parsingSuccessful = cr->parse(
str.c_str(), // JSON字符串起始地址
str.c_str() + str.size(), // JSON字符串结束地址
&root, // 存储解析结果的Json::Value对象
&err // 存储错误信息
);
// 检查解析是否成功
if (!parsingSuccessful) {
std::cerr << "JSON解析失败: " << err << std::endl;
return 1;
}
// 访问并打印JSON数据
std::cout << root["姓名"].asString() << std::endl; // 输出姓名("小明")
std::cout << root["年龄"].asInt() << std::endl; // 输出年龄(18)
// 方法1:通过索引遍历成绩数组
int sz = root["成绩"].size(); // 获取成绩数组大小
for (int i = 0; i < sz; i++) {
std::cout << root["成绩"][i].asFloat() << std::endl; // 输出每个成绩
}
// 方法2:通过迭代器遍历成绩数组
for (auto it = root["成绩"].begin(); it != root["成绩"].end(); it++) {
std::cout << it->asFloat() << std::endl; // 输出每个成绩
}
return 0;
}
编译链接
g++ json_example2.cpp -o json_example2 -ljsoncpp

4.2 第三方库认识-bundle文件压缩库认识
BundleBundle 是⼀个嵌入式压缩库,⽀持23种压缩算法和2种存档格式。使用的时候只需要加入两个⽂件 bundle.h 和 bundle.cpp 即可。
namespace bundle {
// 低级API(直接操作原始指针)
// 检查指针指向的数据是否已打包
bool is_packed(void *ptr, size_t len); // ptr: 数据指针, len: 数据长度
// 检查指针指向的数据是否未打包(原始状态)
bool is_unpacked(void *ptr, size_t len);
// 获取数据的类型标识(返回无符号整数)
unsigned type_of(void *ptr, size_t len);
// 获取数据的原始长度(未压缩时)
size_t len(void *ptr, size_t len);
// 获取数据的压缩后长度(如果是打包状态)
size_t zlen(void *ptr, size_t len);
// 获取指向压缩数据的指针(如果是打包状态)
const void *zptr(void *ptr, size_t len);
// 打包数据:Q表示压缩质量/方法,in/len是输入,out/zlen是输出
bool pack(unsigned Q, void *in, size_t len, void *out, size_t &zlen);
// 解包数据:Q表示压缩质量/方法,in/len是输入,out/zlen是输出
bool unpack(unsigned Q, void *in, size_t len, void *out, size_t &zlen);
// 中级API(模板函数,原地操作)
// 模板版本:检查T类型对象是否已打包
template<typename T> bool is_packed(T);
// 模板版本:检查T类型对象是否未打包
template<typename T> bool is_unpacked(T);
// 模板版本:获取T类型对象的类型标识
template<typename T> unsigned type_of(T);
// 模板版本:获取T类型对象的原始长度
template<typename T> size_t len(T);
// 模板版本:获取T类型对象的压缩长度
template<typename T> size_t zlen(T);
// 模板版本:获取T类型对象的压缩数据指针
template<typename T> const void *zptr(T);
// 原地解包:将打包的src解包到dst
template<typename T> bool unpack(T &dst, T src);
// 原地打包:用质量Q将src打包到dst
template<typename T> bool pack(unsigned Q, T &dst, T src);
// 高级API(模板函数,返回新对象)
// 打包并返回新对象:用质量Q打包src,返回打包后的T
template<typename T> T pack(unsigned Q, T src);
// 解包并返回新对象:解包src,返回解包后的T
template<typename T> T unpack(T src);
}
1. 第三方库认识-bundle库实现文件压缩
#include <iostream>
#include <string>
#include <fstream>
#include "bundle.h" // 引入bundle压缩库头文件
int main(int argc, char *argv[])
{
// 打印使用说明
std::cout << "argv[1] 是原始文件路径名称\n"; // 参数1:待压缩文件路径
std::cout << "argv[2] 是压缩包名称\n"; // 参数2:输出压缩文件路径
// 检查参数数量(至少需要2个参数)
if (argc < 3) return -1; // 参数不足时直接返回错误码-1
// 获取输入输出文件名
std::string ifilename = argv[1]; // 原始文件路径
std::string ofilename = argv[2]; // 压缩文件路径
// 1. 读取原始文件
std::ifstream ifs;
ifs.open(ifilename, std::ios::binary); // 以二进制模式打开原始文件
ifs.seekg(0, std::ios::end); // 将文件指针移动到末尾
size_t fsize = ifs.tellg(); // 获取文件大小(字节数)
ifs.seekg(0, std::ios::beg); // 将文件指针移回开头
// 准备存储文件内容的缓冲区
std::string body;
body.resize(fsize); // 根据文件大小调整字符串容量
ifs.read(&body[0], fsize); // 读取全部文件内容到body字符串
// 2. 执行压缩(使用bundle库)
std::string packed = bundle::pack(bundle::LZIP, body); // 使用LZIP算法压缩数据
// packed现在存储压缩后的数据
// 3. 写入压缩文件
std::ofstream ofs;
ofs.open(ofilename, std::ios::binary); // 以二进制模式创建输出文件
ofs.write(&packed[0], packed.size()); // 写入压缩后的数据
// 4. 清理资源
ifs.close(); // 关闭输入文件流
ofs.close(); // 关闭输出文件流
return 0; // 程序正常结束
}
2.第三方库认识-bundle库实现文件解压缩
#include <iostream>
#include <fstream>
#include <string>
#include "bundle.h" // 引入bundle压缩库头文件
int main(int argc, char *argv[])
{
// 参数检查:至少需要输入文件路径和输出文件路径
if (argc < 3) {
printf("argv[1]是压缩包名称\n"); // 参数1:输入的压缩文件路径
printf("argv[2]是解压后的文件名称\n"); // 参数2:解压后输出的文件路径
return -1; // 参数不足时返回错误码-1
}
// 获取输入输出文件名
std::string ifilename = argv[1]; // 压缩文件路径(输入)
std::string ofilename = argv[2]; // 解压文件路径(输出)
// 1. 读取压缩文件数据
std::ifstream ifs;
ifs.open(ifilename, std::ios::binary); // 以二进制模式打开压缩文件
ifs.seekg(0, std::ios::end); // 将文件指针移动到文件末尾
size_t fsize = ifs.tellg(); // 获取文件大小(字节数)
ifs.seekg(0, std::ios::beg); // 将文件指针重置到文件开头
// 准备存储压缩数据的缓冲区
std::string body;
body.resize(fsize); // 根据压缩文件大小调整字符串容量
ifs.read(&body[0], fsize); // 读取整个压缩文件到body字符串
ifs.close(); // 关闭输入文件流
// 2. 执行解压缩(使用bundle库)
std::string unpacked = bundle::unpack(body); // 调用bundle库解压数据
// unpacked存储解压后的原始数据
// 3. 写入解压后的文件
std::ofstream ofs;
ofs.open(ofilename, std::ios::binary); // 以二进制模式创建输出文件
ofs.write(&unpacked[0], unpacked.size()); // 将解压数据写入新文件
ofs.close(); // 关闭输出文件流
return 0; // 程序正常结束
}

注意:这里bundle库里解压和压缩均用到了线程所以我们编译时要注意链接线程库,我们在检查时也可以通过指令: md5sum 来检测压缩、解压两个过程是否出错。
4.3 第三方库认识- httplib 库
httplib 库,一个 C++11 单文件头的跨平台 HTTP/HTTPS 库。安装起来非常容易。只需包含 httplib.h 在你的代码中即可。
httplib 库实际上是用于搭建一个简单的 http 服务器或者客户端的库,这种第三方网络库,可以 让我们免去搭建服务器或客户端的时间,把更多的精力投入到具体的业务处理中,提高开发效率。
1. 第三方库认识- httplib 库搭建简单服务器
#include "httplib.h" // 引入httplib库,用于创建HTTP服务器
#include <iostream> // 引入标准输入输出库
// 处理"/hi"路径的请求处理函数
void Hello(const httplib::Request &req, httplib::Response &rsp)
{
rsp.set_content("Hello World!", "Text.plain"); // 设置响应内容为"Hello World!"
rsp.status = 200; // 设置HTTP状态码为200(成功)
}
int main()
{
httplib::Server server; // 创建HTTP服务器实例
// 注册GET方法路由"/hi",使用Hello函数处理请求
server.Get("/hi", Hello);
// 注册带正则表达式的GET路由,匹配如"/numbers/123"的路径
// R"(...)"是原始字符串语法,避免转义字符干扰
server.Get(R"(/numbers/(\d+))",
[&](const httplib::Request& req, httplib::Response& res)
{
auto numbers = req.matches[1]; // 从URL中提取匹配的数字
res.set_content(numbers, "text/plain"); // 将数字作为纯文本返回
});
// 注册POST方法路由,用于文件上传
server.Post("/upload",
[&](const httplib::Request &req, httplib::Response& res)
{
auto size = req.files.size(); // 获取上传文件的数量
auto ret = req.has_file("file1"); // 检查是否有名为"file1"的文件
// 获取名为"file1"的上传文件
const auto& file = req.get_file_value("file1");
std::cout << file.filename << std::endl; // 打印文件名
std::cout << file.content_type << std::endl;// 打印文件类型
std::cout << file.content << std::endl; // 打印文件内容
});
// 启动服务器,监听所有网络接口(0.0.0.0)的8888端口
server.listen("0.0.0.0", 8888);
return 0;
}
编译指令:
g++ -std=c++14 server.cpp -o server -lpthread
测试:

2. 第三方库认识-httplib库搭建简单客户端
#include "httplib.h" // 引入httplib库,用于HTTP客户端功能
#define SERVER_IP "43.140.216.191" // 定义服务器IP地址常量
#define SERVER_PORT 8888 // 定义服务器端口常量
int main()
{
// 创建HTTP客户端实例,连接到指定IP和端口
httplib::Client cli(SERVER_IP, SERVER_PORT);
// 发送GET请求到/hi路径
auto res1 = cli.Get("/hi");
// 打印响应状态码和内容
std::cout << res1->status << std::endl;
std::cout << res1->body << std::endl;
// 发送GET请求到/numbers/666路径
auto res2 = cli.Get("/numbers/666");
// 打印响应状态码和内容
std::cout << res2->status << std::endl;
std::cout << res2->body << std::endl;
// 准备多部分表单数据(模拟文件上传)
httplib::MultipartFormDataItems items = {
{"file1", "this is file content", "hello.txt", "text/plain"}
// 参数说明:
// 1. 字段名(file1)
// 2. 文件内容
// 3. 文件名
// 4. 文件类型
};
// 发送POST请求到/upload路径,上传表单数据
auto res3 = cli.Post("/upload", items);
// 打印响应状态码和内容
std::cout << res3->status << std::endl;
std::cout << res3->body << std::endl;
return 0;
}
编译指令:
g++ -std=c++14 client.cpp -o client -lpthread
测试:


5. 项目实现
5.1 工具类实现
5.1.1 服务端工具类实现-文件实用工具类设计
不管是客户端还是服务端,文件的传输备份都涉及到文件的读写,包括数据管理信息的持久化也是如 此,因此首先设计封装文件操作类,这个类封装完毕之后,则在任意模块中对文件进行操作时都将变的简单化。
#ifndef __MY_UTIL__
#define __MY_UTIL__
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <experimental/filesystem>
#include <memory>
#include <jsoncpp/json/json.h>
#include "bundle.h"
namespace cloud
{
namespace fs = std::experimental::filesystem; // C++17支持
class FileUtil
{
public:
FileUtil(const std::string &filename)
: _filename(filename)
{
}
bool Remove() //删除源文件
{
if (this->Exists() == false)
{
return true;
}
remove(_filename.c_str());
return true;
}
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;
}
time_t LastMTime() // 获取最后一次修改时间
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file LastMTime failed!\n";
return -1;
}
return st.st_mtime;
}
time_t LastATime() // 获取最后一次访问时间
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file LastAtime failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName() // 获取文件名称
{
// 形式 ./a/test.txt 获取文件名只需找最后一个‘/’即可
size_t 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 << "get file len is error\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 << "read file Content failed!\n";
ifs.close(); // 关闭文件
return false; // 读取失败返回错误
}
// 关闭文件
ifs.close();
return true; // 操作成功返回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 file Content failed!\n";
ofs.close(); // 关闭文件
return false; // 写入失败返回错误
}
// 关闭文件
ofs.close();
return true; // 操作成功返回true
}
bool Compress(const std::string &packname) // 压缩
{
// 1.获取源文件数据
std::string body;
if (this->GetContent(&body) == false)
{
std::cout << "compress get file content failed!\n";
return false;
}
// 2.对数据进行压缩
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) // 压缩
{
// 将当前压缩包数据读取出来
std::string body;
if (this->GetContent(&body) == false)
{
std::cout << "uncompress get file content failed!\n";
return false;
}
// 对压缩的数据进行解压缩
std::string unpacked = bundle::unpack(body);
// 将解压缩的数据写入到新文件
FileUtil fu(filename);
if (fu.SetContent(unpacked) == false)
{
std::cout << "uncompress write packed data failed!\n";
return false;
}
return true;
}
bool Exists() // 文件是否存在
{
return fs::exists(_filename); // C++17支持
}
bool CreateDirectory() // 创建目录
{
if (this->Exists())
return true; // 如果目录已存在,直接返回 true
return fs::create_directories(_filename); // C++17支持 递归创建多级目录
}
bool ScanDirectory(std::vector<std::string> *arry) // 浏览目录
{
// 扫描指定目录下的所有文件(不包括子目录),并将它们的 相对路径 存入 arry。
for (auto &p : fs::directory_iterator(_filename))
{
if (fs::is_directory(p) == true)
{
continue;
}
// relarive_path 带有路径的文件名
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
private:
std::string _filename;
};
}
#endif
5.1.2 服务端工具类实现- Json 实用工具类设计
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;
if(sw->write(root, &ss) !=0)
{
std::cout<<"json write failed!\n";
return false;
}
*str = ss.str();
return true;
}
static bool UnSerialize(const std::string &str, Json::Value *root)
{
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
bool ret=cr->parse(str.c_str(),str.c_str()+str.size(),root,&err);
if (ret == false)
{
std::cerr << "parse error:" << err << std::endl;
return false;
}
return true;
}
};
5.2 服务端配置信息模块实现-系统配置信息
5.2.1 配置信息
使用文件配置加载一些程序的运行关键信息可以让程序的运行更加灵活。
配置信息:
- 热点判断时间:热点管理:多长时间没有被访问的文件算是非热点文件
- 文件下载URL前缀路径:用于表示客户端请求是一个下载请求
url: http://43.140.216.191:8888/path
当用户发来一个备份列表查看请求/listshow,我们如何判断这个不是一个listshow
的文件下载请求
通过下载前缀路径:/download/来判断
- 压缩包后缀名称 :订立的压缩包命名规则:在原文件名后加后缀 “ .lz ”。
- 上传文件存放路径 :决定了文件上传之后实际存储在服务器哪里
- 压缩文件存放路径 :决定非热点文件压缩后存放的路径
- 服务端备份信息存放文件:服务端记录的备份文件信息的我持久化存储
- 服务器访问 IP 地址 :当程序要运行在其他主机上,不需要修改程序
- 服务器访问端口
{
"hot_time" : 30, // 热数据保留时间(单位:秒)
"server_port" : 8888, // 服务器监听端口号
"server_ip" : "0.0.0.0", // 服务器IP地址
"download_prefix" : "/download/", // 下载路径前缀
"packfile_suffix" : ".lz", // 打包文件的后缀名
"pack_dir" : "./packdir/", // 打包文件的存储目录(相对路径)
"back_dir" : "./backdir/", // 备份文件的存储目录(相对路径)
"backup_file" : "./cloud.dat" // 备份文件的名称(相对路径)
}
5.2.2 服务端配置信息模块实现-单例文件配置类设计
使用单例模式管理系统配置信息,能够让配置信息的管理控制更加统一灵活。

#ifndef __MY_CONFIG__
#define __MY_CONFIG__
// 防止头文件重复包含的宏定义
#include "util.hpp" // 包含自定义工具库
#include <mutex> // 标准库互斥量头文件(用于线程同步)
#include <iostream> // 标准输入输出流
#include <string> // 字符串类
namespace cloud // 定义cloud命名空间
{
#define CONFIG_FIlE "./cloud.conf" // 配置文件的默认路径宏定义
// 配置类(单例模式)
class Config
{
private:
Config()
{
ReadConfigFile();
} // 私有构造函数(禁止外部实例化)
static Config *_instance; // 静态单例实例指针
static std::mutex _mutex; // 静态互斥锁(用于线程安全)
private:
// 配置项成员变量
int _hot_time; // 热点时间阈值(单位:秒)
int _server_port; // 服务器监听端口号
std::string _server_ip; // 服务器IP地址
std::string _download_prefix; // 文件下载URL前缀
std::string _packfile_suffix; // 打包文件后缀名(如.zip)
std::string _pack_dir; // 打包文件存储目录路径
std::string _back_dir; // 备份文件存储目录路径
std::string _backup_file; // 备份元数据文件路径
// 读取配置文件内容
bool ReadConfigFile()
{
FileUtil fu(CONFIG_FIlE); // 创建文件工具对象
std::string body; // 存储文件内容
// 读取文件内容到body
if (fu.GetContent(&body) == false)
{
std::cout << "load config file failed!\n"; // 文件读取失败提示
return false;
}
Json::Value root; // JSON解析根节点
// 反序列化JSON内容
if (JsonUtil::UnSerialize(body, &root) == false)
{
std::cout << "parse config file failed!\n"; // JSON解析失败提示
return false;
}
// 从JSON对象中提取各配置项
_hot_time = root["hot_time"].asInt(); // 获取热点时间
_server_port = root["server_port"].asInt(); // 获取服务器端口
_server_ip = root["server_ip"].asString(); // 获取服务器IP
_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(); // 备份文件
}
public:
// 获取单例实例(线程安全双检锁模式)
static Config *GetInstance()
{
if (_instance == nullptr) // 第一次检查(提高性能)
{
_mutex.lock(); // 加锁
if (_instance == nullptr) // 第二次检查(确保线程安全)
{
_instance = new Config(); // 创建单例实例
}
_mutex.unlock(); // 解锁
}
return _instance; // 返回单例指针
}
// 以下为各配置项的获取方法(访问器)
int GetHotTime() { return _hot_time; } // 获取热点时间
int GetServerPort() { return _server_port; } // 获取服务器端口
std::string GetServerIp() { return _server_ip; } // 获取服务器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; } // 获取备份文件
};
// 静态成员初始化
Config *Config::_instance = nullptr; // 初始化单例指针
std::mutex Config::_mutex; // 初始化互斥锁
}
#endif // 结束头文件保护
5.3 服务端模块实现-数据管理模块实现
5.3.1 服务端模块实现-管理的数据信息
- 文件实际存储路径:当客户端要下载文件时,则从这个文件中读取数据进行响应
- 压缩包存储路径:如果这个文件是一个非热点文件会被压缩,则这个就是压缩包路径名称,如果客户端想要下载文件,需先解压,然后读取解压后的文件数据。
- 文件是否压缩标志:判断文件是否已经被压缩
- 文件大小
- 文件最后一次修改时间
- 文件最后一次访问时间
- 文件访问URL中资源路径path: /download/a.txt
5.3.2 服务端数据管理模块实现-如何管理数据
1.用于数据信息访问:使用hash表在内存中管理数据,以url的path作为key的值--查询速度快。
2.用于持久化存储管理:使用json序列化将所有数据信息保存在文件中
5.3.3 服务端数据管理模块实现-数据管理类的设计

#ifndef __MY_DATA__ // 防止头文件重复包含
#define __MY_DATA__
#include <unordered_map> // 使用哈希表存储备份信息
#include <pthread.h> // 用于线程安全操作
#include "util.hpp" // 工具类头文件
#include "config.hpp" // 配置类头文件
namespace cloud // 定义在cloud命名空间中
{
typedef 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路径
// 根据真实路径创建新的备份信息
bool NewBackupInfo(const std::string &realpath)
{
FileUtil fu(realpath); // 使用文件工具类
if (fu.Exists() == false) // 检查文件是否存在
{
std::cout << "new backupinfo: file not exists!\n";
}
// 获取配置单例
Config *config = Config::GetInstance();
// 从配置中获取各个信息
std::string packdir = config->GetPackDir();
std::string packsuffix = config->GetPackFileSuffix();
std::string download_prefix = config->GetDownloadPrefix();
// 设置备份信息各字段
this->pack_flag = false; // 初始未打包
this->fsize = fu.FileSize(); // 获取文件大小
this->mtime = fu.LastMTime(); // 获取最后修改时间
this->atime = fu.LastATime(); // 获取最后访问时间
this->real_path = realpath; // 设置真实路径
// 构建打包路径:./backdir/a.txt ->./packdir/a.txt.lz
this->pack_path = packdir + fu.FileName() + packsuffix;
// 构建下载URL: ./backdir/a.txt -> /dowmload/a.txt
this->url = download_prefix + fu.FileName();
return true;
}
} BackupInfo;
// 数据管理类
class DataManager
{
private:
std::string _backup_file; // 备份数据存储文件路径
pthread_rwlock_t _rwlock; // 读写锁,读共享,写互斥,保证线程安全
std::unordered_map<std::string, BackupInfo> _table; // 哈希表存储便于查找
public:
// 构造函数
DataManager()
{
// 从配置信息获取备份文件路径
_backup_file = Config::GetInstance()->GetBackupFile();
pthread_rwlock_init(&_rwlock, nullptr); // 初始化读写锁
InitLoad(); // 初始化时加载已有数据
}
// 析构函数
~DataManager()
{
pthread_rwlock_destroy(&_rwlock); // 销毁读写锁
}
// 插入备份信息
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;
}
// 通过URL获取单个备份信息
bool GetOneByURL(const std::string &url, BackupInfo *info)
{
pthread_rwlock_wrlock(&_rwlock); // 加写锁
auto it = _table.find(url); // 查找URL对应的备份信息
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();
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;
}
// 持久化存储数据到文件
bool Storage()
{
// 1.获取所有数据
std::vector<BackupInfo> arry;
this->GetAll(&arry);
// 2.转换为Json格式
Json::Value root;
for (int i = 0; i < arry.size(); i++)
{
Json::Value item;
item["pack_flag"] = arry[i].pack_flag;
item["fsize"] = (Json::Int64)arry[i].fsize;
item["atime"] = (Json::Int64)arry[i].atime;
item["mtime"] = (Json::Int64)arry[i].mtime;
item["real_path"] = arry[i].real_path;
item["pack_path"] = arry[i].pack_path;
item["url"] = arry[i].url;
root.append(item); // 添加数组元素
}
// 3.序列化为字符串
std::string body;
JsonUtil::Serialize(root, &body);
// 4.写入文件
FileUtil fu(_backup_file);
fu.SetContent(body);
return true;
}
// 从文件初始化加载数据
bool InitLoad()
{
// 1. 检查数据文件是否存在
FileUtil fu(_backup_file);
if (fu.Exists() == false)
{
return true; // 文件不存在直接返回
}
// 读取文件内容
std::string body;
fu.GetContent(&body);
// 2.反序列化为Json格式
Json::Value root;
JsonUtil::UnSerialize(body, &root);
// 3. 将Json数据加载到内存表中
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 = root[i]["url"].asString();
Insert(info); // 插入到表中
}
return true;
}
};
}
#endif
5.3.4 服务端数据管理模块实现-数据管理类的测试验证
测试代码:
void DataTest(const std::string &filename)
{
// 测试获取所有备份信息
// cloud::DataManager data;
// std::vector<cloud::BackupInfo> arry;
// data.GetAll(&arry);
// for (auto &a : arry)
// {
// std::cout << a.pack_flag << std::endl;
// std::cout << a.fsize << std::endl;
// std::cout << a.mtime << std::endl;
// std::cout << a.atime << std::endl;
// std::cout << a.real_path << std::endl;
// std::cout << a.pack_path << std::endl;
// std::cout << a.url << std::endl;
// }
//测试信息的插入
cloud::BackupInfo info;
info.NewBackupInfo(filename);
cloud::DataManager data;
std::cout << "-----------insert and GetOneByURL--------\n";
data.Insert(info);
cloud::BackupInfo tmp;
data.GetOneByURL("/download/bundle.h", &tmp);
std::cout << tmp.pack_flag << std::endl;
std::cout << tmp.fsize<< std::endl;
std::cout << tmp.mtime<< std::endl;
std::cout << tmp.atime<< std::endl;
std::cout << tmp.real_path << std::endl;
std::cout << tmp.pack_path<< std::endl;
std::cout << tmp.url<< std::endl;
//测试信息的更新
std::cout << "-----------update and getall--------\n";
info.pack_flag = true;
data.Update(info);
std::vector<cloud::BackupInfo> arry;
data.GetAll(&arry);
for (auto &a : arry){
std::cout << a.pack_flag << std::endl;
std::cout << a.fsize<< std::endl;
std::cout << a.mtime<< std::endl;
std::cout << a.atime<< std::endl;
std::cout << a.real_path << std::endl;
std::cout << a.pack_path<< std::endl;
std::cout << a.url<< std::endl;
}
std::cout << "-----------GetOneByRealPath--------\n";
//测试通过真实路径获取单个备份信息
data.GetOneByRealPath(filename, &tmp);
std::cout << tmp.pack_flag << std::endl;
std::cout << tmp.fsize<< std::endl;
std::cout << tmp.mtime<< std::endl;
std::cout << tmp.atime<< std::endl;
std::cout << tmp.real_path << std::endl;
std::cout << tmp.pack_path<< std::endl;
std::cout << tmp.url<< std::endl;
}
测试结果展示:若cloud.dat文件不存在则生成一个cloud.dat文件来存放数据信息

5.4 服务端热点管理模块实现-热点管理实现
5.4.1 服务端热点管理模块实现-热点管理类的设计思想
热点管理模块:对服务器上备份的文件进行检测,哪些文件长时间没有被访问,则认为是非热点文件,则压缩存储,节省磁盘空间。
实现思路:
遍历所有的文件,检测文件的最后一次访问时间,与当前时间进行相减得到差值,这个差值如果大于设定好的非热点判断时间则认为是非热点文件,则进行压缩存放到压缩路径中,删除源文件遍历所有的文件:
从数据管理模块中遍历所有的备份文件信息
遍历备份文件夹,获取所有的文件进行属性获取,最终判断
选择第二种:遍历文件夹,每次获取文件的最新数据进行判断,并且还可以解决数据信息缺漏的问题
遍历备份目录,获取所有文件路径名称
逐个文件获取最后一次访问时间与当前系统时间进行比较判断
对非热点文件进行压缩处理,删除源文件
修改数据管理模块对应的文件信息(压缩标志-)true
5.4.2 服务端热点管理模块实现-热点管理类的设计

#ifndef __MY_HOT__
#define __MY_HOT__
#include <unistd.h>
#include "data.hpp"
// 声明一个外部的全局DataManager指针变量
extern cloud::DataManager *_data;
namespace cloud
{
class HotManager
{
private:
std::string _back_dir; // 备份目录路径
std::string _pack_dir; // 压缩包存储目录路径
std::string _pack_suffix; // 压缩文件后缀名
int _hot_time; // 热点时间阈值(秒)
private:
// 判断文件是否为非热点文件
bool HotJudge(const std::string &filename)
{
FileUtil fu(filename);
time_t last_atime = fu.LastATime(); // 获取文件最后访问时间
time_t cur_time = time(nullptr); // 获取当前时间
if (cur_time - last_atime > _hot_time)
{
// 如果超过热点时间阈值,返回true(非热点文件)
return true;
}
return false; // 否则返回false(热点文件)
}
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 (true)
{
// 1.遍历备份目录,获取所有文件名
FileUtil fu(_back_dir);
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
// 2.遍历判断文件是否是非热点文件
for (auto &e : arry)
{
if (HotJudge(e) == false)
{
continue; // 热点文件跳过
}
// 3.获取文件的备份信息
BackupInfo bi;
if (_data->GetOneByRealPath(e, &bi) == false)
{
// 如果文件存在但没有备份信息,则创建新的备份信息
bi.NewBackupInfo(e);
}
// 4. 对非热点文件进行压缩处理
FileUtil tmp(e);
tmp.Compress(bi.pack_path);
//5. 删除源文件,修改备份信息
tmp.Remove();
bi.pack_flag = true;
_data->Update(bi);
}
usleep(1000); // 短暂休眠,避免CPU占用过高
}
return true;
}
};
}
#endif
5.5 服务端业务处理模块实现-业务处理实现思路
服务端业务处理模块:将网络通信模块和业务处理进行了合并(网络通信通过httplib库完成)
搭建网络通信服务器:借助httplib完成
业务请求处理
文件上传请求:备份客户端上传的文件,响应上传成功
文件列表请求:客户端浏览器请求一个备份文件的展示页面,响应页面
文件下载请求:通过展示页面,点击下载,响应客户端要下载的文件数据
5.6 服务端业务处理模块实现-网络通信接口设计
业务处理模块要对客户端的请求进行处理,那么我们就需要提前定义好客户端与服务端的通信,明确 客户端发送什么样的请求,服务端处理后应该给与什么样的响应,而这就是网络通信接口的设计。

5.6.1 HTTP文件上传:
POST /upload HTTP/1.1
Content-Length:11
Content-Type:multipart/form-data;boundary= ----WebKitFormBoundary+16字节随机字符
------WebKitFormBoundary
Content-Disposition:form-data;filename="a.txt";
hello world
------WebKitFormBoundary--
HTTP/1.1 200 OK
Content-Length: 0
5.6.2 HTTP文件列表获取:
GET /list HTTP/1.1
Content-Length: 0
HTTP/1.1 200 OK
Content-Length:
Content-Type: text/html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Page of Download</title>
</head>
<body>
<h1>Download</h1>
<table>
<tr>
<td><a href="/download/a.txt"> a.txt </a></td>
<td align="right"> 1994-07-08 03:00 </td>
<td align="right"> 27K </td>
</tr>
</table>
</body>
</html>
5.6.3 HTTP文件下载:
GET /download/a.txt http/1.1
Content-Length: 0
HTTP/1.1 200 OK
Content-Length: 100000
ETags: "filename-size-mtime一个能够唯一标识文件的数据"
Accept-Ranges: bytes
文件数据
5.6.4 HTTP断点续传:

GET /download/a.txt http/1.1
Content-Length: 0
If-Range: "文件唯一标识"
Range: bytes=89-999
HTTP/1.1 206 Partial Content
Content-Length:
Content-Range: bytes 89-999/100000
Content-Type: application/octet-stream
ETag: "inode-size-mtime一个能够唯一标识文件的数据"
Accept-Ranges: bytes
对应文件从89到999字节的数据。
5.6.5 服务端业务处理模块实现-业务处理类设计

ETag头部与Accept-Ranges头部作用:
http的ETag头部字段:其中存储了一个资源的唯一标识
客户端第一次下载文件的时候,会收到这个响应信息,
第二次下载,就会将这个信息发送给服务器,想要让服务器根据这个唯一标识判断
这个资源有没有被修改过,如果没有被修改过,直接使用原先缓存的数据,不用重
新下载了http协议本身对于etag中是什么数据并不关心,只要你服务端能够自己标识就行
因此我们etag就用“文件名-文件大小-最后一次修改时间”组成
而etag字段不仅仅是缓存用到,还有就是后边的断点续传的实现也会用到
因为断点续传也要保证文件没有被修改过。http协议的Accept-Ranges: bytes 字段:用于高速客户端或支持断点续传,并且数据单位以
字节作为单位Content-Type字段的重要性:决定了浏览器如何处理响应正文
// 防止头文件重复包含的宏定义
#ifndef __MY_SERVICE__
#define __MY_SERVICE__
#include "data.hpp" // 数据管理相关头文件
#include "httplib.h" // HTTP服务器库头文件
// 声明外部全局变量,数据管理器实例
extern cloud::DataManager *_data;
namespace cloud
{
// 定义Service类,提供HTTP服务
class Service
{
private:
int _server_port; // 服务器监听端口
std::string _server_ip; // 服务器IP地址
std::string _download_prefix; // 下载URL前缀
httplib::Server _server; // httplib服务器实例
private:
// 静态文件上传处理函数
static void Upload(const httplib::Request &req, httplib::Response &rsp)
{
// 检查请求中是否包含命名为"file"的文件字段
auto ret = req.has_file("file");
if (ret == false)
{
rsp.status = 400; // 如果没有文件,返回400错误
return;
}
// 获取上传文件的信息
// file.filename: 文件名, file.content: 文件内容
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 fu(realpath);
fu.SetContent(file.content);
// 创建备份信息对象并初始化
BackupInfo info;
info.NewBackupInfo(realpath);
// 将备份信息插入数据管理器
_data->Insert(info);
return;
}
// 将时间戳转换为字符串格式
static std::string TimetoStr(time_t t)
{
std::string tmp = std::ctime(&t);
return tmp;
}
// 文件列表显示处理函数
static void ListShow(const httplib::Request &req, httplib::Response &rsp)
{
// 获取所有备份文件信息
std::vector<BackupInfo> arry;
_data->GetAll(&arry);
// 使用字符串流构建HTML页面
std::stringstream ss;
ss << "<html><head><title>Download</title></head>";
ss << "<body><h1>Download</h1><table>";
// 遍历所有备份文件,生成表格行
for (auto &a : arry)
{
ss << "<tr>";
// 从完整路径中提取文件名
std::string filename = FileUtil(a.real_path).FileName();
// 创建下载链接
ss << "<td><a href='" << a.url << "'>" << filename << "</a></td>";
// 显示文件修改时间
ss << "<td align='right'>" << TimetoStr(a.mtime) << "</td>";
// 显示文件大小(转换为KB)
ss << "<td align='right'>" << a.fsize / 1024 << "k</td>";
ss << "</tr>";
}
ss << "</table></body></html>";
// 设置响应体和头部信息
rsp.body = ss.str();
rsp.set_header("Content-Type", "text/html");
rsp.status = 200;
return;
}
// 生成ETag(实体标签),用于缓存验证和断点续传
static std::string GetETag(const BackupInfo &info)
{
// ETag格式: 文件名-文件大小-修改时间
// 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 &rsp)
{
// 获取客户端请求的资源路径path req.path
// 根据请求路径获取对应的备份信息
BackupInfo info;
_data->GetOneByURL(req.path, &info);
// 如果文件被压缩,需要先解压
if (info.pack_flag == true)
{
FileUtil fu(info.pack_path);
// 解压文件到备份目录
fu.UnCompress(info.real_path);
// 删除压缩包
fu.Remove();
// 更新备份信息(标记为未压缩状态)
info.pack_flag = false;
_data->Update(info);
}
// 检查是否需要断点续传
bool retrans = false;
std::string old_etag;
if (req.has_header("If-Range"))
{
old_etag = req.get_header_value("If-Range");
// 如果ETag匹配,则进行断点续传
if (old_etag == GetETag(info))
{
retrans = true;
}
}
// 读取文件内容
FileUtil fu(info.real_path);
if (retrans == false)
{
// 普通下载,读取整个文件内容
fu.GetContent(&rsp.body);
// 设置响应头部
rsp.set_header("Accept-Ranges", "bytes"); // 支持请求范围
rsp.set_header("ETag", GetETag(info)); // 设置ETag
rsp.set_header("Content-Type", "application/octet-stream"); // 二进制流类型
rsp.status = 200; // 成功状态码
}
else
{
// 断点续传,httplib会自动处理范围请求
// httplib内部实现了对于区间请求也就是断点续传请求的处理
// 只需要我们用户将文件所有数据读取到rsp.body中,它内部会自动根据请求
// 区间,从body中取出指定区间数据进行响应
// std::string range = req.get_header_val("Range"); bytes=start-end
fu.GetContent(&rsp.body);
rsp.set_header("Accept-Ranges", "bytes");
rsp.set_header("ETag", GetETag(info));
rsp.set_header("Content-Type", "application/octet-stream");
rsp.status = 206; // 部分内容状态码
}
}
public:
Service()
{
Config *config = Config::GetInstance();
_server_port = config->GetServerPort(); // 获取端口
_server_ip = config->GetServerIp(); // 获取IP
_download_prefix = config->GetDownloadPrefix(); // 获取下载前缀
}
// 运行服务器模块
bool RunModule()
{
// 注册路由和处理函数
_server.Post("/upload", Upload); // 文件上传路由
_server.Get("/listshow", ListShow); // 列表显示路由
_server.Get("/", ListShow); // 跟路由也显示列表
// 动态下载路由(使用正则表达式匹配)
std::string download_url = _download_prefix + "(.*)";
_server.Get(download_url, Download);
// 启动服务器监听
_server.listen(_server_ip.c_str(), _server_port);
// while(1);
return true;
}
};
}
#endif
5.7 客户端数据管理模块实现-数据信息设计

客户端要实现的功能是对指定文件夹中的文件自动进行备份上传。但是并不是所有的文件每次都需要 上传,我们需要能够判断,哪些文件需要上传,哪些不需要,因此需要将备份的文件信息给管理起 来,作为下一次文件是否需要备份的判断。因此需要被管理的信息包含以下:
- 文件路径名称
- 文件唯一标识:由文件名,最后一次修改时间,文件大小组成的一串信息
5.8 客户端文件检测模块实现-文件操作实用类设计
这个其实与服务端的文件实用工具类雷同,只是功能需求并没有服务端那么多,复制过来微调一下即可。
#ifndef __MY_UTIL__
#define __MY_UTIL__
#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <experimental/filesystem>
#include <memory>
namespace cloud
{
namespace fs = std::experimental::filesystem; // C++17支持
class FileUtil
{
public:
FileUtil(const std::string &filename)
: _filename(filename)
{
}
bool Remove() //删除源文件
{
if (this->Exists() == false)
{
return true;
}
remove(_filename.c_str());
return true;
}
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;
}
time_t LastMTime() // 获取最后一次修改时间
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file LastMTime failed!\n";
return -1;
}
return st.st_mtime;
}
time_t LastATime() // 获取最后一次访问时间
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file LastAtime failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName() // 获取文件名称
{
// 形式 ./a/test.txt 获取文件名只需找最后一个‘/’即可
//size_t pos = _filename.find_last_of("\\"); //Windows下路径与Linux下不同需特殊处理
//if (pos == std::string::npos)
//{
// return _filename; // 本身就是文件名
//}
return fs::path(_filename).filename().string(); //C++17支持该提取文件名称操作
//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 << "get file len is error\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 << "read file Content failed!\n";
ifs.close(); // 关闭文件
return false; // 读取失败返回错误
}
// 关闭文件
ifs.close();
return true; // 操作成功返回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 file Content failed!\n";
ofs.close(); // 关闭文件
return false; // 写入失败返回错误
}
// 关闭文件
ofs.close();
return true; // 操作成功返回true
}
bool Exists() // 文件是否存在
{
return fs::exists(_filename); // C++17支持
}
bool CreateDirectory() // 创建目录
{
if (this->Exists())
return true; // 如果目录已存在,直接返回 true
return fs::create_directories(_filename); // C++17支持 递归创建多级目录
}
bool ScanDirectory(std::vector<std::string> *arry) // 浏览目录
{
//创建目录
this->CreateDirectory();
// 扫描指定目录下的所有文件(不包括子目录),并将它们的 相对路径 存入 arry。
for (auto &p : fs::directory_iterator(_filename))
{
if (fs::is_directory(p) == true)
{
continue;
}
// relarive_path 带有路径的文件名
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
private:
std::string _filename;
};
}
#endif
5.9 客户端数据管理模块实现-数据管理类设计
#ifndef __MY_DATA__
#define __MY_DATA__
#include <unordered_map>
#include <sstream>
#include "util.hpp"
namespace cloud
{
class DataManager // 数据管理类,负责数据的持久化存储和读取
{
private:
std::string _backup_file; //备份信息的持久化存储文件
std::unordered_map<std::string, std::string> _table; // 哈希表,用于内存中存储键值对数据
public:
// 构造函数,初始化备份文件路径并加载已有数据
DataManager(const std::string& backup_file)
:_backup_file(backup_file)
{
InitLoad(); // 初始化时从文件加载数据到内存
}
bool Storage() // 将内存中的数据持久化存储到文件中
{
//1.获取所有的备份信息
std::stringstream ss;
auto it = _table.begin();
for (; it != _table.end(); it++)
{
//2.将所有信息进行指定持久化格式的组织 "key value\n"
ss << it->first << " " << it->second << "\n";
}
// 3.持久化存储
FileUtil fu(_backup_file);
fu.SetContent(ss.str());
return true;
}
// 字符串分割函数,将字符串按指定分隔符分割成多个子字符串
int Split(const std::string& str, const std::string& sep, std::vector<std::string>* arry)
{
int count = 0; // 计数器,记录分割出的子字符串数
size_t pos = 0; // 存储分隔符位置
size_t idx = 0; // 当前查找起始位置
while (1)
{
pos = str.find(sep, idx);
if (pos == std::string::npos) // 如果找不到分隔符,退出循环
{
break;
}
if (pos == idx) // 如果分隔符就在当前位置,跳过连续的分隔符
{
idx = pos + sep.size();
continue;
}
std::string tmp = str.substr(idx, pos - idx);
arry->push_back(tmp);
count++;
idx = pos + sep.size();
}
// 处理最后一个分隔符之后的剩余字符串
if (idx < str.size())
{
arry->push_back(str.substr(idx));
count++;
}
return count;
}
bool InitLoad()
{
//1,从文件中读取所有数据
FileUtil fu(_backup_file);
std::string body;
fu.GetContent(&body);
//2.进行数据解析,添加到表中
std::vector<std::string> arry;
Split(body, "\n", &arry);
for (auto& a : arry)
{
//格式:b.txt b.txt-3213-21454
std::vector<std::string> tmp;
Split(a, " ", &tmp); // 按空格分割每行内容
if (tmp.size() != 2)
{
continue;
}
_table[tmp[0]] = tmp[1];
}
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
5.10 客户端文件备份模块实现-文件备份类设计

#ifndef __MY_CLOUD__
#define __MY_CLOUD__
#include "data.hpp"
#include "httplib.h"
#include <windows.h>
namespace cloud
{
#define SERVER_IP "43.140.216.191" //定义服务端IP地址
#define SERVER_PORT 8888 //定义服务端端口号
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 fu(filename); //创建文件工具对象
std::stringstream ss; //创建字符串流用于格式化
ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastMTime();
return ss.str(); //返回格式化后的标识符
}
//上传文件到服务器
bool Upload(const std::string& filename)
{
//1.获取文件数据内容
FileUtil fu(filename); //创建文件工具对象
std::string body; //存储文件内容
fu.GetContent(&body); //读取文件全部内容
//2.搭建HTTP客户端并上传文件数据
//创建HTTP客户端
httplib::Client client(SERVER_IP, SERVER_PORT);
//准备多部分表单数据
httplib::MultipartFormData item;
item.content = body; //文件内容
item.filename = fu.FileName(); //文件名
item.name = "file"; //表单字段名
item.content_type = "application/octet-stream"; // 内容类型:二进制流
httplib::MultipartFormDataItems items; //创建表单数据项集合
items.push_back(item); //添加文件数据项
//发送POST请求到服务器的/upload路径
auto res = client.Post("/upload", items);
// 检查响应结果,请求成功且HTTP状态码为200
if (!res || res->status != 200)
{
return false; //上传失败
}
return true;
}
//判断文件是否需要上传
bool IsNeedUpload(const std::string& filename)
{
//需要上传的文件:
//1. 文件是新增的(没有历史备份信息)
//2.文件不是新增但被修改过(历史标识与当前标识不一致)
std::string id; // 存储历史备份标识
//检查是否有该文件的历史备份信息
if (_data->GetOneByKey(filename, &id) != false)
{
//有历史信息,比较当前标识与历史标识
std::string new_id = GetFileIdentifier(filename);
if (new_id == id)
{
return false; //标识相同,不需要上传(文件未修改)
}
}
//防止大文件再拷贝过程中的误判;
//大文件拷贝需要时间,如果在此期间频繁检查会导致重复上传
FileUtil fu(filename);
//如果文件3秒内被修改过,认为文件还在修改中,暂不上传
if (time(nullptr) - fu.LastMTime() < 3)
{
return false;
}
std::cout << filename << "need upload! \n";
return true; //需要上传
}
//运行备份模块的主循环
bool RunModule()
{
while (true)
{
//1.遍历获取备份目录中的所有文件
FileUtil fu(_back_dir); //创建备份目录的
std::vector<std::string> arry; //存储文件路径列表
fu.ScanDirectory(&arry); //扫描目录获取文件列表
//2.逐个判断文件是否需要上传
for (auto& a : arry) //遍历所有文件
{
if (IsNeedUpload(a) == false)
{
continue; //不需要上传,跳过
}
//3. 需要上传则上传文件
if (Upload(a) == true)
{
//上传成功
//更新备份信息,记录当前文件标识
_data->Insert(a, GetFileIdentifier(a));
std::cout << a << "upload success!\n";
}
}
Sleep(1); //休眠1毫秒,避免CPU占用过高
//std::cout << "-------------------------loopend---------------------------\n";
}
}
};
}
#endif
6. 项目总结
项目名称:云备份系统
项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到 服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件 进行热点管理,将长时间无访问文件进行压缩存储。
开发环境: centos7.6/vim、g++、gdb、makefile 以及 windows11/vs2022
技术特点: http 客户端/服务器搭建, json 序列化,文件压缩,热点管理,断点续传,线程池, 读写锁,单例模式
项目模块:
1、服务端:
a. 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
b. 业务处理模块:搭建 http 服务器与客户端进行通信处理客户端的上传,下载,查看请求,并 支持断点续传
c. 热点管理模块:对备份的文件进行热点管理,将长时间无访问文件进行压缩存储,节省磁盘空间。
2、 客户端:
a. 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
b. 文件检索模块:基于 c++17 文件系统库,遍历获取指定文件夹下所有文件。
c. 文件备份模块:搭建 http 客户端上传备份文件。
1647

被折叠的 条评论
为什么被折叠?



