【实战项目】云备份

项目认识

能够将客户主机上面的文件自动备份到服务器上面存储起来,备份到服务器上之后客户端还要随时的通过浏览器来访问我们的服务器查看备份过的文件,同时支持下载,而且下载如果中间中断了,当前的项目还支持断点续传的功能;服务端对上传的文件进行备份存储,服务器会分辨文件是否是一个热点文件(即近期是否被访问,没有被访问过就认为是一个非热点文件),对于非热点文件会压缩存储更节省服务器的磁盘空间,对于压缩的文件当我们下载的时候会进行解压之后才下载。

使用jsoncpp进行序列化实现

//使用jsoncpp进行序列化实现
#include <iostream>
#include <memory>
#include <sstream>
#include <jsoncpp/json/json.h>
int main()
{
    //1、先将所有的数据保存在Json::Value对象中
    //2、使用Json::StreamWriter和Json::StreamWriterBuilder类将Json::Value对象中的数据进行序列化

    //先定义几个数据对象
    const char* name = "小明";
    int age = 18;
    float score[] = {99.9,88.5,77,64};
    //实例化一个Json::Value对象
    Json::Value root;
    //将上面的数据对象放入到Json::Value对象中
    root["姓名"] = name;
    root["年龄"] = age;
    for(int i = 0 ; i < 3; i++)
    {
        //数组要通过append函数来完成
        root["成绩"].append(score[i]);
    }
    //实例化一个Json::StreamWriterBuilder对象
    Json::StreamWriterBuilder swb;
    //再定义一个Json::StreamWriter类型的智能指针,调用newStreamWriter返回一个StreamWriter对象的指针
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
    std::stringstream ss;//stringstream头文件sstream
    //通过sw调用writer函数进行序列化,第一个参数是Value类对象,第二个参数是stringstream类对象(ostream的派生类)
    sw->write(root,&ss);//第二个参数是指针类型
    std::cout << ss.str() << std::endl;
    //编译时要链接第三方库jsoncpp,所以指令g++ -o json_example json_example.cpp -ljsoncpp
}

运行结果:

image-20230907172022524

使用json完成反序列化

//使用json完成反序列化
//反序列化就是将json格式的字符串解析成为多个数据对象
// 这里的反序列化是将其存放在Value对象中,然后从Value对象中取数据
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <memory>
int main()
{
    //先定义一个原始字符串作为json格式字符串,要注意这里双引号会存在歧义,所以我们可以使用C++11中的R"()"或'\'转义
    std::string str = R"({"姓名":"小黄","年龄":18,"成绩":[88.6,96.4,82.9]})";
    //定义一个Json::Value对象用来保存数据
    Json::Value root;
    //借助Json::CharReaderBuilder实例化一个对象,调用newCharReader
    Json::CharReaderBuilder crb;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
    //定义一个字符串用来获取错误信息
    std::string err;
    //调用函数parse完成解析,函数第一个参数是字符串的起始位置,第二个参数是字符串的结束位置,第三个参数
    // 是Value类对象的指针,第四个参数是错误信息的保存位置
    bool ret = cr->parse(str.c_str(),str.c_str()+str.size(),&root,&err);
    //如果ret等于false表示解析失败了
    if(ret == false)
    {
        std::cout << "parse error!\n";
        return -1;
    }
    //从Value对象中获取数据进行打印判断
    std::cout << root["姓名"].asString() << std::endl;
    std::cout << root["年龄"].asInt() << std::endl;
    int sz = root["成绩"].size();
    for(int i = 0; i < sz; i++)
    {
        std::cout << root["成绩"][i].asFloat()<< std::endl;
    }
}

结果:

image-20230907175658642

bundle文件压缩库

对文件进行压缩处理:

//使用bundle库实现文件压缩
#include "bundle.h"
#include <iostream>
#include <string>
#include <fstream>
int main(int argc,char* argv[])
{
    //1、从运行参数里面获取文件压缩名和文件压缩包名,然后打开要压缩的文件获取其中的数据
    //2、将数据进行压缩存放到压缩包文件中
    std::cout << "argv[1]是原始文件名称\n";
    std::cout << "argv[2]是压缩包文件名称\n";
    if(argc < 3)
    {
        //如果参数个数小于3,表明参数个数不够就返回,运行程序本身也算上
        std::cout << "参数个数错误\n";
        return -1;
    }
    std::string ifilename = argv[1];//将要压缩的文件名传给ifilename
    std::string ofilename = argv[2];//将压缩包名传给ofilename
    //通过ifstream定义句柄
    std::ifstream ifs;
    ifs.open(ifilename,std::ios::binary);//以二进制形式打开文件
    //跳转读写位置到末尾
    ifs.seekg(0,std::ios::end);
    //通过偏移量计算文件大小;获取当前读写位置相较于文件起始位置的偏移量,跳转到文件末尾获取的就是文件大小
    size_t fsize = ifs.tellg();
    //再让其跳转到文件起始位置
    ifs.seekg(0,std::ios::beg);
    std::string body;//用来存储压缩之后的数据
    body.resize(fsize);//调整body大小为fsize
    //因为body.c_str()返回的是const char*,所以这里使用&body[0]
    ifs.read(&body[0],fsize);//读取文件所有数据到body中
    //关闭文件
    ifs.close();
    //读取完成之后就要进行文件的压缩,pack函数是bundle库中的,所以指定命名空间中的压缩格式
    std::string packed = bundle::pack(bundle::LZIP,body);//以LZIP压缩格式压缩
    //此时packed里面就是压缩之后的数据
    std::ofstream ofs;
    ofs.open(ofilename,std::ios::binary);//以二进制形式打开
    //进行数据的写入,将压缩之后的文件写入到压缩包中
    ofs.write(&packed[0],packed.size());
    //写完之后关闭文件
    ofs.close();
}

要将bundle.h和bundle.cpp拷贝到当前文件夹,但是为了能够更快的编译,我们这里将bundle.cpp生成一个静态库:

先生成一个.o文件:

g++ -c bundle.cpp -o bundle.o

编译完成之后有一个bundle.o文件,我们就可以生成一个静态库,依赖的是bundle.o文件:

ar -cr libbundle.a bundle.o 

生成之后就可以删除 bundle.cpp和bundle.o文件了可以删除,也可以不删,然后我们创建一个文件夹用来放静态库:

同时在编译的时候加上-L …/lib 和-lbundle

g++ -L../lib Compress.cpp -o compress -lpthread -lbundle

因为要链接线程库,所以加上-lpthread。

压缩成功:

image-20230907201721887

bundle库解压缩

我们将上一节的压缩文件进行解压缩:

//使用bundle库实现解压缩
#include "bundle.h"
#include <iostream>
#include <fstream>
#include <string>
int main(int argc,char* argv[])
{
    std::cout << "argv[1]是压缩包文件名称\n";
    std::cout << "argv[2]是要解压之后的文件名称\n";
    if(argc < 3)
    {
        std::cout << "参数个数错误\n";
        return -1;
    }
    std::string ifilename = argv[1];//压缩包名称
    std::string ofilename = argv[2];//要解压的文件名称
    //将压缩包中的数据读取出来
    std::ifstream ifs;
    ifs.open(ifilename,std::ios::binary);
    //跳转到文件末尾
    ifs.seekg(0,std::ios::end);
    //计算文件大小
    size_t fsize = ifs.tellg();
    //跳转到文件开始位置
    ifs.seekg(0,std::ios::beg);
    //定义一个string用来保存数据
    std::string body;
    body.resize(fsize);
    ifs.read(&body[0],fsize);
    //关闭文件
    ifs.close();
    //对文件进行解压
    std::string unpacked = bundle::unpack(body);//直接将body传入就可以解压
    //打开要解压之后的文件
    std::ofstream ofs;
    ofs.open(ofilename,std::ios::binary);//以二进制形式打开文件
    ofs.write(&unpacked[0],unpacked.size());//将解压后的文件写入当前文件
    ofs.close();
}

编译运行之后解压成功:

g++ -o uncompress UnCompress.cpp -L../lib -lbundle -lpthread

image-20230907203054623

httplib库中Request结构体

首先是客户端用来组织http请求的一个结构体,里面包含了所有的请求数据在里面,只需要把这些数据按照http请求格式组织成请求的字符串发送给服务器就可以了,服务器收到了http请求,会按照http请求格式进行解析,解析之后得到的数据就会放到Request这个结构体当中。

作用:

1、客户端保存所有的http请求相关信息,最终组织http请求发送给服务器

2、服务器收到http请求之后进行解析,将解析的数据保存在Request结构体中,等待后续处理;后续处理就是根据Request这个结构当中的各项数据,也就是客户端所提交的数据会进行各个处理

httplib库中的Response结构体

上面是http的响应格式,响应是我们对客户端的请求进行处理之后,然后给客户端所进行的一个响应,在httplib里面组织了一个结构体叫Response,里面专门用来存储响应里面的一些关键信息,存储了之后只需要把数据放到结构体里面,httplib这个库就会把结构体里面的这些数据组织成一个http响应,发送给客户端。

Response功能:用户将响应数据放到结构体中,httplib会将其中的数据按照http响应格式组织成为http响应,发送给客户端。

httplib库搭建简单服务器

发送请求,根据客户端发送的请求组织成为一个Request结构体,然后根据Request结构体中的资源路径和请求方法去Handlers表中查找对应的处理函数,根据处理函数将数据放到Response结构体中,httplib库将其组织成为一个http响应发送给客户端

//测试httplib库中的Server类-搭建简单服务器
// 发送请求,根据客户端发送的请求组织成为一个Request结构体,
// 然后根据Request结构体中的资源路径和请求方法去Handlers表中查找对应的处理函数,
// 根据处理函数将数据放到Response结构体中,httplib库将其组织成为一个http响应发送给客户端
#include "httplib.h"
//自己定义一个处理函数
void Hello(const httplib::Request& req,httplib::Response& rsp)
{
    //调用Response中的set_content函数设置正文内容和类型
    // 设置正文为Hello World! 正文类型为text/plain,是一个文本数据
    rsp.set_content("Hello World!","text/plain");
    //设置响应状态码
    rsp.status = 200;
    //设置正文和响应状态码之后,httplib库会根据请求中的协议版本等组织成为一个正文响应发送给客户端
}
void Numbers(const httplib::Request &req,httplib::Response& rsp)
{
    //捕捉数据,下标0保存的是整个path路径,往后下标保存的是捕捉的数据
    auto num = req.matches[1];
    //调用Response中的set_content函数设置正文内容和类型
    // 设置正文为num 正文类型为text/plain,是一个文本数据
    rsp.set_content(num,"text/plain");
    //设置响应状态码
    rsp.status = 200;
    //设置正文和响应状态码之后,httplib库会根据请求中的协议版本等组织成为一个正文响应发送给客户端
}
//Multipart是一个文件的上传,多区域文件上传
void Multipart(const httplib::Request& req,httplib::Response& rsp)
{
    //has_file判断是否包含某个文件,从成员变量MultipartFormDataMap中files的成员变量里面去查看的
    auto ret = req.has_file("file");
    if(ret == false)
    {
        std::cout << "not file upload!\n";
        //设置rsp的响应状态码
        rsp.status = 404;
        return ;
    }
    // 在Request结构体中MultipartFormDataMap类型的成员变量files中保存的是文件相关数据
    //如果文件存在获取文件字段区域数据信息
    const auto& file = req.get_file_value("file");//file就是MultipartFormData类型
    //先将响应正文清空
    rsp.body.clear();
    //获取文件名称
    rsp.body = file.filename;
    rsp.body += "\n";
    //获取文件内容
    rsp.body += file.content;
    //设置头信息,text/plain表示是一个文本内容
    rsp.set_header("Content-Type","text/plain");
    //设置响应状态码
    rsp.status = 200;
    return ;
}
int main()
{
    httplib::Server server;
    //注册一个针对/hi的Get请求的处理函数映射关系,当向服务器发送/hi的Get请求时会调用Hello函数进行处理
    //处理完毕之后将Response对象rsp中的数据组织成一个http响应,发送给客户端
    server.Get("/hi",Hello);
    //注册一个针对/numbers/(\\d+)的Get请求的处理函数映射关系
    // ()中的括号是为了捕捉数据,\d+这叫正则表达式里面匹配的是一个数字
    server.Get(R"(/numbers/(\d+))",Numbers);
    // 定义一个multipart是请求资源路径,调用Multipart这个函数来进行处理
    server.Post("/multipart",Multipart);
    //搭建并启动服务器,开放9090端口
    server.listen("0.0.0.0",9090);
}

编译时候要链接线程库

g++ server.cpp -o server -lpthread

运行之后打开浏览器输入ip和端口号,以及请求资源路径,能够得到对应的正文内容:

image-20230908095015423

注意这里使用的是公网ip。

httplib库搭建简单客户端

实例化一个Client对象,告诉请求的服务器IP和port端口号,使用Post请求上传区域文件数据信息来进行演示:

在搭建的时候首先实例化一个Client对象告诉他请求的服务器ip地址和端口,文件上传区域可以组织成MultipartFormData字段,里面有四个信息,字段名称、文件内容、文件名称和正文类型,完成之后直接向服务器发送请求得到一个响应,得到响应之后,我们可以打印响应的状态码,也可以获取到响应的正文,直接进行打印就可以了

//使用httplib库搭建简单客户端--Client类
#include "httplib.h"
#define SERVER_IP "121.4.57.191"
#define SERVER_PORT 9090//访问的端口号,监控的端口号是多少就访问多少
int main()
{
    //实例化一个Client对象,用来搭建客户端
    httplib::Client client(SERVER_IP,SERVER_PORT);
    //区域文件信息的结构体
    httplib::MultipartFormData item;
    //这里的字段名称要和服务器中的字段名称要一致,因为查找的时候就是根据字段来查找的
    item.name = "file";//字段名称
    item.filename = "hello.txt";//文件名
    item.content = "Hello World!";//正文内容
    item.content_type = "text/plain";//文件内容的文本类型 
    //再定义一个MultipartFormDataItems类型的数组
    httplib::MultipartFormDataItems items;
    //将其添加到items中
    items.push_back(item);
    //添加之后要请求服务器将其上传给服务器
    //res是一个Response类型的指针,这里提交的参数也要和服务器中的一致
    auto res = client.Post("/multipart",items);
    //进行打印,查看是否和服务器中数据是否一致
    std::cout << res->status << std::endl;
    std::cout << res->body << std::endl;
    return 0;
}

编译:

 g++ client.cpp -o client -lpthread

同时启动客户端和服务端:此时客户端会打印出我们设置的信息:

image-20230908103611015

和服务端这里对应:

image-20230908103715826

服务端工具类实现

获取文件大小、文件名称、文件最后一次访问时间和最后一次修改时间

我们使用stat函数获取文件的属性,支持跨平台,在不同的操作系统中运行

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
stat是通过文件的路径名称访问文件获取文件的状态属性信息放入到buf中去
int stat(const char *path, struct stat *buf);能获取到文件的大小,最后一次读写和访问时间
//如果返回值小于0,表示失败
#ifndef __UTIL_H__
#define __UTIL_H__
#include <iostream>
#include <string>
#include <vector>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
namespace nmzcloud{
    class FileUtil{
    private:
        std::string _filename;//文件名称
    public:
        //构造函数
        FileUtil(const std::string& filename)
        :_filename(filename)
        {}
        //获取文件大小
        size_t FileSize()
        {
            //定义一个struct stat结构体对象
            struct stat st;
            //使用stat函数传入文件名和st的地址
            if(stat(_filename.c_str(),&st) < 0)
            {
                //获取失败
                std::cout << "get file size failed!\n";
                return -1;
            }
            return st.st_size;//返回文件大小(字节)
        }
        //获取文件最后一次访问时间
        time_t LastATime()
        {
            struct stat st;
            if(stat(_filename.c_str(),&st) < 0)
            {
                std::cout << "get LastATime failed!\n";
                return -1;
            }
            //返回最后一次访问时间
            return st.st_atime;
        }
        //获取文件最后一次修改时间
        time_t LastMTime()
        {
            struct stat st;
            if(stat(_filename.c_str(),&st) < 0)
            {
                std::cout << "get LastMTime failed!\n";
                return -1;
            }
            return st.st_mtime;
        }
        //获取文件路径中的文件名称-服务器管理的文件不需要文件路径;如_filenmae=./a/c/t.txt我们只需要t.txt即可
        std::string FileName()
        {
            //获取文件名称只需要找到最后一个/符号即可
            size_t pos = _filename.find_last_of("/");
            if(pos == std::string::npos)
            {
                //没有找到/,说明本身就是文件名称
                return _filename;
            }
            //返回/之后的字符,进行字符串截断,从/的下一个位置开始直到结束
            return _filename.substr(pos+1);

        }
        // 将body中的数据写入到文件中-参数body中保存的就是要写入文件中的数据
        bool SetContent(const std::string &body)
        {

        }
        //读取文件的所有数据将文件数据放到body中
        bool GetContent(std::string *body)
        {

        }
        //读取文件指定位置开始指定长度的数据-用于后续支持断点续传-参数body用来保存数据
        bool GetPosLen(std::string* body,size_t pos,size_t len)
        {

        }
        //判断当前文件是否存在
        bool Exist()
        {

        }
        //创建文件目录,创建的文件名称就是成员变量_filename
        bool CreateDirectory()
        {

        }
        //遍历文件夹中所有的文件,将其放在array数组中
        bool GetDirectory(std::vector<std::string>* array)
        {

        }
        //文件的压缩-参数是压缩包名称,把压缩之后的数据访问压缩包文件中
        bool Compress(const std::string &packname)
        {

        }
        //文件的解压缩-参数是解压缩之后的文件名称-把解压后的数据放入该文件中
        bool UnCompress(const std::string &unpackname)
        {

        }
    };
}
#endif

测试:

#include "util.hpp"
//测试获取文件名称、文件大小、文件最后一次访问时间和修改时间
void FileUtilTest(const std::string &filename)
{
    //实例化一个对象
    nmzcloud::FileUtil fu(filename);
    std::cout << fu.FileSize() << std::endl;
    std::cout << fu.LastATime() << std::endl;
    std::cout << fu.LastMTime() << std::endl;
    std::cout << fu.FileName() << std::endl;
}
int main(int argc,char* argv[])
{
    FileUtilTest(argv[1]);
}

image-20230908112753668

实现文件读写

//将body中的数据写入到文件中-参数body中保存的就是要写入文件中的数据
        bool SetContent(const std::string &body)
        {
            std::ofstream ofs;
            //二进制形式打开文件
            ofs.open(_filename,std::ios::binary);
            if(ofs.is_open() == false)
            {
                std::cout << "write open file failed!\n";
                return false;
            }
            //打开成功之后写入数据
            ofs.write(&body[0],body.size());
            if(ofs.good() == false)
            {
                std::cout << "write open file failed!\n";
                ofs.close();
                return false;
            }
            ofs.close();//关闭文件
            return true;
        }
        //读取文件指定位置开始指定长度的数据-用于后续支持断点续传-参数body用来保存数据
        bool GetPosLen(std::string* body,size_t pos,size_t len)
        {
            //获取文件大小
            size_t fsize = FileSize();
            //判断从指定位置开始的数据长度是否有误
            if(pos + len > fsize)
            {
                std::cout << "get file len is error!\n";
                return false;
            }
            //从文件里面读取数据
            std::ifstream ifs;
            //以二进制形式打开当前文件
            ifs.open(_filename,std::ios::binary);
            //判断文件是否打开成功
            if(ifs.is_open() == false)
            {
                std::cout << "read open file failed!\n";
                return -1;
            }
            //从文件起始位置开始偏移,偏移到pos位置
            ifs.seekg(pos,std::ios::beg);
            //偏移之后修改body大小为len大小
            body->resize(len);
            //调整之后读取文件数据到body中,body此时是指针,所以要先解引用
            ifs.read(&(*body)[0],len);
            //判断上面操作是否正常
            if(ifs.good() == false)
            {
                std::cout << "get file content failed!\n";
                ifs.close();
                return false;
            }
            ifs.close();
            return true;
        }
        //读取文件的所有数据将文件数据放到body中
        bool GetContent(std::string *body)
        {
            //此时调用GetPosLen函数,此时len的大小为文件大小
            GetPosLen(body,0,FileSize());
        }

测试的时候只需要将一个文件打开进行读取,然后写入到另一个文件中,然后再根据md5值判断是否一致。

//测试读取文件内容和将数据写入文件中
void FileUtilTest2(const std::string &filename)
{
    //实例化一个对象
    nmzcloud::FileUtil fu(filename);
    std::string body;
    //将文件内容写到body中
    fu.GetContent(&body);
    //打开一个文件,将数据写入到新文件中
    nmzcloud::FileUtil nfu("./hello.txt");
    nfu.SetContent(body);
}
int main(int argc,char* argv[])
{
    FileUtilTest2(argv[1]);
}

测试结果:

image-20230908143206375

判断文件是否存在、创建目录、遍历目录

借助C++17中的文件系统库filesystem,C++14也支持

create_directory /create_directories,两个函数一个是单层级目录的创建,一个是多层级目录的创建(即如果上层目录存在,把上层目录也创建了),保存文件是否存在exists函数

创建exists时,包含头文件<experimental/filesystem>,同时简化命名空间:

#include <experimental/filesystem>//文件系统的头文件
namespace nmzcloud{
    //简化命名空间
    namespace fs = std::experimental::filesystem;
   }
        //判断当前文件是否存在
        bool Exist()
        {
            //直接调用函数判断文件是否存在
            return fs::exists(_filename);
        }
        //创建文件目录,创建的文件名称就是成员变量_filename
        bool CreateDirectory()
        {
            //判断文件是否存在
            if(Exist())
            {
                //如果文件存在就不用创建了
                return true;
            }
            //创建的时候将文件路径名传进去就可以了,多层级的目录创建,单层级就不创建了
            return fs::create_directories(_filename);
        }
        //遍历文件夹中所有的文件,将其放在array数组中
        bool GetDirectory(std::vector<std::string>* array)
        {
            //通过调用函数directory_iterator遍历
            for(auto &p : fs::directory_iterator(_filename))
            {
                //还要进行判断是否是普通文件,因为目录下还可能存在目录,不能将目录传上去
                //使用接口is_directory函数来判断是否是一个目录
                if(fs::is_directory(p) == true)
                {
                    //如果是一个目录就跳过
                    continue;
                }
                //如果不是目录就将其添加到数组中,注意p不是string类对象,所以不能直接添加
                // 要使用这个path先来进行实例化,然后调用对应的函数,数组中保存的是带路径的文件名
                // 所以可以使用relative_path,获取相对路径,返回值也是path对象,然后调用string()
                // 获取的就是带路径名的string对象
                array->push_back(fs::path(p).relative_path().string());
            }
            return true;
        }
        //将文件删除
        bool Remove()
        {
            //如果文件不存在直接返回
            if(Exist() == false)
            {
                return true;
            }
            //删除文件后返回
            remove(_filename.c_str());
            return true;
        }

测试:思想就是创建一个目录,然后遍历这个目录,最后再删除

因为用到C++14所以要链接库-lstdc++fs

g++ cloud_test.cpp -o cloud_test -lpthread -lstdc++fs

测试:

//测试读取目录中的文件
void FileUtilTest3(const std::string &filename)
{
    //实例化一个对象
    nmzcloud::FileUtil fu(filename);
    //创建目录
    fu.CreateDirectory();
    //遍历
    std::vector<std::string> arr;
    fu.ScanDirectory(&arr);
    for(auto& e : arr)
    {
        std::cout << e << std::endl;
    }
}
int main(int argc,char* argv[])
{
    FileUtilTest3(argv[1]);
}

因为创建目录后里面没有文件,所以没有打印内容:但是创建出来文件了:

image-20230908151425464

我们在创建的目录下创建几个文件之后再运行:发现打印出来而且都是带有路径名

image-20230908151640844

文件压缩和解压缩

bundle库实现文件压缩和解压缩

        //文件的压缩-参数是压缩包名称,把压缩之后的数据访问压缩包文件中
        bool Compress(const std::string &packname)
        {
            //1、我们要先把文件中的数据读出来
            std::string body;
            if(GetContent(&body) == false)
            {
                std::cout << "compress get file content failed!\n";
                return false;
            }
            //2、对数据进行压缩-使用pack函数进行压缩
            std::string packed = bundle::pack(bundle::LZIP,boby);
            //此时packed里面放的就是压缩之后的数据
            //3、将要压缩的数据存储到压缩包文件中
            nmzcloud::FileUtil fu(packname);
            //将packed里面的数据写入到新文件packname中
            if(fu.SetContent(packed) == false)
            {
                std::cout << "compress write packed data failed!\n";
                return false;
            }
            return true;
        }
        //文件的解压缩-参数是解压缩之后的文件名称-把解压后的数据放入该文件中
        bool UnCompress(const std::string &unpackname)
        {
            //1、解压缩文件,将压缩包中的文件数据提取出来
            std::string body;
            //调用函数读取数据
            if(GetContent(&body) == false)
            {
                std::cout << "uncompress get file content failed!\n";
                return false;
            }
            // 2、对解压的数据进行解压缩
            std::string unpacked = bundle::unpack(body);
            //3、将压缩之后的数据写入新文件
            nmzcloud::FileUtil fu(unpackname);
            //将packed里面的数据写到新文件中
            if(fu.SetContent(unpacked) == false)
            {
                std::cout << "uncompress write packed data failed!\n";
                return false;
            }
            return true;
        }

测试:

//测试压缩和解压缩
void FileUtilTest4(const std::string &filename)
{
    nmzcloud::FileUtil fu(filename);
    //压缩包名称
    std::string packedname = filename + ".lz";
    fu.Compress(packedname);
    //压缩之后进行解压
    nmzcloud::FileUtil nfu(packedname);
    nfu.UnCompress("uncompress.txt");
}
int main(int argc,char* argv[])
{
    FileUtilTest4(argv[1]);
}

测试的时候要链接bundle.cpp生成的静态库

g++ $^ -o $@ -lpthread -lstdc++fs -L../lib -lbundle

运行之后生成对应的文件解压之后对比两者的md5值:

image-20230908153723200

Json实用工具类实现

Json工具类主要实现json序列化和json反序列化,因为jsoncpp提供的接口需要使用到智能指针等,外部使用起来比较麻烦,所以就将其封装起来:将其定义为静态成员函数:

    class JsonUtil{
    public:
        // 序列化就是将Json::Value中的数据转换成为json格式的字符串
        static bool Serialize(const Json::Value& root,std::string *str)
        {
            Json::StreamWriterBuilder swb;
            std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
            //调用write将root中的内容写入到字符串流中
            std::stringstream ss;
            //如果返回值不等于0说明写入时有错误
            if(sw->write(root,&ss) != 0)
            {
                std::cout << "json write failed!\n";
                return false;
            }
            *str = ss.str();
            return true;
        }
        //反序列化就是将json格式的字符串放入到Json::Value对象中
        static bool UnSerialize(const std::string& str,Json::Value *root)
        {
            Json::CharReaderBuilder crb;
            std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
            //用来记录错误信息
            std::string err;
            if(cr->parse(str.c_str(),str.c_str()+str.size(),root, &err) == false)
            {
                std::cout << "prase error:" << err << std::endl;
                return false;
            }
            return true;
        }
    };

测试的时候就是先将多个数据对象保存在Json::Value中,然后将Json::Value中的数据转换成为json格式的字符串,再将json格式的字符串转换成Json::Value中的数据。

将jsoncpp库链接起来:

g++ $^ -o $@ -lpthread -lstdc++fs -L../lib -lbundle -ljsoncpp

测试代码:

//测试JsonUtil中的序列化和反序列化
void JsonUtilTest()
{
    const char* name = "小明";
    int age = 19;
    float score[] = {99.6,64.8,85.6};
    //将其放到Json::Value中
    Json::Value root;
    root["姓名"] = name;
    root["年龄"] = age;
    for(int i = 0 ; i < 3; i++)
    {
        root["成绩"].append(score[i]);
    }
    //序列化直接调用其中的函数
    std::string str;
    nmzcloud::JsonUtil::Serialize(root,&str);
    std::cout << str << std::endl;
    //再进行反序列化
    Json::Value tmp;
    nmzcloud::JsonUtil::UnSerialize(str,&tmp);
    //将root中的数据打印出来
    std::cout << tmp["姓名"].asString() << std::endl;
    std::cout << tmp["年龄"].asInt() << std::endl;
    for(int i = 0; i < 3; i++)
    {
        std::cout << tmp["成绩"][i].asFloat() << std::endl;
    }
}
int main(int argc,char* argv[])
{
   JsonUtilTest();
}

运行结果:

image-20230909090938166

配置文件加载模块

首先我们定义一个配置信息:

{
    "hot_time" : 30, 
    "server_port":9090,
    "server_ip":"121.4.57.191",
    "download_prefix":"/download/",
    "pack_dir":"./packdir/",
    "packfile_suffix":".lz",
    "back_dir":"./backdir/",
    "backup_file":"./cloud.dat"
}

采用单例类来进行设计:

//单例模式配置类实现
#ifndef __CONFIG_H__
#define __CONFIG_H__
#include "util.hpp"
#include <mutex>
//采用宏定义配置文件
#define CONFIG_FILE "./cloud.conf"
namespace nmzcloud{
    class Config{
    private:
        //配置信息中管理的数据
        int _hot_time;//热点时间
        std::string _server_ip;//服务器ip地址
        int _server_port;//服务器端口号
        std::string _download_prefix;//下载的前缀名
        std::string _packfile_suffix;//压缩包的后缀名称
        std::string _pack_dir;//压缩包存放目录
        std::string _back_dir;//备份文件存放目录
        std::string _backup_file;//全部已备份文件,方便查看已备份文件
    private:
        //单例模式-构造函数私有化
        Config()
        {
            //在构造函数中对各个成员变量进行赋值,将配置信息数据放入对应位置
            ReadConfigFile();
        }
        //在类内实例化静态成员指针
        static Config* _instance;
        //定义一个互斥锁-读的时候不加锁,写的时候加锁
        static std::mutex _mutex;
    private:
        //提供一个接口读取配置文件,将配置文件中的数据放到对应的成员变量中
        bool ReadConfigFile()
        {
            //采用文件工具类
            nmzcloud::FileUtil fu(CONFIG_FILE);
            //获取文件中的全部数据
            std::string body;
            if(fu.GetContent(&body) == false)
            {
                std::cout << "load config file failed!\n";
                return false;
            }
            //进行反序列化,将字符串中的内容放到Json::Value中
            Json::Value root;
            if(nmzcloud::JsonUtil::UnSerialize(body,&root) == false)
            {
                std::cout << "parse config file failed!\n";
                return false;
            }
            //把配置文件中的信息读出来
            _hot_time = root["hot_time"].asInt();//热点时间
            _server_port = root["server_port"].asInt();//服务器端口信息
            _server_ip = root["server_ip"].asString();//服务器ip
            _backup_file = root["backup_file"].asString();//全部已备份信息
            _back_dir = root["back_dir"].asString();//热点文件存放路径
            _pack_dir = root["pack_dir"].asString();//非热点文件存放路径
            _download_prefix = root["download_prefix"].asString();//下载的前缀
            _packfile_suffix = root["packfile_suffix"].asString();//压缩包的后缀名称
            return true;
        }
    public:
        //
        static Config* GetInstance()
        {
            //二次检测,如果为空再次进行检测
            if(_instance == nullptr)
            {
                _mutex.lock();
                if(_instance == nullptr)
                {
                    _instance = new Config;
                }
                //创建完成之后进行解锁
                _mutex.unlock();
            }
            return _instance;
        }
        //提供统一的接口获取配置文件中的信息
        // 获取热点时间
        int GetHotTime()
        {
            return _hot_time;
        }
        //获取服务端端口
        int GetServerPort()
        {
            return _server_port;
        }
        //获取服务端ip地址
        std::string GetServerIp()
        {
            return _server_ip;
        }
        //获取文件下载前缀
        std::string GetDownloadPrefix()
        {
            return _download_prefix;
        }
        //获取压缩包后缀
        std::string GetPackfileSuffix()
        {
            return _packfile_suffix;
        }
        //获取非热点文件存放目录
        std::string GetPackDir()
        {
            return _pack_dir;
        }
        //获取热点文件存放目录
        std::string GetBackDir()
        {
            return _back_dir;
        }
        //获取存放全部已备份文件的路径名
        std::string GetBackUpFile()
        {
            return _backup_file;
        }
    };
    //静态成员在类外进行定义
    Config* Config::_instance = nullptr;
    std::mutex Config::_mutex;
}
#endif

测试:主要检测的就是获取配置文件中所有的信息

//配置单例类的测试
void ConfigTest()
{
    //检测的就是获取配置文件中所有的信息
    nmzcloud::Config* config = nmzcloud::Config::GetInstance();
     std::cout << config->GetHotTime() <<std::endl;
    std::cout << config->GetServerPort() <<std::endl;
    std::cout << config->GetServerIp() <<std::endl;

    std::cout << config->GetDownloadPrefix() <<std::endl;
    std::cout << config->GetPackfileSuffix() <<std::endl;
    std::cout << config->GetPackDir() <<std::endl;

    std::cout << config->GetBackDir() <<std::endl;
    std::cout << config->GetBackUpFile() <<std::endl;
}
int main(int argc,char* argv[])
{
   ConfigTest();
}

测试结果:

image-20230909100529132

服务端数据管理模块

数据信息结构体类:

    //数据信息结构体
    struct BackupInfo{
        bool pack_flag;//是否被压缩的标志
        size_t fsize;//文件大小
        time_t atime;//最后一次访问时间
        time_t mtime;//最后一次修改时间
        std::string real_path;//文件的实际存储路径
        std::string pack_path;//压缩包存储路径名称
        std::string url;//请求的资源路径
        //获取文件各项信息,将文件信息填充到BackupInfo中
        void NewBackupInfo(const std::string realpath)
        {
            //实例化一个工具类对象
            nmzcloud::FileUtil fu(realpath);
            //压缩标志位为false
            pack_flag = false;
            //调用函数完成文件大小计算及访问和修改时间
            fsize = fu.FileSize();
            mtime = fu.LastMTime();
            atime = fu.LastATime();
            //文件的实际存储路径等于我们传入的路径
            real_path = realpath;
            //获取单例类的操作句柄
            nmzcloud::Config* config = nmzcloud::Config::GetInstance();
            //获取压缩文件目录和压缩包后缀名
            std::string packdir = config->GetPackDir();
            std::string packsuffix = config->GetPackfileSuffix();
            //压缩包相对完整路径 -压缩文件目录+文件名+压缩包后缀
            pack_path = packdir + fu.FileName() + packsuffix;
            //获取下载前缀
            std::string download_prefix = config->GetDownloadPrefix();
            url = download_prefix + fu.FileName();//下载前缀+纯文件名称
        }
    };

测试:

// 通过传递一个参数将文件信息获取出来
void DataTest(const std::string &filename)
{
    nmzcloud::BackupInfo info;
    info.NewBackupInfo(filename);
    //调用这个函数之后就会把所有的信息赋值,我们输出一遍进行查看
    std::cout << info.pack_flag << std::endl;
    std::cout << info.fsize << std::endl;
    std::cout << info.mtime << std::endl;
    std::cout << info.atime << std::endl;
    std::cout << info.real_path << std::endl;
    std::cout << info.pack_path << std::endl;
    std::cout << info.url << std::endl;
}
int main(int argc,char* argv[])
{
   DataTest(argv[1]);
}

运行结果:

image-20230909113922563

当我们传入的文件不存在时:会获取失败,所以我们要对代码进行修改,再加上

image-20230909114024076

所以我们要对代码进行修改,再加上一条判断语句,判断是否存在:

bool NewBackupInfo(const std::string realpath)
        {
            //实例化一个工具类对象
            nmzcloud::FileUtil fu(realpath);
            //判断文件是否存在
            if(fu.Exist() == false)
            {
                std::cout << "new backupinfo file not exists!\n";
                return false;
            }
            //压缩标志位为false
            pack_flag = false;
            //调用函数完成文件大小计算及访问和修改时间
            fsize = fu.FileSize();
            mtime = fu.LastMTime();
            atime = fu.LastATime();
            //文件的实际存储路径等于我们传入的路径
            real_path = realpath;
            //获取单例类的操作句柄
            nmzcloud::Config* config = nmzcloud::Config::GetInstance();
            //获取压缩文件目录和压缩包后缀名
            std::string packdir = config->GetPackDir();
            std::string packsuffix = config->GetPackfileSuffix();
            //压缩包相对完整路径 -压缩文件目录+文件名+压缩包后缀
            pack_path = packdir + fu.FileName() + packsuffix;
            //获取下载前缀
            std::string download_prefix = config->GetDownloadPrefix();
            url = download_prefix + fu.FileName();//下载前缀+纯文件名称
            return true;
        }

修改之后:

image-20230909114505634

数据管理类-增改查

// 数据管理类
    class DataManager{
    private:
        //数据信息访问是通过哈希表来进行存储的,而持久化存储是通过文件来进行存储的
        std::string _backup_file;//持久化存储的文件
        //这是一个哈希表,首先是通过string作为key值,以BackupInfo作为val值存储在内存中的
        std::unordered_map<std::string ,BackupInfo> _table;
        //使用读写锁-读共享,写互斥-头文件<pthread.h>
        pthread_rwlock_t _rwlock;
    public:
        DataManager()
        {
            //持久化存储文件从配置文件中读取,直接通过配置单例类来获取
            // backup_file是实际的持久化存储文件,在配置信息里面所有的,通过配置信息类来获取
            _backup_file = Config::GetInstance()->GetBackUpFile();
            //然后就是锁的初始化,调用函数
            pthread_rwlock_init(&_rwlock,NULL);
        }
        //析构函数中要对读写锁进行释放
        ~DataManager()
        {
            pthread_rwlock_destroy(&_rwlock);
        }
        // 初始化加载,在每次系统重启都要加载以前的数据
        bool InitLoad();
        //新增数据,新增的是BackupInfo,以url作为key存入哈希表中就可以了
        bool Insert(const BackupInfo& info)
        {
            //每次新增的时候就要加锁
            pthread_rwlock_wrlock(&_rwlock);
            //url和info形成键值对
            _table[info.url] = info;
            //然后解锁
            pthread_rwlock_unlock(&_rwlock);
            return true;
        }
        //修改数据-压缩之后标志位改变
        bool Update(const BackupInfo& info)
        {
            //修改和新增本质差不多
            pthread_rwlock_wrlock(&_rwlock);
            _table[info.url] = info;//因为当key值相同时会覆盖掉之前的值
            pthread_rwlock_unlock(&_rwlock);
            return true;
        }
        // 查询接口,当我们要下载时获取的是单个文件的信息,如果是要展示的界面获取的是文件所有的信息
        bool GetOneByUrl(const std::string &url,BackupInfo* info)
        {
            //通过url来查询数据,因为要操作table,所以进行一个加锁操作
            pthread_rwlock_wrlock(&_rwlock);
            //因为url是table表中的key值可以通过find来查找
            auto it = _table.find(url);
            pthread_rwlock_unlock(&_rwlock);
            if(it == _table.end())
            {
                //没有找到
                return false;
            }
            *info = it->second;
            return true;
        }
        //根据文件实际存储路径获取文件信息,获取不到说明文件没有插入成功,重新插入
        bool GetOneByRealPath(const std::string &realpath,BackupInfo* info)
        {
            //这里不能根据key值去找,所以只能遍历去找
            //先加锁
            pthread_rwlock_wrlock(&_rwlock);
            auto it = _table.begin();
            for(;it != _table.end();it++)
            {
                if(it->second.real_path == realpath)
                {
                    *info = it->second;
                    //解锁
                    pthread_rwlock_unlock(&_rwlock);
                    return true;
                }
            }
            pthread_rwlock_unlock(&_rwlock);
            return false;
        }
        //获取所有的文件信息-将数据放到数组中
        bool GetAll(std::vector<BackupInfo>* arr)
        {
            pthread_rwlock_wrlock(&_rwlock);
            auto it = _table.begin();
            for(;it != _table.end();it++)
            {
                arr->push_back(it->second);
            }
            pthread_rwlock_unlock(&_rwlock);
            return true;
        }
        // 每次数据新增或者修改都要持久化存储,避免数据丢失
        bool Storage();
    };
//测试数据管理类的增改查
void DataManagerTest1(const std::string& filename)
{
    nmzcloud::BackupInfo info;
    info.NewBackupInfo(filename);
    nmzcloud::DataManager data;
    data.Insert(info);
    //测试获取单个信息
    nmzcloud::BackupInfo tmp;
    data.GetOneByUrl("/download/bundle.h",&tmp);
    std::cout <<"-------------insert and getonebyurl -------------\n";
    //调用这个函数之后就会把所有的信息赋值,我们输出一遍进行查看
    std::cout << tmp.pack_flag << std::endl;
    std::cout << tmp.fsize << std::endl;
    std::cout << tmp.mtime << std::endl;
    std::cout << tmp.atime << std::endl;
    std::cout << tmp.real_path << std::endl;
    std::cout << tmp.pack_path << std::endl;
    std::cout << tmp.url << std::endl;

    //更改压缩标志位
    std::cout <<"---------------update and getall --------------\n";
    info.pack_flag = true;
    data.Update(info);
    std::vector<nmzcloud::BackupInfo> array;
    data.GetAll(&array);
    for(auto &a :array)
    {
        std::cout << a.pack_flag << std::endl;
        std::cout << a.fsize << std::endl;
        std::cout << a.mtime << std::endl;
        std::cout << a.atime << std::endl;
        std::cout << a.real_path << std::endl;
        std::cout << a.pack_path << std::endl;
        std::cout << a.url << std::endl;
    }
    //根据实际路径来查
    std::cout <<"---------------------getonbyrealpath--------------\n";
    data.GetOneByRealPath(filename,&tmp);
    std::cout << tmp.pack_flag << std::endl;
    std::cout << tmp.fsize << std::endl;
    std::cout << tmp.mtime << std::endl;
    std::cout << tmp.atime << std::endl;
    std::cout << tmp.real_path << std::endl;
    std::cout << tmp.pack_path << std::endl;
    std::cout << tmp.url << std::endl;
}
int main(int argc,char* argv[])
{
   DataManagerTest1(argv[1]);
}

运行结果:

image-20230909144321972

持久化存储

// 每次数据新增或者修改都要持久化存储,避免数据丢失
        bool Storage()
        {
            //将所有的数据获取出来,转换成Json格式的字符串存储到文件中
            // 1、获取所有数据
            std::vector<nmzcloud::BackupInfo> array;
            GetAll(&array);
            // 2、将所有的数据放到Json::Value类对象中
            Json::Value root;
            for(int i = 0; i < array.size();i++)
            {
                Json::Value item;
                item["pack_flag"] = array[i].pack_flag;//是否压缩标志
                item["real_path"] = array[i].real_path;//实际存储路径-根据这个路径获取资源
                item["fsize"] = (Json::Int64)array[i].fsize;//文件大小
                item["atime"] = (Json::Int64)array[i].atime;//文件最后一次访问时间
                item["mtime"] = (Json::Int64)array[i].mtime;//文件最后一次修改时间
                item["pack_path"] = array[i].pack_path;//文件压缩包存储路径
                item["url"] = array[i].url;//请求资源路径
                //添加数组元素使用append,添加到root里面作为整体文件备份信息数组里面的一个元素
                root.append(item);
            }
            //3、对Json::Value对象数组进行序列化,存放到body中
            std::string body;
            //调用函数进行序列化
            nmzcloud::JsonUtil::Serialize(root,&body);
            //4、存储文件-_backup_file是成员变量持久化存储文件
            nmzcloud::FileUtil fu(_backup_file);
            //将数据写入文件
            fu.SetContent(body);
            return true;
        }

在新增或者修改的时候就调用持久化存储函数:

	//新增数据,新增的是BackupInfo,以url作为key存入哈希表中就可以了
        bool Insert(const BackupInfo& info)
        {
            //每次新增的时候就要加锁
            pthread_rwlock_wrlock(&_rwlock);
            //url和info形成键值对
            _table[info.url] = info;
            //然后解锁
            pthread_rwlock_unlock(&_rwlock);
            Storage();
            return true;
        }
        //修改数据-压缩之后标志位改变
        bool Update(const BackupInfo& info)
        {
            //修改和新增本质差不多
            pthread_rwlock_wrlock(&_rwlock);
            _table[info.url] = info;//因为当key值相同时会覆盖掉之前的值
            pthread_rwlock_unlock(&_rwlock);
            Storage();
            return true;
        }

此时我们仍旧用上面的测试代码进行测试:

在打印出上述内容的同时会生成一个文件用来记录文件信息:

image-20230909150111987

初始化

初始化函数就是将配置文件中已经存储的信息都拿出来,放入backupinfo中:

// 初始化加载,在每次系统重启都要加载以前的数据
        bool InitLoad()
        {
            //打开文件,将其中的数据读取出来
            FileUtil fu(_backup_file);
            //当文件不存在
            if(fu.Exist() == false)
            {
                std::cout << "InitLoad file not exist!\n";
                return false;
            }
            std::string body;
            //将数据放入body中
            fu.GetContent(&body);
            //进行反序列化-将字符串放入Json::Value中
            Json::Value root;
            JsonUtil::UnSerialize(body,&root);
            //将反序列化得到的数据添加到table中
            for(int i = 0 ; i < root.size();i++)
            {
                //定义一个对象
                BackupInfo info;
                //注意这时候不能使用NewBackupinfo,因为信息都是从配置文件中拿来的
                info.atime = root[i]["atime"].asInt64();
                info.mtime = root[i]["mtime"].asInt64();
                info.fsize = root[i]["fsize"].asInt64();
                info.pack_flag = root[i]["pack_flag"].asBool();
                info.pack_path = root[i]["pack_path"].asString();
                info.real_path = root[i]["real_path"].asString();
                info.url = root[i]["url"].asString();
                //调用一个插入接口
                Insert(info);
            }
            return true;
        }

在构造函数中加载:

        DataManager()
        {
            //持久化存储文件从配置文件中读取,直接通过配置单例类来获取
            // backup_file是实际的持久化存储文件,在配置信息里面所有的,通过配置信息类来获取
            _backup_file = Config::GetInstance()->GetBackUpFile();
            //然后就是锁的初始化,调用函数
            pthread_rwlock_init(&_rwlock,NULL);
            InitLoad();
        }

测试:此时经过上面的测试,此时配置文件中已经有数据了,所以直接测试

持久化存储文件信息:

image-20230909152249295

void DataManagerTest2()
{
    nmzcloud::DataManager data;//实例化对象的时候就会加载配置文件,加载之后里面就会有数据
    std::vector<nmzcloud::BackupInfo> array;
    data.GetAll(&array);
    for(auto &a: array)
    {
        std::cout << a.pack_flag << std::endl;
        std::cout << a.fsize << std::endl;
        std::cout << a.mtime << std::endl;
        std::cout << a.atime << std::endl;
        std::cout << a.real_path << std::endl;
        std::cout << a.pack_path << std::endl;
        std::cout << a.url << std::endl;
    }
}
int main(int argc,char* argv[])
{
   DataManagerTest2();
}

image-20230909152316212

热点管理模块

//热点管理类
#ifndef __HOT_H__
#define __HOT_H__
#include "util.hpp"
#include "data.hpp"
#include "config.hpp"
#include <unistd.h>
extern nmzcloud::DataManager* _data;
namespace nmzcloud{
    //将数据管理对象的指针传递进来
    class HotManager{
    private:
        std::string _back_dir;//备份文件路径/目录
        std::string _pack_dir;//压缩文件路径
        int _hot_time;//热点判断时间
        std::string _pack_suffix;//压缩包后缀名
    private:
        //热点判断接口,非热点文件返回真,热点文件返回假
        bool HotJudge(const std::string& filename)
        {
            nmzcloud::FileUtil fu(filename);
            //获取文件最后一次访问时间
            time_t last_atime = fu.LastATime();
            //获取当前系统时间
            time_t cur_time = time(NULL);
            //判断如果当前时间-最后一次访问时间大于热点时间
            if(cur_time - last_atime > _hot_time)
            {
                return true;
            }
            return false;
        }
    public:
        HotManager()
        {
            //构造函数获取操作句柄
            nmzcloud::Config* config = nmzcloud::Config::GetInstance();
            _back_dir = config->GetBackDir();
            _pack_dir = config->GetPackDir();
            _pack_suffix = config->GetPackfileSuffix();
            _hot_time = config->GetHotTime();
            //如果不存在存储路径和压缩路径就创建他
            nmzcloud::FileUtil tmp1(_back_dir);
            tmp1.CreateDirectory();
            nmzcloud::FileUtil tmp2(_pack_dir);
            tmp2.CreateDirectory();
        }
        //运行模块函数
        bool RunModule()
        {
            //整体的模块并不是一次就运行完的,所以要死循环
            while(1)
            {
                // 1、遍历备份目录,获取所有文件名
                //先实例化文件操作工具
                nmzcloud::FileUtil fu(_back_dir);
                //调用函数浏览目录,放在数组中
                std::vector<std::string> array;
                fu.ScanDirectory(&array);
                // 2、遍历判断文件是否是非热点文件
                for(auto &a: array)
                {
                    if(HotJudge(a) == false)
                    {
                        //是热点文件并不需要进行处理
                        continue;
                    }
                    //如果是非热点文件就要进行压缩
                    nmzcloud::FileUtil tmp(a);
                    //数据管理结构体
                    nmzcloud::BackupInfo info;
                    //然后使用全局数据管理获取数据
                    if(_data->GetOneByRealPath(a,&info) == false)
                    {
                        //此时说明文件存在,但是没有备份信息,漏记录了
                        //设置新的备份信息
                        info.NewBackupInfo(a);
                    }
                    //对非热点文件进行压缩,此时pack_path就是压缩包路径名称
                    tmp.Compress(info.pack_path);
                    // 删除源文件,修改备份信息
                    tmp.Remove();
                    info.pack_flag = true;
                    _data->Update(info);
                }
                //避免空目录循环遍历,消耗cpu资源过高
                usleep(1000);//1000微秒也就是1ms,unistd.h头文件
            }
            return true;
        }
    };
}
#endif

测试:

//测试热点管理类
//定义一个全局变量
nmzcloud::DataManager* _data;
void HotManagerTest()
{
    //实例化一个数据管理类对象
    _data = new nmzcloud::DataManager;
    nmzcloud::HotManager hot;
    hot.RunModule();
}
int main(int argc,char* argv[])
{
   HotManagerTest();
}

我们是先将一个文件拷贝到backdir目录下,经过配置文件中的热点时间(这里是30s之后),再查看packdir目录下是否多了一个压缩包:运行起来之后,程序一直运行,我们观察现象:

image-20230909171257825

image-20230909171332047

服务端业务处理模块

//服务端业务处理模块
#ifndef __SERVICE_H__
#define __SERVICE_H__
#include "hot.hpp"
#include "httplib.h"
extern nmzcloud::DataManager* _data;//因为也会访问数据管理类
namespace nmzcloud{
    class Service{
    private:
        int _server_port;//服务器端口号
        std::string _server_ip;//服务器ip地址
        std::string _download_prefix;//下载请求前缀,带有这个前缀就认为是文件下载请求
        //上面三个都可以从配置信息中拿
        //搭建服务器使用的是httplib库-实例化一个server对象,通过上面三个信息搭建一个服务器
        httplib::Server _server;
    public:
        //构造函数完成各个成员变量的初始化
        Service()
        {
            //_server直接实例化完毕
            nmzcloud::Config* config = nmzcloud::Config::GetInstance();
            _server_port = config->GetServerPort();
            _server_ip = config->GetServerIp();
            _download_prefix = config->GetDownloadPrefix();
            //当文件的备份目录不存在要进行创建
            std::string back_dir = config->GetBackDir();
            FileUtil fu(back_dir);
            if(fu.Exist() == false)
            {
                fu.CreateDirectory();
            }
        }
        //运行模块
        bool RunModule()
        {
            //Post请求的upload表示文件资源的上传,这里的处理函数必须是静态的
            _server.Post("/upload",Upload);
            //文件列表查看请求
            _server.Get("/listshow",ListShow);
            _server.Get("/",ListShow);
            //文件下载请求
            //正则表达式 .*表示匹配任意一个字符任意次
            std::string download_url = _download_prefix + "(.*)";
            _server.Get(download_url,DownLoad);
            // 监听
            _server.listen(_server_ip,_server_port);
            return true;
        }
    private:
         //业务处理请求-文件上传请求、展示页面获取请求以及文件下载请求
         //业务上传请求处理
         static void Upload(const httplib::Request& req,httplib::Response& rsp)
         {
            //文件上传是一个Post请求的upload才可以
            //判断有没有这个上传的文件区域
            auto ret = req.has_file("file");
            if(ret == false)
            {
                rsp.status = 400;//设置状态码为400
                return ;
            }
            //如果有的话就获取文件数据,file是MultipartFormData类型的数据
            const auto& file = req.get_file_value("file");
            //然后需要在备份目录下创建这个文件并把数据写进去
            std::string back_dir = Config::GetInstance()->GetBackDir();//获取备份目录
            //获取文件的实际存储路径
            std::string realpath = back_dir + FileUtil(file.filename).FileName();
            FileUtil fu(realpath);
            //将文件中的正文数据放入文件
            fu.SetContent(file.content);
            //组织备份的文件信息
            BackupInfo info;
            info.NewBackupInfo(realpath);
            //向数据管理模块添加备份的文件信息
            _data->Insert(info);
            return ;
         }
        //  获取页面展示请求的业务处理函数
        static void ListShow(const httplib::Request& req,httplib::Response& rsp)
        {

        }
        // 文件下载请求的处理函数
        static void DownLoad(const httplib::Request& req,httplib::Response& rsp)
        {

        }
    };
}
#endif

上面完成的是文件请求处理和构造函数以及运行模块:

我们测试的时候需要将htpplib.h移动当前文件夹中,同时要将配置文件中的ip地址改为0.0.0.0否则无法监听。

在这个类中也要定义一个数据管理类:

此时编译:

//测试服务端业务处理-业务上传请求
//定义一个全局变量
nmzcloud::DataManager* _data;
void ServiceTest()
{
    _data = new nmzcloud::DataManager;
    nmzcloud::Service server;
    server.RunModule();
}
int main(int argc,char* argv[])
{
   ServiceTest();
}

我们通过html页面上传文件,查看是否在backdir目录下:

<html>
	<body>
		<form action = "http://121.4.57.191:9090/upload" method = "post"
		enctype = "multipart/form-data">
		<div>
			<input type = "file" name = "file">
		</div>
		<div><input type = "submit" value = "上传"></div>
		</form>
	</body>
</html>

image-20230909191038709

首次上传时候出现提示是因为持久化存储文件不存在,后面再进行持久化存储的时候才会创建起来:

image-20230909192135889

持久化存储文件中内容:

image-20230909192357304

展示页面请求处理实现

//实现一个接口将时间戳转换为字符串
        static std::string TimetoString(time_t t)
        {
            return std::ctime(&t);
        }
        //  获取页面展示请求的业务处理函数
        static void ListShow(const httplib::Request& req,httplib::Response& rsp)
        {
            //将时间戳转换成时间格式
            // 1、获取所有的文件备份信息
            // 服务器一运行就从原来备份的持久化存储文件中加载数据
            std::vector<BackupInfo> array;
            //获取到所有的数据信息
            _data->GetAll(&array);
            // 2、根据备份信息组织成一个html文件数据
            std::stringstream ss;//使用字符串流
            ss << "<html><meta charset = 'utf-8'><head><title>DownLoad</title></head>";
            ss << "<body><h1>DownLoad</h1><table>";
            for(auto &a :array)
            {
                //根据每一个文件组织中间的数据
                ss << "<tr>";
                //url链接随着资源的不同而不同
                //html中不区分单引号和双引号,所以改成单引号
                std::string filename = FileUtil(a.real_path).FileName();//只要文件名
                ss << "<td> <a href = '" << a.url << "'>" << filename << "</a></td>";
                //要将时间戳转换为时间格式
                ss << "<td align = 'right'>" << TimetoString(a.atime) << "</td>";
                ss << "<td align = 'right'>" << a.fsize/1024 << "K</td>"; //文件大小,以K为单位
                ss << "</tr>";
            }
            ss<< "</table></body></html>";//结尾
            rsp.body = ss.str();
            //设置头部信息,正文内容是html类型文本
            rsp.set_header("Content-Type","text/html");
            rsp.status = 200;
            return ;
        }

测试,我们直接在浏览器输入内容进行查看已备份的文件:我将其网页字符集设置为utf-8编码格式:

运行之后:

image-20230909194613985

下载请求的处理实现

根据Etag判断文件是否修改:

//先生成ETag判断文件是否修改-根据文件名-文件大小-文件最后一次修改时间组成
        static std::string GetETag(const BackupInfo& info)
        {
            FileUtil fu(info.real_path);
            std::string ETag = fu.FileName();
            ETag += "-";
            ETag += std::to_string(info.fsize);
            ETag += "-";
            ETag += std::to_string(info.mtime);
            return ETag;
        }
        // 文件下载请求的处理函数
        static void DownLoad(const httplib::Request& req,httplib::Response& rsp)
        {
            //1、获取客户端请求的资源路径path,其实就是req里面的path
            // 2、根据req资源路径,获取文件备份信息
            BackupInfo info;
            _data->GetOneByUrl(req.path,&info);
            // 3、判断文件是否被压缩,如果被压缩要先解压
            if(info.pack_flag == true)
            {
                FileUtil fu(info.pack_path);
                fu.UnCompress(info.real_path);
                //删除压缩包,修改文件信息
                info.pack_flag = false;
                fu.Remove();
                //更新
                _data->Update(info);
            }
            //4、读取文件数据,放入rsp.body中
            FileUtil fu(info.real_path);
            fu.GetContent(&rsp.body);
            // 5、设置响应头部字段ETag
            rsp.set_header("Accept-Ranges","bytes");
            rsp.set_header("ETag",GetETag(info));
            //设置头部字段,表示如何处理数据,这里表示想要的是二进制流,常用于文件下载
            rsp.set_header("Content-Type","application/octet-stream");
            rsp.status = 200;
        }

下载功能测试:

运行下载之后再验证md5是否一致:

image-20230909200345763

此时进行下载测试的时候为什么会下载之后暂停一会就会重新下载

而且当我们在下载时中断服务器的时候再次点击下载会下载一个1B的文件。

断点续传实现

断点续传在Service类的下载请求类中实现:

在httplib中已经实现了,其实前面的代码改不改都可以:

    // 文件下载请求的处理函数
            static void DownLoad(const httplib::Request& req,httplib::Response& rsp)
            {
                //1、获取客户端请求的资源路径path,其实就是req里面的path
                // 2、根据req资源路径,获取文件备份信息
                BackupInfo info;
                _data->GetOneByUrl(req.path,&info);
                // 3、判断文件是否被压缩,如果被压缩要先解压
                if(info.pack_flag == true)
                {
                    FileUtil fu(info.pack_path);
                    fu.UnCompress(info.real_path);
                    //删除压缩包,修改文件信息
                    info.pack_flag = false;
                    fu.Remove();
                    //更新
                    _data->Update(info);
                }
                //4、读取文件数据,放入rsp.body中
                FileUtil fu(info.real_path);
                bool retrans = false;//标记当前是否是断点续传
                std::string old_etag;//下载时已有的ETag的值
                //判断是否存在If-Range头部字段,如果有就是断点续传,如果没有就不是
                if(req.has_header("If-Range"))
                {
                    old_etag = req.get_header_value("If-Range");
                    //如果没有If-Range字段则是正常下载,或者如果有这个字段,但是与当前etag不一致
                    //也必须重新下载
                    //有If-Range字段且这个字段的值与请求文件的最新的etag一致则符合断点续传
                    if(old_etag == GetETag(info))
                    {
                        retrans = true;
                    }
                }
                //如果retrans == false 则是文件的正常下载
                if(retrans == false)
                {
                    fu.GetContent(&rsp.body);
                    //5、设置响应头部字段ETag
                    // 5、设置响应头部字段ETag,Accept-Ranges:bytes
                    rsp.set_header("Accept-Ranges","bytes");
                    rsp.set_header("ETag",GetETag(info));
                    //设置头部字段,表示如何处理数据,这里表示想要的是二进制流,常用于文件下载
                    rsp.set_header("Content-Type","application/octet-stream");
                    rsp.status = 200;
                }
                else
                {
                    //此时表示是一个断点续传,取出Range字段了解请求区间的范围然后去读取指定区间的数据
                    //httplib中已经进行了解析,我们要做的就是读取文件到rsp.body中
                    fu.GetContent(&rsp.body);
                    rsp.set_header("Accept-Ranges","bytes");
                    rsp.set_header("ETag",GetETag(info));
                    //设置头部字段,表示如何处理数据,这里表示想要的是二进制流,常用于文件下载
                    rsp.set_header("Content-Type","application/octet-stream");
                    rsp.status = 206;//其实我们不用给,httplib里都已经操作了
                }
            }

测试,这里需要注意我们的断点续传测试时如果将服务器断开之后,必须重新点击链接才能继续下载。我们可以根据下载的文件名来判断是不是同一个文件。

而且如果修改文件结束之后,下载1B结束。

服务端整体功能联调

我们启动热点管理类和服务类时都是死循环,所以我们要用多线程:头文件

代码保存并退出:

//测试热点管理类
void HotManagerTest()
{
    //实例化一个数据管理类对象
    _data = new nmzcloud::DataManager;
    nmzcloud::HotManager hot;
    hot.RunModule();
}
//测试服务端业务处理-业务上传请求
//定义一个全局变量
nmzcloud::DataManager* _data;
void ServiceTest()
{
    nmzcloud::Service server;
    server.RunModule();
}
int main(int argc,char* argv[])
{
   _data = new nmzcloud::DataManager;
   //创建多线程
   //运行的时是HotManagerTest这个函数,参数是线程入口函数,还可以传递参数,可以传递多个参数
   std::thread thread_hot_manager(HotManagerTest);
   std::thread thread_service(ServiceTest);
    //两个线程创建完毕各自运行一个模块的功能
   //等待线程退出
   thread_hot_manager.join();
   thread_service.join();
}

扩展:

而且我们在热点管理文件的时候可以采用多线程的时候进行压缩,因为目前是一个一个文件进行压缩的,如果有大文件压缩的话其他的文件都没有被压缩,会被卡在这里。

客户端文件操作类

客户端使用的是VS:

#ifndef  __DATA_H__
#define __DATA_H__
#include "util.hpp"
#include <unordered_map>
namespace nmzcloud {
	class DataManager {
	private:
		//备份信息的持久化存储文件
		std::string _backup_file;
		std::unordered_map<std::string, std::string> _table;//映射表
	public:
		//构造函数
		DataManager(const std::string &backup_file)
			:_backup_file(backup_file)
		{
			InitLoad();
		}
		//持久化存储
		bool Storage()
		{
			std::stringstream ss;
			//获取所有的备份信息
			//遍历table获取所有的备份信息
			auto it = _table.begin();
			for (; it != _table.end(); it++)
			{
				//2、将所有的信息进行指定格式的持久化组织,组织成指定格式的数据
				ss << it->first << " " << it->second << "\n";
				//此时ss里面保存的就是组织好的数据
			}
			//3、持久化存储,将数据写入文件就搞定了
			FileUtil fu(_backup_file);
			fu.SetContent(ss.str());
			return true;
		}
		//定义一个字符串分割函数,参数分别是字符串,分隔符,最后一个数组用来存放分割后的数据
		int split(const std::string& str, const std::string& sep, std::vector<std::string>* array)
		{
			int count = 0;
			size_t pos = 0;//pos是分割符位置
			size_t idx = 0;//偏移量位置
			//持久化的一个字符串分割
			while (1)
			{
				//find函数进行查找-第一个参数是要找的字符,第二个参数是从什么位置开始
				pos = str.find(sep, idx);
				if (std::string::npos == pos)
				{
					break;//没找到跳出循环
				}
				//数据之间多个空格或者文件末尾还有空格要进行特殊处理
				if (pos == idx)
				{
					idx = pos + sep.size();
					continue;
				}
				//此时找到了对应空格的位置,进行字符串的截取
				//substr两个参数(截取起始位置,长度)
				std::string tmp = str.substr(idx, pos - idx);
				array->push_back(tmp);
				count++;
				idx = pos + sep.size();
			}
			//当我们跳出循环后,还要进行操作,就是当idx没到字符串的末尾
			if (idx < str.size())
			{
				array->push_back(str.substr(idx));//直接从文件偏移量位置截取到文件末尾
				count++;
			}
			//返回字符串分割之后数据的个数
			return count;
		}
		//初始化加载
		bool InitLoad()
		{
			//从文件中读取所有的数据
			FileUtil fu(_backup_file);
			std::string body;
			fu.GetContent(&body);
			//2、进行数据解析-按照持久化存储文件的格式进行解析-添加到表中
			std::vector<std::string> array;
			//按照换行将其分割
			split(body, "\n", &array);
			//遍历array,再将其按照空格进行分割放入表中
			for (auto& a : array)
			{
				//a格式 b.txt b.txt-文件大小-文件的时间
				std::vector<std::string> tmp;
				split(a, " ", &tmp);
				if (tmp.size() != 2)
				{
					//如果不等于2表明数据有问题
					continue;
				}
				_table[tmp[0]] = tmp[1];//进行hash表的赋值
			}
			return true;
		}
		//插入数据,插入两个,一个是文件名,一个是文件信息
		bool Insert(const std::string& key, const std::string& val)
		{
			_table[key] = val;
			Storage();
			return true;
		}
		//更新
		bool Updata(const std::string& key, const std::string& val)
		{
			_table[key] = val;
			Storage();
			return true;
		}
		//获取其中一个数据
		bool GetOneByKey(const std::string& key, std::string* val)
		{
			//获取其中的数据
			auto it = _table.begin();
			if (it == _table.end())
			{
				return false;
			}
			*val = it->second;
			return true;
		}
	};
}
#endif // ! __DATA_H__


客户端文件备份模块实现

#ifndef  __CLOUD_H__
#define __CLOUD_H__
#include "data.hpp"
#include <Windows.h>
namespace nmzcloud {
	class Backup {
	private:
		std::string _back_dir;
		DataManager* _data;
	public:
		//构造函数-第一个参数是备份目录,第二个参数是备份文件
		Backup(const std::string& back_dir, const std::string& back_file)
			:_back_dir(back_dir)
		{
			_data = new DataManager(back_file);//数据管理类的对象
		}
		//创建文件的唯一标识
		std::string GetFileIdentfier(const std::string& filename)
		{
			FileUtil fu(filename);
			std::stringstream ss;
			ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastMTime();
			return ss.str();
			
		}
		bool RunModule()
		{
			//死循环,一直检测
			while (1)
			{
				//遍历一个目录获取所有的文件信息,然后创建每一个文件标识,最后生成配置文件
				nmzcloud::FileUtil fu(_back_dir);
				std::vector<std::string> array;
				fu.ScanDirectory(&array);
				//遍历数组获取到每一个文件下的名称然后生成文件的唯一标识
				for (auto& a : array)
				{
					std::string id = GetFileIdentfier(a);
					//插入数据一个是文件名称,一个是文件唯一标识
					_data->Insert(a, id);
				}
				Sleep(1);//1ms
			}
		}
	};
}
#endif // ! __CLOUD_H__


客户端文件上传功能接口

#ifndef  __CLOUD_H__
#define __CLOUD_H__
#include "data.hpp"
#include "httplib.h"
#include <Windows.h>
namespace nmzcloud {
#define SERVER_ADDR "121.4.57.191"//ip地址
#define SERVER_PORT 9090//端口
	class Backup {
	private:
		std::string _back_dir;
		DataManager* _data;
	public:
		//构造函数-第一个参数是备份目录,第二个参数是备份文件
		Backup(const std::string& back_dir, const std::string& back_file)
			:_back_dir(back_dir)
		{
			_data = new DataManager(back_file);//数据管理类的对象
		}
		//创建文件的唯一标识
		std::string GetFileIdentfier(const std::string& filename)
		{
			FileUtil fu(filename);
			std::stringstream ss;
			ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastMTime();
			return ss.str();
		}

		//判断文件是否需要上传的接口
		bool IsNeedUpLoad(const std::string& filename)
		{
			//需要上传的文件的判断条件:文件是新增的,不是新增的但是文件被修改过
			std::string id;
			if (_data->GetOneByKey(filename, &id) != false)
			{
				//有历史信息
				std::string new_id = GetFileIdentfier(filename);
				if (new_id == id)
				{
					//如果相等意味着唯一标识不需要再次上传
					return false;
				}
			}
			//根据修改时间判断,如果一直在修改则不需要上传
			FileUtil fu(filename);
			if (time(NULL) - fu.LastMTime() < 3)
			{
				//如果3s之内文件还在上传,则认为文件还在修改,就不上传
				return false;
			}
			return true;
		}
		//客户端文件上传功能
		bool Upload(const std::string& filename)
		{
			//1、获取文件数据
			nmzcloud::FileUtil fu(filename);
			std::string body;
			fu.GetContent(&body);
			//2、搭建http客户端上传文件数据
			httplib::Client client(SERVER_ADDR, SERVER_PORT);
			//接下来定义一个文件上传信息的结构体
			httplib::MultipartFormData item;
			item.content = body;
			item.filename = fu.FileName();
			item.name = "file";
			item.content_type = "application/octet-stream";
			httplib::MultipartFormDataItems items;
			items.push_back(item);
			auto res = client.Post("/upload", items);
			if (!res || res->status != 200)
			{
				return false;
			}
			return true;
		}
		bool RunModule()
		{
			//死循环,一直检测
			while (1)
			{
				//遍历一个目录获取所有的文件信息,然后创建每一个文件标识,最后生成配置文件
				nmzcloud::FileUtil fu(_back_dir);
				std::vector<std::string> array;
				fu.ScanDirectory(&array);
				//遍历数组获取到每一个文件下的名称然后生成文件的唯一标识
				for (auto& a : array)
				{
					//判断是否需要上传
					if (IsNeedUpLoad(a) == false)
					{
						//不需要上传
						continue;
					}
					if (Upload(a) == true)
					{
						//上传文件
						std::string id = GetFileIdentfier(a);
						//插入数据一个是文件名称,一个是文件唯一标识
						_data->Insert(a, id);
					}
				}
				Sleep(1);//1ms
			}
		}
	};
}
#endif // ! __CLOUD_H__
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 良

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值