云备份的C/C++实现

云备份的C/C++实现

一、开发环境

centos7.6/vim、g++、gdb、makefile
windows10/vs2017
对于文件操作,后续会使用到experimental/filesystem,它需要C++17的支持

二、项目概述

自动将用户机指定文件夹中需要备份的文件自动上传到服务器中实现备份。并且能够通过浏览器进行查看下载,其中下载支持断点续传功能功能,而且服务器也会对上传文件实现热点管理,将非热点文件进行压缩,节省磁盘空间

三、实现目标

达成两端程序,一是部署在用户机的客户端程序,上传需要备份的文件,以及再服务器上的服务端程序,实现文件的备份存储和管理,两端合作,实现云备份的目的。

四、服务端程序负责的功能

1.对客户端上传的文件进行备份存储

2.支持客户端通过浏览器访问文件列表

3.支持客户端通过浏览器进行下载,并且支持断点续传功能

4.能够对备份文件进行热点管理,节省磁盘空间

五、服务端功能模块划分

1.网络通信模块——搭建网络通信服务器,实现与客户端的通信

2.业务处理模块——对不同的请求提供不同的处理方法

3.数据管理模块——负责服务器上备份文件的信息管理

4.热点管理模块——负责文件的热点判断,对非热点文件进行压缩

六、客户端程序负责的功能

1.自动检测客户端指定文件,判断文件是否需要上传

2.将需要备份的文件进行上传

七、客户端功能模块划分

1.网络通信模块——搭建网络通信客户端,实现与服务器的通信

2.文件检索模块——遍历获取指定文件夹的所有文件信息和路径

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  //添加环境变量

2.安装jsoncpp库

sudo yum install epel-release
sudo yum install jsoncpp-devel
[dev@localhost ~]$ ls /usr/include/jsoncpp/json/  //查看是否安装成功
assertions.h config.h   forwards.h reader.h version.h
autolink.h   features.h json.h     value.h   writer.h

这里有可能因为centos版本不同,安装的头文件内容也会发生变化,这里需要根据自己的版本来寻找。

3.安装boudle数据压缩库

sudo yum install git
git clone https://github.com/r-lyeh-archived/bundle.gi

这个是从github中找到的,下载失败的话可以去github中下载
下载连接:
Github下载地址

4.下载httplib库

git clone https://github.com/yhirose/cpp-httplib.git

这个库的主要作用是为了以后搭建网络通信可以简单一点,因为时间有限,并且自己写的可能并没有别人那么完美,但是还是建议大家去看一下源代码,学习别人的代码思路,理解代码的底层实现,这样才是最好的学习方式。
下载链接:
Github下载地址

九、json库的认识与使用

1.json的认识

json是一个数据交换格式,将对象中的数据转换为字符串,这样就可以很简单的再函数中传递这些数据,而且格式易于人的阅读和编写以及机器的解析和生成

string name = "小明";
int age = 18;
float score[3] = {77.5,88,99.3};

string name = "小黑";
int age = 18;
float score[3] = {88.5,99,58};

json将数据进行组织后:
[
   {
        "姓名" : "小明",
        "年龄" : 18,
        "成绩" : [77.5, 88, 99.3]
   },
   {
        "姓名" : "小黑",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
   }
]

json数据类型:对象,数组,字符串,数字
对象:用 {} 括起来的表示一个对象
数组:用 [] 括起来的表示一个数组
字符串:使用 “” 括起来的表示一个字符串
数字:整型和浮点型都可以,直接使用

2.jsoncpp库的使用

首先需要将jsoncpp库拷贝到工作目录下,然后将其当作头文件引入即可

#include "jsoncpp/json"

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::Value这个类实例化的对象中,然后通过序列化类和反序列化类对Json::Value的对象进行序列化和反序列化,操作很简单。

//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 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}


//这两个类的主要使用方法就是
StreamWriterBuilder swb;
StreamWriter* writer = swb.newStreamWriter();
std::stringstream ss;
writer->write(val,ss);//将Json::value实例化的对象的内容写入字符串流中
std::string body;
body = ss.str();//ss并不是一个字符串,所以要用str()将其中的内容转化为字符串
delete writer  //使用完不要忘记释放,new出来的对象不释放会造成内存泄漏
//这样我们就完成了对val中存储的数据的序列化

CharReaderBuilder crb;
CharReader* reader = crb.newStreamReader();  
std::string str;
/*这一步是完成对body中存储的json序列化后的格式的数据进行解析然
后存入val对象中,之后就可以通过val来访问数据成员,拿到我们想要的数据*/
reader->parse(body.c_str(),body.c_str()+body.size(),val,&str);
delete reader;


//将数据成员添加到Json::Value实例化的的对象中
const char *name = "小明";
int age = 18;
float score[] = {88.5, 98, 58};
Json::Value val;
val["姓名"] = name;
val["年龄"] = age;
val["成绩"].append(score[0]);
val["成绩"].append(score[1]);
val["成绩"].append(score[2]);

//读取val对象中存储的数据
std::cout<<val["姓名"].asString()<<std::endl;
std::cout<<val["年龄"].asInt()<<std::endl;
int sz = val["成绩"].size();
//这里也可以使用迭代器
for (auto it = val["成绩"].begin(); it != val["成绩"].end(); it++)
 	std::cout << it->asFloat() << std::endl; 
for(int i = 0;i < sz;i++)
	std::cout<<val["成绩"][i].asFloat()<<std::endl;

经过这些练习,应该对json的使用已经熟悉,这样就可以开始学习剩下的库的使用

十、bundle库的认识和使用

1.bundle的认识

BundleBundle 是一个嵌入式压缩库,支持23种压缩算法和2种存档格式。使用的时候只需要加入两个文件 bundle.h 和 bundle.cpp 即可

#include "bundle.h"
namespace bundle 
{ 
 在该项目中我们只需要使用这两个接口
 T pack( unsigned Q, T ); //T为存放将要被压缩的内容的string,Q为bundle提供的压缩格式这里我们用bundle::LZIP;
 //string body;
 //read(&body);  //这里是一个我们自己写的文件读取函数,后面我们需要自己实现一个,这里举例使用,将文件内容读入body中
 //string pack_body = bundle::pack(bundle::LZIP,body);//将body中内容进行压缩,将被压缩之后的内容存入pack_body中
 //wrtie(&pack_body)//将压缩后的内容写入指定文件当中完成压缩操作
 T unpack( T ); 
 //unpack操作同上,只是调用的函数的不同eg:bundle::unpack(body);
} 

2.bundle库的使用

 T pack( unsigned Q, T ); //T为存放将要被压缩的内容的string,Q为bundle提供的压缩格式这里我们用bundle::LZIP;
 string body;
 read(&body);  //这里是一个我们自己写的文件读取函数,后面我们需要自己实现一个,这里举例使用,将文件内容读入body中
 string pack_body = bundle::pack(bundle::LZIP,body);//将body中内容进行压缩,将被压缩之后的内容存入pack_body中
 wrtie(&pack_body)//将压缩后的内容写入指定文件当中完成压缩
 //之后删除被压缩文件即可
 remove_all(filename);


 T unpack( T ); 
 //unpack操作同上,只是调用的函数的不同eg:boudle::unpack(body);

这里主要的困难就是文件的读写操作,这里将在后面的工具类中写到。压缩和解压缩操作并不困难,学会使用即可。

十一、httplib库的认识和使用

1.httplib库的认识

httplib 库,一个 C++11 单文件头的跨平台 HTTP/HTTPS 库。安装起来非常容易。只需包含 httplib.h 在代码中即可

#include "httplib.h"

namespace httplib
{ 
 struct MultipartFormData  //这里是html中的东西
 { 
	 std::string name;   //区域名称标识
	 std::string content; //文件正文
	 std::string filename;  //文件名
	 std::string content_type;  //文件传输格式,一般都是二进制传输
 }; 
 //这里面保存了一个vector数组,里面每一个元素是一个MultipartFormData
 //应为有可能一次传输多个文件,所以要将每个文件对应格式的MultipartFormData创建队对应的区域然后进行传输
 using MultipartFormDataItems = std::vector<MultipartFormData>; 
 struct Request 
 { 
	 std::string method; //请求方法
	 std::string path; //URL中的资源路径
	 Headers headers; //map<string,string>存放头部字段
	 std::string body;  //存放正文
	 // for server 
	 std::string version; 
	 Params params;   //URL中的查询字符串
	 /*MultipartFormDataMap的内部其实是一个multimap<std::string,MultipartFormData>*/
	 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字段值是file的标识区域
	 MultipartFormData get_file_value(const char *key) const; //获取解析后的区域数据
 }; 
 struct Response  //响应类
 { 
 	 std::string version; //协议版本
	 int status = -1; //响应状态码默认是-1;
	 std::string reason;
	 Headers headers; //map<string,string>存放头部字段
	 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   //服务器类
 { 
	 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); 
	 bool listen(const char *host, int port, int socket_flags = 0); //启动服务器开始监听
 }; 
 class Client   //客户端类
 { 
	 Client(const std::string &host, int port); //服务器IP地址,服务器端口号
	 Result Get(const char *path, const Headers &headers); 
	 Result Post(const char *path, const char *body, size_t content_length,const char *content_type); 
	 Result Post(const char *path, const MultipartFormDataItems &items); 
 } 
} 

这个红框框就是一个name区域标识符对应的区域,里面包含了MultipartFormData中的四个数据
在这里插入图片描述

2.httplib的使用

//简单服务器的搭建
#include "httplib.h"
bool Upload(const httplib::Request &req,httplib::Response &rsp)
{}
bool List(const httplib::Request &req,httplib::Response &rsp)
{}
bool Download(const httplib::Request &req,httplib::Response &rsp)
{}
int main(void) 
{ 
 std::string _download_prefix = "/download/";
 using namespace httplib; 
 Server svr; 
 svr.Post("upload",Upload);   //Post(请求的资源路径,对应的业务处理函数)
 svr.Get("list",List);
 string download_path = _download_prefix + (".*");//使用正则表达式获取文件名
 svr.Get(download_path,Download);
 svr.listen("0.0.0.0", 9090); //运行服务器
 return 0; 
} 

十二、项目开发

服务器开发

服务器建立思想

目标功能:
能够通过浏览器进行查看下载,其中下载支持断点续传功能功能,而且服务器也会对上传文件实现热点管理,将非热点文件进行压缩,节省磁盘空间

思想:
1.接收请求数据并进行解析,将解析得到的请求数据存放到了一个Request结构体变量中

2.根据请求信息(请求方法&资源路径),到映射表中查找有没有对应的资源,如果没有则返回404

3.如果有对应的映射表信息,则调用对应的业务处理函数,将解析得到的req传入接口中,并传入一个空的Response结构体变量rsp

4.处理函数中会根据req中的请求信息进行对应的业务处理,并且在处理的过程中向rsp结构体中添加响应信息

5.等处理函数执行完毕,则httplib得到了一个填充了响应信息的Respose结构体变量rsp

6.根据rsp中的信息,组织一个http格式的响应,回复给客户端

7.如果是短连接则关闭连接处理下一个,如果是长连接,则等待请求,超时则关闭处理下一个

服务器模块划分:

1.网络通信模块——搭建网络通信服务器,实现与客户端的通信
2.业务处理模块——对不同的请求提供不同的处理方法
3.数据管理模块——负责服务器上备份文件的信息管理
4.热点管理模块——负责文件的热点判断,对非热点文件进行压缩

FileTools——文件工具类

#ifndef __MY_TOOLS__
#define __MY_TOOLS__

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <sstream>
#include <fstream>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
#include <cstdint>
#include "bundle.h"

namespace fs = std::experimental::filesystem;

namespace cloud
{
  class FileTool
  {
    private:
      std::string _name;
    public:
      FileTool(const std::string name):_name(name)
      {}
      std::string Name()
      {
        return fs::path(_name).filename().string();
      }
      bool Exist()  //文件是否存在
      {
        return fs::exists(_name);
      }
      size_t Size() //文件大小
      {
        if(!Exist())
          return 0;
        return fs::file_size(_name);
      }
      time_t LastChange_Tm()  //最后一次修改时间
      {
        if(!Exist())
          return 0;
        auto ftime = fs::last_write_time(_name);
        std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
        return cftime;
      }
      time_t LastVisit_Tm()   //最后一次访问时间
      {
        if(!Exist())
          return 0;
        struct stat st;
        stat(_name.c_str(),&st);
        return st.st_atime;

      }
      bool Write(const std::string &body)  //文件写入
      {
        if(!Exist())
          CreateDirectory();
        std::ofstream writer;
        writer.open(_name,std::ios::binary);  //以二进制格式打开文件
        if(!writer.is_open())
        {
          std::cout<<"writer open file false"<<std::endl;
          writer.close();
          return false;
        }
        writer.write(body.c_str(),body.size());
        if(!writer.good())
        {
          std::cout<<"writer write false"<<std::endl;
          writer.close();
          return false;
        }
        writer.close();
        return true;
      }
      bool Read(std::string* body)  //文件读取
      {

        if(!Exist())
          return 0;
        std::ifstream reader;
        reader.open(_name,std::ios::binary);
        if(!reader.is_open())
        {
          std::cout<<"reader open file false"<<std::endl;
          reader.close();
          return false;
        }
        size_t fsize = Size();
        body->resize(fsize);    //给body开辟_name文件大小的空间
        reader.read(&(*body)[0],fsize);  //将_name中数据读取到body中
        if(!reader.good())
        {
          std::cout<<"reader read false"<<std::endl;
          reader.close();
          return false;
        }
        reader.close();
        return true;
      }
      bool CreateDirectory() //创建目录
      {
        if(!Exist())
          return 0;
        fs::create_directories(_name);
        return true;
      }
      bool ScanDirectory(std::vector<std::string>& arry) //遍历目录
      {
        if(!Exist())
          return 0;
        for(auto &a : fs::directory_iterator(_name))
        {
          if(fs::is_directory(a) == true)
            continue;   //当前文件是一个文件夹,则不处理,只遍历普通文件
          //std::string filename = fs::path(a).filename().string();   //纯文件名
          std::string pathname = fs::path(a).relative_path().string(); //带路径的文件名
          arry.push_back(pathname);
        }
        return true;
      }
      bool Compress(const std::string &packname) //压缩文件
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"compress read file false"<<std::endl;
          return false;
        }
        std::string packbody = bundle::pack(bundle::LZIP,body);
        if(FileTool(packname).Write(packbody) == false)
        {
          std::cout<<"compress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name); //删除被压缩了的源文件
        return true;
      }
      bool Uncompress(const std::string &filename) //解压缩
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"Uncompress read file false"<<std::endl;
          return false;
        }
        std::string unpack_body = bundle::unpack(body);
        if(!FileTool(filename).Write(unpack_body))
        {
          std::cout<<"Uncompress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
      bool Remove()
      {
        if(!Exist())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
  };
  
  //序列化和反序列化
  class Json_Tool
  {
    public:
      static bool Serialize(const Json::Value &val, std::string *body)   //序列化
      {
        Json::StreamWriterBuilder swb;
        Json::StreamWriter *sw = swb.newStreamWriter();
        std::stringstream ss;
        int ret = sw->write(val,&ss);
        if(ret != 0)
        {
          std::cout<<"serialize false"<<std::endl;
          delete sw;
          return false;
        }
        *body = ss.str();
        delete sw;
        return true;
      }

      static bool UnSerialize(const std::string& body,Json::Value *val)
      {
        Json::CharReaderBuilder crb;
        Json::CharReader *cr = crb.newCharReader();
        std::string str;
        bool ret = cr->parse(body.c_str(),body.c_str() + body.size(),val,&str);
        if(!ret)
        {
          std::cout<<"unserialize false"<<std::endl;
          delete cr;
          return false;
        }
        delete cr;
        return true;
      }
    };
}


#endif
文件读写操作
class FileTool
  {
    private:
      std::string _name;
    public:
      FileTool(const std::string name):_name(name)
      {}
      std::string Name()
      {
        return fs::path(_name).filename().string();
      }
      bool Exist()  //文件是否存在
      {
        return fs::exists(_name);
      }
      size_t Size() //文件大小
      {
        if(!Exist())
          return 0;
        return fs::file_size(_name);
      }
      time_t LastChange_Tm()  //最后一次修改时间
      {
        if(!Exist())
          return 0;
        auto ftime = fs::last_write_time(_name);
        std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
        return cftime;
      }
      time_t LastVisit_Tm()   //最后一次访问时间
      {
        if(!Exist())
          return 0;
        struct stat st;
        stat(_name.c_str(),&st);
        return st.st_atime;

      }
      bool Write(const std::string &body)  //文件写入
      {
        if(!Exist())
          CreateDirectory();
        std::ofstream writer;
        writer.open(_name,std::ios::binary);  //以二进制格式打开文件
        if(!writer.is_open())
        {
          std::cout<<"writer open file false"<<std::endl;
          writer.close();
          return false;
        }
        writer.write(body.c_str(),body.size());//从body起始到结束
        if(!writer.good())
        {
          std::cout<<"writer write false"<<std::endl;
          writer.close();
          return false;
        }
        writer.close();
        return true;
      }
      bool Read(std::string* body)  //文件读取
      {

        if(!Exist())
          return 0;
        std::ifstream reader;
        reader.open(_name,std::ios::binary);
        if(!reader.is_open())
        {
          std::cout<<"reader open file false"<<std::endl;
          reader.close();
          return false;
        }
        size_t fsize = Size();
        body->resize(fsize);    //给body开辟_name文件大小的空间
        reader.read(&(*body)[0],fsize);  //将_name中数据读取到body中
        if(!reader.good())
        {
          std::cout<<"reader read false"<<std::endl;
          reader.close();
          return false;
        }
        reader.close();
        return true;
      }
 };

首先说明,这里的操作基本都都是C++文件操作库中的接口调用,所以需要注意的就是文件操作接口的正确调用,以及保证自己的编译器要支持C++17,不然会报错。

两个标准库IO流操作:

Read接口ifstream类实例化文件输入流对象,通过该对象,调用open接口,以二进制格式打开文件,打开成功后,将文件内容读取到body中,但是这里需要注意一点,要先给body开辟对应文件大小的空间,保证接收内容完整;其次是应为写入要从头写入,所以需要获取body空间的首地址,又因为我们传入进来的是个指针,所以需要代码中所示方法(这里是因为vim报了特殊的错误,所以采用这种方式解决)读取成功则关闭文件,失败则报错,然后关闭文件。

Write接口ifstream类实例化文件输出流对象,将body中内容写入,关闭文件等操作。与读取不同的地方在于,如果没有对应的文件,要创建一个新的文件,将内容写入。

获取文件最后访问时间,和获取文件最后修改时间,以及文件是否存在,这三个操作,就只是调用库中提供的接口,但是要注意一点就是他们返回的都是时间戳,所以还需要借助其中的一些操作将其转换成我们需要的时间。

#include <iostream>
#include <chrono>
#include <iomanip>
#include <fstream>
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
using namespace std::chrono_literals;
int main()
{
    fs::path p = fs::current_path() / "example.bin";
    std::ofstream(p.c_str()).put('a'); // create file
    auto ftime = fs::last_write_time(p);
    //下面的操作就可以将时间戳,转换为可读的年月日时分秒
    std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
}
文件目录创建和遍历
bool CreateDirectory() //创建目录
{
  if(!Exist())
    return 0;
  fs::create_directories(_name);
  return true;
}
bool ScanDirectory(std::vector<std::string>& arry) //遍历目录
{
  if(!Exist())
    return 0;
  for(auto &a : fs::directory_iterator(_name))
  {
    if(fs::is_directory(a) == true)
      continue;   //当前文件是一个文件夹,则不处理,只遍历普通文件
    //std::string filename = fs::path(a).filename().string();   //纯文件名
    std::string pathname = fs::path(a).relative_path().string(); //带路径的文件名
    arry.push_back(pathname);
  }
  return true;
}

对于这里,要说的就是目录遍历时,可能其中还有目录,所以我们只遍历当前目录下的文件,如果要遍历所有文件,这里可以选择一个简单的递归调用,就能解决这个问题。

string>>& arry**,这是用来将遍历到的文件名进行存储,等遍历完成之后就可以从中访问,所以是引用传参,直接修改外部的arry文件目录数组

文件压缩和解压缩
      bool Compress(const std::string &packname) //压缩文件
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"compress read file false"<<std::endl;
          return false;
        }
        std::string packbody = bundle::pack(bundle::LZIP,body);
        if(FileTool(packname).Write(packbody) == false)
        {
          std::cout<<"compress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name); //删除被压缩了的源文件
        return true;
      }
      bool Uncompress(const std::string &filename) //解压缩
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"Uncompress read file false"<<std::endl;
          return false;
        }
        std::string unpack_body = bundle::unpack(body);
        if(!FileTool(filename).Write(unpack_body))
        {
          std::cout<<"Uncompress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
      bool Remove()
      {
        if(!Exist())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
  };

文件压缩和解压缩,这里使用的是bondle库,实现该功能的主要思想是:
压缩:
1.读取文件内容
2.将其中内容用bonldle中所支持的压缩方式进行压缩,该项目中选择的是LZIP,这个压缩的压缩率是很高的 (pack(压缩格式,string &body))
3.将压缩内容写入压缩文件
4.删除压缩前的文件,只保留一个压缩包,需要时再解压缩,节省磁盘空间

解压缩:
1.读取压缩包文件内容
2.将其中内容解压缩(unpack(string &body)
3.将解压缩内容写入文件
4.删除源压缩包

文件持久化存储
class Json_Tool
{
  public:
    static bool Serialize(const Json::Value &val, std::string *body)   //序列化
    {
      Json::StreamWriterBuilder swb;
      Json::StreamWriter *sw = swb.newStreamWriter();
      std::stringstream ss;
      int ret = sw->write(val,&ss);
      if(ret != 0)
      {
        std::cout<<"serialize false"<<std::endl;
        delete sw;
        return false;
      }
      *body = ss.str();
      delete sw;
      return true;
    }

    static bool UnSerialize(const std::string& body,Json::Value *val)
    {
      Json::CharReaderBuilder crb;
      Json::CharReader *cr = crb.newCharReader();
      std::string str;
      bool ret = cr->parse(body.c_str(),body.c_str() + body.size(),val,&str);
      if(!ret)
      {
        std::cout<<"unserialize false"<<std::endl;
        delete cr;
        return false;
      }
      delete cr;
      return true;
    }
  };

Json的使用方法再上面已经说过了,所以这里不再多说。

使用Json目的,是为了实现文件的持久化存储,并且可以在进程间传递。可以让数据的访问更加方便。

使用stringstream字节流对象存储被序列化的数据,然后通过stringstream字节流对象对string body赋值,但是这里需要将其内部序列化数据转化成string格式才能赋值,所以出现了ss.str()。

部分总结

这个类,大部分工作其实都已经通过库函数完成了,我们只是进行库函数的调用,所以可以展现的东西并不多,需要思考的就是怎么调用,他给你接口了又该怎么实现自己想要的功能。

Data_Management——数据管理类

#ifndef __MY_DATA__
#define __MY_DATA__

#include "file_tools.hpp"
#include <unordered_map>
#include <vector>

namespace cloud
{
  struct _FileInfo
  {
    std::string filename;   //文件名称
    std::string url_path;   //文件下载路径
    std::string real_path;  //文件真实存储路径
    size_t file_size;       //文件大小
    time_t back_time;       //最后备份时间
    bool pack_flag;         //压缩标志
    std::string pack_path;  //压缩包路径
  };

  class Data_Management
  {
    private:
      std::string _file_back_path = "./back_dir/";   //备份文件夹
      std::string _file_pack_path = "./pack_dir/";   //压缩包文件夹
      std::string _download_prefix = "/download/";  //下载链接前缀
      std::string _pack_suffix = ".zip";   //压缩包后缀名
      std::string _back_info_file = "./back_info.dat";   //存储备份信息的文件
      std::unordered_map<std::string , _FileInfo> _back_info;  //备份信息
    public:
      Data_Management()
      {
        FileTool(_file_back_path).CreateDirectory();
        FileTool(_file_pack_path).CreateDirectory();
        if(FileTool(_back_info_file).Exist())
        {
          InitLoad();
        }
      }
      bool InitLoad()
      {
        std::string body;
        bool ret = FileTool(_back_info_file).Read(&body);
        if(ret == false)
        {
          std::cout<<"InitLoad read file filed"<<std::endl;
          return false;
        }
        Json::Value infos;
        ret = Json_Tool::UnSerialize(body,&infos);
        if(ret == false)
        {
          std::cout<<"InitLoad UnSerialize filed"<<std::endl;
          return false;
        }
        for(int i = 0; i < infos.size();i++)
        {
          _FileInfo info;
          info.filename = infos[i]["filename"].asString();
          info.url_path = infos[i]["url_path"].asString();
          info.real_path = infos[i]["real_path"].asString();
          info.file_size = infos[i]["file_size"].asInt64();
          info.back_time = infos[i]["back_time"].asInt64();
          info.pack_flag = infos[i]["pack_flag"].asBool();
          info.pack_path = infos[i]["pack_path"].asString();

          _back_info[info.url_path] = info;
        }
        return true;
      }
      bool Storage()
      {
        Json::Value infos;
        std::vector<_FileInfo> arry;
        Select_All(arry);
        for(auto &e : arry)
        {
          Json::Value info;
          info["filename"] = e.filename;
          info["url_path"] = e.url_path;
          info["real_path"] = e.real_path;
          info["file_size"] = (Json::UInt64)e.file_size;
          info["back_time"] = (Json::UInt64)e.back_time;
          info["pack_flag"] = e.pack_flag;
          info["pack_path"] = e.pack_path;

          infos.append(info);
        }
        std::string body;
        Json_Tool::Serialize(infos,&body);
        FileTool(_back_info_file).Write(body);

        return true;
      }
      bool Insert(const std::string &filename)
      {
        if(FileTool(filename).Exist() == false)
        {
          std::cout<<"Inset file is not exist"<<std::endl;
          return false;
        }
        _FileInfo info;
        info.filename = FileTool(filename).Name();
        info.url_path = _download_prefix + info.filename;
        info.real_path = filename;
        info.file_size = FileTool(filename).Size();
        info.back_time = FileTool(filename).LastChange_Tm();
        info.pack_flag = false;
        info.pack_path = _file_pack_path + info.filename + _pack_suffix;
        
        _back_info[info.url_path] = info;
        Storage();
        return true;
      }
      bool Update_packflag(const std::string &filename,bool status)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }

        it->second.pack_flag = status;
        Storage();
        return true;
      }
      bool Select_All(std::vector<_FileInfo> &v)
      {
        for(auto e = _back_info.begin();e != _back_info.end();e++)
          v.push_back(e->second);
        return true;
      }
      bool Select_One(const std::string &filename,_FileInfo *info)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        *info = it->second;
        return true;
      }
      bool Delete(const std::string &filename)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
          std::cout<<"not have this file"<<std::endl;

        _back_info.erase(url_path); 
        Storage();
        return true;
      }
  };
}

数据管理类,是我们整个项目中,最重要的类,任何操作,都不能避免数据的访问,那么就需要有人管理这些数据,对数据进行组织,这样程序工作才能井然有序。

_FileInfo——文件信息结构体
struct _FileInfo
  {
    std::string filename;   //文件名称
    std::string url_path;   //文件下载路径
    std::string real_path;  //文件真实存储路径
    size_t file_size;       //文件大小
    time_t back_time;       //最后备份时间
    bool pack_flag;         //压缩标志
    std::string pack_path;  //压缩包路径
  };

要管理,那就需要赋予文件特定的标识,没有标识,又怎么能分辨出来那个文件是哪个文件呢,文件名字是必须的。既然是备份,那用户就需要下载,那么我们就要给一个用户下载时需要的路径,来找到文件,但是这里我们其实就再后面实现下载功能时,就可以通过该路径确定是哪个文件需要下载,文件真实存储路径,就是我们在服务器主机硬盘中备份文件的位置,那再进行热点管理的时候,经常访问的文件不会压缩,不经常访问的文件需要被压缩,那是不是就得有一个东西,标识它是否被压缩。那么经常访问和不经常访问,又要根据什么来判断呢,这里就有一个最后的备份时间,也就是用户最后修改文件的时间,如果它大于某一个限定,就将其压缩。

 class Data_Management
  {
    private:
      std::string _file_back_path = "./back_dir/";   //备份文件夹
      std::string _file_pack_path = "./pack_dir/";   //压缩包文件夹
      std::string _download_prefix = "/download/";  //下载链接前缀
      std::string _pack_suffix = ".zip";   //压缩包后缀名
      std::string _back_info_file = "./back_info.dat";   //存储备份信息的文件
      std::unordered_map<std::string , _FileInfo> _back_info;  //备份信息
    public:
      Data_Management()
      {
        FileTool(_file_back_path).CreateDirectory();
        FileTool(_file_pack_path).CreateDirectory();
        if(FileTool(_back_info_file).Exist())
        {
          InitLoad();
        }
      }
      bool InitLoad()
      {
        std::string body;
        bool ret = FileTool(_back_info_file).Read(&body);
        if(ret == false)
        {
          std::cout<<"InitLoad read file filed"<<std::endl;
          return false;
        }
        Json::Value infos;
        ret = Json_Tool::UnSerialize(body,&infos);
        if(ret == false)
        {
          std::cout<<"InitLoad UnSerialize filed"<<std::endl;
          return false;
        }
        for(int i = 0; i < infos.size();i++)
        {
          _FileInfo info;
          info.filename = infos[i]["filename"].asString();
          info.url_path = infos[i]["url_path"].asString();
          info.real_path = infos[i]["real_path"].asString();
          info.file_size = infos[i]["file_size"].asInt64();
          info.back_time = infos[i]["back_time"].asInt64();
          info.pack_flag = infos[i]["pack_flag"].asBool();
          info.pack_path = infos[i]["pack_path"].asString();

          _back_info[info.url_path] = info;
        }
        return true;
      }
      bool Storage()
      {
        Json::Value infos;
        std::vector<_FileInfo> arry;
        Select_All(arry);
        for(auto &e : arry)
        {
          Json::Value info;
          info["filename"] = e.filename;
          info["url_path"] = e.url_path;
          info["real_path"] = e.real_path;
          info["file_size"] = (Json::UInt64)e.file_size;
          info["back_time"] = (Json::UInt64)e.back_time;
          info["pack_flag"] = e.pack_flag;
          info["pack_path"] = e.pack_path;

          infos.append(info);
        }
        std::string body;
        Json_Tool::Serialize(infos,&body);
        FileTool(_back_info_file).Write(body);

        return true;
      }
      bool Insert(const std::string &filename)
      {
        if(FileTool(filename).Exist() == false)
        {
          std::cout<<"Inset file is not exist"<<std::endl;
          return false;
        }
        _FileInfo info;
        info.filename = FileTool(filename).Name();
        info.url_path = _download_prefix + info.filename;
        info.real_path = filename;
        info.file_size = FileTool(filename).Size();
        info.back_time = FileTool(filename).LastChange_Tm();
        info.pack_flag = false;
        info.pack_path = _file_pack_path + info.filename + _pack_suffix;
        
        _back_info[info.url_path] = info;
        Storage();
        return true;
      }
      bool Update_packflag(const std::string &filename,bool status)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }

        it->second.pack_flag = status;
        Storage();
        return true;
      }
      bool Select_All(std::vector<_FileInfo> &v)
      {
        for(auto e = _back_info.begin();e != _back_info.end();e++)
          v.push_back(e->second);
        return true;
      }
      bool Select_One(const std::string &filename,_FileInfo *info)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        *info = it->second;
        return true;
      }
      bool Delete(const std::string &filename)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
          std::cout<<"not have this file"<<std::endl;

        _back_info.erase(url_path); 
        Storage();
        return true;
      }
  };
}
Insert——数据的插入
std::unordered_map<std::string , _FileInfo> _back_info;  //备份信息
bool Insert(const std::string &filename)
{
  if(FileTool(filename).Exist() == false)
  {
    std::cout<<"Inset file is not exist"<<std::endl;
    return false;
  }
  _FileInfo info;
  info.filename = FileTool(filename).Name();
  info.url_path = _download_prefix + info.filename;
  info.real_path = filename;
  info.file_size = FileTool(filename).Size();
  info.back_time = FileTool(filename).LastChange_Tm();
  info.pack_flag = false;
  info.pack_path = _file_pack_path + info.filename + _pack_suffix;
  
  _back_info[info.url_path] = info;
  Storage();
  return true;
}

这里的插入数据,是将保存的数据插入到内存当中,内存中使用的存储结构我们使用的是查找效率较高的unordered_map结构,使用Key=Val键值对,Key为我们的定义的url_path,也就是所谓的下载地址,Val是文件信息,这样可以通过Key快速找到对应的文件,可以方便后续功能的开发。对于文件信息的序列化,在Json的认识当中已经说过啦,可以回去看看。但是这里插入的文件信息,都是已经备份了的文件,不存在的文件,是没办法插入的。其实就是实现了一个内存中的文件管理。

Update_packflag——更新文件信息
bool Update_packflag(const std::string &filename,bool status)
{
  std::string url_path = _download_prefix + FileTool(filename).Name();
  auto it = _back_info.find(url_path);
  if(it == _back_info.end())
  {
    std::cout<<"not have this file"<<std::endl;
    return false;
  }

  it->second.pack_flag = status;
  Storage();
  return true;
}

文件压缩的时候默认是没有压缩,因为是刚上传上来,还没有超出系统设定的热点和非热点时间,所以默认是false没有压缩,但是压缩之后,所以需要将其设定为true,其实最后也就更新了一个压缩标志而已

Select_All——查找所有数据
bool Select_All(std::vector<_FileInfo> &v)
{
  for(auto e = _back_info.begin();e != _back_info.end();e++)
    v.push_back(e->second);
  return true;
}

外界传入一个数组,里面每个元素都是_FileInfo类型,用来保存遍历到的文件信息。

Select_One——查找单个数据
bool Select_One(const std::string &filename,_FileInfo *info)
{
  std::string url_path = _download_prefix + FileTool(filename).Name();
  auto it = _back_info.find(url_path);
  if(it == _back_info.end())
  {
    std::cout<<"not have this file"<<std::endl;
    return false;
  }
  *info = it->second;
  return true;
}

因为我们插入数时插入的是url_path,它的组成方式是_download_prefix + info.filename,也就是一个/download/前缀加文件名,所以我们查找时,需要组合出来该Key值,进行查找。

三个接口搭配使用——初始化管理
std::string _back_info_file = "./back_info.dat";   //存储备份信息的文件
 
Data_Management()
      {
        FileTool(_file_back_path).CreateDirectory();
        FileTool(_file_pack_path).CreateDirectory();
        if(FileTool(_back_info_file).Exist())
        {
          InitLoad();
        }
      }
      bool InitLoad()
      {
        std::string body;
        bool ret = FileTool(_back_info_file).Read(&body);
        if(ret == false)
        {
          std::cout<<"InitLoad read file filed"<<std::endl;
          return false;
        }
        Json::Value infos;
        ret = Json_Tool::UnSerialize(body,&infos);
        if(ret == false)
        {
          std::cout<<"InitLoad UnSerialize filed"<<std::endl;
          return false;
        }
        for(int i = 0; i < infos.size();i++)
        {
          _FileInfo info;
          info.filename = infos[i]["filename"].asString();
          info.url_path = infos[i]["url_path"].asString();
          info.real_path = infos[i]["real_path"].asString();
          info.file_size = infos[i]["file_size"].asInt64();
          info.back_time = infos[i]["back_time"].asInt64();
          info.pack_flag = infos[i]["pack_flag"].asBool();
          info.pack_path = infos[i]["pack_path"].asString();

          _back_info[info.url_path] = info;
        }
        return true;
      }
      bool Storage()
      {
        Json::Value infos;
        std::vector<_FileInfo> arry;
        Select_All(arry);  //读取全部备份文件,更新历史备份信息
        for(auto &e : arry)
        {
          Json::Value info;
          info["filename"] = e.filename;
          info["url_path"] = e.url_path;
          info["real_path"] = e.real_path;
          info["file_size"] = (Json::UInt64)e.file_size;
          info["back_time"] = (Json::UInt64)e.back_time;
          info["pack_flag"] = e.pack_flag;
          info["pack_path"] = e.pack_path;

          infos.append(info);
        }
        std::string body;
        Json_Tool::Serialize(infos,&body);
        FileTool(_back_info_file).Write(body);

        return true;
      }

在每次加再程序的同时,需要将历史备份信息表也加载出来,不然服务器怎么知道它备份过什么文件呢,所以就有了InitLoad接口,每次在服务器运行时,如果有备份信息,就需要将其读取到内存中,判断新上传的文件是新文件还是已经备份过得修改的文件。起到了一个初始化的作用。

Stroage接口,是用来更新历史备份信息,如果历史备份信息发生变化,那么就需要将其更新,历史备份信息文件就需要重新写入,所以这里,只要会导致数据发生变化的操作,都需要调用该函数,来保证备份信息的实时性。

因为一切都需要在程序运行时同时加再,所以需要将初始化接口在构造函数中调用,只要程序的运行起来,数据管理模块初始化的对象就会将信息读入,等待数据的到来。

部分总结

这个类主要完成了对数据的管理和组织,完成后台数据的存储和更新。后面有一个删除接口没有说,其实这个完全不需要,服务器不可能会自主删除用户的数据,所以只是一个扩展项。

hot——热点管理类

class HotManager
{
  private:
    time_t _hot_time = 30;
    std::string _back_dir = "./back_dir";
  public:
    HotManager()
    {}
    bool _Is_Hot(const std::string filename)
    {
      time_t _last_visit_time = FileTool(filename).LastVisit_Tm();  //文件最后访问时间
      time_t _now_time = time(NULL);  //本机实际时间
      if((_now_time - _last_visit_time) > _hot_time)
        return false;
      return true;
    }
    bool RunModule()
    {
      while(1)
      {
      //1.遍历目录
        std::vector<std::string> arry;
        FileTool(_back_dir).ScanDirectory(arry);
      //2.遍历信息
        for(auto &filename : arry)
        {
          //3.获取文件时间属性,进行热点判断
          if(!_Is_Hot(filename))
          {
            _FileInfo info;
            if(!_data->Select_One(filename,&info))
            {
              //4.1 文件备份信息不存在则直接删除文件
              FileTool(filename).Remove();
              continue;
              //4.1 文件备份信息不存在则创建
              //FileTool(filename).
            }
          //5.非热点文件则压缩
            FileTool(filename).Compress(info.pack_path);
            std::cout<<filename<<"已经被压缩"<<std::endl;
          //6.压缩后修改文件备份信息
            _data->Update_packflag(filename,true);
          }
        }
        usleep(10000);
      }
      return true;
    }
};

我们的hot管理思想如下:
1.遍历目录,获取所有文件信息
2.遍历文件信息
3.获取文件最后备份时间,判断是否需要压缩
——(1)找到了需要压缩的文件
——1.1)如果只在硬盘中存在,并没有存在于历史备份信息中,说明文件异常,直接删除
——(2)对非热点文件进行压缩
——(3)修改文件备份标志
4.重复上述操作

Server——服务器类

extern cloud::Data_Management *_data;
class Server
{
  private:
    int _srv_port = 9090;   //服务器绑定的端口号
    std::string _url_prefix = "/download/";
    std::string _back_dir = "./back_dir/";  //文件上传之后的备份路径
    httplib::Server _srv;
  private:
    static void Upload(const httplib::Request &req,httplib::Response &rsp)
    {
      std::string _back_dir = "./back_dir/";
      if(req.has_file("file") == false)   //判断有没有对应标识的文件上传区域数据
      {
        std::cout<<"upload file data format error"<<std::endl;
        rsp.status = 400;    
        return;
      }
      //获取解析后的区域数据
      httplib::MultipartFormData data = req.get_file_value("file"); 
      //组织文件的实际存储路径名
      std::string real_path = _back_dir + data.filename;
      //备份文件
      if(FileTool(real_path).Write(data.content) == false)
      {
        std::cout<<"back file filed"<<std::endl;
        return;
      }
      //新增备份信息
      if(_data->Insert(real_path) == false)
      {
        std::cout<<"Insert back info filed"<<std::endl;
        rsp.status = 500;
        return;
      }
      rsp.status = 200;
      return;
    }
    static void List(const httplib::Request &req,httplib::Response &rsp)
    {
      std::vector<_FileInfo> arry;
      if(_data->Select_All(arry) == false)
      {
        std::cout<<"List Select_All filed";
        rsp.status = 400;
        return;
      }

      std::stringstream ss;
      ss<< "<html>";
      ss<<  "<head>";
      ss<<      "<meta charset='utf-8' />";
      ss<<      "<meta http-equiv='Content-Type'>"; 
      ss<<      "<meta content='text/html'>";
      ss<<      "<title>Download</title>";
      ss<<  "</head>";
      ss<<  "<body>";
      ss<<      "<h1>Download</h1>";
      ss<<      "<table>";
      for(auto &e : arry)
      {
                ss<<  "<tr>";
                ss<<    "<td><a href='"<<e.url_path<<"'download=>"<<e.filename<<" </a></td>";
                ss<<    "<td align='right'>"<<std::asctime(std::localtime(&e.back_time))<<"</td>";
                ss<<    "<td align='rgiht'>"<<e.file_size / 1024<<"KB </td>";
                ss<<  "</tr>";
      }
      ss<<      "</table>";
      ss<<  "</body>";
      ss<< "</html>";

      rsp.set_content(ss.str(),"text/html");
      rsp.status = 200;
      return;
    }
    static std::string StrEtag(const _FileInfo info)
    {
      time_t last_time = info.back_time;
      size_t fsize = info.file_size;
      std::stringstream ss;
      ss<<fsize<<"-"<<last_time;
      return ss.str();
    }
    static void Download(const httplib::Request &req,httplib::Response &rsp)
    {
      _FileInfo info;
      if(_data->Select_One(req.path,&info) == false)
      {
        std::cout<<"not have this file"<<std::endl;
        rsp.status = 404;
        return;
      }  
      if(info.pack_flag == true)
      {
        FileTool(info.pack_path).Uncompress(info.real_path);
        _data->Update_packflag(info.filename,false);
        std::cout<<info.filename<<"已经解压缩"<<std::endl;
      }
      if(req.has_header("If-Range"))
      {
        std::string old_etag = req.get_header_value("If-Range");
        std::string now_etag = StrEtag(info);
        if(old_etag == now_etag)
        {
          //Range:bytes = 200-1000
          //size_t start = req.Ranges[0].first;
          //size_t end = req.Ranges[0].second; 如果没有end数字,则表示到文件末尾,httplib将second设置为-1
          FileTool(info.real_path).Read(&rsp.body);
          //rsp.set_header("Content-Type","appliaction/octet stream");
          rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
          //rsp.set_header("Content-Type","byte start-end/fsize"); //httplib会自动设置
          rsp.status = 206;
          return;
        }
      }
      FileTool(info.real_path).Read(&rsp.body);
      //rsp.set_header("Content-Type","application/octet-stream"); //设置正文类型为二进制流
      rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
      rsp.set_header("ETag",StrEtag(info));
      rsp.status = 200;
      return;
    }
  public:
    Server()
    {
      FileTool(_back_dir).CreateDirectory();
    }
    bool RunModule()
    {
      _srv.Post("/upload",Upload);  //Post(请求的资源路径,对应的业务处理函数)
      _srv.Get("/list",List);
      std::string _download_path = _url_prefix + "(.*)";
      _srv.Get(_download_path,Download);
      //运行服务器
      _srv.listen("0.0.0.0",_srv_port);
      return true;
    }
};
Upload——上传业务处理
static void Upload(const httplib::Request &req,httplib::Response &rsp)
{
  std::string _back_dir = "./back_dir/";
  if(req.has_file("file") == false)   //判断有没有对应标识的文件上传区域数据
  {
    std::cout<<"upload file data format error"<<std::endl;
    rsp.status = 400;    
    return;
  }
  //获取解析后的区域数据
  httplib::MultipartFormData data = req.get_file_value("file"); 
  //组织文件的实际存储路径名
  std::string real_path = _back_dir + data.filename;
  //备份文件
  if(FileTool(real_path).Write(data.content) == false)
  {
    std::cout<<"back file filed"<<std::endl;
    return;
  }
  //新增备份信息
  if(_data->Insert(real_path) == false)
  {
    std::cout<<"Insert back info filed"<<std::endl;
    rsp.status = 500;
    return;
  }
  rsp.status = 200;
  return;
}

//拿过来方便看
struct MultipartFormData  //这里是html中的东西
 { 
	 std::string name;   //区域名称标识
	 std::string content; //文件正文
	 std::string filename;  //文件名
	 std::string content_type;  //文件传输格式,一般都是二进制传输
 }; 

先解释为什么要设置为静态函数,因为库函数在封装的时候,只有两个参数,但是要知道,类内函数,都不是你看到的样子,除了友元和静态函数,其他都含有一个隐藏的this指针,所以如果不声明称静态函数,编译的时候就会报错的。

实现思想:
1.检查name标识区域是否有file标识的文件上传区域
2.解析区域内内容,将其存入MultipartFormData中对应的数据中
3.组织文件实际存储路径
4.备份文件(在存储备份文件的文件夹中创建对应文件,写入文件内容,完成备份)

List——用户使用客户端浏览历史备份记录
 static void List(const httplib::Request &req,httplib::Response &rsp)
 {
   std::vector<_FileInfo> arry;
   if(_data->Select_All(arry) == false)
   {
     std::cout<<"List Select_All filed";
     rsp.status = 400;
     return;
   }

   std::stringstream ss;
   ss<< "<html>";
   ss<<  "<head>";
   ss<<      "<meta charset='utf-8' />";
   ss<<      "<meta http-equiv='Content-Type'>"; 
   ss<<      "<meta content='text/html'>";
   ss<<      "<title>Download</title>";
   ss<<  "</head>";
   ss<<  "<body>";
   ss<<      "<h1>Download</h1>";
   ss<<      "<table>";
   for(auto &e : arry)
   {
             ss<<  "<tr>";
             ss<<    "<td><a href='"<<e.url_path<<"'download=>"<<e.filename<<" </a></td>";
             ss<<    "<td align='right'>"<<std::asctime(std::localtime(&e.back_time))<<"</td>";
             ss<<    "<td align='rgiht'>"<<e.file_size / 1024<<"KB </td>";
             ss<<  "</tr>";
   }
   ss<<      "</table>";
   ss<<  "</body>";
   ss<< "</html>";

   rsp.set_content(ss.str(),"text/html");
   rsp.status = 200;
   return;
 }

实现思想:

1.遍历历史备份信息
2.通过html语言完成一个页面,在<<a href= " " -->>标签中,加入下载连接,就可以实现下载文件的功能,传递过来的Get请求会被Download接口匹配,然后调用对应的Download业务处理函数
3.设置正文是text/html,这一个超文本格式,也就是返回了一个浏览界面
4.设置服务器状态码

Download——下载业务处理
static void Download(const httplib::Request &req,httplib::Response &rsp)
{
  _FileInfo info;
  if(_data->Select_One(req.path,&info) == false)
  {
    std::cout<<"not have this file"<<std::endl;
    rsp.status = 404;
    return;
  }  
  if(info.pack_flag == true)
  {
    FileTool(info.pack_path).Uncompress(info.real_path);
    _data->Update_packflag(info.filename,false);
    std::cout<<info.filename<<"已经解压缩"<<std::endl;
  }
  if(req.has_header("If-Range"))
  {
    std::string old_etag = req.get_header_value("If-Range");
    std::string now_etag = StrEtag(info);
    if(old_etag == now_etag)
    {
      //Range:bytes = 200-1000
      //size_t start = req.Ranges[0].first;
      //size_t end = req.Ranges[0].second; 如果没有end数字,则表示到文件末尾,httplib将second设置为-1
      FileTool(info.real_path).Read(&rsp.body);
      //rsp.set_header("Content-Type","appliaction/octet stream");
      rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
      //rsp.set_header("Content-Type","byte start-end/fsize"); //httplib会自动设置
      rsp.status = 206;
      return;
    }
  }
  FileTool(info.real_path).Read(&rsp.body);
  //rsp.set_header("Content-Type","application/octet-stream"); //设置正文类型为二进制流
  rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
  rsp.set_header("ETag",StrEtag(info));
  rsp.status = 200;
  return;
}

实现思想:

1.通过传递过来的下来路径,在哈希表中寻找对应的文件,将文件信息存储到info中
2.判断文件是否被压缩,压缩则解压缩,修改压缩标志
3.判断是否有头不字段是“If-Range”,如果有说明需要断点续传
4.判断下载文件ETag是否跟备份文件ETag值相同,相同则是相同文件,可以执行断点续传,如果不同,则是备份文件被修改,需要重新下载。
5.将文件内容读取到响应正文中
6.设置头部字段(“Accept-Ranges”,“Bytes”),告诉客户端支持断点续传
7.设置头部字段(set_header(“ETag”,StrEtag(info)))设置ETag值,由于断点续传中的判断
8.设置响应状态码,如果是断点续传则返回206,如果是普通下载则返回200.

主要需要说一下断点续传的实现,虽然大部分功能都由httplib实现了,但是主要思想还是需要理解,服务器在执行下载业务时,会在头部字段中设置ETag值和支持断点续传的头部字段,如果客户端因为种种原因中断了下载,那么下次服务器重启,或者客户端重新下载时,客户端发送的请求中就有If-Range头部字段,其后跟的就是文件ETag值,如果值相同,就是相同的文件,可以断点续传,如果不同,则代表文件在期间发生了修改,所以需要重新下载。并且请求头中还有一个Range:bytes,后面跟随的就是文件还需要下载的大小,举个例子,假如长度为1000的文件,第一次只下载了200,那么它后面就会跟着一个200-999或者200-,这时httplib就会在后面补一个 -1 ,-1 就代表到文件末尾,如果有 - 右边有数字,那么请求的文件长度就是999 - 200 + 1 也就是800这里时包括900的,如果时 - 1,那么就是到文件末尾也就是999,所以只需要999-200就可以了。那么影响正确的时候就会在响应头部添加Content-Range:bytes:200-1000/文件大小。正文长度就是剩下的长度。

Listen——服务器监听接口
int _srv_port = 9090;   //服务器绑定的端口号
_srv.listen("0.0.0.0",_srv_port);

服务器开始监听才可以接收客户端的请求,0.0.0.0IP地址表示监听当前主机下所有IP地址,然后绑定一个端口号即可

部分总结

服务器的实现主要是借助了httplib,因为时间的限制,所以没办法自己完成一个完整的服务器,但是需要理解其中的思想的

客户端开发

客户端建立思想

目标功能: 对客户端主机上指定文件夹中的文件进行检测,哪个文件需要备份,则将其上传到服务器备份起来

思想:

  1. 目录检索遍历指定文件夹,获取所有文件路径名
  2. 获取所有文件的属性,判断该文件是否需要备份
    2.1.新增文件——如果根据文件路径找不到对应的文件备份信息,则说明是新增文件,需要备份
    2.2.备份过但是被修改的文件——有历史备份信息,但是唯一标识与备份中的不同,说明是修改过的文件,需要备份
    2.3.如果文件持续被修改,那么每次检索都会造成一次备份
    实际处理——当前文件没有被任何进程占用,则进行备份操作
    简单处理——当前文件在指定修改时间间隔内都没有进行修改,则进行上传
  3. 如果一个文件需要备份,则创建一个http客户端,进行上传
  4. 上传文件后,添加历史备份信息

模块划分

数据管理模块——管理客户端的备份信息
目录检索模块——获取指定文件夹下的所有文件
网络通信模块——将所需要备份的文件通过http客户端进行上传

FileTools——文件工具类

#include <iostream>
#include <string>
#include <sstream>
#include <fstream>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
#include <cstdint>

namespace fs = std::experimental::filesystem;

namespace cloud
{
    class FileTool
    {
    private:
        std::string _name;
    public:
        FileTool(const std::string name) :_name(name)
        {}
        std::string Name()
        {
            return fs::path(_name).filename().string();
        }
        bool Exist()
        {
            return fs::exists(_name);
        }
        size_t Size()
        {
            if (!Exist())
                return 0;
            return fs::file_size(_name);
        }
        time_t LastChange_Tm()
        {
            if (!Exist())
                return 0;
            auto ftime = fs::last_write_time(_name);
            std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
            return cftime;
        }
        time_t LastVisit_Tm()
        {
            if (!Exist())
                return 0;
            struct stat st;
            stat(_name.c_str(), &st);
            return st.st_atime;

        }
        bool Write(const std::string& body)
        {
            if (!Exist())
                my_CreateDirectory();
            std::ofstream writer;
            writer.open(_name, std::ios::binary); 
            if (!writer.is_open())
            {
                std::cout << "writer open file false" << std::endl;
                writer.close();
                return false;
            }
            writer.write(body.c_str(), body.size());
            if (!writer.good())
            {
                std::cout << "writer write false" << std::endl;
                writer.close();
                return false;
            }
            writer.close();
            return true;
        }
        bool Read(std::string* body)  
        {

            if (!Exist())
                return 0;
            std::ifstream reader;
            reader.open(_name, std::ios::binary);
            if (!reader.is_open())
            {
                std::cout << "reader open file false" << std::endl;
                reader.close();
                return false;
            }
            size_t fsize = Size();
            body->resize(fsize);
            reader.read(&(*body)[0], fsize);  
            if (!reader.good())
            {
                std::cout << "reader read false" << std::endl;
                reader.close();
                return false;
            }
            reader.close();
            return true;
        }
        bool my_CreateDirectory()
        {
            if (Exist() == true)
                return 0;
            fs::create_directories(_name);
            return true;
        }
        bool ScanDirectory(std::vector<std::string>& arry) 
        {
            if (!Exist())
                return 0;
            for (auto& a : fs::directory_iterator(_name))
            {
                if (fs::is_directory(a) == true)
                    continue; 
                 
                std::string pathname = fs::path(a).relative_path().string(); 
                arry.push_back(pathname);
            }
            return true;
        }
        bool Remove()
        {
            if (!Exist())
            {
                std::cout << "not have this file" << std::endl;
                return false;
            }
            fs::remove_all(_name);
            return true;
        }
    };
}
   

其实服务器和客户端的文件管理工具类并没有区别,因为本来就是使用的是可以跨平台的库来实现的,所以直接留下需要的接口,实现了代码复用

Data_Management——数据管理模块


#include "file_tools.hpp"
#include <unordered_map>
#include <vector>

class Data_Management
{
  private:
      std::string _backup_dat = "./backup.dat";
      std::unordered_map<std::string, std::string> _back_info;
  public:
    Data_Management()
    {
        FileTool(_backup_dat).my_CreateDirectory();
        InitLoad();
    }
    static int Split(const std::string body,std::string key,std::vector<std::string> &arry)
    {
        int count = 0;
        int pr = 0, pos = 0;
        while (pos < body.size())
        {
            pos = body.find(key, pr);
            if (pos == std::string::npos) //找不到分隔符
                break;
            if (pos == pr)    //防止分隔符连在一起
            {
                pr += key.size();
                continue;
            }
            std::string val = body.substr(pr,pos);
            arry.push_back(val);
            pr = pos + key.size();   
            count++;
        }
        if (pos < body.size() - 1)
        {
            std::string val = body.substr(pr);
            arry.push_back(val);
            count++;
        }
        return count;
    }
    bool InitLoad()
    {
        std::string body;
        if (FileTool(_backup_dat).Read(&body) == false)
            return false;
        std::vector<std::string> arry;
        int count = Split(body, "\n", arry);
        for (int i = 0; i < arry.size(); i++)
        {
            int key = arry[i].find("=");
            if (key == std::string::npos)
                continue;
            _back_info[arry[i].substr(0, key)] = arry[i].substr(key + 1);
        }
        return true;
    }
    bool Storage()
    {
        std::stringstream body;
        for (auto it = _back_info.begin(); it != _back_info.end(); it++)
            body << it->first << "=" << it->second;
        FileTool(_backup_dat).Write(body.str());
        return true;
    }
    std::string FileETag(const std::string& filename)
    {
        size_t fsize = FileTool(filename).Size();
        time_t ftime = FileTool(filename).LastChange_Tm();
        std::stringstream ss;
        ss << fsize << "-" << ftime;
        return ss.str();
    }
    bool Insert(const std::string &filename)
    {
        std::string etag = FileETag(filename);
        _back_info[filename] = etag;
        Storage();
        return true;
    }
    bool Updata(const std::string &filename)
    {
        std::string ETag = FileETag(filename);
        _back_info[filename] = ETag;
        Storage();
        return true;
    }
    bool Select_All(std::vector<std::pair<std::string,std::string>> &infos)
    {
        for (auto it = _back_info.begin(); it != _back_info.end(); it++)
        {
            std::pair<std::string, std::string> info;
            info.first = it->first;
            info.second = it->second;
            infos.push_back(info);
        }
        return true;
    }
    bool Select_One(const std::string &filename,std::string &ETag)
    {
        auto it = _back_info.find(filename);
        if (it == _back_info.end())
            return false;
        ETag = it->second;
        return true;
    }
    bool Delete(const std::string &filename)
    {
        auto it = _back_info.find(filename);
        if (it == _back_info.end())
            return false;
        _back_info.erase(it);
        Storage();
        return true;
    }
};

建立一个信息映射表,根据文件路径名,查找对应文件属性。与服务器不同的是,Updata接口更新的是文件属性也就是自己定义的ETag,该字符串是由文件大小以及文件最后修改时间来确定,也是用来确定文件是否被修改的唯一标识。其他都跟服务器思想相同,只不过查找和保存的信息发生了变化

Client——客户端类

#include "data.hpp"
#include "httplib.h"
#include <Windows.h>

namespace cloud
{
class Client
{
private:
	std::string _srv_ip;
	int _srv_port;
	std::string _back_dir = "./backup.dir";
	Data_Management* data = nullptr;
protected:
	std::string FileETag(const std::string& filename)
	{
		size_t fsize = FileTool(filename).Size();
		time_t ftime = FileTool(filename).LastChange_Tm();
		std::stringstream ss;
		ss << fsize << "-" << ftime;
		return ss.str();
	}
	bool Is_NeedBackup(const std::string filename)
	{
		std::string old_ETag;
		if (data->Select_One(filename, old_ETag) == false)
			return true;
		std::string new_ETag = FileETag(filename);
		if (new_ETag != old_ETag)
		{
			time_t ftime = FileTool(filename).LastChange_Tm();
			time_t ntime = time(NULL);
			if (ntime - ftime > 5)
				return true;
		}
		return false;
	}
	bool Upload(const std::string filename)
	{   //添加name标识区域信息
		httplib::Client client(_srv_ip, _srv_port);
		httplib::MultipartFormDataItems items;
		httplib::MultipartFormData item;
		item.name = "file";
		item.filename = FileTool(filename).Name();
		FileTool(filename).Read(&item.content);
		item.content_type = "accplication/octet-stream";
		items.push_back(item);

		auto res = client.Post("/upload", items);
		if (res && res->status != 200)
			return false;
		return true;
	}
public:
	Client(const std::string srv_ip, int srv_port)
		:_srv_ip(srv_ip),_srv_port(srv_port)
	{}
	void RunMoudle()
	{
		//1.初始化,初始化数据管理对象,创建管理目录
		FileTool(_back_dir).my_CreateDirectory();
		data = new Data_Management();
		while (1)
		{
			//2.遍历目录,获取目录下文件
			std::vector<std::string> arry;
			FileTool(_back_dir).ScanDirectory(arry);
			//3.根据历史备份信息,判断是否需要备份
			for (auto& e : arry)
			{
				if (Is_NeedBackup(e) == false)
					continue;
				//4.需要备份则进行上传
				std::cout << e << "need backup!" << std::endl;
				if (Upload(e))
					std::cout << e << "upload success!" << std::endl;
				//5.添加备份信息
				data->Insert(e);
			}
			Sleep(10);
		}
	}
};

实现思想:
1.初始化,将历史备份信息读入内存中的映射表中
2.遍历目录,将目录中的文件路径进行存储
3.根据文件路径,寻找历史备份信息中的ETag标识,判断是否需要备份
4.需要备份的文件则进行上传,否则继续遍历
5.上传备份后,添加备份信息

Upload——上传函数
bool Upload(const std::string filename)
	{   //添加name标识区域信息
		httplib::Client client(_srv_ip, _srv_port);
		httplib::MultipartFormDataItems items;
		httplib::MultipartFormData item;
		item.name = "file";
		item.filename = FileTool(filename).Name();
		FileTool(filename).Read(&item.content);
		item.content_type = "accplication/octet-stream";
		items.push_back(item);

		auto res = client.Post("/upload", items);
		if (res && res->status != 200)
			return false;
		return true;
	}

实现思想:
1.创建http客户端
2.填充请求中name标识区域的数据信息
3.使用POST请求上传文件

Is_NeedBackup——是否需要备份判断函数
bool Is_NeedBackup(const std::string filename)
	{
		std::string old_ETag;
		if (data->Select_One(filename, old_ETag) == false)
			return true;
		std::string new_ETag = FileETag(filename);
		if (new_ETag != old_ETag)
		{
			time_t ftime = FileTool(filename).LastChange_Tm();
			time_t ntime = time(NULL);
			if (ntime - ftime > 5)
				return true;
		}
		return false;
	}

实现思想:
1.查找是否由该文件的历史备份信息,如果有则往下走,没有则是新文件,需要备份
2.有历史备份信息,计算当先ETag值,与文件历史备份信息中的ETag进行比较,如果不同,则说明文件被修改,需要备份

十三、项目总结

项目名称: 云备份——CloudBackup

项目简介: 本项目实现了云备份服务器与客户端,其中客户端部署在用户机上,实现对指定文件夹中文件进行自动备份;服务器端提供客户端的文件备份存储,并且在服务器端进行压缩操作,节省磁盘空间,并且支持客户端通过浏览器获取自己的文件信息目录,并进行下载,下载支持断点续传功能

开发环境: centos7/g++,gdb,makefile,vim && Windows10/vs2020

技术特点: http服务器,文件压缩,热点管理,断点续传,json序列化

项目实现:
服务器:
1.网络通信模块——搭建网络通信服务器,实现与客户端的通信

2.业务处理模块——对不同的请求提供不同的处理方法

3.数据管理模块——负责服务器上备份文件的信息管理

4.热点管理模块——负责文件的热点判断,对非热点文件进行压缩

客户端:
1.网络通信模块——搭建网络通信客户端,实现与服务器的通信

2.文件检索模块——遍历获取指定文件夹的所有文件信息和路径

3.数据管理模块——负责客户端文件信息的管理,通过这些信息判断文件是否需要备份

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值