文章目录
- 项目介绍
- 项目演示
- 环境配置
- Json
- bundle
- httplib
- 项目实现
- 服务端
- `util.hpp`-工具
- `stat`-获取文件的相关信息
- `struct stat` 结构体-定义文件的信息
- `find_last_of`-查找指定字符或字符串最后一次出现的位置
- `std::experimental::filesystem:::exists()`-判断文件是否存在
- `std::experimental::filesystem::create_directory()`-创建目录
- `std::experimental::filesystem::directory_iterator`-目录迭代器
- `std::experimental::filesystem::path`-表示文件系统路径
- ``std::experimental::filesystem::is_directory``-判断路径是否为目录
- `std::experimental::filesystem::relative_path`-计算相对路径
- `remove`-删除文件
- `cloud.json`-配置
- `config.hpp`-配置信息加载
- `data.hpp`-数据管理
- `hot.hpp`-热点管理
- `server.hpp`-处理客户端业务
- 客户端
- `util.hpp`-工具
- `data.hpp`-数据管理
- `backup.hpp`-文件备份
- `cloud.cpp`-客户端
- 启动
- 总结
项目介绍
项目名称:云备份系统
项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件进行热点管理,将长时间无访问文件进行压缩存储。
开发环境: 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
类是服务端发给客户端
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
创建文件成功
Get
和Post
Client
类中的 Get
和 Post
向服务器发送 HTTP 请求
Get
方法:用于从服务器端获取数据。Post
方法:向服务器上传数据,上传文件。
// 发送 GET 请求
auto getRes = cli.Get("/data");
// 发送 POST 请求
std::string postBody = "param1=value1¶m2=value2";
auto postRes = cli.Post("/submit", postBody, "application/x-www-form-urlencoded");
Server
类中的 Get
和 Post
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);
p
:std::experimental::filesystem::path
类型的对象,文件名或者文件目录。可以传入字符串
std::experimental::filesystem::create_directory()
-创建目录
#include <experimental/filesystem>
//编译需要链接库 -lstdc++fs
bool create_directory(const path& p);
p
:std::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
-文件备份
- 遍历指定文件夹,获取所有文件信息
- 逐一判断文件是否需要上传备份(创建文件的唯一标识)
- 需要备份的文件进行上传备份
// 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文件有记录 - 如果上传大文件时候,服务器挂了,重连服务器后,可以断点续传
总结
常见问题:
- 说说你的项目
- 为什么要做这个项目
- 多个客户端上传文件怎么处理
- 断点续传怎么实现的
- 云备份速率多少怎么测速,客户端从开始到结束计时
- 支持多少个客户端,基于什么环境创建30个进程就出现请求丢失