一、项目概述
我们要做的项目是云备份项目,云备份项目的功能是将客户端的指定目录下的所有文件进行备份管理,将文件上传到服务端进行备份,并且对备份的文件进行管理。
该项目的核心技术是要了解掌握有关于http网络协议等知识,该项目采用C++语言进行开发,客户端的主要功能就是将目录下的文件通过http协议,上传到服务端,并且我们需要一个简单的可视化界面,能够看到已经上传的文件的部分基本信息,并且还可以通过该界面去将上传备份的文件进行本地的下载,客户端的开发环境采用Windows下的VStudio2022(要求至少支持C++17的版本),服务端的业务主要就是支持客户端上传和下载文件,并且将文件进行管理,其中还涉及到热点管理,也就是当判断某个文件长时间未被访问,则为了节省空间,我们会对文件进行压缩处理,服务端的开发选择在Linux环境下采用VSCode远程连接云服务器的环境。
本篇博客会从项目准备、环境搭建、第三方库的认识和简单使用、整个项目开发逻辑和各个板块的开发思路以及具体实现,一步步整理总结该项目的内容。
二、实现目标
这个云备份项⽬需要我们实现两端程序,在客户端上的客户端程序需要实现将文件上传到服务端的功能,在服务器上的服务端程序需要实现对上传文件的存储、管理和下载等功能,两端联合实现云服务器的功能
1. 服务端程序
1.1 功能细分
(1)支持客户端文件上传的功能
(2)支持客户端文件备份列表的查看功能
(3)支持客户端文件下载功能(断点续传)
(4)热点文件管理功能(对长时间无访问的文件进行压缩存储)
1.2 模块划分
(1)数据管理模块(管理备份数据的信息,以便随时获取)
(2)网络通信模块(实现客户端和服务端的网络通信)
(3)业务处理模块(上传、查看、下载(断点续传))
(4)热点管理模块(对长时间无访问的文件进行压缩存储)
2. 客户端程序
2.1 客户端功能细分
(1)对指定的文件夹中的文件进行检测(获取文件夹中有什么文件)
(2)判断指定的文件是否需要备份
(3)将需要备份的文件上传备份到服务器中
2.2 客户端模块
(1)数据管理模块(备份文件的数据)
(2)文件检测模块(检测文件夹内需要进行备份的数据)
(3)文件备份模块(将文件上传备份到服务端,也就是网络通信的部分)
三、环境搭建
1. gcc升级到7.3版本
bash
sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
2. 安装jsoncpp库
(1)首先先执行以下两行命令
bash
sudo yum install epel-release //由于版本不同可能安装不成功,则可以暂时忽视这条
sudo yum install jsoncpp-devel
(2)查看"/usr/include"目录下是否存在jsoncpp(或者相关的文件夹),然后看文件夹内是否有json
bash
ls /usr/include/jsoncpp/json/

3. 从GitHub上下载bundle数据压缩库和httplib库
cpp
sudo yum install git //确保有git工具
git clone https://github.com/r-lyeh-archived/bundle.git
git clone https://github.com/yhirose/cpp-httplib.git
ps:建议用外网去下载,不然也可以直接登录到GitHub上去下载到本地先
四、第三方库的认识
1. json库
1.1 介绍
json库是用于进行序列化和反序列化操作的一个库,其中提供的接口能够按照相应的格式,将我们的数据进行序列化和反序列化,而序列化就是把结构化的数据转化成字符串的意思,这便于我们在网络中进行信息的传输,而反序列化则是接收方接受到数据后,将序列化后的信息解析还原成原来的样子,并且能够提取出来,就叫反序列化,json库提供的接口帮我们完成的就是序列化和反序列的工作。
1.2 使用方式
1.2.1 json的数据类型
json对象 : 用{ }括起来
数组:用[ ]括起来
字符串:用" "括起来
数字:直接写,不区分浮点型和整形
例如:
// 原结构化数据
char name = "小明";
int age = 18;
float score[3] = {88.5,99,58};
转换到json内的存储格式如下
{
"name" : "小明",
"age" : 18,
"score" : [88.5,99,58]
}
1.2.2 json的增删查改
要使用json库,首先要包含其头文件,然后就是定义出一个json对象
json::Value root; // json对象
将数据放入json对象的方式如下:
对于一个字符串或者是整形数据而言
root["姓名"] = "小明";
root["年龄"] = 18;
//如此在json对象内存储的格式可以认为是如下:
{
"姓名" : "小明",
"年龄" : 18
}
对于一个数组而言
cpp
//假设当前数据score为{10,20,30}
float score[] = {10,20,30};
root["成绩"].appent(score[0]);
root["成绩"].appent(score[1]);
root["成绩"].appent(score[2]);
//在json中的格式:
{
"成绩" : [10,20,30]
}
将数据成功放到json对象后,通过json提供的接口进行序列化
json提供的接口中有说明,需要序列化,首先要构建一个StreamWriteBuilder类型的对象,然后再通过这个对象提供的方法,使用智能指针的方式去构建SteamWrite类型的对象,再使用里面的write方法去实现序列化,write内的参数,一个是存着数据的json对象,第二个是流类型
cpp
Json::StreamWriterBulider swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::StringStream ss;
sw->write(root,&ss);
而反序列化的步骤类似,反序列化的场景是我接受到了一段json格式的字符串,现在需要将这个字符串内的数据读取出来,重新构建出一个json对象,然后能够让我依次拿到json对象里我需要的数据
所以还是先构建Json对象,然后通过CharReaderBuilder对象提供的接口去创建ChaeReader对象,然后使用其提供的parse接口去反序列化
parse(json序列化后的char*类型的首地址,末尾地址,要将数据写入的json对象,错误描述字符串);
cpp
// 假如str就是接受到的json格式的字符串
std::string str = R"({"姓名":"小黑","年龄":19,"成绩":[60,65,88.5]})";
//反序列化
Json::Value root;
Json::CharReaderBulider crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
cr->parse(str.cstr(),str.cstr()+str.size(),&root,&err);
//从Value类型的对象中读取到数据
root["姓名"].asString();
root["年龄"].asInt();//字符也是这样读
root["成绩"][0]; // 可以使用for循环去读数组类型
root["成绩"][1];
root["成绩"][2];
1.3 示例
序列化
cpp
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
const char* name = "小明";
int age = 18;
float score[] = {77.5,88,60};
// 将数据放到Value中
Json::Value root;
root["姓名"] = name;
root["年龄"] = age;
for(int i = 0;i < sizeof(score); i++)
{
root["成绩"].appent(score[i]);
}
// 序列化
Json::StreamWriterBuilder swb;
std::unique<Json::StreamWriter> sw(swb.newStreamWriter());
std::StringStream ss;
sw->write(root,&ss);
std::cout << ss.str() << std::endl;
return 0;
}
反序列化
cpp
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
//假如str就是接受到的json格式的字符串
std::string str = R"({"姓名":"小黑","年龄":19,"成绩":[60,65,88.5]})";
//反序列化
Json::Value root;
Json::CharReaderBulider crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
cr->parse(str.cstr(),str.cstr()+str.size(),&root,&err);
//从Value类型的对象中读取到数据
root["姓名"].asString();
root["年龄"].asInt();//字符也是这样读
root["成绩"][0]; // 可以使用for循环去读数组类型
root["成绩"][1];
root["成绩"][2];
return 0;
}
2. bundle库
2.1 介绍
bundle库是一个嵌入式压缩库,嵌入式压缩库可以简单理解就是,将库下载后,需要拿到对应的.cpp文件和.hpp文件,将文件拷贝到工程项目中直接使用,这样的使用方式就是嵌入式库,该库的主要作用是用来将文件进行压缩和解压处理的,其中支持23种压缩算法和2种存档格式。
2.2 使用方式
这部分的知识点主要是关于文件读写的操作,我们将文件内容从原文件里读出来,然后对内容进行压缩,库里面直接提供了pack接口,选择压缩的格式,然后是需要压缩的内容,最后会得到压缩好的结果,解压则是unpack
2.3 示例
压缩
cpp
#include<iostream>
#include<string>
#include<fstream>
#include"bundle.h"
int main(int argc,char* argv[])
{
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);//读取文件内容,并放到body中
std::string packed = bundle::pack(bundle::LZIP,body);
//将压缩好后的数据放到ofilename中
std::ofstream ofs;
ofs.open(ofilename,std::ios::binary);
ofs.write(&packed[0],packed.size());
ifs.close();
ofs.close();
return 0;
}
解压
cpp
#include<iostream>
#include<fstream>
#include<string>
#include"bundle.h"
int main(int argc,char* argv[])
{
if(agrc != 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::beg);
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);
//将解压后的内容放到ofilename文件中
std::ofstream ofs;
ofs.open(ofilename,std::ios::binary);
ofs.write(&unpacked[0],unpacked.size());
ofs.close();
return 0;
}
3. httplib库
3.1 介绍
httplib库是一个C++11支持的、支持跨平台Http/Https协议编写的库,只需要将头文件httplib.h包含在代码中即可使用。它帮助我们快速的能够搭建Http协议的服务器,能够让我们把更多的时间花在具体的业务处理上,提高开放的效能。
3.2 使用方式
3.2.1 基本了解
首先要先对Http有基本的了解,Http协议是帮助我们实现网络通信的协议,它有其规定的格式,在Http协议中的request和response的格式如下

通常在编写服务器的时候,我们会将request和response结构化,也就是写成一个类,其中包含重要的字段内容,以及相关的接口,然后通常由客户端构建好request后,将request按上面的格式进行序列化发送到服务器去等待服务器处理并且返回响应response,而httplib库就提供了已经写好的request类和response类
request类
cpp
struct Request
{
//成员
std::string method;//请求方法
std::string path;//资源路径
Headers headers;//头部字段
std::string body; //正文
std::string version;//协议版本
params params;//查询字符串
MultipartFormDataMap files;//保存的是客户端上传的文件信息
Ranges ranges;//用于实现断点续传的,表示请求文件的区间
//接口
//检查头部字段是否存在
bool has_header(const char* key) const;
//获取key值对应的value
std::string get_header_value(const char* key,size_t id = 0) const;
//设置Request中的头部字段,用于构建Request
void set_header(const char*key,const char* val);
//检查是否包含特定的文件上传字段
bool has_file(const char* key) const;
//获取指定文件上传字段的相关信息
MultipartFormData get_file_value(const char* key)const;
};
// Headers就是用来描述头部字段的结构
struct Headers
{
//成员
std::map<std::string, std::string> fields;
//接口
// 添加头部字段
void set(const std::string& key, const std::string& value);
// 获取头部字段值
std::string get(const std::string& key) const;
// 判断是否存在指定头部字段
bool has(const std::string& key) const;
};
// MultipartFormData是用于储存文件相关数据的结构体,而MultipartFormDataMap则是文件数组的感觉
response类
cpp
struct Response
{
std::string version;//协议版本
int status = -1;//响应状态码
Headers headers; //头部字段
std::string body;//响应给客户端的正文
//接口
//设置头部字段
void set_header(const char* key,const char* val);
//设置正文
void set_content(const std::string& s,const char* content_type);
}
然后就是在httplib中,也已经写好了服务器类的接口和客户端接口,可以直接使用这两个类提供的接口去搭建自己的服务器
Server类
cpp
class Server
{
//函数指针类型 void func(const Request&,Response&)
using Handler = std::function<void(const Request&,Response&)>;
using Handlers = std::vector<std::pair<std::regex,Handler>>;
std::function<TaskQueue* (void)> new_task_queue;//线程池
// 用于建立Get方法下,pattern路径对应的handler方法的映射
Server& Get(const std::string& pattern,Handler handler);
// 用于建立Post方法下,pattern路径对应的handler方法的映射
Server& Post(const std::string& pattern,Handler handler);
// 搭建并启动服务器,其中host为主机IP,post为端口
bool Listen(const char* host,int post,int socket_flag = 0);
//...
};
Handler:函数指针类型,定义了一个httplib请求处理问题回调函数的固定格式,即
void (const Request&,Response&);
Handlers: 请求路由数组,可以将其理解成一张表,映射了请求路径与对应的处理函数,当服务器解析到request后,会根据资源请求方法+路径去找到对应匹配的相关方法,这个handlers表的作用就是建立起路由与方法的映射关系。
Client类
cpp
class Client
{
Cilent(const std::string& host,int port);// 传入服务器IP和端口
Result Get(const char* path,const Headers& headers);// 向服务器发送Get请求
Result Post(const char* path,const char* body,size_t content_len,const char* content_type);//向服务器提交Post请求
//Post请求提交多区域数据,常用于提交多文件上传
Result Post(const char* path,const MultiportFormDataltems& items);
}
3.3 示例
搭建一个简单的服务器
cpp
#include<iostream>
#include<httplib.h> // 相关头文件
void Hello(const httplib::Request& req,httplib::Response& rsp)
{
rsp.set_content("Hello World","text/plain");
rsp.status = 200;
}
void Numbers(const httplib::Request& req,httplib::Response& rsp)
{
auto num = req.matches[1];
rsp.set_content(num,"text/plain");
rsp.status = 200;
}
void Multipart(const httplib::Request& req,httplib::Response& rsp)
{
auto ret = req.has_file("file");
if(ret == false)
{
std::cout << "not file upload\n";
rsp.status = 400;
return;
}
const auto& file = req.get_file_value("file");
rsp.body.clear();
rsp.body = file.filename;
rsp.body += "\n";
rsp.body += file.content;
rsp.set_header("Content-Type","text/plain");
rsp.status = 200;
return;
}
int main()
{
httplib::Server server;
server.Get("/hi",Hello);
server.Get(R"(/numbers/(\d+))",Numbers);
server.Post("/multipart",Multipart);
server.listen("0.0.0.0",9090);
return 0;
}
客户端,用于上传多个文件的简单样例
cpp
// 搭建一个简单客户端示例
#include "httplib.h"
#include <iostream>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 80
int main()
{
httplib::Client client(SERVER_IP,SERVER_PORT);
httplib::MultipartFormData item;
item.filename = "hello.txt";
item.content = "Hello World";
item.content_type = "text/plain";
httplib::MultipartFormDataItems items;
items.push_back(item);
auto res = client.Post("/multipart",items);
std::cout << res->status << std::endl;
std::cout << res->body << std::endl;
return 0;
}
这部分主要考查关于对http的了解,我们在这个项目中,我们采用现有的http去搭建服务器,但不代表我们不能够自己搭建,我们应该要清晰掌握http的底层相关逻辑,在项目中我们采用http库是为了减少开发的负担,将更多的精力放到具体的业务开发中。
总结
本篇讲的是关于云备份项目的一个介绍和开发准备,具体的实现思路和代码在下一篇文章中会继续进一步整理。