之前的很长一段时间我们学习了C++、Linux中的基础知识,在这篇博客中我们就来使用之前所学习过的知识点来完成一个云备份的项目。
云备份认识
我们都使用过网盘之类的工具,我们可以主动将文件进行上传备份,并随时浏览下载。本文所写的云备份项目能够自动将本地计算机上指定文件夹中需要备份的文件上传至服务器中。并且能够随时通过浏览器进行下载,其中下载的过程支持断点续传的功能,而服务器也能够对上传的文件进行热点管理,将非热点的文件进行压缩存储,节省磁盘空间。
实现目标
本文的云备份项目需要实现两端程序,其中包括部署在用户机上的客户端程序,以及运行在服务器上服务端程序实现,备份文件的存储和管理,两端合作实现自动云备份功能。
服务端程序负责功能
- 针对客户端上传的文件进行备份存储;
- 能够对文件进行热点文件管理,对非热点文件进行压缩存储,节省磁盘空间;
- 支持客户端浏览器查看文件列表;
- 支持客户端浏览器下载文件,并且下载支持断点续传。
服务端功能划分模块
- 数据管理模块:负责服务器上备份文件的信息管理;
- 网络通信模块:搭建网络通信服务器,实现客户端的通信;
- 业务处理模块:针对客户端的各个请求进行对应业务处理并相应返回;
- 热点管理模块,负责文件的热点判断,以及非热点文件的压缩存储。
客户端程序负责功能
- 能够自动检测客户机指定文件夹中的文件,并判断是否需要备份;
- 将需要备份的文件逐个上传到服务器
客户端功能划分模块
- 数据管理模块:负责自动检测客户端备份的文件信息管理,通过这些数据可以确定一个文件是否需要备份;
- 文件检测模块:遍历指定文件夹中所有文件路径名称;
- 网络通信模块:搭建网络通信客户端,实现将文件数据备份上传到服务器。
环境搭建
本项目需要gcc升级7.3版本,以及安装jsoncpp库,bundle库,httplib库。
# 安装gcc
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可以查看对应的gcc版本
# 安装jsoncpp库
sudo yum install epel-release
sudo yum install jsoncpp-devel
# ls /usr/include/jsoncpp/json/ # 在include目录下可以查看到相关的库文件
# 下载bundle数据压缩库
sudo yum install git
git clone https://github.com/r-lyeh-archived/bundle.git
# 下载httplib库
git clone https://github.com/yhirose/cpp-httplib.git
json的认识
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 数据类型:对象,数组,字符串,数字
对象:使用花括号 {} 括起来的表示一个对象。
数组:使用中括号 [] 括起来的表示一个数组。
字符串:使用常规双引号 “” 括起来的表示一个字符串。
数字:包括整形和浮点型,直接使用。
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;
}
jsoncpp使用例子
/*
将所有数据保存在Json::Value对象中,使用Json::StreamWriterBuilder int write(Value const& root, std::ostream* sout) = 0; std::stringstream。
定义一个Json::Value root;借助Json::CharReaderBuilder, Json::CharReader类完成反序列化,bool parse(char const* beginDoc, char const* endDoc, Value* root, std::string* errs)。
*/
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include "jsoncpp/json/json.h"
int main()
{
const char *name = "小明";
int age = 18;
float score[] = {77.5, 88, 93.6};
Json::Value root;
root["姓名"] = name;
root["年龄"] = age;
root["成绩"].append(score[0]);
root["成绩"].append(score[1]);
root["成绩"].append(score[2]);
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(root, &ss);
std::cout << ss.str() << std::endl;
std::string str = R"({"姓名":"小黑","年龄":19,"成绩":[58.5,66,35.5]})"; // R保证后面的字符串中的字符都是原始字符串
Json::Value root_P;
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_P, &err);
if (ret == false)
{
std::cout << "parse error: " << err <<std::endl;
return -1;
}
std::cout << root_P["姓名"].asString() << std::endl;
std::cout << root_P["年龄"].asInt() << std::endl;
int sz = root_P["成绩"].size();
for(int i = 0; i < sz; i++)
{
std::cout << root_P["成绩"][i] << std::endl;
}
return 0;
}
bundle文件压缩库认识
BundleBundle 是一个嵌入式压缩库,支持23种压缩算法和2种存档格式。使用的时候只需要加入两个文件
bundle.h 和 bundle.cpp 即可
namespace bundle
{
// low level API (raw pointers)
bool is_packed( *ptr, len );
bool is_unpacked( *ptr, len );
unsigned type_of( *ptr, len );
size_t len( *ptr, len );
size_t zlen( *ptr, len );
const void *zptr( *ptr, len );
bool pack( unsigned Q, *in, len, *out, &zlen );
bool unpack( unsigned Q, *in, len, *out, &zlen );
// medium level API, templates (in-place)
bool is_packed( T );
bool is_unpacked( T );
unsigned type_of( T );
size_t len( T );
size_t zlen( T );
const void *zptr( T );
bool unpack( T &, T );
bool pack( unsigned Q, T &, T );
// high level API, templates (copy)
T pack( unsigned Q, T );
T unpack( T );
}
bundle库实现文件压缩
#include <iostream>
#include <string>
#include <fstream>
#include "bundle.h"
int main(int argc, char *argv[])
{
std::cout << "argv[1] 是原始文件路径名称\n";
std::cout << "argv[2] 是压缩包名称\n";
if (argc < 3)
return -1;
std::string ifilename = argv[1];
std::string ofilename = argv[2];
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); // 调整body大小为文件大小
ifs.read(&body[0], fsize); // 读取文件所有数据到body中
std::string packed = bundle::pack(bundle::LZIP, body); // 以lzip格式压缩文件数据
std::ofstream ofs;
ofs.open(ofilename, std::ios::binary); // 打开压缩包文件
ofs.write(&packed[0], packed.size()); // 将压缩后的数据写入压缩包文件
ifs.close();
ofs.close();
return 0;
}
bundle库实现文件解压缩
#include <iostream>
#include <fstream>
#include <string>
#include "bundle.h"
int main(int argc, char *argv[])
{
if (argc < 3)
{
printf("argv[1]是压缩包名称\n");
printf("argv[2]是解压后的文件名称\n");
return -1;
}
std::string ifilename = argv[1]; // 压缩包名
std::string ofilename = argv[2]; // 解压缩后文件名
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);
ifs.close();
std::string unpacked = bundle::unpack(body); // 对压缩包数据解压缩
std::ofstream ofs;
ofs.open(ofilename, std::ios::binary);
ofs.write(&unpacked[0], unpacked.size());
ofs.close();
return 0;
}
在这里我们使用MD5算法对压缩前的文件以及解压后的文件进行校对,可以得到这是两个内容完全相同的文件。
httplib库认识
httplib 库,一个 C++11 单文件头的跨平台 HTTP/HTTPS 库。安装起来非常容易。只需包含 httplib.h 在你的代码中即可。
httplib 库实际上是用于搭建一个简单的 http 服务器或者客户端的库,这种第三方网络库,可以让我们免去搭建服务器或客户端的时间,把更多的精力投入到具体的业务处理中,提高开发效率。
struct MultipartFormData {
std::string name;
std::string content;
std::string filename;
std::string content_type;
};
struct Request {
std::string method; // 请求方法
std::string path; // 资源路径
Headers headers; // 头部字段
std::string body; // 正文
// for server
std::string version; // 协议版本
Params params; // 查询字符串
MultipartFormDataMap files; // 保存的是客户端上传的文件信息
Ranges ranges; // 用于实现断点续传的请求文件区间
bool has_header(const char *key) const;
std::string get_header_value(const char *key, size_t id = 0) const;
void set_header(const char *key, const char *val);
bool has_file(const char *key) const;
MultipartFormData get_file_value(const char *key) const;
};
// Request结构体功能:1、客户端保存所有http请求相关信息最终组织http请求发送给服务器;2、服务器收到http请求后进行解析,将解析的数据保存在Request结构体中,待后续处理。
struct Response {
std::string version; // 协议版本
int status = -1; // 相应状态码
std::string reason;
Headers headers; // 头部字段
std::string body; // 响应给客户端的正文
std::string location; // Redirect location
void set_header(const char *key, const char *val); // 设置头部字段
void set_content(const std::string &s, const char *content_type); // 设置正文
};
// Response结构体功能:用户将响应数据放到结构体中,httplib会将其中的数据按照http相应格式组成http响应,发送给客户端。
class Server {
using Handler = std::function<void(const Request &, Response &)>; // 函数指针类型
using Handlers = std::vector<std::pair<std::regex, Handler>>; // 请求与处理函数映射表
std::function<TaskQueue *(void)> new_task_queue; // 线程池用于处理请求
Server &Get(const std::string &pattern, Handler handler); // GET("/hello", Hello--函数指针)
Server &Post(const std::string &pattern, Handler handler);
Server &Put(const std::string &pattern, Handler handler);
Server &Patch(const std::string &pattern, Handler handler);
Server &Delete(const std::string &pattern, Handler handler);
Server &Options(const std::string &pattern, Handler handler);
bool listen(const char *host, int port, int socket_flags = 0); // 搭建并
};
// Server类功能:用于搭建http服务器
/*
Handler 函数指针类型:定义了一个http请求处理回调函数格式
httplib搭建的服务器收到请求后,进行解析,得到一个Request结构体,其中包含请求数据,根据请求数据我们就可以处理这个请求了,这个处理函数定义的格式就是Handler格式
std::function<void(const Request &, Response &)>
Request参数,保存请求数据,让用户能够根据请求数据进行业务处理
Response参数,需要用户在业务处理中填充数据,最终要响应给客户端
Handlers 是一个请求路由数组:其中包含两个信息
std::vector<std::pair<std::regex, Handler>>
regex:正则表达式-用于匹配http请求资源路径
Handler:请求处理函数指针
可以理解为Handlers是一张表,映射了客户端请求资源路径和一个处理函数(用户自己定义的函数),当服务器收到请求解析得到Request就会根据资源路径以及请求方法到这张表中查看有没有对应的处理函数,如果有则调用这个函数进行请求处理,如果没有则响应404
hanslers表就决定了哪个请求应该用哪个函数处理
new_task_queue 线程池-处理http请求
httplib收到一个新建连接,则将新的客户端连接抛入线程池中
线程池中线程的工作:
1、接受请求,解析请求,得到Request结构体即请求数据
2、在Handlers映射表中,根据请求信息查找处理函数,如果有则调用函数处理
3、当处理函数调用完毕,根据函数返回的Response结构体中的数据组织http响应发送给客户端
*/
class Client {
Client(const std::string &host, int port); // 传入服务器IP地址和端口
Result Get(const char *path, const Headers &headers); // 向服务器发送GET请求
Result Post(const char *path, const char *body, size_t content_length, const char *content_type);
Result Post(const char *path, const MultipartFormDataItems &items); // POST请求提交多区域数据,常用语多文件上传
}
// Client类功能:用于搭建http客户端
httplib库搭建简单服务端
#include "httplib.h"
void Hello(const httplib::Request &req, httplib::Response &rsp)
{
rsp.set_content("Hello World!", "text/plain");
rsp.status = 200;
}
void Numbers(const httplib::Request &req, httplib::Response &rsp)
{
auto num = req.matches[1]; // 0里面保存的是整体path,往后下标中保存的是捕捉的数据
rsp.set_content(num, "text/plain");
rsp.status = 200;
}
void Multipart(const httplib::Request &req, httplib::Response &rsp)
{
auto ret = req.has_file("file");
if (ret == false)
{
std::cout << "not file upload\n";
rsp.status = 400;
return ;
}
const auto &file = req.get_file_value("file");
rsp.body.clear();
rsp.body = file.filename; // 文件名称
rsp.body += "\n";
rsp.body += file.content; // 文件内容
rsp.set_header("Content-Type", "text/plain");
rsp.status = 200;
}
int main()
{
httplib::Server server; // 实例化一个server类对象
server.Get("/hi", Hello); // 注册一个针对/hi的Get请求的处理函数
server.Get(R"(/numbers/(\d+))", Numbers);
server.Post("/multipart", Multipart);
server.listen("0.0.0.0", 8989); // 匹配服务器上的所有ip地址(监控所有的网卡)
return 0;
}
httplib库搭建简单客户端
#include "httplib.h"
#define SERVER_IP "118.31.170.52"
#define SERVER_PORT 8989
int main()
{
httplib::Client client(SERVER_IP, SERVER_PORT); // 实例化client对象,用于搭建客户端
httplib::MultipartFormData item;
item.name = "file";
item.filename = "hello.txt";
item.content = "Hello World!"; // 上传文件时,这里给的就是文件内容
item.content_type = "text/plain";
httplib::MultipartFormDataItems items;
items.push_back(item);
auto res = client.Post("/multipart", items);
std::cout << res->status << std::endl;
std::cout << res->body << std::endl;
return 0;
}
http的ETag头部字段:其中存储了一个资源的唯一标识客户端第一次下载文件的时候,会收到这个响应信息,第二次下载,就会将这个信息发送给服务器,想要让服务器根据这个唯一标识判断这个资源有没有被修改过,如果没有被修改过,直接使用原先缓存的数据,不用重新下载了。
http协议本身对于etag中是什么数据并不关心,只要你服务端能够自己标识就行因此我们etag就用“文件名-文件大小-最后一次修改时间"组成。而etag字段不仅仅是缓存用到,还有就是后边的断点续传的实现也会用到因为断点续传也要保证文件没有被修改过。
http协议的Accept-Ranges: bytes字段:用于高速客户端我支持断点续传,并且数据单位以字节作为单位
http的ETag头部字段:其中存储了一个资源的唯一标识客户端第一次下载文件的时候,会收到这个响应信息,
第二次下载,就会将这个信息发送给服务器,想要让服务器根据这个唯一标识判断这个资源有没有被修改过,如果没有被修改过,直接使用原先缓存的数据,不用重新下载了
http协议本身对于etag中是什么数据并不关心,只要你服务端能够自己标识就行因此我们etag就用“文件名-文件大小-最后一次修改时间"组成
而etag字段不仅仅是缓存用到,还有就是后边的断点续传的实现也会用到因为断点续传也要保证文件没有被修改过。
http协议的Accept-Ranges: bytes字段:用于高速客户端我支持断点续传,并且数据单位以字节作为单位
Content-Type字段的重要性:决定了浏览器如何处理响应正文