【云备份项目】

一、项目框架定义

1.项目需求

实现本地文件通过客户端上传到网络服务器主机中,客户端也可以实现通过请求服务器查看上传的文件,可以下载云备份的文件,如果下载过程中客户端与服务器终端,还有断点续传功能,就是可以接着中断前的进度下载.

2. 服务端框架搭建思想

按照功能划分:服务端要支持客户端文件上传,备份文件列表查看,文件下载,断点续传,热点文件识别和管理功能,当文件长期不被访问时,对备份文件压缩存储.
按照模块划分:有数据管理模块,负责备份数据的信息管理,网络通信模块,负责与客户端建立通信,业务处理模块,负责文件长传,列表查看,文件下载功能,热点管理模块,负责对

3.客户端框架搭建思想

按照功能划分:客户端要支持指定目录/文件夹的文件检测,如果目录中有文件就将文件获取上传,判断文件是否需要备份,最新一次的上传而且是修改过的文件,并且经过一段时间间隔仍然没有修改的文件就备份.将需要备份的文件上传到服务器.
按照模块儿划分,有数据管理模块,管理备份过的文件信息,文件检测模块,监控指定目录是否有文件,判断文件是否需要备份.文件上传模块.

二、环境搭建

1.gcc编译器升级

sudo yum install -y centos-release-scl-rh centos-release-scl命令获取源信息
sudo yum install -y devtool-7-gcc devtool-7-gcc-c++命令安装7.3版本的gcc和g++
source /opt/rh/devtoolset-7/enable 临时环境配置
echo “source /opt/rh/devtoolset-7/enable” >> ~/.bashrc 将临时环境配置信息追加重定向到.bashrc文件完成永久配置.
使用g++ -v gcc -v验证安装成功

2.安装第三方库

sudo yum install epel-release
sudo yum install jsoncpp-devel安装jsonc库
ls /usr/include/jsoncpp/json/json.h验证是否安装成功
sudo yum install -y git 安装git工具
git clone https://github.com/r-lyeh-archived/bundle.git 从github获取bundle压缩包
git clone https://github.com/yhirose/cpp-httplib.git 从github获取httplib压缩包
bundle库是嵌入式库,就是不用包含头文件,直接将源码拿过来使用的

三、认识第三方库

1.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;
}

Json库的序列化和反序列化类有新老两个版本,推荐是用最新版charReader 反序列化类,和StreamWriter序列化类.,这两个类的实例化对象需要用CharReaderBuilder类和StreamWriterBuilder类的成员函数创建.,其中writer函数需要用到ostringstream类进行序列化操作.

序列化和反序列化都要用到Json的Value类,被序列化和反序列化的数据首先要存储在Value对象中格式化保存,Value类中重载了[]参数const char* 返回值Value类型,还重载了[]参数int index,返回值Value.以及类型转换函数,asString(),asInt (),asFloat()等分别会将Value内的成员转换为字符串型数据,整型数据,浮点型数据.

序列化

#include<iostream>
#include<sstream>
#include<string>
#include<memory>
#include<jsoncpp/json/json.h>

int main()
{
    const char*name="张三";
    int age = 15;
    float score[]={89,99,55.7};
    
    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::ostringstream os;
    sw->write(root,&os);

    std::string str=os.str();
    std::cout<<str<<std::endl;
    
    return 0;
}

反序列化

#include<iostream>
#include<sstream>
#include<memory>
#include<jsoncpp/json/json.h>
#include<string>

int main()
{
    std::string str=R"({"姓名":"张三","年龄":15,"成绩":[89.1,88,133]})";

    Json::Value root;
    Json::CharReaderBuilder crb;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());

    std::string err;
    cr->parse(str.c_str(),str.c_str()+str.size(),&root,&err);

    std::cout<<root["姓名"].asString()<<std::endl;
    std::cout<<root["年龄"].asInt()<<std::endl;
    int size=root["成绩"].size();
    for(int i=0;i<size;i++)
    {
        std::cout<<root["成绩"][i].asFloat()<<std::endl;
    }
}

2.bundle库使用

bundle库是嵌入式库,可以直接将源码拿过来使用,使用时可以将源文件拷贝过来复用.
压缩
在读取文件的时候,因为要一次读取整个文件,所以要通过获取文件指针偏移量的方式来获取文件大小,以便设置接收文件内容的缓冲区的大小.
文件的binary打开模式,会自动创建不存在的文件.
fstream的读写功能参数只涉及到缓冲区地址和大小.
pack参数一个事压缩文件的类型,一个是要压缩的内容.

#include<iostream>
#include<fstream>
#include"bundle.h"


int main(int argc,char* argv[])
{
    //接收压缩文件
    std::string compressfile=argv[1];
    std::string newfile=argv[2];

    //打开压缩文件
    std::ifstream ifs(compressfile,std::ios::binary);

    //将文件指针偏移到文件末尾
    ifs.seekg(0,std::ios::end);

    //获取文件偏移量(文件字节大小)
    int filesize=ifs.tellg();

    //将文件指针偏移到文件起始
    ifs.seekg(0,std::ios::beg);

    //接收文件内容
    std::string content;
    content.resize(filesize);
    ifs.read(&content[0],filesize);
    ifs.close();

    //获取文件大小
    std::string packa=bundle::pack(bundle::LZIP,content);
    
    //打开存储文件
    std::ofstream ofs(newfile,std::ios::binary);
    
    //将pack文件写入文件
    ofs.write(&packa[0],packa.size());

    //关闭文件
    ifs.close();
    return 0;
}

解压缩

#include<string>
#include<fstream>
#include"bundle.h"

int main(int argc,char* argv[])
{

    //接收文件名
    std::string compressfile=argv[1];
    std::string newfile=argv[2];

    //打开压缩文件
    std::ifstream ifs(compressfile,std::ios::binary);

    //将文件指针移动至末尾
    ifs.seekg(0,std::ios::end);

    //获取文件大小
    size_t filesize=ifs.tellg();

    //将文件指针移动至起始
    ifs.seekg(0,std::ios::beg);

    //设置接收字符串大小
    std::string body;
    body.resize(filesize);

    //读取压缩文件内容
    ifs.read(&body[0],filesize);

    //解压内容
    std::string packa=bundle::unpack(body);

    //打开解压文件
    std::ofstream ofs(newfile,std::ios::binary);

    //将解压内容写入文件
    ofs.write(&packa[0],packa.size());

    //关闭两个文件
    ifs.close();
    ofs.close();
    return 0;
}

最后可以用md5sum 文件名 命令来验证压缩后再解压缩是不是与源文件相同,md5sum是将文件内容和进行哈希映射值.

3.httplib库使用

Request类认识
作用:客户端将用户请求格式化为http请求相关信息,最终组织http请求发送
服务端收到http之后,解析请求,将解析的数据保存在Resquest类中等待处理.

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;//是否含有某个文件
        MultipartFormData get_file_value(const char *key) const;//获取文件的值
   };

Response类认识
格式化响应,组织响应

    struct Response {
        std::string version;//服务器版本号
        int status = -1; //状态码
        std::string reason;//原因
        Headers headers;//头部信息
        std::string body;//主体信息
        std::string location; // 重定向的地址
 		void set_header(const char *key, const char *val);//设置头部信息
        void set_content(const std::string &s, const char *content_type);
        //设置内容
   };

server类认识
用于搭建服务器

 class Server {
 		//请求处理回调函数类型
        using Handler = std::function<void(const Request &, Response &)>;
        //请求路由表类型,regex:正则表达式,用于匹配http资源路径,handler回调函数
        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);
 };

client类认识
搭建客户端

    class Client {
    	//接收IP和端口号
        Client(const std::string &host, int port);
        //向服务器发送GET请求
 		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);
   }
}

http::Server原理:当启动Server对象时,会创建并绑定套接字,设置监听,循环接收客户端链接请求,接收到客户端请求时,会创建新的线程,新线程从找到用户传递的回调处理函数,解析客户端请求字段,包括请求方法,URL,版本号,请求包头,主体.设置响应字段.

4.简单服务器搭建

#include<iostream>
#include"httplib.h"
#include<string>

int main()
{
    httplib::Server sr;

    //请求方法为GET,请求内容为纯文本
    sr.Get("/hi",[&](const httplib::Request& req,httplib::Response& resp)->void{
        resp.set_content("hello world","text/plain");
        resp.status=200;
    });

    //请求方法为GET,请求内容为纯文本,(\d+)为正则表达式,\d表示匹配一个数字,+表示重复匹配一个或多个,()表示捕获数字
    sr.Get(R"(/numbers/(\d+))",[&](const httplib::Request& req,httplib::Response& resp)->void{
        //下标为0的内容是这个path,后面捕获的内容
        auto number=req.matches[1];  

        //设置响应内容
        resp.set_content(number,"text/plain");

        //设置状态码 
        resp.status=200;
    });

    //请求方法是post,请求内容是文件请求
    sr.Post("/multipart",[&](const httplib::Request& req,httplib::Response& resp)->void{
        //获取文件个数
        size_t size=req.files.size();

        //判断是否存在指定文件
        bool ret=req.has_file("name1");
        if(ret)
        {
            std::cout<<"文件获取失败"<<std::endl;
        }

        //获取文件内容
        httplib::MultipartFormData file= req.get_file_value("name1");
        
        //设置响应主体为文件内容
        resp.body=file.filename;
        resp.body="\n";
        resp.body=file.content;

        //设置报头
        resp.set_header("Content-Type","text/plain");

        //状态码
        resp.status=200;
    });

    sr.listen("0.0.0.0",9999);

    return 0;
}

5.简单客户端搭建

#include"httplib.h"

int main()
{
    //启动客户端
    httplib::Client cl("127.0.0.1",9999);

    //发送请求
    httplib::Result res=cl.Get("/hi");
    std::cout<<res->status<<std::endl;
    std::cout<<res->body<<std::endl;

    httplib::Result res=cl.Get("/numbers/678");
    std::cout<<res->status<<std::endl;
    std::cout<<res->body<<std::endl;

     httplib::MultipartFormDataItems items = {
     { "file1", "this is file content", "hello.txt", "text/plain" },
   };
    httplib::Result res=cl.Post("/upload",items);
    std::cout<<res->status<<std::endl;
    std::cout<<res->body<<std::endl;
    return 0;
}

四、文件使用工具类设计

1.类的功能结构

成员变量:文件名
成员函数:获取文件大小,获取文件最后修改时间,获取文件最后访问时间,获取文件名,设置文件内容,获取文件内容,获取文件指定位置的指定长度的内容,获取指定目录中所有文件信息,判断文件是否存在,创建目录,文件压缩,文件解压缩;

2.功能实现

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#include <fstream>
#include"bundle.h"



namespace cloud
{
    class FileUtil
    {
    public:
        // 获取文件名
        bool Getfilename(const std::string &path)
        {
            // 从后往前查找第一个'/'
            size_t pos = path.find_last_of("/");
            if (pos == std::string::npos)
            {
                std::cout << "get file name fail" << std::endl;
                return false;
            }

            // 提取文件名
            _filename = path.substr(pos + 1);

            return true;
        }

        // 获取文件大小
        size_t GetFileSize()
        {
            // 填充文件属性结构体stat
            struct stat sta;
            int ret = stat(_filename.c_str(), &sta);

            // 判断填充是否成功
            if (ret == -1)
            {
                std::cout << "get file access time fail,error code: " << errno << "error info: " << strerror(errno) << std::endl;
                return -1;
            }

            // 提取文件最后访问时间属性
            return sta.st_size;
        }

        // 获取文件最后一次访问时间
        time_t GetFileATime()
        {
            // 填充文件属性结构体stat
            struct stat sta;
            int ret = stat(_filename.c_str(), &sta);

            // 判断填充是否成功
            if (ret == -1)
            {
                std::cout << "get file access time fail,error code: " << errno << "error info: " << strerror(errno) << std::endl;
                return -1;
            }

            // 提取文件最后访问时间属性
            return sta.st_atime;
        }

        // 获取文件最后一次修改时间
        time_t GetFileMTime()
        {
            // 填充文件属性结构体stat
            struct stat sta;
            int ret = stat(_filename.c_str(), &sta);

            // 判断填充是否成功
            if (ret == -1)
            {
                std::cout << "get file access time fail,error code: " << errno << "error info: " << strerror(errno) << std::endl;
                return -1;
            }

            // 提取文件最后修改时间属性
            return sta.st_mtime;
        }

        // 设置文件内容
        bool SetFileContent(const std::string &Content)
        {
            // 以写方式打开文件
            std::ofstream ofs;
            ofs.open(_filename, std::ios::binary);

            // 写文件
            ofs.write(Content.c_str(), Content.size());

            // 判断流是否错误
            if (!ofs.good())
            {
                std::cout << "file write" << _filename << "fail" << std::endl;
                return false;
            }

            ofs.close();

            return true;
        }

        // 获取文件指定位置指定长度内容
        bool GetFileSpecifiedContent(std::string &content, const int pos, const int len)
        {
            // 以读方式打开文件
            std::ifstream ifs(_filename, std::ios::binary);

            // 设置文件下一个字符访问位置
            ifs.seekg(pos, std::ios::beg);

            // 设置接收缓冲区大小
            char buffer[len];

            // 读取文件
            ifs.read(buffer, len);

            // 判断流是否错误
            if (!ifs.good())
            {
                std::cout << "file write specified content " << _filename << "fail" << std::endl;
                return false;
            }

            // 提取文件内容
            content = buffer;

            ifs.close();

            return true;
        }

         // 获取文件内容
        bool GetFileContent(std::string &content)
        {
            size_t size=this->GetFileSize();
            return GetFileSpecifiedContent(content,0,size);
        }

        // 文件压缩
        bool Compress(const std::string &Comfilename)
        {
            std::string content;
            if(!GetFileContent(content))
            {
                return false;
            }

            // 获取压缩文件内容
            std::string package = bundle::pack(bundle::LZIP, content);

            // 以写方式打开压缩内容存储文件
            std::ofstream ofs(Comfilename, std::ios::binary);

            // 写文件
            ofs.write(package.c_str(), package.size());

            // 判断流是否错误
            if (!ofs.good())
            {
                std::cout << "file" << _filename << "compress fail" << std::endl;
                return false;
            }

            // 关闭文件
            ofs.close();

            return true;
        }

        // 文件解压缩
        bool Uncompress(const std::string uncomfilename)
        {
            std::string content;
            if(!GetFileContent(content))
            {
                return false;
            }

            // 解压内容
             std::string package=bundle::unpack(content);

            // 打开解压内容存储文件
            std::ofstream ofs(uncomfilename, std::ios::binary);

            // 写入文件
            ofs.write(&package[0], package.size());

            // 判断流是否错误
            if (!ofs.good())
            {
                std::cout << "file" << _filename << "uncompress fail" << std::endl;
                return false;
            }

            // 关闭文件
            ofs.close();

            return true;
        }

    public:
        std::string _filename;
    };
}

3.常见压缩算法

RLE(run length encoding 运行长度编码)
将连续的重复字符替换为字符+重复次数进行压缩.
HUFFMAN(哈佛曼算法)算法
1.统计字符出现频率
2.将字符用二进制从0,01,11,等开始依次编码,编码顺序由字符出现频率从高到低开始编码.
3.构建哈夫曼二叉树,字符和频率作为节点创建最小堆,取出频率最小的两个堆将他们将的字符合并,频率相加组成新节点插入堆中,依次归并,最终得到所以字符频率之和的新节点结束.
4.从根节点遍历哈夫曼树,路径往左标记0,往右标记1,遍历到叶子节点,记录路径上所有标记形成编码,将叶子节点的字符与编码组成映射关系记录到表中
5.将对应的字符转化编码进行压缩.
DEFLATE算法
1.使用RLE算法去重
2.使用哈夫曼算法堆字符编码

五、json工具类实现

#pragma once
#include<sstream>
#include<iostream>
#include<jsoncpp/json/json.h>
#include<memory>

namespace cloud
{
    class JsonUtil
    {
    public:
        //序列化
        static bool Serialize(const Json::Value& root,std::string* str)
        {
            Json::StreamWriterBuilder swb;
            std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
            std::ostringstream oss;
            sw->write(root,&oss);
            *str=oss.str();
            return true;
        }

        //反序列化
        static bool Deserialize(Json::Value* root,const std::string& str)
        {
            Json::CharReaderBuilder crb;
            std::string err;
            std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
            bool ret=cr->parse(&str[0],&str[0]+str.size(),root,&err);
            if(!ret)
            {
                std::cout<<err<<std::endl;
                return false;
            }
            return true;
        }
    };
}


六、服务端配置信息模块

1.设计思想

程序各个模块需要使用到热点判断时间,文件下载前缀名,文件压缩包后缀名,上传文件存放路径,文件压缩存放路径,备份信息存储文件,IP 地址,端口这个样的固定的关键信息,使用一个类统一管理这些信息,方便管理,便于后续维护.又因为需要不止一个模块,需要访问这些信息,而且使用的时机也不同,所以需要这个类是一个全局唯一对象,单例模式的类管理信息,用文件存储配置信息.在创建对象的时候读取配置文件,将最新的配置信息加载到程序中,使程序每次启动使用的是最新的信息.

2.实现

#pragma once
#include "fileutil.hpp"
#include "json.hpp"
#include <mutex>

#define CNFFILE_NAME "./config.txt"

namespace cloud
{
    class Config
    {
    private:
        // 全局静态对象
        static Config *cnf;
        static std::mutex _mutex;

    public:
        static Config *Instance()
        {
            if (cnf == nullptr)
            {
                _mutex.lock();
                if (cnf == nullptr)
                {
                    cnf = new Config;
                }
            }
            _mutex.unlock();
            return cnf;
        }
        Config()
        {
            Readfile(CNFFILE_NAME);
        }
        bool Readfile(const std::string &filename)
        {
            // 获取配置文件内容
            cloud::FileUtil fu(filename);
            std::string content;
            fu.GetFileContent(content);

            // 反序列化文件内容
            Json::Value root;
            cloud::JsonUtil::Deserialize(&root, content);

            _hot_time = root["hot_time"].asInt();
            _server_ip = root["server_ip"].asString();
            _server_port = root["server_port"].asInt();
            _url_prefix = root["url_prefix"].asString();
            _arc_suffix = root["arc_suffix"].asString();
            _back_dir = root["back_dir"].asString();
            _pack_dir = root["pack_dir"].asString();
            _manager_file = root["manager_file"].asString();
        }

    public:
        // 热点中断时间
        time_t _hot_time;

        // 文件下载URL前缀路径
        std::string _url_prefix;

        // 压缩包后缀名称
        std::string _arc_suffix;

        // 上传文件存放路径
        std::string _back_dir;

        // 压缩文件存放路径
        std::string _pack_dir;

        // 服务端备份信息存放文件
        std::string _manager_file;

        // 服务器访问 IP 地址
        std::string _server_ip;

        // 服务器访问端口
        uint16_t _server_port;
    };

    Config* Config::cnf = nullptr;
    std::mutex Config::_mutex;

}

七、服务端数据管理模块

1.设计思想

对客户端上传的文件进行热点管理,将长时间没有被访问的文件进行压缩存储,需要用到文件的服务器本地存储路径,服务器本地压缩路径,文件最后一次访问时间,文件最后一次修改时间,文件压缩标志;客户端下载文件,需要用到文件索引信息URL,浏览备份文件列表,需要用向用户展示文件大小.为了方便维护管理,所以需要用一个单独的模块封装设计.

**功能设计:**文件上传涉及到信息插入,文件列表展示以及热点管理涉及到信息查询,所以类功能设计主要是插入和查询功能.如果一直有用户的上传的文件需要维护,文件的信息就一直需要维护,所以考虑用文件实现信息的持久化存储,每次数据更新就将数据持久化存储.服务器启动时需要读取加载文件信息到程序中,所以还需要一个初始化模块,用户加载文件信息到程序中.以供使用.

管理结构:考虑到热点管理模块对与查询文件信息的操作比较频繁,用哈希表结构管理文件信息,提高查询效率.

数据会被服务器,热点管理多个线程访问,服务器模块本身使用的是一个线程池,处理应用程序发来的请求,请求中有查询和插入操作,热点管理只是查询操作,所以对于数据的访问是读多写少,使用读写锁加锁可以提高并发性能.

2.实现

#pragma once
#include "fileutil.hpp"
#include "config.hpp"
#include "json.hpp"
#include <unistd.h>
#include <unordered_map>
namespace cloud
{
    struct BackUpInfo
    {
        // 文件最后一次访问时间
        Json::UInt64 last_a_time = 0;

        // 文件最后一次修改时间
        Json::UInt64 last_m_time = 0;

        // 文件大小
        Json::UInt64 file_size = 0;

        // 文件压缩标志
        bool pack_flag = false;

        // 文件实际存储路径,backdir
        std::string real_path;

        // 文件压缩存储路径 packdir
        std::string pack_path;

        // 用户请求
        std::string url;

        // 填充结构信息
        bool NewBackUpInfo(const std::string &realpath)
        {
            // 获取文件名
            FileUtil fu(realpath);

            // 如果文件不存在就不处理
            if (fu.Exists() == false)
            {
                std::cout << "file not exists" << std::endl;
                return false;
            }

            // 填充文件大小
            file_size = fu.GetFileSize();

            // 填充文件最后访问时间
            last_a_time = fu.GetFileATime();

            // 填充文件最后修改时间
            last_m_time = fu.GetFileMTime();

            // 填充文件实际存储路径
            real_path = realpath;

            // 填充文件压缩存储路径
            pack_path = Config::Instance()->GetPackDir() + fu.FileName() + Config::Instance()->GetPackSuffix();

            // 用户请求路径
            url = Config::Instance()->GetDownLoadPreFix() + fu.FileName();

            // 填充文件压缩标志
            pack_flag = false;
        }
    };

    // 数据管理类
    class InfoManager
    {
    public:
        // 构造
        InfoManager()
        {
            // 初始化读写锁
            pthread_rwlock_init(&_rwlock, nullptr);

            // 获取上传文件信息管理文件
            _backup_file = Config::Instance()->GetBackUpFile();

            // 初始化加载
            InitLoad();
        }

        // 初始化加载
        void InitLoad()
        {
            // 读文件
            std::string str;
            _backup_file.GetFileContent(str);

            // 反序列化
            Json::Value root;
            JsonUtil::Deserialize(&root, str);

            // 填充BackUpInfo结构体信息字段
            for (Json::Value::ArrayIndex i = 0; i < root.size(); i++)
            {
                BackUpInfo info;
                info.file_size = root[i]["file_size"].asUInt64();
                info.last_a_time = root[i]["last_a_time"].asUInt64();
                info.last_m_time = root[i]["last_m_time"].asUInt64();
                info.real_path = root[i]["real_path"].asString();
                info.pack_flag = root[i]["pack_flag"].asBool();
                info.pack_path = root[i]["pack_path"].asString();
                info.url = root[i]["url"].asString();

                // 将BackUpInfo对象插入哈希表中
                Insert(info);
            }
        }

        // 持久存储
        bool Storge()
        {
            std::vector<BackUpInfo> arr;
            if (!GetAll(&arr))
            {
                std::cout<<"get all file backup info fail"<<std::endl;
                return false;
            }

            Json::Value root;
            for (size_t i = 0; i < arr.size(); i++)
            {
                // 添加到Json::Value
                Json::Value item;
                item["file_size"] = arr[i].file_size;
                item["last_a_time"] = arr[i].last_a_time;
                item["last_m_time"] = arr[i].last_m_time;
                item["real_path"] = arr[i].real_path;
                item["pack_flag"] = arr[i].pack_flag;
                item["pack_path"] = arr[i].pack_path;
                item["url"] = arr[i].url;

                root.append(item);

                // 序列化
                std::string str;
                JsonUtil::Serialize(root, &str);

                // 将信息写入到上传文件信息备份文件中
                _backup_file.SetFileContent(str);
            }

            return true;
        }

        // 向table插入数据
        bool Insert(const BackUpInfo &info)
        {
            // 加锁
            pthread_rwlock_wrlock(&_rwlock);

            // 插入数据
            _table.insert(std::make_pair(info.url, info));

            // 解锁
            pthread_rwlock_unlock(&_rwlock);

            //持久化存储
            Storge();
            return true;
        }

        // 更新数据
        bool Updata(const BackUpInfo &info)
        {
            // 加锁
            pthread_rwlock_wrlock(&_rwlock);

            // 插入数据
            _table.insert(std::make_pair(info.url, info));
            
            // 解锁
            pthread_rwlock_unlock(&_rwlock);
            
            //持久化存储
            Storge();
            return true;
        }

        // 通过url获取一个备份文件数据
        bool GetOneByUrl(const std::string &url, BackUpInfo *info)
        {
            // 加锁
            pthread_rwlock_wrlock(&_rwlock);

            // 查找
            if (_table.count(url) == false)
            {
                std::cout << "not find file info" << std::endl;
                return false;
            }

            // 提取数据
            *info = _table[url];

            // 解锁
            pthread_rwlock_unlock(&_rwlock);
            return true;
        }

        // 通过实际存储路径获取一个备份文件数据
        bool GetOneByRealPath(const std::string &realpath, BackUpInfo *info)
        {
            // 加锁
            pthread_rwlock_wrlock(&_rwlock);
            // 查找
            for (auto it = _table.begin(); it != _table.end(); it++)
            {
                if (it->second.real_path == realpath)
                {
                    *info = it->second;
                    return true;
                }
            }
            pthread_rwlock_unlock(&_rwlock);
            // 解锁
            std::cout << "not find file info" << std::endl;
            return false;
        }

        // 获取全部信息
        bool GetAll(std::vector<BackUpInfo> *arr)
        {
            // 加锁
            pthread_rwlock_wrlock(&_rwlock);

            // 遍历获取数据
            for (auto e : _table)
            {
                arr->push_back(e.second);
            }

            // 解锁
            pthread_rwlock_unlock(&_rwlock);
            return true;
        }
        ~InfoManager()
        {
            pthread_rwlock_destroy(&_rwlock);
        }

    private:
        // 文件操作工具
        FileUtil _backup_file;

        // 读写锁
        pthread_rwlock_t _rwlock;

        // hash表
        std::unordered_map<std::string, BackUpInfo> _table;
    };
}

八、服务端热点管理模块

1.设计思想

对于客户端上传的文件,实时检测文件是否被经常访问,对于不经常被访问的文件,压缩存储可以减少服务器磁盘压力.

热点对象访问全局数据对象获取所有备份文件信息,遍历所有文件信息,利用数据中的文件访问时间与当前时间作比较,判断该文件是否为热点文件,将非热点文件压缩存储在新的路径下,删除源文件可以避免源文件被重复检测.

2.代码

#pragma once
#include "DataManager.hpp"
#include "config.hpp"
#include "fileutil.hpp"

extern cloud::InfoManager *_data;
namespace cloud
{
    class HotManager
    {
    public:
        HotManager()
        {
            // 从配置文件获取成员信息
            _backdir = Config::Instance()->GetBackDir();
            _packdir = Config::Instance()->GetPackDir();
            _hottime = Config::Instance()->GetHotTime();
            _pack_suffix = Config::Instance()->GetPackSuffix();

            // 目录不存在就创建目录
            FileUtil(_backdir).CreateDirectory();
            FileUtil(_packdir).CreateDirectory();
        }
        bool HotJudge(const std::string &filename)
        {
            // 获取最近访问时间与当前时间
            time_t last_atime = FileUtil(filename).GetFileATime();
            time_t cur_time = time(NULL);

            // 非热点文件
            if (cur_time - last_atime > _hottime)
            {
                return true;
            }

            // 热点文件
            return false;
        }
        void RunModule()
        {
            while (1)
            {
                // 遍历目录获取文件名
                std::vector<std::string> array;
                FileUtil(_backdir).ScanDirectory(&array);

                // 判断是否为热点文件
                for (auto a : array)
                {
                    // 热点文件不处理
                    if (HotJudge(a) == false)
                    {
                        continue;
                    }

                    // 获取文件备份信息
                    BackUpInfo info;
                    if (_data->GetOneByRealPath(a, &info) == false)
                    {
                        info.NewBackUpInfo(a);
                    }

                    // 压缩非热点文件7
                    // 删除源文件
                    FileUtil fu(a);
                    fu.Compress(info.pack_path);
                    fu.Remove();

                    // 修改备份文件信息
                    info.pack_flag = true;
                    _data->Updata(info);
                }
            }
        }

    private:
        // 备份文件信息管理类对象
        std::string _backdir;
        std::string _packdir;
        std::string _pack_suffix;
        int _hottime;
    };
}

九、服务端业务处理模块

1.实现思想

服务端要能响应客户端的文件列表查看请求,文件下载请求,文件上传请求.

文件上传功能的实现:判断响应报文中是否设置了"file"(自定义头部键值),使用request对象获取文件内容,将内容写入上传目录中,更新全局数据对象结构中的数据.

断点续传功能的实现原理:
正常文件下载,客户端请求POST /download/filename HTTP/1.1+请求报头+主体;
服务端响应:HTTP/1.1 200 OK+请求报头(“Etag: asasdfjashdfoa(文件唯一标识)”,“Accept-Range: bytes”(告诉客户端服务端支持断点续传))+body(文件内容);

异常中断后,客户端请求断点续传,POST /download/filename HTTP/1.1+请求报头(Range: bytes=10-900)(If-Range: 服务器响应的唯一标识)+body;

首先服务端检测是否有If-Range头部字段,判断是否需要断点续传,在判断If-Range对应的etag文件唯一标识符是否与当前文件的唯一标识符一致,不一致说明文件在此之前已经被修改了,需要重新下载文件,否则就获取Range请求报头的区间值,Range值设置方式有Range: bytes 10-100或者Range: bytes 10-表示10-文件末尾,将区间内的文件内容设置到body中响应给客户端

服务器响应: HTTP/1.1 206 partial content+请求报头(“Etag: asasdfjashdfoa(文件唯一标识)”,“Accept-Range: bytes”(告诉客户端服务端支持断点续传),“Content-Range: bytes 10-900/891”)+body(文件内容);

2.代码

#pragma once
#include "httplib.h"
#include "config.hpp"
#include "DataManager.hpp"
#include <sstream>
#include <ctime>

extern cloud::InfoManager *_data;

namespace cloud
{

    class Service
    {
    public:
        Service()
        {
            _Download_prefix = Config::Instance()->GetDownLoadPreFix();
            _server_ip = Config::Instance()->GetServerIp();
            _server_port = Config::Instance()->GetServerPort();
        }

        // 上传
        static void UpLoad(const httplib::Request &req, httplib::Response &resp)
        {
            // 判断有没有文件上传区域
            bool flag = req.has_file("file");
            if (flag == false)
            {
                std::cout << "not file contant" << std::endl;
                resp.status = 400;
                return;
            }

            // 获取文件信息
            auto file = req.get_file_value("file");

            // 获取文件上传路径
            std::string backdir = Config::Instance()->GetBackDir();

            // 获取文件实际存储路径
            std::string realpath = backdir + FileUtil(file.filename).FileName();

            // 向文件写入数据
            FileUtil(realpath).SetFileContent(file.content);

            // 向数据管理模块添加信息
            BackUpInfo info;
            info.NewBackUpInfo(realpath);
            _data->Insert(info);
        }

        static std::string GetETag(const BackUpInfo &info)
        {
            FileUtil fu(info.real_path);
            std::string etag = fu.FileName();
            etag += '-';
            etag += std::to_string(info.file_size);
            etag += '-';
            etag += std::to_string(info.last_m_time);
            return etag;
        }
        // 下载
        static void Download(const httplib::Request &req, httplib::Response &resp)
        {
            // 根据url获取文件备份信息
            BackUpInfo info;
            _data->GetOneByUrl(req.path, &info);

            // 判断文件是否被压缩,如果被压缩,要先解压缩
            if (info.pack_flag == true)
            {
                // 将压缩包解压到上传路径下
                FileUtil fu(info.pack_path);
                fu.Uncompress(info.real_path);

                // 删除压缩包
                fu.Remove();

                // 修改备份信息
                info.pack_flag = false;

                // 更新备份信息
                _data->Updata(info);
            }
            bool resume_flag = false;

            // 判断请求字段中是否有需要热点续传
            if (req.has_header("If-Range"))
            {
                auto old_etag = req.get_header_value("If-Range");
                if (old_etag == GetETag(info))
                {
                    resume_flag = true;
                }
            }

            // 读取文件信息将内容放入响应内容
            FileUtil(info.real_path).GetFileContent(resp.body);
            if (resume_flag)
            {
                // 设置头部字段
                resp.set_header("Accept-Ranges", "bytes");

                // 热点续传标志
                resp.set_header("ETag", GetETag(info));

                // 响应文件类型
                resp.set_header("Content-Type", "application/octet-stream");
                resp.status = 206;
            }
            else
            {
                // 设置头部字段
                resp.set_header("Accept-Ranges", "bytes");
                
                // 热点续传标志
                resp.set_header("ETag", GetETag(info));

                // 响应文件类型
                resp.set_header("Content-Type", "application/octet-stream");
                resp.status = 200;
            }
        }
        static const char *Gettime(time_t time)
        {
            return std::ctime(&time);
        }
        // 列表显示
        static void ListShow(const httplib::Request &req, httplib::Response &resp)
        {
            (void)req;
            // 获取所有文件信息
            std::vector<BackUpInfo> array;
            _data->GetAll(&array);
            std::stringstream ss;
            ss << "<html><head><title>Download</title></head>";
            ss << "<body><h1>Download</h1><table>";
            ss << "<meta charset='UTF-8'>";
            for (auto a : array)
            {
                ss << "<tr>";
                FileUtil fu(a.real_path);
                ss << "<td><a href='" << a.url << "'>" << fu.FileName() << "</a></td>";
                ss << "<td align='right'>" << Gettime(a.last_m_time) << "</td>";
                ss << "<td align='right'>" << a.file_size / 1024 << "k </td>";
                ss << "</tr>";
            }
            ss << "</table></body></html>";

            // 设置响应展示页面
            resp.body = ss.str();
            resp.set_header("Content-Type", "text/html");
            resp.status = 200;
        }

        // 运行
        void RunModule()
        {
            // 创建服务器
            // 接收上传请求
            _server.Post("/upload", UpLoad);

            // 接收下载请求
            std::string str = Config::Instance()->GetDownLoadPreFix() + "(.*)";
            _server.Get(str.c_str(), Download);

            // 接收列表展示请求
            _server.Get("/listshow", ListShow);
            _server.Get("/", ListShow);

            // 启动服务器
            _server.listen(_server_ip.c_str(), _server_port);
        }

    private:
        std::string _server_ip;
        uint16_t _server_port;
        std::string _Download_prefix;
        httplib::Server _server;
    };
}

十、客户端数据模块

1.实现思想

客户端要实现的功能是对指定文件夹中的文件自动进行备份上传。但是并不是所有的文件每次都需要上传,需要判断哪些文件需要上传哪些不需要,因此需要将备份的文件信息给管理起来,作为下一次文件是否需要备份的判断。因此需要被管理的信息包含以下:文件路径名称,文件唯一标识:由文件名,最后一次修改时间,文件大小组成的一串信息

2.代码

#pragma once
#include<sstream>
#include<string>
#include<unordered_map>
#include"fileutil.hpp"

namespace cloud
{
	class DataManager
	{
	public:
		DataManager(const std::string& back_file):_back_file(back_file)
		{
			InitLoad();
		}

		//持久化存储
		bool Storage()
		{
			std::stringstream ss;
			//遍历获取表中的信息
			for (auto a : _table)
			{
				ss << a.first << " " << a.second << "\n";
			}

			//向管理信息文件写入信息
			FileUtil fu(_back_file);
			fu.SetFileContent(ss.str());
			return true;
		}

		bool Split(const std::string& str, const std::string& sep, std::vector<std::string>* array)
		{
			//分隔符位置,开始查找位置
			size_t pos = 0, indx = 0;
			while (1)
			{
				//从indx位置查找
				pos=str.find(sep, indx);

				//没找到
				if (pos == std::string::npos)
				{
					return false;
				}

				//将string转化为stringstream插入array
				std::string tmp = str.substr(indx, pos - indx);
				array->push_back(tmp);

				//更新下次查找开始位置
				indx = pos + sep.size();
			}

			if (indx < str.size())
			{
				array->push_back(str.substr(indx));
			}
			return true;
		}

		//初始化加载
		bool InitLoad()
		{
			//读取文件信息
			std::string str;
			FileUtil fu(_back_file);
			fu.GetFileContent(str);
			
			//按照格式分割文件内容
			std::vector<std::string> array;
			Split(str, "\n", &array);
			

			//将格式化信息插入表中
			for (auto a : array)
			{
				std::stringstream ss(a);
				std::string filename, flag;
				ss >> filename>>flag;
				_table.insert(std::make_pair(filename, flag));
			}
			return true;
		}

		//插入数据
		bool Insert(const std::string& key, const std::string& value)
		{
			_table[key] = value;
			Storage();
			return true;
		}

		//更新数据
		bool UpDate(const std::string& key, const std::string& value)
		{
			_table[key] = value;
			Storage();
			return true;
		}

		//通过key值获取一个数据
		bool GetOneByKey(const std::string& key, std::string* value)
		{
			auto it = _table.count(key);
			if (it == false)
			{
				return false;
			}
			*value = _table[key];
			return true;
		}
	private:
		//数据管理表
		std::unordered_map<std::string, std::string> _table;

		//持久化存储文件
		std::string _back_file;
	};
}

十一、客户端业务处理模块

循环检测指定目录中的文件是否需要将上传,被更改的,或者是没有上床过的文件都需要上传,上传的同时将文件路径名作为key值,文件名+文件大小+最后一次修改时间组成的文件唯一标识符作为value存储在哈希表中,并持久化存储.

#pragma once
#include"data.h"
#include"httplib.h"
#include<windows.h>
#define BACKDIR "./backdir/"
#define BACKFILE "backfile.dat"
#define SERVER_IP "170.106.107.74"
#define SERVER_PORT 9090
namespace cloud
{
	class BackUp
	{
	public:
		BackUp(const std::string& back_dir= BACKDIR) :_back_dir(back_dir)
		{
			_data = new DataManager(BACKFILE);
		}
		
		std::string GetFileIdentifier(const std::string& filename)
		{
			FileUtil fu(filename);
			std::string size = std::to_string(fu.GetFileSize());
			std::string mtime = std::to_string(fu.GetFileMTime());
			std::stringstream ss;
			ss << filename << "-" << size << "-" << mtime;
			return ss.str();
		}

		//判断是否需要上传
		bool IsUpLoad(const std::string& filename)
		{
			FileUtil fu(filename);
			std::string id;
			bool ret=_data->GetOneByKey(filename, &id);
			if ((ret&& GetFileIdentifier(filename) == id)
				|| (time(NULL) - fu.GetFileMTime() < 3))
			{
				return false;
			}
			return true;
		}

		//上传
		bool UpLoad(const std::string& filename)
		{
			//获取文件内容
			FileUtil fu(filename);
			std::string body;
			fu.GetFileContent(body);

			//创建Client对象
			httplib::Client client(SERVER_IP, SERVER_PORT);

			//设置文件上传格式
			httplib::MultipartFormData item;
			item.filename = fu.FileName();
			item.content = body;
			item.content_type = "application/octet-stream";
			item.name = "file";
			httplib::MultipartFormDataItems items;
			items.push_back(item);

			//发送上传请求
			auto ret=client.Post("/upload", items);

			//判断是否成功
			if (!ret || ret->status != 200)
			{
				return false;
			}
			return true;
		}


		void RunModule()
		{
			while (1)
			{
				//遍历获取目录文件名称
				FileUtil fu(_back_dir);
				std::vector<std::string> array;
				fu.ScanDirectory(&array);

				for (auto a : array)
				{
					//判断文件是否需要上传
					if (IsUpLoad(a) == true)
					{
						//文件上传成功就更新数据
						if (UpLoad(a) == true)
						{
							_data->Insert(a, GetFileIdentifier(a));
						}
					}
				}
				Sleep(1);
			}
		}
	private:
		std::string _back_dir;
		DataManager* _data;
	};
}

十二、项目类关系图

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值