【项目】C++云备份-包含运行教程


文章目录


项目介绍

项目名称:云备份系统

项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件进行热点管理,将长时间无访问文件进行压缩存储。

开发环境: centos7.6/vscode、g++、gdb、makefile 以windows10/vs2019

技术特点: http 客户端/服务器搭建, json 序列化,文件压缩,热点管理,断点续传,线程池,读写锁,单例模式

项目演示

在浏览器输入网址 http://119.91.60.49:8080/listshow 即可体验

启动服务器,在浏览器输入网址和端口号,进入展示页面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击选择文件,然后上传,后台可以看到上传的文件,上传完毕后回到展示页面,发现刚上传的文件出现了

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同时查看目录backdir发现了刚上传的文件,等超过热点时间就自动压缩到目录packdir中

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击文件可以直接下载

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

文件下载到一半服务器挂了

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

重启服务器后可以断点续传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

客户端也可以指定备份文件,本地可以看到上传的文件和路径

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

服务器也可以看到上传的内容

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

环境配置

升级gcc

//切换root
su -
//打开文件
nano /etc/sudoers
//找到下面root,添加用户aaa
## Allow root to run any commands anywhere
root    ALL=(ALL)       ALL
aaa     ALL=(ALL)       ALL

## Allows members of the 'sys' group to run networking, software,
//添加完后用ctrl+x,再按y,再按回车
//依次升级
sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc  devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
//升级完,检查下是不是gcc version 7.3.1版本
g++ -v
gcc -v

注意:升级不了看这里:https://www.cnblogs.com/Jedi-Pz/p/18447117

安装Jsoncpp库

在root 下进行

sudo yum install epel-release
sudo yum install jsoncpp-devel
//执行下面语句后出现对应的文件表示成功
ls /usr/include/jsoncpp/json/
assertions.h config.h forwards.h reader.h version.h
autolink.h features.h json.h value.h writer.h 

安装bundle数据压缩库

检查有没有git,我这里没有,所以要安装

[root@VM-12-6-centos yum.repos.d]# git
-bash: git: command not found
[root@VM-12-6-centos yum.repos.d]# sudo yum install git
[root@VM-12-6-centos yum.repos.d]# git --version
git version 1.8.3.1

安装完后clone,注意要在aaa 用户下对应的文件夹下进行

git clone https://github.com/r-lyeh-archived/bundle.git

下载不了的去网站下载zip然后传到服务器解压

安装httplib数据库

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

项目结构

project
	bundle
		bundle_test.cpp
		bundle.cpp
		bundle.h
		Makefile
	json
		json.cpp
		Makefile
third
	bundle
	cpp-httplib

Json

对象:使用{}扩起来的表示一个对象

数组:使用[]扩起来的表示一个数组

字符串:使用常规双引号""扩起来的表示一个字符串

数字:包括整形和浮点型,直接使用

[
    {
        "employeeId": 101,
        "name": "李四",
        "age": 32,
        "department": "技术部",
        "skills": ["Java", "Python", "数据库设计"],
        "contact": {
            "email": "lisi@example.com",
            "phone": "13800138000"
        }
    },
    {
        "employeeId": 102,
        "name": "王五",
        "age": 28,
        "department": "市场部",
        "skills": ["市场营销", "文案撰写"],
        "contact": {
            "email": "wangwu@example.com",
            "phone": "13900139000"
        }
    }
]

Value.h

class Json::Value{
    Value &operator=(const Value &other); 
    // 功能:重载赋值运算符,用于将一个 `Value` 对象赋值给另一个 `Value` 对象。
    // 示例:`Json::Value val1, val2; val1 = val2;`

    Value& operator[](const std::string& key);
    // 功能:重载下标运算符,用于通过字符串键访问 JSON 对象中的值。如果键不存在,会创建一个新的键值对。
    // 示例:`Json::Value val; val["姓名"] = "小明";`

    Value& operator[](const char* key);
    // 功能:与上面的重载类似,只是接受 `const char*` 类型的键。
    // 示例:`Json::Value val; val["age"] = 20;`

    Value removeMember(const char* key);
    // 功能:从 JSON 对象中移除指定键的键值对,并返回被移除的值。
    // 示例:`Json::Value removed = val.removeMember("age");`

    const Value& operator[](ArrayIndex index) const; 
    // 功能:重载下标运算符,用于通过数组索引访问 JSON 数组中的值。
    // 示例:`Json::Value val; val["成绩"] = Json::Value(Json::arrayValue); val["成绩"].append(100); int score = val["成绩"][0].asInt();`

    Value& append(const Value& value);
    // 功能:向 JSON 数组中添加一个新元素。
    // 示例:`Json::Value val; val["成绩"] = Json::Value(Json::arrayValue); val["成绩"].append(100);`

    ArrayIndex size() const;
    // 功能:返回 JSON 数组的元素个数。
    // 示例:`Json::Value val; val["成绩"] = Json::Value(Json::arrayValue); val["成绩"].append(100); int size = val["成绩"].size();`

    std::string asString() const;
    // 功能:将 `Value` 对象转换为 `std::string` 类型。如果 `Value` 不是字符串类型,会进行相应的转换。
    // 示例:`std::string name = val["name"].asString();`

    const char* asCString() const;
    // 功能:将 `Value` 对象转换为 `const char*` 类型。
    // 示例:`const char* name = val["name"].asCString();`

    Int asInt() const;
    // 功能:将 `Value` 对象转换为整数类型。
    // 示例:`int age = val["age"].asInt();`

    float asFloat() const;
    // 功能:将 `Value` 对象转换为浮点数类型。
    // 示例:`float score = val["score"].asFloat();`

    bool asBool() const;
    // 功能:将 `Value` 对象转换为布尔类型。
    // 示例:`bool isStudent = val["isStudent"].asBool();`
};

Writer.h

//json序列化类,优先使用
class JSON_API StreamWriter {
    virtual int write(Value const& root, std::ostream* sout) = 0;
    // 功能:纯虚函数,用于将 `Value` 对象序列化到指定的输出流中。
}

class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
    virtual StreamWriter* newStreamWriter() const;
    // 功能:用于创建新的 `StreamWriter` 对象。
    //使用
    Json::StreamWriterBuilder builder;
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
    std::ostringstream os;
    writer->write(root, &os);
    std::string str = os.str();
    std::cout << str << std::endl;
}
class JSON_API Writer {
    virtual std::string write(const Value& root) = 0;
    // 功能:纯虚函数,用于将 `Value` 对象序列化为 JSON 字符串。具体实现由派生类完成。
}

class JSON_API FastWriter : public Writer {
    virtual std::string write(const Value& root);
    // 功能:实现 `Writer` 类的 `write` 方法,以快速但格式紧凑的方式将 `Value` 对象序列化为 JSON 字符串。
}

class JSON_API StyledWriter : public Writer {
    virtual std::string write(const Value& root);
    // 功能:实现 `Writer` 类的 `write` 方法,以格式化的方式将 `Value` 对象序列化为 JSON 字符串,便于阅读。
}

Reader.h

//json反序列化类,高版本更推荐 
class JSON_API CharReader {
    virtual bool parse(char const* beginDoc, char const* endDoc,
         Value* root, std::string* errs) = 0;
    // 功能:纯虚函数,用于将字符数组 `[beginDoc, endDoc)` 中的 JSON 数据解析为 `Value` 对象 `root`。如果解析出错,错误信息会存储在 `errs` 中。
}

class JSON_API CharReaderBuilder : public CharReader::Factory {
    virtual CharReader* newCharReader() const;
    // 功能:用于创建新的 `CharReader` 对象。
    //使用:auto reader = Json::CharReaderBuilder().newCharReader(); 
    std::string str = "{\"姓名\":\"⼩明\", \"年龄\":20, \"成绩\":[76.5, 100, 77]}";
    Json::CharReaderBuilder crb;
    std::string err;
    Json::Value root;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
    cr->parse(str.c_str(), str.c_str() + str.size(), &root, &err);
}
class JSON_API Reader {
    bool parse(const std::string& document, Value& root, bool collectComments = true);
    // 功能:将 JSON 字符串 `document` 解析为 `Value` 对象 `root`。`collectComments` 参数用于指定是否收集注释。
    // 示例:`Json::Value root; Json::Reader reader; std::string jsonStr = "{\"name\": \"小明\", \"age\": 20}"; reader.parse(jsonStr, root);`
}

json使用

使用注意:包含头文件<jsoncpp/json/json.h>,链接库-ljsoncpp

序列化,把各个数据对象变成json格式字符串

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

//实现序列化
int main()
{
    const char *name = "⼩明";
    int age = 18;
    float score[] = {88.5, 98, 66};
    Json::Value val;
    val["姓名"] = name;
    val["年龄"] = age;
    val["成绩"].append(score[0]);
    val["成绩"].append(score[1]);
    val["成绩"].append(score[2]);
    Json::StreamWriterBuilder builder;
    std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
    std::stringstream os;
    writer->write(val, &os);
    std::cout << os.str() << std::endl;
    return 0;
}
//Makefile,第三方库要链接
json:json.cpp
	g++ $^ -o $@ -ljsoncpp

结果

[aaa@VM-12-6-centos json]$ ./json
{
        "姓名" : "⼩明",
        "年龄" : 18,
        "成绩" : 
        [
                88.5,
                98,
                66
        ]
}

反序列化,把json 格式字符串变成各个数据对象

//json.cpp
#include<iostream>
#include<sstream>
#include<string>
#include<memory>
#include<jsoncpp/json/json.h>
 
//实现反序列化
int main()
{
    std::string str = "{\"姓名\":\"⼩明\", \"年龄\":20, \"成绩\":[76.5, 100, 77]}";
    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 sz = root["成绩"].size();
    for (int i = 0; i < sz; i++)
    {
        std::cout << root["成绩"][i].asFloat() << std::endl;
    }
    return 0;
}
//Makefile,第三方库要链接
json:json.cpp
	g++ $^ -o $@ -ljsoncpp

结果

[aaa@VM-12-6-centos json]$ ./json
⼩明
20
76.5
100
77

bundle

使用注意:编译的时候要加上bundle.cpp 文件,同时文件要包含bundle.h 头文件,链接需要加上 -lpthread

压缩库,使用时包含这两个 bundle.h 和 bundle.cpp

namespace bundle
{
    // low level API (raw pointers)
    bool is_packed(*ptr, len);
    bool is_unpacked(*ptr, len);
    unsigned type_of(*ptr, len);
    size_t len(*ptr, len);
    size_t zlen(*ptr, len);
    const void *zptr(*ptr, len);
    bool pack(unsigned Q, *in, len, *out, &zlen);
    bool unpack(unsigned Q, *in, len, *out, &zlen);
    // medium level API, templates (in-place)
    bool is_packed(T);
    bool is_unpacked(T);
    unsigned type_of(T);
    size_t len(T);
    size_t zlen(T);
    const void *zptr(T);
    bool unpack(T &, T);
    bool pack(unsigned Q, T &, T);
    // high level API, templates (copy)
    T pack(unsigned Q, T);
    T unpack(T);
}

压缩文件

//bundle_test.cpp
#include <iostream>
#include <string>
#include <fstream>
#include "bundle.h"
int main(int argc, char *argv[])
{
    std::cout << "argv[1] 是原始⽂件路径名称\n";
    std::cout << "argv[2] 是压缩包名称\n";
    if (argc < 3)
        return -1;
    std::string ifilename = argv[1];
    std::string ofilename = argv[2];
    std::ifstream ifs;
    ifs.open(ifilename, std::ios::binary); // 打开原始⽂件
    ifs.seekg(0, std::ios::end);           // 跳转读写位置到末尾
    size_t fsize = ifs.tellg();            // 获取末尾偏移量--⽂件⻓度
    ifs.seekg(0, std::ios::beg);           // 跳转到⽂件起始
    std::string body;
    body.resize(fsize);                                    // 调整body⼤⼩为⽂件⼤⼩
    ifs.read(&body[0], fsize);                             // 读取⽂件所有数据到body找给你
    
    std::string packed = bundle::pack(bundle::LZIP, body); // 以lzip格式压缩⽂件数据
    
    std::ofstream ofs;
    ofs.open(ofilename, std::ios::binary); // 打开压缩包⽂件
    ofs.write(&packed[0], packed.size());  // 将压缩后的数据写⼊压缩包⽂件
    ifs.close();
    ofs.close();
    return 0;
}
//Makefile,用到多线程要连接
bundle:bundle_test.cpp bundle.cpp
	g++ $^ -o $@ -lpthread

结果,将bundle.cpp 文件压缩为bundle.cpp.lz 压缩包

[aaa@VM-12-6-centos bundle]$ ./bundle bundle.cpp bundle.cpp.lz
argv[1] 是原始⽂件路径名称
argv[2] 是压缩包名称

解压文件

//bundle_test.cpp
#include <iostream>
#include <fstream>
#include <string>
#include "bundle.h"
int main(int argc, char *argv[])
{
    std::cout << "argv[1] 是原始⽂件路径名称\n";
    std::cout << "argv[2] 是压缩包名称\n";
    if (argc < 3)
        return -1;
    std::string ifilename = argv[1]; // 压缩包名
    std::string ofilename = argv[2]; // 解压缩后⽂件名
    std::ifstream ifs;
    ifs.open(ifilename, std::ios::binary);
    ifs.seekg(0, std::ios::end);
    size_t fsize = ifs.tellg();
    ifs.seekg(0, std::ios::beg);
    std::string body;
    body.resize(fsize);
    ifs.read(&body[0], fsize);
    ifs.close();

    std::string unpacked = bundle::unpack(body); // 对压缩包数据解压缩

    std::ofstream ofs;
    ofs.open(ofilename, std::ios::binary);
    ofs.write(&unpacked[0], unpacked.size());
    ofs.close();
    return 0;
}

结果,将bundle.cpp.lz 压缩包解压为bundle2.cpp 文件

[aaa@VM-12-6-centos bundle]$ ./bundle bundle.cpp.lz bundle2.cpp
argv[1] 是原始⽂件路径名称
argv[2] 是压缩包名称

httplib

封装了简单的接口,用来搭建服务器和客户端

Request类和Response

下面是请求和相应包含部分,Request类是客户端发给服务端,Response类是服务端发给客户端

image-20230215113707995

namespace httplib
{
    // 定义一个结构体来表示 multipart/form-data 格式的数据项
    struct MultipartFormData
    {
        std::string name;       // 数据项的名称,通常用于标识字段或文件的名称
        std::string content;    // 数据项的内容,如果是文件则为文件内容,如果是普通字段则为字段值
        std::string filename;   // 如果是文件数据项,这里存储文件名
        std::string content_type; // 数据项的内容类型,例如 "text/plain" 或 "image/jpeg"
    };
    // 定义一个类型别名,用于表示 MultipartFormData 类型的向量,即多个 multipart/form-data 数据项的集合
    using MultipartFormDataItems = std::vector<MultipartFormData>;

    // 定义一个结构体来表示 HTTP 请求
    struct Request
    {
        std::string method; // 请求方法,例如 "GET"、"POST"、"PUT" 等
        std::string path;   // 资源路径,指定请求的目标资源
        Headers headers;    // 头部字段,包含了请求的各种元数据信息,如 Content-Type、User-Agent 等
        std::string body;   // 正文,存储请求的主体内容,如 POST 请求提交的数据

        // 以下字段主要用于服务器端处理请求
        std::string version; // 协议版本,例如 "HTTP/1.1"
        Params params;       // 查询字符串,存储在 URL 中问号后面的参数
        MultipartFormDataMap files; // 保存的是客户端上传的文件信息,以键值对形式存储,键为文件标识,值为文件相关数据
        Ranges ranges; // 用于实现断点续传的请求文件区间

        // 判断请求中是否有某个报头
        bool has_header(const char *key) const;

        // 获取指定报头的内容,id 用于处理有多个相同报头的情况,默认为 0 表示获取第一个
        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;
        
    };

    // 定义一个结构体来表示 HTTP 响应
    struct Response
    {
        std::string version; // 协议版本,例如 "HTTP/1.1"
        int status = -1;     // 响应状态码,如 200 表示成功,404 表示未找到等
        Headers headers;     // 头部字段,包含了响应的各种元数据信息,如 Content-Type、Content-Length 等
        std::string body;    // 正文,存储响应的主体内容,如返回的页面内容或数据

        // 设置头部字段,将指定的键值对添加到响应的头部
        void set_header(const char *key, const char *val);

        // 设置正文内容,并指定内容类型
        void set_content(const std::string &s, const char *content_type);
    };
}

Server

用于搭建服务器

class Server
{
    // 定义一个函数类型别名 Handler,它是一个 std::function 对象,接受一个 const Request 引用和一个 Response 引用作为参数,没有返回值
    // 可以用来表示处理 HTTP 请求的函数
    using Handler = std::function<void(const Request &, Response &)>; 

    // 定义一个 Handlers 类型别名,它是一个 std::vector,其中每个元素是一个 std::pair,pair 的第一个元素是一个 std::regex(正则表达式),
    // 用于匹配请求路径,第二个元素是一个 Handler 类型的函数,用于处理匹配到的请求
    // 这个容器可以看作是请求和处理函数的映射表
    using Handlers = std::vector<std::pair<std::regex, Handler>>;    

    // 定义一个 std::function 类型的成员变量 new_task_queue,它是一个函数指针,指向一个返回 TaskQueue* 的函数,
    // 用于创建线程池来处理请求,这里的线程池用于处理 HTTP 请求任务
    std::function<TaskQueue *(void)> new_task_queue;                 

    // 定义一个成员函数 Get,它接受一个字符串类型的参数 pattern(表示请求路径的模式)和一个 Handler 类型的参数 handler(处理该请求的函数)
    // 该函数的作用是为 GET 请求方法的某个请求路径设定映射的处理函数,返回 Server 对象的引用,以便进行链式调用
    //内容主要写在报头
    Server &Get(const std::string &pattern, Handler handler);         

    // 定义一个成员函数 Post,与 Get 函数类似,只是针对 POST 请求方法,为某个请求路径设定映射的处理函数
    // 返回 Server 对象的引用,可用于链式调用,例如:ServerObject.Post("/SomePath", SomeHandlerFunction);
    //内容主要写在body
    Server &Post(const std::string &pattern, Handler handler);        

    // 定义一个成员函数 Put,针对 PUT 请求方法,为某个请求路径设定映射的处理函数,返回 Server 对象的引用
    Server &Put(const std::string &pattern, Handler handler);

    // 定义一个成员函数 Patch,针对 PATCH 请求方法,为某个请求路径设定映射的处理函数,返回 Server 对象的引用
    Server &Patch(const std::string &pattern, Handler handler);

    // 定义一个成员函数 Delete,针对 DELETE 请求方法,为某个请求路径设定映射的处理函数,返回 Server 对象的引用
    Server &Delete(const std::string &pattern, Handler handler);

    // 定义一个成员函数 Options,针对 OPTIONS 请求方法,为某个请求路径设定映射的处理函数,返回 Server 对象的引用
    Server &Options(const std::string &pattern, Handler handler);

    // 定义一个成员函数 listen,它接受主机名(const char* 类型)、端口号(int 类型)和套接字标志(int 类型,默认值为 0)作为参数
    // 该函数的作用是搭建并启动 HTTP 服务器,尝试绑定到指定的主机和端口,并开始监听传入的请求,返回一个布尔值表示服务器启动是否成功
    bool listen(const char *host, int port, int socket_flags = 0); 
};

搭建简单服务器

g++ -std=c++11 -o server server.cpp -lpthread

编译完成后选择一个test 执行,test1对应Get使用,test2对应 Psot使用,test3 对应文件使用,后面客户端也要选对应的代码

//server.cpp
#include"httplib.h"
#include <iostream>
#include <fstream>

void test1(const httplib::Request& req, httplib::Response& rsp)
{
    rsp.set_content("服务器给客户端响应","text/plain");
    std::cout<<"服务器收到了"<<std::endl;
}

void test2(const httplib::Request& req, httplib::Response& rsp)
{
    if(!req.body.empty()){
        std::cout << "收到内容为: " << req.body << std::endl;
        rsp.set_content(req.body,"text/plain");
    }
}

void test3(const httplib::Request& req, httplib::Response& rsp)
{
    //判断是否有指定的文件标识
    if(req.has_file("file")){
        //获取指定文件标识对应的文件内容数据
        auto file = req.get_file_value("file");
        std::string filename = file.filename;
        std::ofstream outFile(filename, std::ios::binary);
        outFile.write(file.content.c_str(), static_cast<std::streamsize>(file.content.size()));
        outFile.close();
        std::cout<<"创建文件成功"<<std::endl;
        rsp.set_content("服务器给客户端响应", "text/plain");
    }
}

int main()
{
    httplib::Server server;//实例化server对象
    // server.Get("/test1",test1);//Get使用
    //server.Post("/test2",test2);//Post使用
    //server.Post("/test3",test3);//文件
    server.listen("0.0.0.0",8080);
    return 0;
}

Client

用于搭建客户端

// 定义一个客户端类,用于与服务器进行 HTTP 通信
class Client
{
    // 构造函数,用于初始化客户端实例
    // 参数:
    // - host: 服务器的 IP 地址或域名,以字符串形式传入
    // - port: 服务器监听的端口号,整数类型
    // 功能:创建一个客户端对象,指定要连接的服务器地址和端口
    Client(const std::string &host, int port);

    // 向服务器发送 GET 请求的方法
    // 参数:
    // - path: 请求的资源路径,以 C 风格字符串形式传入,指示要访问服务器上的具体资源
    // - headers: 请求头信息,是一个 Headers 类型的常量引用,包含了如 User - Agent、Content - Type 等请求相关的元数据
    // 返回值:
    // - Result 类型,指针,包含了请求的结果信息,如响应状态码、响应头、响应体等
    Result Get(const char *path, const Headers &headers);

    // 向服务器发送 POST 请求的方法,用于发送带有特定内容的请求
    // 参数:
    // - path: 请求的资源路径,以 C 风格字符串形式传入
    // - body: 请求体的内容,以 C 风格字符串形式传入,即要发送给服务器的数据
    // - content_length: 请求体的长度,以字节为单位,用于告知服务器请求体的大小
    // - content_type: 请求体的内容类型,以 C 风格字符串形式传入,如 "application/json"、"text/plain" 等
    // 返回值:
    // - Result 类型,指针,包含请求的结果信息
    Result Post(const char *path, const char *body, size_t content_length, const char *content_type);

    // 向服务器发送 POST 请求的方法,用于提交多区域数据,常用于多文件上传场景
    // 参数:
    // - path: 请求的资源路径,以 C 风格字符串形式传入
    // - items: 多部分表单数据项的集合,是一个 MultipartFormDataItems 类型的常量引用,每个数据项可能代表一个文件或一个表单字段
    // 返回值:
    // - Result 类型,指针,包含请求的结果信息
    Result Post(const char *path, const MultipartFormDataItems &items);
};

搭建简单客户端

g++ -std=c++11 -o client client.cpp -lpthread

先运行服务器然后运行客户端,下面分别是test1,用来测试Get接收,test2,用来测试Post接收,test3,用来测试文件接收

//client.cpp,test1测试
#include"httplib.h"
int main (){
    httplib::Client cli("10.1.12.6",8080);//实例化client对象
    auto res = cli.Get("/test1");
    std::cout<<res->body<<std::endl;
    return 0;
}
//结果,先运行服务器,后运行客户端,打印内容如下
[aaa@VM-12-6-centos httplib]$ ./client
服务器给客户端响应
[aaa@VM-12-6-centos httplib]$ ./server
服务器收到了
//client.cpp ,test2测试
#include"httplib.h"
int main (){
    // HTTP
    httplib::Client cli("10.1.12.6",8080);
    std::string data;
    std::cin>>data;
    auto res = cli.Post("/test2",data,"text/plain");
    std::cout<<res->body<<std::endl;
    return 0;
}
//结果,先运行服务器,后运行客户端,在客户端输入内容,客户端会打印,同时服务器端也会打印
[aaa@VM-12-6-centos httplib]$ ./client
123
123
[aaa@VM-12-6-centos httplib]$ ./server
收到内容为: 123
//client.cpp 
#include"httplib.h"
#include <fstream>

int main (){
    // HTTP
    httplib::Client cli("10.1.12.6",8080);
    //单个文件信息组织   
    httplib::MultipartFormData file;    
    file.name="file"; //字段名,和服务器对应       
    file.content="hello world";    //文件内容
    file.filename="Hello.txt";    //文件名
    file.content_type="text/plain";    //文件类型
    //MultipartFormDataItems 文件信息数组
    httplib::MultipartFormDataItems items;    
    items.push_back(file);    
    //请求服务器上/file资源,发送item文件集合给服务器
    auto res = cli.Post("/test3",items);
    std::cout<<res->body<<std::endl;
    return 0;
}
//结果,先运行服务器,后运行客户端,在客户端打印下面内容,在服务器打印下面内容,并创建Hello.txt文件
[aaa@VM-12-6-centos httplib]$ ./client
服务器给客户端响应
[aaa@VM-12-6-centos httplib]$ ./server
创建文件成功

GetPost

Client 类中的 GetPost

向服务器发送 HTTP 请求

  • Get 方法:用于从服务器端获取数据。
  • Post 方法:向服务器上传数据,上传文件。
// 发送 GET 请求
auto getRes = cli.Get("/data");

// 发送 POST 请求
std::string postBody = "param1=value1&param2=value2";
auto postRes = cli.Post("/submit", postBody, "application/x-www-form-urlencoded");

Server 类中的 GetPost

  • Get 方法:用于处理客户端发起的 GET 请求
  • Post 方法:用于处理客户端发起的 POST 请求
// 注册 GET 请求处理函数
svr.Get("/data", [](const httplib::Request& req, httplib::Response& res) {
    res.set_content("This is data from GET request", "text/plain");
});

// 注册 POST 请求处理函数
svr.Post("/submit", [](const httplib::Request& req, httplib::Response& res) {
    res.set_content("Received POST data: " + req.body, "text/plain");
});

项目实现

服务端

util.hpp-工具

  • 获取文件大小
  • 获取文件最后一次修改时间
  • 获取文件最后一次访问时间
  • 获取文件路径中的文件名
  • 向文件写入数据、获取文件数据
  • 获取文件指定位置,指定长度的数据(断点重传功能的实现需要该接口)
  • 判断文件是否存在
  • 创建文件目录、浏览文件目录
  • 压缩文件、解压文件
  • 删除文件
  • 序列化反序列化
//util.hpp
#pragma once
#include<iostream>
#include<vector>
#include<string>
#include<fstream>
#include <sys/stat.h>
#include <experimental/filesystem>
#include<jsoncpp/json/json.h>
#include"bundle.h"


namespace myspace{

    class JsonUtil{
        public:
            //序列化
            static bool Serialize(const Json::Value &root, std::string *str)
            {
                Json::StreamWriterBuilder swb;
                std::unique_ptr<Json::StreamWriter> writer(swb.newStreamWriter());
                std::stringstream sstream;
                writer->write(root,&sstream);
                *str=sstream.str();
            }
            //反序列化
            static bool UnSerialize(const std::string &str, Json::Value *root)
            {
                std::string err;
                Json::CharReaderBuilder crb;
                std::shared_ptr<Json::CharReader> cr(crb.newCharReader());
                cr->parse(str.c_str(),str.c_str()+str.size(),root,&err);
                return true;
            }
    };


    class FileUtil{
    private:
        std::string _filename;
    public:
        FileUtil(const std::string& filename):_filename(filename) {}
        //获取文件大小
        size_t FileSize(){
            struct stat st;
            if(stat(_filename.c_str(),&st)<0)
            {
                std::cout<<"get file size failed"<<std::endl;
                return -1;
            }
            return st.st_size;
        }
        //获取文件最后一次修改时间
        time_t LastModifyTime(){
            struct stat st;
            if(stat(_filename.c_str(),&st)<0)
            {
                std::cout<<"get file size failed"<<std::endl;
                return -1;
            }
            return st.st_mtime;
        }
        //获取文件最后一次访问时间
        time_t LastAcccessTime(){
            struct stat st;
            if(stat(_filename.c_str(),&st)<0)
            {
                std::cout<<"get file size failed"<<std::endl;
                return -1;
            }
            return st.st_atime;
        }
        //获取文件路径名中的文件名
        std::string FileName(){
            size_t pos = _filename.find_last_of("/\\");//查找最后一个 / 或 \ 符号
            if (pos == std::string::npos)
            {
                return _filename;
            }
            return _filename.substr(pos + 1);
        }
        //向文件写入数据
        bool SetContent(const std::string& body){
            std::ofstream ofs;
            ofs.open(_filename,std::ios::binary);
            if (!ofs.is_open()) {
                std::cout << "打开文件失败" << _filename << std::endl;
                return false;
            }
            ofs.write(body.c_str(),static_cast<std::streamsize>(body.size()));
            if (!ofs.good()) {
                std::cout << "写入失败" << std::endl;
                ofs.close();
                return false;
            }
            ofs.close();
            return true;
        }
        //获取文件所有数据
        bool GetContent(std::string *body){
            size_t fsize=this->FileSize();
            return GetPosLen(body,0,fsize);
        }
        //获取文件指定位置 指定长度的数据
        bool GetPosLen(std::string *body, size_t pos, size_t len){
            size_t fsize=this->FileSize();
            if(pos+len>fsize)
            {
                std::cout<<"长度错误\n";
                return false;
            }
            std::ifstream ifs;
            ifs.open(_filename,std::ios::binary);
            if (!ifs.is_open()) {
                std::cout << "打开文件失败" << _filename << std::endl;
                return false;
            }
            //从文件开头偏移pos长度
            ifs.seekg(pos, std::ios::beg);
            body->resize(len);
            ifs.read(&(*body)[0],len);
            if (!ifs.good()) {
                std::cout << "读取失败" << std::endl;
                ifs.close();
                return false;
            }
            ifs.close();
            return true;
        }
        //压缩文件
        bool Compress(const std::string& packname){
            //1.获取原文件数据
            std::string body;
            if(!this->GetContent(&body))
            {
                std::cout<<"压缩失败"<<std::endl;
                return false;
            }
            //2.对数据进行压缩bundle::pack
            std::string packed=bundle::pack(bundle::LZIP, body);
            //3.压缩后的数据存储到压缩文件中
            FileUtil fu(packname);
            if(!fu.SetContent(packed))
            {
                std::cout<<"压缩失败"<<std::endl;
                return false;
            }
            return true;
        }
        //解压文件
        bool UnCompress(const std::string& filename){
            //1.获取当前压缩包得数据
            std::string body;
            if(!this->GetContent(&body))
            {
                std::cout<<"解压失败"<<std::endl;
                return false;
            }
            //2.对数据进行解压bundle::unpack
            std::string unpacked=bundle::unpack(body);
            //3.将解压后的数据保存到新文件中
            FileUtil fu(filename);
            if(!fu.SetContent(unpacked))
            {
                std::cout<<"解压失败"<<std::endl;
                return false;
            }
            return true;
        }
        //移除
        bool Remove()
        {
            if (!Exists()) return true;
            remove(_filename.c_str());
            return true;
        }
        //判断文件是否存在
        bool Exists(){
            return std::experimental::filesystem::exists(_filename);
        }
        //创建目录
        bool CreateDirectory(){
            if(this->Exists())return true;
            return std::experimental::filesystem::create_directory(_filename);
        }
        //获取目录下所有文件名
        bool GetDirectory(std::vector<std::string>& arry){
            for(auto& p:std::experimental::filesystem::directory_iterator(_filename))
            {
                if(std::experimental::filesystem::is_directory(p)) continue;
                arry.push_back(std::experimental::filesystem::path(p).relative_path().string());
            }
            return true;
        }
    };
}

stat-获取文件的相关信息

#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
//使用
struct stat st;
if(stat("test.txt", &st)<0) cout<<"失败"<<endl;
  • pathname:文件路径
  • statbuf:存储文件相关信息,指针是struct stat
  • 成功返回0,失败返回-1

struct stat 结构体-定义文件的信息

struct stat {
    dev_t     st_dev;         /* 文件所在设备的 ID */
    ino_t     st_ino;         /* 文件的 inode 编号 */
    mode_t    st_mode;        /* 文件的类型和权限 */
    nlink_t   st_nlink;       /* 文件的硬链接数 */
    uid_t     st_uid;         /* 文件所有者的用户 ID */
    gid_t     st_gid;         /* 文件所有者的组 ID */
    dev_t     st_rdev;        /* 设备文件的设备 ID */
    off_t     st_size;        /* 文件的大小,以字节为单位 */
    blksize_t st_blksize;     /* 文件系统的块大小 */
    blkcnt_t  st_blocks;      /* 文件占用的块数 */
    time_t    st_atime;       /* 文件的最后访问时间 */
    time_t    st_mtime;       /* 文件的最后修改时间 */
    time_t    st_ctime;       /* 文件的最后状态改变时间 */
};

find_last_of-查找指定字符或字符串最后一次出现的位置

size_t find_last_of(charT c, size_t pos = npos) const;
size_t find_last_of(const basic_string& str, size_t pos = npos) const;
//使用
string str = "hello, world"
// 查找字符串 \"lo\" 中任意字符最后一次出现在位置
size_t pos1 = str.find_last_of("lo");
  • 查找字符c最后一次出现的位置,或者字符串 str 中包含的任意字符最后出现位置
  • 不指定pos,则默认从后往前搜索
  • 找到返回最后一次出现的位置,没找到返回npos

std::experimental::filesystem:::exists()-判断文件是否存在

#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
bool exists(const path& p);
  • pstd::experimental::filesystem::path 类型的对象,文件名或者文件目录。可以传入字符串

std::experimental::filesystem::create_directory()-创建目录

#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
bool create_directory(const path& p);
  • pstd::experimental::filesystem::path 类型的对象,文件名或者文件目录。可以传入字符串

std::experimental::filesystem::directory_iterator-目录迭代器

遍历指定目录中所有文件和子目录,不会递归进入子目录

#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
std::experimental::filesystem::directory_iterator dirIt("test_dir");//遍历test_dir目录

std::experimental::filesystem::path-表示文件系统路径

#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
std::experimental::filesystem::path path1("example.txt");

std::experimental::filesystem::is_directory-判断路径是否为目录

#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
std::experimental::filesystem::path path("test_dir");
if(std::experimental::filesystem::is_directory(path)){...}

std::experimental::filesystem::relative_path-计算相对路径

计算一个路径相对于另一个路径的相对路径

#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
std::experimental::filesystem::path p = std::experimental::filesystem:::current_path();//把文件当前路径存给p
p.relative_path();//获得文件相对路径
//如果当前路径是C:\Users\abcdef\Local Settings\temp
//那相对路径是Users\abcdef\Local Settings\temp

remove-删除文件

//C
#include <iostream>
int remove(const char *filename);
//C++
#include <algorithm>
template< class ForwardIt, class T >
ForwardIt remove( ForwardIt first, ForwardIt last, const T& value );
//使用
std::vector<int> numbers = {1, 2, 3, 2, 4, 2, 5};
auto new_end = std::remove(numbers.begin(), numbers.end(), 2);
  • filename:指向要删除的文件的名称的字符串指针
  • first:指向要处理的元素范围的起始位置的迭代器。
  • last:指向要处理的元素范围的结束位置的迭代器。
  • value:要移除的元素的值。

cloud.json-配置

配置信息,以后要修改信息在这里改,不用在代码里面改

  • hot_time 热点判断时间
  • server_port 端口
  • server_ip 0.0.0.0表示任何ip都可以访问
  • download_prefix 文件下载的url前缀路径,表示客户端是要下载文件
  • packfile_suffix 压缩包后缀名
  • pack_dir 上传文件存放路径
  • back_dir 压缩包存放路径
  • backup_file 服务端备份信息存放文件
{
	"hot_time" : 30,
	"server_port" : 8080,
	"server_ip" : "0.0.0.0",
	"download_prefix" : "/download/",
	"packfile_suffix" : ".lzip",
	"pack_dir" : "./packdir/",
	"back_dir" : "./backdir/",
	"backup_file" : "./cloud.dat"
}

config.hpp-配置信息加载

采用单例模式-懒汉模式,加载文件的相关信息,具体消息在cloud.json,这里只是加载

// config.hpp
#pragma once
#include "util.hpp"
#include <mutex>
namespace myspace
{
    class Config
    {
    public:
        bool ReadConfig(const std::string &filename);
        int GetHotTime()
        {
            return _hot_time;
        }
        int GetServerPort()
        {
            return _server_port;
        }
        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;
        }
        // 读取配置文件
        void ReadConfigFile()
        {
            FileUtil file("./cloud.json");
            std::string body;
            if (!file.GetContent(&body))
            {
                std::cout << "load config failed\n";
            }
            Json::Value root;
            if (!JsonUtil::UnSerialize(body, &root))
            {
                std::cout << "parse config failed\n";
            }
            _hot_time = root["hot_time"].asInt();
            _server_port = root["server_port"].asInt();
            _server_ip = root["server_ip"].asString();
            _download_prefix = root["download_prefix"].asString();
            _packfile_suffix = root["packfile_suffix"].asString();
            _pack_dir = root["pack_dir"].asString();
            _back_dir = root["back_dir"].asString();
            _backup_file = root["backup_file"].asString();
        }
        static Config &GetInstance()
        {
            static Config _instance;  // 声明
            return _instance;
        }

    private:
        time_t _hot_time;             // 热点判断时间
        int _server_port;             // 服务器监听端口
        std::string _server_ip;       // 服务器ip
        std::string _download_prefix; // 下载的url前缀路径
        std::string _packfile_suffix; // 压缩包的后缀
        std::string _pack_dir;        // 压缩包存放路径
        std::string _back_dir;        // 备份文件存放路径
        std::string _backup_file;     // 备份信息的存放文件

        // 构造函数私有化
        Config() { ReadConfigFile(); }
    };
}

data.hpp-数据管理

  • 文件的实际存储路径:

  • 文件压缩包存放路径名:

  • 文件是否压缩的标志位:判断文件是否已经被压缩了

  • 文件大小

  • 文件最后一次修改时间

  • 文件最后一次访问时间,这些属性信息都单独拿出来

  • 文件访问URL中资源的路径

使用 hash 在内存中管理数据,使用 json 保存信息

// data.hpp
#pragma once
#include <unordered_map>
#include <shared_mutex>
#include "util.hpp"
#include "config.hpp"
namespace myspace
{
    // 数据信息结构体
    typedef struct BackupInfo
    {
        std::string _real_path; // 文件实际存储路径名
        std::string _pack_path; // 压缩报存储路径名
        bool _pack_flag;        // 是否压缩标志
        size_t _fsize;          // 文件大小
        time_t _mtime;          // 最后修改时间
        time_t _atime;          // 最后访问时间
        std::string _url_path;  // 请求的资源路径
        // 数据填充(获取各项属性信息,存储到BackupInfo结构体)
        bool NewBackupInfo(const std::string &realpath)
        {
            FileUtil file(realpath);
            if (!file.Exists())
            {
                std::cout << "new backupfile file not find\n";
                return false;
            }
            Config config = Config::GetInstance();
            std::string packdir = config.GetPackDir();
            std::string packsuffix = config.GetPackFileSuffix();
            std::string downloadprefix = config.GetDownloadPrefix();
            _real_path=realpath;
            _fsize = file.FileSize();
            _mtime = file.LastModifyTime();
            _atime = file.LastAcccessTime();
            // ./backdir/a.txt --> ./packdir/a.txt.lz
            _pack_path = packdir + file.FileName() + packsuffix;
            // ./backdir/a.txt --> /download/a.txt
            _url_path = downloadprefix + file.FileName();
            return true;
        }
    } BackupInfo;

    // 数据管理类
    class DataManager
    {
    public:
        DataManager()
        {
            _backup_file = Config::GetInstance().GetBackupFile();
            InitLoad();
        }
        // 初始化加载,每次系统重启都要加载以嵌的数据
        bool InitLoad()
        {
            // 1.读取backup_file备份信息的存放文件中的数据
            FileUtil f(_backup_file);
            if (!f.Exists())
            {
                return true;
            }
            std::string body;
            f.GetContent(&body);
            // 2.反序列化
            Json::Value root;
            JsonUtil::UnSerialize(body, &root);
            // 3.将反序列化得到的Json::Value中的数据添加到table中
            for (int i = 0; i < root.size(); i++)
            {
                BackupInfo info;
                info._pack_flag = root[i]["pack_flag"].asBool();
                info._fsize = root[i]["fsize"].asInt64();
                info._atime = root[i]["atime"].asInt64();
                info._mtime = root[i]["mtime"].asInt64();
                info._pack_path = root[i]["pack_path"].asString();
                info._real_path = root[i]["real_path"].asString();
                info._url_path = root[i]["url_path"].asString();
                Insert(info);
            }
            return true;
        }
        // 把信息存档到json当中
        bool Storage()
        {
            // 1.获取所有数据
            std::vector<BackupInfo> arr;
            this->GetAll(&arr);
            // 2.填充到Json::Value中
            Json::Value root;
            for (int i = 0; i < arr.size(); i++)
            {
                Json::Value val;
                val["pack_flag"] = arr[i]._pack_flag;
                val["fsize"] = static_cast<Json::Int64>(arr[i]._fsize);
                val["atime"] = static_cast<Json::Int64>(arr[i]._atime);
                val["real_path"] = arr[i]._real_path;
                val["pack_path"] = arr[i]._pack_path;
                val["url_path"] = arr[i]._url_path;
                root.append(val);
            }
            // 3.对Json::Value序列化
            std::string body;
            JsonUtil::Serialize(root, &body);
            // 4.写文件
            FileUtil f(_backup_file);
            f.SetContent(body);
            return true;
        }
        // 新增
        bool Insert(const BackupInfo &info)
        {
            std::shared_lock<std::shared_mutex> lock(_rwlock);
            _table[info._url_path] = info;
            Storage();
            std::cout << "插入的数据 for URL: " << info._url_path << std::endl;
            return true;
        }
        // 修改
        bool Update(const BackupInfo &info)
        {
            std::shared_lock<std::shared_mutex> lock(_rwlock);
            _table[info._url_path] = info;
            Storage();
            return true;
        }
        // 根据请求url获取对应文件信息(用户根据url请求下载文件)
        bool GetOneByURL(const std::string &url, BackupInfo &info)
        {
            std::shared_lock<std::shared_mutex> lock(_rwlock);
            auto it = _table.find(url);      // url是key值直接find查找
            if (it == _table.end())
            {
                return false;
            }
            info = it->second;
            return true;
        }
        // 根据真实路径获取文件信息,(服务器端测备份文件 热点文件判断)
        bool GetOneByRealPath(const std::string &realpath, BackupInfo& info)
        {
            std::shared_lock<std::shared_mutex> lock(_rwlock);
            auto it = _table.begin();
            // 真实路径需要遍历unordered_map 中second的real_path
            while (it != _table.end())
            {
                if (it->second._real_path == realpath)
                {
                    info = it->second;
                    return true;
                }
                it++;
            }
            return false;
        }
        // 获取所有文件信息
        bool GetAll(std::vector<BackupInfo> *arry)
        {
            std::shared_lock<std::shared_mutex> lock(_rwlock);
            // 遍历
            auto it = _table.begin();
            for (; it != _table.end(); it++)
            {
                arry->push_back(it->second);
            }
            return true;
        }
        private:
            std::string _backup_file;                           // 持久化存储文件
            std::shared_mutex _rwlock;                           // 读写锁,读共享,写互斥
            std::unordered_map<std::string, BackupInfo> _table; // 内存中hash存储的文件信息管理表
    };
}

测试,在当前目录下创建test_file.txt 文件,然后随便输入点数据,运行完程序()后,数据存储在cloud.dat

#include "data.hpp"
#include <iostream>
#include <string>

int main() {
    // 测试 BackupInfo 结构体
    myspace::BackupInfo info;
    std::string realPath = "./test_file.txt";
    if (info.NewBackupInfo(realPath)) {
        std::cout << "BackupInfo 数据填充成功" << std::endl;
        std::cout << "Real Path: " << info._real_path << std::endl;
        std::cout << "Pack Path: " << info._pack_path << std::endl;
        std::cout << "Pack Flag: " << (info._pack_flag ? "true" : "false") << std::endl;
        std::cout << "File Size: " << info._fsize << std::endl;
        std::cout << "Last Modify Time: " << info._mtime << std::endl;
        std::cout << "Last Access Time: " << info._atime << std::endl;
        std::cout << "URL Path: " << info._url_path << std::endl;
    } else {
        std::cout << "BackupInfo 数据填充失败" << std::endl;
    }

    // 测试 DataManager 类
    myspace::DataManager dataManager;

    // 测试 Insert 方法
    if (dataManager.Insert(info)) {
        std::cout << "数据插入成功" << std::endl;
    } else {
        std::cout << "数据插入失败" << std::endl;
    }

    // 测试 GetOneByURL 方法
    myspace::BackupInfo retrievedInfo;
    if (dataManager.GetOneByURL(info._url_path, retrievedInfo)) {
        std::cout << "通过 URL 获取数据成功" << std::endl;
        std::cout << "Real Path: " << retrievedInfo._real_path << std::endl;
    } else {
        std::cout << "通过 URL 获取数据失败" << std::endl;
    }

    // 测试 Update 方法
    retrievedInfo._pack_flag = true;
    if (dataManager.Update(retrievedInfo)) {
        std::cout << "数据更新成功" << std::endl;
    } else {
        std::cout << "数据更新失败" << std::endl;
    }

    // 测试 GetOneByRealPath 方法
    myspace::BackupInfo infoByRealPath;
    if (dataManager.GetOneByRealPath(realPath, infoByRealPath)) {
        std::cout << "通过真实路径获取数据成功" << std::endl;
        std::cout << "URL Path: " << infoByRealPath._url_path << std::endl;
    } else {
        std::cout << "通过真实路径获取数据失败" << std::endl;
    }

    // 测试 GetAll 方法
    std::vector<myspace::BackupInfo> allInfos;
    if (dataManager.GetAll(&allInfos)) {
        std::cout << "获取所有数据成功,数据数量: " << allInfos.size() << std::endl;
    } else {
        std::cout << "获取所有数据失败" << std::endl;
    }

    return 0;
}

pthread_rwlock_t-读写锁

并发读,多个线程同时读,独占写,一个线程写其他等着,读写互斥,读的时候不能写,写的时候不能读

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);//初始化
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁
  • rwlock:指向要初始化的读写锁的指针
  • attr:一般写NULL

hot.hpp-热点管理

对服务器上备份的文件进行检测,长时间没有访问认为是非热点文件,进行压缩存储。

遍历备份文件,检测文件的最后一次访问时间,与当前时间相减得到插值,得到的插值大于设定好的值则认为是非热点文件

  • 遍历备份目录,获取所有文件路径名称
  • 逐个文件获取最后一次访问时间与当前系统时间进行比较判断
  • 对非热点文件进行压缩处理,删除源文件
  • 修改数据管理模块对应的文件信息(压缩标志(_pack_flag)–>true)
// hot.hpp
#pragma once
#include "data.hpp"
#include <unistd.h>


extern myspace::DataManager *_data; // 先声明一个数据管理类的全局变量,外部变量
namespace myspace
{
    class HotManager
    {
    public:
        HotManager()
        {
            Config config = Config::GetInstance();
            _back_dir = config.GetBackDir();
            _pack_dir = config.GetPackDir();
            _pack_suffix = config.GetPackFileSuffix();
            _hot_time = config.GetHotTime();
            //如果目录不存在,创建目录
            FileUtil tmp1(_back_dir);
            FileUtil tmp2(_pack_dir);
            tmp1.CreateDirectory();
            tmp2.CreateDirectory();
        }
        // 热点文件压缩功能实现
        bool RunModule()
        {
            while (1)
            {
                // 1. 遍历备份目录,获取所有文件路径名称
                FileUtil file(_back_dir);
                std::vector<std::string> arr;
                file.GetDirectory(arr);
                // 2. 判断是否位热点文件
                for (auto &a : arr)
                {
                    if (!HotJudge(a))
                    {
                        continue; // 热点文件不处理
                    }
                    // 3. 对非热点文件进行压缩处理
                    // 获取文件的备份信息
                    BackupInfo bi;
                    if (!_data->GetOneByRealPath(a, bi))
                    {
                        // 如果文件存在,但是没有备份信息--设置一个新的备份信息
                        bi.NewBackupInfo(a);
                    }
                    FileUtil tmp(a);
                    tmp.Compress(bi._pack_path);
                    // 4. 删除源文件,修改数据管理模块对应的文件信息(压缩标志(_pack_flag)–>true)
                    tmp.Remove();
                    bi._pack_flag = true;
                    _data->Update(bi);
                }
                usleep(1000);//每一千毫秒检测一次
            }
        }
        // 判断是否是热点文件
        bool HotJudge(const std::string &filename)
        {
            // 逐个文件获取最后一次访问时间与当前系统时间进行比较判断
            FileUtil file(filename);
            time_t last_atime = file.LastAcccessTime();
            time_t cur_time = time(NULL);
            if (cur_time - last_atime > _hot_time)
            {
                return true;
            }
            return false;
        }
    private:
        std::string _back_dir;    // 备份文件的路径
        std::string _pack_dir;    // 压缩文件的路径
        std::string _pack_suffix; // 压缩文件后缀
        int _hot_time;            // 热点判断时间
    };

}


测试,第一次运行会在当前目录创建backdir和packdir两个文件夹,我们随便复制一个文件到backdir充当热点文件,等待热点时间过去(设置是30S),再次运行程序,热点文件被压缩到packdir,backdir里面热点文件被删除

#include "hot.hpp"
#include "util.hpp"
#include "config.hpp"
#include <iostream>
#include <string>

// 定义全局变量 _data
myspace::DataManager* _data;
int main() {
    // 创建 DataManager 对象并赋值给全局变量 _data
    _data = new myspace::DataManager();

    // 创建 HotManager 对象
    myspace::HotManager hotManager;

    // 调用 RunModule 方法进行测试
    // 由于 RunModule 是一个无限循环,这里我们可以选择在一定时间后终止测试
    // 例如,使用线程和定时器来控制测试时间
    // 这里简单地调用一次 RunModule 中的单次处理逻辑
    std::vector<std::string> arr;
    myspace::FileUtil file("./backdir/");
    file.GetDirectory(arr);
    for (auto& a : arr) {
        if (!hotManager.HotJudge(a)) {
            continue;
        }
        myspace::BackupInfo bi;
        if (!_data->GetOneByRealPath(a, bi)) {
            bi.NewBackupInfo(a);
        }
        myspace::FileUtil tmp(a);
        tmp.Compress(bi._pack_path);
        tmp.Remove();
        bi._pack_flag = true;
        _data->Update(bi);
    }

    // 释放 _data 内存
    delete _data;

    return 0;
}

server.hpp-处理客户端业务

云备份项目中 ,业务处理模块是针对客户端的业务请求进行处理,并最终给与响应。而整个过程中包含以下要实现的功能:

  • 用httplib库搭建http服务器
  • 文件上传请求,备份客户端上传的文件,响应上传成功
  • 文件列表展示请求,客户端浏览器请求一个备份文件的展示页面,响应页面
  • 文件下载请求,通过展示页面,点击下载,响应客户端要下载的文件数据

文件上传

POST方法的/upload情求,HTTP/1.1 200 OK响应,服务器判断是否有文件名字段,之后提取信息写如到backdir目录下,同时备份

页面展示

GET方法的/listshow情求,HTTP/1.1 200 OK响应,展示页面数据,用stringstream 流将前端页面组织起来,放到响应当中

文件下载

GET方法的/download情求,HTTP/1.1 200 OK响应,返回正文数据。

ETag头部字段:标识符,客户端第一次下载会收到标识符,第二次下载服务器根据这个唯一标识判断这个资源有没有被修改过,如果没有被修改过,直接使用原先缓存的数据,不用重新下载,etag=文件名-文件大小-最后一次修改时间

Accept-Ranges:bytes字段告诉客户端其支持断点续传功能(从上次下载断开的位置,重新下载)

Content-Type字段:决定了浏览器如何处理响应正文

If-Range字段:判断与服务端中与上一次请求资源是否一致,如果一致则断点续传,如果不一致,则重新开始下载

Range字段:客户端告诉服务器所需要的区间范围

//server.hpp
#pragma once
#include "data.hpp"
#include "httplib.h"

extern myspace::DataManager *data;
namespace myspace
{
    class Service
    {
    public:
        Service()
        {
            Config cf = Config::GetInstance();
            _server_port = cf.GetServerPort();
            _server_ip = cf.GetServerIp();
            _download_prefix = cf.GetDownloadPrefix();
        }
        // 服务启动(httplib 绑定对应处理函数)
        bool RunModule()
        {
            _server.Post("/upload", Upload);    // 文件上传请求
            _server.Get("/listshow", ListShow); // 文件查看请求
            _server.Get("/", ListShow);
        //  .  匹配除换行符以外的任意单个字符。例如,a.c 可以匹配 abc、adc   
        //  *  匹配前面的元素零次或多次。例如,ab* 可以匹配 a、ab、abb  
        // .*  匹配任意字符任意次数
            std::string download_url = _download_prefix + "(.*)";
            _server.Get(download_url, Download); // 文件下载请求
            _server.listen(_server_ip.c_str(), _server_port);
            return true;
        }
        // 注意回调函数都需要设置成static,因为httplib库中函数要求的参数只有两个
        // 如果不用static修饰,那么会多出来一个this指针
        // 上传文件
        static void Upload(const httplib::Request &req, httplib::Response &rsp)
        {
            // post /upload  文件数据在正文中(正文并不全是文件数据)
            std::cout<<"uploading ..."<<std::endl;
            auto ret = req.has_file("file"); // 判断有没有上传的文件区域
            if (ret == false)
            {
                rsp.status = 400;
                return;
            }
            const auto &file = req.get_file_value("file");
            // file.filename//文件名称    file.content//文件数据
            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);           // 向数据管理模块添加备份的文件信息
        }

        static std::string TimetoStr(time_t t)
        {
            std::string tmp = std::ctime(&t);
            return tmp;
        }
        // 文件列表展示
        // http://119.91.60.49:8080/listshow
        static void ListShow(const httplib::Request &req, httplib::Response &rsp)
        {
            // 1. 获取所有的文件备份信息
            std::vector<BackupInfo> arry;
            data->GetAll(&arry);
            // 2. 根据所有备份信息,组织html文件数据
            std::stringstream ss;
            ss << "<html><body>";
            ss<<"<form action='/upload' method='post' enctype='multipart/form-data'>";
            ss<<" <input type='file' name='file'>";
            ss<<"<input type='submit' value='upload'>";
            ss<<" </form></body></html>";
              
            ss<< "<html><head><title>Download</title></head>";
            ss << "<body><h1>Download</h1><table>";
            for (auto &a : arry)
            {
                ss << "<tr>";
                std::string filename = FileUtil(a._real_path).FileName();
                ss << "<td><a href='" << a._url_path << "'>" << filename << "</a></td>";
                ss << "<td align='right'>" << TimetoStr(a._mtime) << "</td>";
                ss << "<td align='right'>" << a._fsize / 1024 << "k</td>";
                ss << "</tr>";
            }
            ss << "</table></body></html>";
            rsp.body = ss.str();
            rsp.set_header("Content-Type", "text/html");
            rsp.status = 200;
            return;
        }
        // 组装文件
        static std::string GetETag(const BackupInfo &info)
        {
            // etg :  filename-fsize-mtime
            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. 根据资源路径,获取文件备份信息
            BackupInfo info;
            data->GetOneByURL(req.path, info);
            // 3. 判断文件是否被压缩,如果被压缩,要先解压缩,
            if (info._pack_flag == true)
            {
                FileUtil fu(info._pack_path);
                fu.UnCompress(info._real_path); // 将文件解压到备份目录下
                // 4. 删除压缩包,修改备份信息(已经没有被压缩)
                fu.Remove();
                info._pack_flag = false;
                data->Update(info);
            }

            bool retrans = false; // 是否需要断点续传标志
            std::string old_etag;
            if (req.has_header("If-Range"))
            {
                old_etag = req.get_header_value("If-Range");
                // 有If-Range字段且,这个字段的值与请求文件的最新etag一致则符合断点续传
                if (old_etag == GetETag(info))
                {
                    retrans = true;
                }
            }

            // 4. 读取文件数据,放入rsp.body中
            FileUtil fu(info._real_path);
            if (retrans == false)
            {
                fu.GetContent(&rsp.body);
                // 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
            {
                // httplib内部实现了对于区间请求也就是断点续传请求的处理
                // 只需要我们用户将文件所有数据读取到rsp.body中,它内部会自动根据请求
                // 区间,从body中取出指定区间数据进行响应
                //  std::string  range = req.get_header_val("Range"); bytes=start-end
                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.set_header("Content-Range", "bytes start-end/fsize");
                rsp.status = 206; // 区间请求响应的是206
            }
        }
    private:
        int _server_port;
        std::string _server_ip;
        std::string _download_prefix;
        httplib::Server _server;
    };
}

测试

编译完成后运行客户端,之后在浏览器输入http://119.91.60.49:8080/listshow访问展示页面,选择文件可以上传文件(不要带中文),之后upload上传,上传完后回到展示页面,可以看到刚上传的文件,点击文件就是下载,下载过程中关闭服务器,就会中断下载,此时重新启动服务器,点击继续下载就会继续下载。

g++ -std=c++17 -o cloud cloud.cpp bundle.cpp -lpthread -lstdc++fs -ljsoncpp
//client.cpp
#include "httplib.h"
#include "data.hpp"
#include "util.hpp"
#include "config.hpp"
#include "server.hpp"

myspace::DataManager* data;
int main() {
    data = new myspace::DataManager();
    myspace::Service service;
    service.RunModule();
    return 0;
}  

上面测试有缺陷,因为是单线程,无法判断热点文件,下面是多线程,超出热点时间的文件会从backdir 目录被压缩到packdir 目录,展示页面还是能看到文件,点击文件,先从packdir 目录解压到backdir 目录然后再下载,过了热点文件又会自动压缩

#include <thread>
#include "httplib.h"
#include "data.hpp"
#include "util.hpp"
#include "config.hpp"
#include "server.hpp"
#include "hot.hpp"

myspace::DataManager* data;

void HotTest()
{
	myspace::HotManager hot;
	hot.RunModule();
}

void ServiceTest()
{
	myspace::Service srv;
	srv.RunModule();
}
int main() {
    data = new myspace::DataManager();
	std::thread thread_hot_manager(HotTest);
	std::thread thread_service(ServiceTest);
	thread_hot_manager.join();
	thread_service.join();
    return 0;
}    

客户端

自动将指定文件夹中的文件备份到服务器,在VS2019完成

  • 数据管理模块:管理备份的文件信息,判断文件是否需要备份,备份新增的和被修改过的文件
  • 目录遍历模块:获取指定文件夹中的所有文件路径名
  • 文件备份模块:将需要备份的文件上传备份到服务器

util.hpp-工具

将服务器端util.hpp 复制,删除json 类头文件,Compress函数和UnCompress函数和头文件,将头文件\#include <experimental/filesystem> 改成\#include <filesystem>std::experimental::filesystem改成std::filesystem

//util.hpp
#pragma once
#include<iostream>
#include<vector>
#include<string>
#include<fstream>
#include <sys/stat.h>
#include <filesystem>

namespace myspace {
    class FileUtil {
    private:
        std::string _filename;
    public:
        FileUtil(const std::string& filename) :_filename(filename) {}
        //获取文件大小
        size_t FileSize() {
            struct stat st;
            if (stat(_filename.c_str(), &st) < 0)
            {
                std::cout << "get file size failed" << std::endl;
                return -1;
            }
            return st.st_size;
        }
        //获取文件最后一次修改时间
        time_t LastModifyTime() {
            struct stat st;
            if (stat(_filename.c_str(), &st) < 0)
            {
                std::cout << "get file size failed" << std::endl;
                return 0;
            }
            return st.st_mtime;
        }
        //获取文件最后一次访问时间
        time_t LastAcccessTime() {
            struct stat st;
            if (stat(_filename.c_str(), &st) < 0)
            {
                std::cout << "get file size failed" << std::endl;
                return 0;
            }
            return st.st_atime;
        }
        //获取文件路径名中的文件名
        std::string FileName() {
            size_t pos = _filename.find_last_of("/\\");//查找最后一个 / 或 \ 符号
            if (pos == std::string::npos)
            {
                return _filename;
            }
            return _filename.substr(pos + 1);
        }
        //向文件写入数据
        bool SetContent(const std::string& body) {
            std::ofstream ofs;
            ofs.open(_filename, std::ios::binary);
            if (!ofs.is_open()) {
                std::cout << "打开文件失败" << _filename << std::endl;
                return false;
            }
            ofs.write(body.c_str(), static_cast<std::streamsize>(body.size()));
            if (!ofs.good()) {
                std::cout << "写入失败" << std::endl;
                ofs.close();
                return false;
            }
            ofs.close();
            return true;
        }
        //获取文件所有数据
        bool GetContent(std::string* body) {
            size_t fsize = this->FileSize();
            return GetPosLen(body, 0, fsize);
        }
        //获取文件指定位置 指定长度的数据
        bool GetPosLen(std::string* body, size_t pos, size_t len) {
            size_t fsize = this->FileSize();
            if (pos + len > fsize)
            {
                std::cout << "长度错误\n";
                return false;
            }
            std::ifstream ifs;
            ifs.open(_filename, std::ios::binary);
            if (!ifs.is_open()) {
                std::cout << "打开文件失败" << _filename << std::endl;
                return false;
            }
            //从文件开头偏移pos长度
            ifs.seekg(pos, std::ios::beg);
            body->resize(len);
            ifs.read(&(*body)[0], len);
            if (!ifs.good()) {
                std::cout << "读取失败" << std::endl;
                ifs.close();
                return false;
            }
            ifs.close();
            return true;
        }
        
        //移除
        bool Remove()
        {
            if (!Exists()) return true;
            remove(_filename.c_str());
            return true;
        }
        //判断文件是否存在
        bool Exists() {
            return std::filesystem::exists(_filename);
        }
        //创建目录
        bool CreateDirectory() {
            if (this->Exists())return true;
            return std::filesystem::create_directory(_filename);
        }
        //获取目录下所有文件名
        bool GetDirectory(std::vector<std::string>& arry) {
            for (auto& p : std::filesystem::directory_iterator(_filename))
            {
                if (std::filesystem::is_directory(p)) continue;
                arry.push_back(std::filesystem::path(p).relative_path().string());
            }
            return true;
        }
    };
}

data.hpp-数据管理

  • 内存存储用hash,自定义序列化格式 key val\n
// data.hpp
#pragma once
#include <unordered_map>
#include<sstream>
#include "util.hpp"
namespace myspace
{
    // 数据管理类
    class DataManager
    {
	public:
		//字符串分割,对序列化字符串进行分割
		// 按指定的分隔符sep进行分割,将分割后的每一跳数据放到数组中
		//"key val key" -> "key" "val" "key"
		int Split(const std::string& str, const std::string& seq, std::vector<std::string>* arry)
		{
			size_t count = 0;
			size_t pos = 0, idx = 0;
			while (idx < str.size())
			{
				pos = str.find(seq, idx);
				if (pos == std::string::npos) break;
				arry->push_back(str.substr(idx, pos - idx));
				idx = pos + 1;
				count++;
			}

			if (idx < str.size())
			{
				//说明str还有最后一截字符串
				arry->push_back(str.substr(idx));
				count++;
			}
			return count;//分割之后数据的个数
		}
		DataManager(const std::string& backup_file)
			:_backup_file(backup_file)
		{
			InitLoad();
		}
		//程序运行时加载以前的数据
		bool InitLoad()
		{
			//1.从文件中读取所有数据
			FileUtil f(_backup_file);
			std::string body;
			f.GetContent(&body);
			//2.按照自定义格式进行数据解析,"key val\nkey val" ->"kay val" "key val"
			//字符串分割函数得到每一项数据
			std::vector<std::string> arr;
			Split(body, "\n", &arr);
			for (auto& a : arr)
			{
				//再字符串分割函数得到key 和 val
				std::vector<std::string> tmp;
				//"key val" -> "key" "val"
				Split(a, " ", &tmp);
				if (tmp.size() != 2) continue;
				//添加到_table中
				_table[tmp[0]] = tmp[1];

			}

			return true;
		}
		//持久化存储
		bool Storage()
		{
			std::stringstream ss;
			//1.获取所有备份信息
			auto it = _table.begin();
			for (; it != _table.end(); it++)
			{
				//2.自定义持久化存储格式组织  key val\nkey val\n
				ss << it->first << " " << it->second << "\n";
			}

			//3.持久化存储
			FileUtil f(_backup_file);
			f.SetContent(ss.str());
			return true;

		}
		bool Insert(const std::string& key, const std::string& val)
		{
			_table[key] = val;
			Storage();
			return true;
		}
		bool Update(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.find(key);
			if (it == _table.end()) return false;
			*val = it->second;
			return true;
		}
    private:
        std::string _backup_file;                           // 持久化存储文件
        std::unordered_map<std::string, std::string> _table; // 内存中hash存储的文件信息管理表
    };
}

backup.hpp-文件备份

  1. 遍历指定文件夹,获取所有文件信息
  2. 逐一判断文件是否需要上传备份(创建文件的唯一标识)
  3. 需要备份的文件进行上传备份
// backup.hpp
#pragma once
#include"data.hpp"
#include"httplib.h"
#include<Windows.h>//Sleep,注意头文件顺序问题,win要在httplib后面

namespace myspace
{
#define SERVER_IP "119.91.60.49"
#define SERVER_PORT 8080
	class BackUp
	{
	public:
		BackUp(const std::string& back_dir, const std::string& back_file)
			:_back_dir(back_dir)
			, _data(new DataManager(back_file))
		{}
		//获取文件的唯一标识
		std::string GetFileIdentifier(const std::string& filename)
		{
			//a.txt-fsize-mtime
			FileUtil f(filename);
			std::stringstream ss;
			ss << f.FileName() << "-" << f.FileSize() << "-" << f.LastModifyTime();
			return ss.str();
		}
		bool Upload(const std::string& filename)
		{
			//1.获取文件数据
			FileUtil f(filename);
			std::string body;
			f.GetContent(&body);//获取文件数据
			//2.搭建http客户端,上传文件
			//httplib实例化一个client对象
			httplib::Client client(SERVER_IP, SERVER_PORT);
			httplib::MultipartFormData item;
			item.content = body;//正文就是文件数据
			item.filename = f.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 JudgeUpload(const std::string& filename)
		{
			//文件新增||被修改过 需要上传
			//新增判断:有没有备份信息  修改判断:有备份信息 但是文件的唯一标识不一致
			std::string id;
			// 找到历史信息
			if (_data->GetOneByKey(filename, &id))
			{
				//有历史信息判断是否修改过(文件唯一标识)
				std::string new_id = GetFileIdentifier(filename);
				if (new_id == id)//文件标识符一致,不用上传
				{
					return false;
				}
			}
			//注意:这里有一种情况
			//一个大文件正在被拷贝,拷贝需要一个过程,该文件的唯一标识时时刻刻都不一致
			//如果文件唯一标识不一致就上传,该文件会被上传很多次
			//因此我们对于被修改文件的上传应该再加一个条件
			//一段时间没有被修改过  修改时间间隔大于3秒
			FileUtil f(filename);
			//当前时间-最后修改时间
			if (time(NULL) - f.LastModifyTime() < 3)
			{
				//修改时间间隔小于3 认为文件还在修改中,不上传
				return false;
			}
			std::cout << filename << " need upload "<<std::endl;
			return true;

		}
		//客户端整体的逻辑合并 运行模块
		bool RunModule()
		{
			while (1)
			{
				//1. 遍历指定文件夹,获取所有文件信息
				FileUtil f(_back_dir);
				std::vector<std::string> arr;
				f.GetDirectory(arr);
				//2. 逐一判断文件是否需要上传
				for (auto& a : arr)
				{
					if (JudgeUpload(a) == false)
					{
						continue;
					}
					if (Upload(a) == true)//3. 需要备份的文件进行上传备份
					{
						//如果文件上传成功了 新增文件备份信息
						//向数据管理模块插入文件名称和唯一标识
						_data->Insert(a, GetFileIdentifier(a));
						std::cout << a << " upload sucess" << std::endl;
					}
				}
				//等待一秒
				Sleep(1);
			}
		}
	private:
		std::string _back_dir;//监控文件
		DataManager* _data;//数据管理
	};
}

cloud.cpp-客户端

在VS2019完成,项目目录下创建test_backup.txt 文件用来记录备份文件路径,test_dir 目录存放备份文件的目录

#include "util.hpp"
#include "data.hpp"
#include "backup.hpp"

#define BACKUP_FILE "./test_backup.txt"//指定备份文件路径
#define BACKUP_DIR "./test_dir"//指定备份文件路径
int main()
{
	myspace::BackUp backup(BACKUP_DIR, BACKUP_FILE);
	backup.RunModule();
	return 0;
}

启动

服务器:在服务器当前目录下创建backdir 目录和packdir 目录,cloud.dat文件,并且清空,之后启动服务器

客户端:在客户端当前目录下创建test_dir 目录和test_backup.txt 文件,并且清空,之后VS2019启动客户端

操作:

  • 创建test_file.txt 文件用于测试,内容随便写
  • 将test_file.tx 文件复制到test_dir 目录,test_backup.txt 文件记录备份路径,同时客户端控制台输出测试文件相关信息
  • 浏览器访问http://119.91.60.49:8080/listshow查看访问页面有测试文件,点击可以下载,cloud.dat文件有测试文件相关信息,backdir 目录存放了测试文件,超过热点时间就会压缩到packdir 目录
  • 浏览器访问http://119.91.60.49:8080/listshow可以选择文件夹,点击upload上传,刷新可以看到上传的文件,在backdir 目录和cloud.dat文件有记录
  • 如果上传大文件时候,服务器挂了,重连服务器后,可以断点续传

总结

常见问题

  1. 说说你的项目
  2. 为什么要做这个项目
  3. 多个客户端上传文件怎么处理
  4. 断点续传怎么实现的
  5. 云备份速率多少怎么测速,客户端从开始到结束计时
  6. 支持多少个客户端,基于什么环境创建30个进程就出现请求丢失
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

penguin_bark

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

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

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

打赏作者

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

抵扣说明:

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

余额充值