云备份项目认识
我们要做的这个项目一共要做到三点
- 将本地计算机指定文件夹中的文件备份到服务器上
- 可以通过浏览器查看并下载文件 支持断点续传功能
- 服务器对于文件进行热点管理 对于非热点文件进行压缩以节省磁盘空间
实现目标
该项目需要我们实现两端程序 包括运行在用户主机上的客户端 以及运行在Linux平台的服务器
通过这两端协作来完成上述的功能
服务端功能细分
- 支持客户端上传文件
- 支持客户端查看备份文件
- 支持客户端文件下载并且支持断点续传功能
- 热点文件管理
服务端模块化
服务端一共分为四个模块
- 数据管理模块 主要功能是管理备份文件 以便于随时获取
- 网络通信模块 主要功能是实现与客户端之间的网络通信
- 业务处理模块 主要功能是文件上传 文件列表查看 下载
- 热点管理 主要功能是对于长时间无访问的文件进行压缩
客户端功能细分
- 指定文件夹中的文件检测 (获取文件夹中有什么文件)
- 判断指定文件夹中的文件是否需要备份 (新增 已备份但是又需要修改 上次备份后又修改过 但是间隔了三秒没有修改 )
- 将需要备份的文件上传到服务器
客户端模块划分
客户端可以划分成三个模块
- 数据管理模块 (管理备份的文件信息)
- 文件检测模块 (监控指定的文件夹)
- 文件备份模块 (上传需要的文件数据)
环境搭建
g++升级7.3版本
由于我们的项目需要用到更高版本的gcc 所以说我们需要先进行下环境搭建
我们可以通过
g++ --version
来进行g++版本的查看
接下来分别在命令行中输入下面四条指令
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
我们的g++版本就能迭代成最新的了
安装jsoncpp库
我们可以通过下面两个指令来安装jsoncpp库
sudo yum install epel-release
sudo yum install jsoncpp-devel
可以通过下面的指令来查看jsoncpp库是否安装成功
ls /usr/include/jsoncpp/json/ -1
下载bundle数据压缩库
在下载bundle数据压缩库之前我们首先要安装git
git的安装方法可以参考我的这篇博客 git
之后使用下面这段指令就可以下载
git clone https://github.com/r-lyeh-archived/bundle.git
下载httplib库
使用下面这段指令就可以下载
git clone https://github.com/yhirose/cpp-httplib.git
第三方库认识
json认识
json 是一种数据交换格式 采用完全独立于编程语言的文本格式来存储和表示数据。
比如说我们要将小明同学的信息使用json存储
原来的信息如下
char name = "小明"; int age = 18; float score[3] = {88.5, 99, 58};
转变为json存储之后如下
{ "姓名" : "小明", "年龄" : 18, "成绩" : [88.5, 99, 58] }
json存储实际上是将原来的数据变成了一个字符串 (也就是说上面的代码实际上就是一个字符串)
json 数据类型:对象,数组,字符串,数字 介绍如下
- 对象:使用花括号 {} 括起来的表示一个对象。
- 字符串:使用常规双引号 “” 括起来的表示一个字符串
- 数字:包括整形和浮点型,直接使用。
- 数组:使用中括号 [] 括起来的表示一个数组。
也就是说 如果我们有多组同类型的数据 我们可以使用数组组织起来
代码表示如下
[ { "姓名" : "小明", "年龄" : 18, "成绩" : [88.5, 99, 58] }, { "姓名" : "小黑", "年龄" : 18, "成绩" : [88.5, 99, 58] } ]
jsoncpp – value类
jsoncpp 库用于实现 json 格式的序列化和反序列化,完成将多个数据对象组织成为 json 格式字符串,以及将 json格式字符串解析得到多个数据对象的功能
这其中最重要的是三个类 它们分别是value writer reader
value类的主要函数以及作用如下
//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 };
jsoncpp – writer类
//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序列化有两个类 一个低版本一个高版本
它们之间的区别在于 低版本的类直接返回一个字符串 而高版本的类返回一个文件描述符
jsoncpp – reader类
//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; }
reader类也分为低版本和高版本
其中低版本的reader类需要我们传入字符串 value对象 还有一个bool值
高版本的reader类需要我们传入字符串的起始位置 字符串的终止位置 value对象 还有一个string对象
jsoncpp – 实现序列化
比如说我们目前拥有以下的数据
{ "name" : "xiaoming", "age" : 18, "score" : [88.5, 99, 58] }
现在我们要将这些输出实现序列化的话 我们要分为以下两步走
- 将所有的数据存放在json::Value中
- 使用json::StreamWriter进行序列化
将所有的数据存放在json::Value中
代码表示如下
const char* name = "xiaoming"; int age = 18; float score[] = {77.5 , 88 , 93.6}; Json::Value root; root["name"] = name; root["age"] = age; root["score"].append(score[0]); root["score"].append(score[1]); root["score"].append(score[2]); Json::StreamWriterBuilder swb; std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); std::stringstream ss; sw->write(root , &ss);
序列化后结果如下
jsoncpp – 实现反序列化
比如说我们目前拥有以下的jason字符串类格式类型的数据
string str = R"({"name":"xiaohei" , "age":19 , "score":[58.5 , 66 , 35,5]})";
这里介绍下 C++11中 R()语法
使用该语法之后括号内将为原生字符串 不受任何特殊字符的影响
举个例子 \n本来应该是换行符 可如果在()内的话 它就是两个字符组成的字符串“\n“
反序列化的本质其实就是将json格式的字符串转化为一个value对象
所以说实现反序列化也是分两步走
- 定义一个value对象
- 使用json::CharReader进行反序列化
代码表示如下
// deserialize string str = R"({"name":"xiaohei" , "age":19 , "score":[58.5 , 66 , 35,5]})"; Json::Value root; // instantiate a value objict Json::CharReaderBuilder crb; // subclass std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); string err; bool set = cr->parse(str.c_str() , str.c_str()+str.size() , &root , &err); if (set == false) { std::cerr << "json parse error!" << std::endl; } std::cout << root["name"].asString() << std::endl; std::cout << root["age"].asString() << std::endl;
bundle库认识
Bundle 是一个嵌入式压缩库 支持23种压缩算法和两种存档格式 使用的之后只需要加入两个头文件 bundle.cpp
和 bundle.h
即可
需要注意的是 这里的嵌入式和我们所熟知的嵌入式工程不同 它的意思是bundle库可以不需要编译 直接在我们的代码种使用
下面是bundle库的一些方法 大家简单浏览下即可
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库实现文件压缩
我们使用bundle库进行文件压缩实际上只需要用到一句代码
string pack(bundle::LZIP , string& body);
返回值说明:
它会返回一个string对象
参数说明:
- 第一个参数是压缩的格式
- 第二个参数是一个strting对象
实现bundle库文件压缩的代码如下
#include <iostream> #include <fstream> #include <string> #include "bundle.h" using std::cout; using std::endl; int main(int argc , char* argv[]) { if (argc < 3) { cout << "no enough arguements" << endl; return -1; } std::string istreamstring = argv[1]; std::string ostreamstring = argv[2]; // open and copy the content of istreamstring std::ifstream ifs; ifs.open(istreamstring.c_str() , std::ios::binary); // get the size of istreamstring ifs.seekg(0 , std::ios::end); int size = ifs.tellg(); ifs.seekg(0 , std::ios::beg); // get end std::string body; body.resize(size); ifs.read(&body[0] , size); // &body[0] is a wonderful code because if we use c_str we just get a const str but &body[0] can give us a str whihout const std::string packed = bundle::pack(bundle::LZIP , body); // compress in lzip format std::ofstream ofs; ofs.open(ostreamstring , std::ios::binary); ofs.write(&packed[0] , packed.size()); ifs.close(); ofs.close(); return 0; }
我们编译运行之后可以发现
文件压缩后的大小确实比文件压缩前的大小要小不少
bundle库实现文件解压缩
我们要证明压缩后的文件确实是源文件的压缩的话 就需要解压之后对比两个文件的md5值即可
于是乎我们下面就来实现一个文件解压缩的代码
bundle库解压缩的代码如下
string unpack(string body);
返回值说明:
它会返回一个string对象
参数说明:
它的参数只有一个 是一个string对象 里面是被压缩的文件内容
实现bundle库文件解压缩的代码如下
#include <iostream> using std::cout; using std::endl; #include <string> using std::string; #include <fstream> #include "bundle.h" int main(int argc , char* argv[]) { if (argc < 3) { cout << "no enough arguements" << endl; return -1; } string ifilestring = argv[1]; string ofilestring = argv[2]; // open and copy the file std::ifstream ifs; ifs.open(ifilestring , std::ios::binary); ifs.seekg(0 , std::ios::end); size_t size = ifs.tellg(); ifs.seekg(0 , std::ios::beg); string body; body.resize(size); ifs.read(&body[0] , size); // uncompress string unpacked = bundle::unpack(body); // write to ofilestring std::ofstream ofs; ofs.open(ofilestring , std::ios::binary); ofs.write(&unpacked[0] , unpacked.size()); // close stream ifs.close(); ofs.close(); return 0; }
编译运行之后我们可以发现 MD5值和大小一模一样 所以说压缩和解压缩功能正常
httplib库认识
httplib 库 一个 C++11 单文件头的跨平台 HTTP/HTTPS 库 安装起来非常容易 只需包含 httplib.h 在你的代码中即可
httplib 库实际上是用于搭建一个简单的 http 服务器或者客户端的库 这种第三方网络库 可以让我们免去搭建服务器或客户端的时间 把更多的精力投入到具体的业务处理中 提高开发效率
博主自己其实也实现过一个简单的Http服务器 如果大家有兴趣可以参考博客
自主实现http服务器
httplib 库搭建简单服务器
下面介绍的内容是如何使用httplib库来搭建一个简单的服务器
首先我们需要利用httplib里面的Sever类实例化一个对象
httplib::Server server; // instantiate a sever object
之后调用这个对象的函数去针对请求的各种资源注册各种函数
server.Get("/hi" , Hello); // register a function method for get request resource hi
void Hello(const httplib::Request& req , httplib::Response& res) { res.body = "Hello"; res.status = 200; }
之后让这个对象去监听服务器的网卡
server.listen("0.0.0.0" , 8081);
最后让服务器运行起来之后我们使用浏览器访问特定资源便可以收到特定的回复
httplib 库搭建简单客户端
首先我们需要利用httplib库里面的Client类实例化一个对象
httplib::Client client(SEVER_IP, SEVER_PORT); // ip port
上面的IP和端口均为服务器的
紧接着我们就可以使用这个对象去调用函数来得到一个指针
使用该指针可以获得我们想要的各种数据
auto res = client.Get("/hi"); cout << res->status << endl; cout << res->body << endl;
而如果我们要请求的是post方法 我们则需要进行下面的操作
// 2. send data to client httplib::Client client(SERVER_ADDR, SERVER_PORT); httplib::MultipartFormData item; item.content = body; item.filename = fu.FileName(); item.name = "file"; // mark 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; }
我们需要定义一个item 并且将数据填充完毕之后添加到items中 zu欧维一个Post函数的参数上传