云备份项目
1. 项目认识
1.1 功能了解
自动将本地计算机上指定文件夹中需要备份的文件上传备份到服务器中。并且能够随时通过浏览器进行查看并且下
载,其中下载过程支持断点续传功能,而服务器也会对上传文件进行热点管理,将非热点文件进行压缩存储,节省磁盘空间。
1.2 实现目标
这个云备份项目需要我们实现两端程序,其中包括部署在用户机的客户端程序,上传需要备份的文件,以及运行在服务器上的服务端程序,实现备份文件的存储和管理,两端合作实现总体的自动云备份功能。
1.3 服务端程序负责功能
- 对客户端上传的文件进行备份存储
- 能够对文件进行热点文件管理,对非热点文件(长时间不访问的文件)进行压缩存储,节省磁盘空间。
- 支持客户端浏览器查看访问文件列表。
- 支持客户端浏览器下载文件,并且下载支持断点续传。
1.4 服务端功能模块划分
- 数据管理模块:负责服务器上备份文件的信息管理。
- 网络通信模块:搭建网络通信服务器,实现与客户端通信。
- 业务处理模块:针对客户端的各个请求进行对应业务处理并响应结果。
- 热点管理模块:负责文件的热点判断,以及非热点文件的压缩存储。
1.5 客户端程序负责功能
- 能够自动检测客户机指定文件夹中的文件,并判断是否需要备份
- 将需要备份的文件逐个上传到服务器
1.6 客户端功能模块划分
- 数据管理模块:负责客户端备份的文件信息管理,通过这些数据可以确定一个文件是否需要备份。
- 文件检测模块:遍历获取指定文件夹中所有文件路径名称。
- 网络通信模块:搭建网络通信客户端,实现将文件数据备份上传到服务器。
2. 环境搭建
2.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
2.2 下载bundle数据压缩库
执行如下命令:
sudo yum install git
git clone https://github.com/r-lyeh-archived/bundle.git
2.3 下载httplib库
执行如下命令:
git clone https://github.com/yhirose/cpp-httplib.git
3. 第三方库的基本认识
3.1 json
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
数据类型:对象,数组,字符串,数字
- 对象:使用花括号{} 括起来的表示一个对象。
- 数组:使用中括号[] 括起来的表示一个数组。
- 字符串:使用常规双引号"" 括起来的表示一个字符串
- 数字:包括整形和浮点型,直接使用。
3.1.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;
}
3.1.2 jsoncpp使用
使用jsoncpp
时注意事项:
- 要包含
jsoncpp
的头文件 - 在编译链接时需要,
-ljsoncpp
来指定库名
makefile文件写法:
(1) jsoncpp实现序列化
// 测试json实现序列化
#include<memory>
#include<sstream>
#include<iostream>
#include<jsoncpp/json/json.h> // 要包含jsoncpp的头文件
int main()
{
const char*name="小李";
int age=22;
float score[]={100,99,89};
Json::Value val;
val["姓名"]=name;
val["年龄"]=age;
val["成绩"].append(score[0]);
val["成绩"].append(score[1]);
val["成绩"].append(score[2]);
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(val,&ss);
std::cout<<ss.str()<<std::endl;
}
运行结果:
(2) jsoncpp实现反序列化
// 测试json实现反序列化
#include<string>
#include<memory>
#include<jsoncpp/json/json.h>
#include<iostream>
int main()
{
// R"(...)" 表示(...)是一个字符串
std::string str = R"({"姓名":"小李", "年龄":22, "成绩":[77.5,88,99]})";
Json::Value val;
Json::CharReaderBuilder cb;
std::unique_ptr<Json::CharReader> cr(cb.newCharReader());
std::string error;
bool ret = cr->parse(str.c_str(), str.c_str() + str.size(), &val, &error);
if (ret == false)
{
std::cout << "parse error: " << error << std::endl;
return -1;
}
std::cout << val["姓名"].asString() << std::endl;
std::cout << val["年龄"].asString() << std::endl;
// 两种遍历方式:
int sz = val["成绩"].size();
for (int i = 0; i < sz; ++i)
{
std::cout << val["成绩"][i].asFloat() << std::endl;
}
for(auto it=val["成绩"].begin();it!=val["成绩"].end();++it)
{
std::cout<<it->asFloat()<<std::endl;
}
}
运行结果:
3.2 bundle
3.2.1 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 );
}
3.2.2 bundle库的使用
(1) 文件压缩
// 测试bundle实现压缩文件
#include<string>
#include<fstream>
#include<iostream>
#include"bundle.h"
int main(int argc,char*argv[])
{
if (argc != 3)
{
std::cout << "argv[1] 是原始文件路径名称\n";
std::cout << "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); // 调整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();
}
由于此过程需要用到线程库,在编译时我们要-lpthread
来指定线程库
文件压缩前后大小对比:
(2) 文件解压缩
// 测试bundle实现解压缩文件
#include<string>
#include"bundle.h"
#include<fstream>
int main(int argc,char*argv[])
{
if (argc != 3)
{
std::cout << "argv[1] 是压缩包名称\n";
std::cout << "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();
}
3.3 httplib
httplib
库,一个C++11 单文件头的跨平台HTTP/HTTPS
库。安装起来非常容易。只需包含httplib.h
在你的代码中即可。
httplib
库实际上是用于搭建一个简单的http
服务器或者客户端的库,这种第三方网络库,可以让我们免去搭建服务器或客户端的时间,把更多的精力投入到具体的业务处理中,提高开发效率(使用第三方库的原因)
3.3.1 httplib认识
namespace httplib{
struct MultipartFormData {
std::string name;
std::string content;
std::string filename;
std::string content_type;
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
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; // 根据name字段判断是否上传了文件
MultipartFormData get_file_value(const char *key) const; // 获取文件信息
};
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); // 设置正文
};
class Server { // 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);
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);
// 搭建并启动http服务器
bool listen(const char *host, int port, int socket_flags = 0);
};
class Client { // 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);
// Post请求提交多区域数据, 常用于多文件上传
Result Post(const char *path, const MultipartFormDataItems &items);
}
}
Request类的作用:
-
客户端保存的所有
http
请求相关信息,最终组织http
请求发送给服务器 -
服务器收到
http
请求之后进行解析,将解析的数据保存在Request类中,待后续处理
Response类的作用:
- 用户将响应数据放到Response类中,
httplib
会将其中的数据按照http
响应格式组织成为http
响应,发送给客户端
难点解析:
Server类中的Handler
函数对象:是一种函数指针类型,定义了一个http请求处理回调函数的格式
-
httplib
搭建的服务器收到请求后,进行解析,得到一个Request结构体,其中包含了请求数据 -
根据请求数据我们就可以处理这个请求了,这个处理函数定义的格式就是
Handler
格式 -
void(const Request &, Response &)
- Request参数:保存请求数据,让用户能够根据请求进行业务处理
- Response参数:需要用户在业务处理中,填充数据,最终要响应给客户端
Server类中的Handlers
:
-
是一个请求路由数组,其中包含两个信息:
std::vector<std::pair<std::regex, >>
regex
:正则表达式,用于匹配http
请求资源路径Handler
:请求处理函数指针
-
可以理解为,
Handlers
是一张表,映射了一个客户端请求资源路径和一个处理函数(用户自己定义的函数)- 当服务器收到请求解析得到
Request
就会根据资源路径以及请求放到这张表中查看有没有对应的处理函数。如果有,则调用这个函数进行请求处理,没有则响应404 - 比如:
- 当服务器收到请求解析得到
- 总结:通俗来说,
Handlers
这个表就决定了,哪个请求应该用哪个函数处理
Server类中的new_task_queue
:线程池,处理http
请求
httplib
收到一个新建连接,则将新的客户端连接放入线程池中- 线程池中线程的工作:
- 接受请求,解析请求,得到Request结构体也就是请求的数据
- 在
Handlers
映射表中,根据请求信息查找处理函数,如果有则调用函数处理 - 当处理函数调用完毕后,根据函数返回的Response结构体中的数据组织
http
响应发送给客户端
3.3.2 httplib使用
httplib
库搭建简单服务器:
#include"httplib.h"
void Hello(const httplib::Request&req, httplib::Response&resp)
{
resp.set_content("Hello yj!", "text/plain"); // 设置正文格式
resp.status=200;
}
int main()
{
httplib::Server svr; // 实例化Server类的对象用于搭建服务器
svr.Get("/hi",Hello); // 注册一个针对/hi的Get请求的处理函数映射关系
svr.Get(R"(/numbers/(\d+))",[&](const httplib::Request&req, httplib::Response&resp){
auto num=req.matches[1]; // 0里面保存的是整体path, 往后下标中保存的是捕捉的数据
resp.set_content(num, "text/plain");
resp.status=200;
});
svr.Post("/multipart",[&](const httplib::Request&req, httplib::Response&resp){
auto ret=req.has_file("file");
if(ret==false)
{
std::cout<<"not file upload\n";
resp.status=400;
return;
}
const auto&file=req.get_file_value("file");
resp.body.clear();
resp.body=file.filename; // 文件名称
resp.body+="\n";
resp.body+=file.content; // 文件内容
resp.set_header("Content-Type","text/plain");
resp.status=200;
});
svr.listen("0.0.0.0",8080);
}
注意:使用httplib
库编译时要-lpthread
链接线程库
启动服务器:后用浏览器充当客户端来访问
httplib
库搭建简单客户端:
#include"httplib.h"
#define SERVER_IP "111.231.169.213"
#define SERVER_PORT 8080
int main()
{
httplib::Client client(SERVER_IP,SERVER_PORT); // 实例化Client类的对象用于搭建客户端
auto res=client.Get("/hi");
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
res=client.Get("/numbers/789");
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
httplib::MultipartFormData item;
item.name="file";
item.filename="hello.txt";
item.content="hello yj!"; // 上传文件时, 这里给的就是文件内容
item.content_type="text/plain";
httplib::MultipartFormDataItems items;
items.push_back(item);
res=client.Post("/multipart",items);
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
return 0;
}
makefile文件写法:
先启动服务端,然后再启动客户端测试: