【毕业项目】 云备份

2023-09-18 02:01:41

项目介绍: 运用之前学过的知识做出一个云备份程序

云备份项目认识

我们要做的这个项目一共要做到三点

  • 将本地计算机指定文件夹中的文件备份到服务器上
  • 可以通过浏览器查看并下载文件 支持断点续传功能
  • 服务器对于文件进行热点管理 对于非热点文件进行压缩以节省磁盘空间

在这里插入图片描述

实现目标

该项目需要我们实现两端程序 包括运行在用户主机上的客户端 以及运行在Linux平台的服务器

通过这两端协作来完成上述的功能

服务端功能细分

  1. 支持客户端上传文件
  2. 支持客户端查看备份文件
  3. 支持客户端文件下载并且支持断点续传功能
  4. 热点文件管理

服务端模块化

服务端一共分为四个模块

  • 数据管理模块 主要功能是管理备份文件 以便于随时获取
  • 网络通信模块 主要功能是实现与客户端之间的网络通信
  • 业务处理模块 主要功能是文件上传 文件列表查看 下载
  • 热点管理 主要功能是对于长时间无访问的文件进行压缩

客户端功能细分

  1. 指定文件夹中的文件检测 (获取文件夹中有什么文件)
  2. 判断指定文件夹中的文件是否需要备份 (新增 已备份但是又需要修改 上次备份后又修改过 但是间隔了三秒没有修改 )
  3. 将需要备份的文件上传到服务器

客户端模块划分

客户端可以划分成三个模块

  • 数据管理模块 (管理备份的文件信息)
  • 文件检测模块 (监控指定的文件夹)
  • 文件备份模块 (上传需要的文件数据)

环境搭建

g++升级7.3版本

由于我们的项目需要用到更高版本的gcc 所以说我们需要先进行下环境搭建

我们可以通过

g++ --version

来进行g++版本的查看

接下来分别在命令行中输入下面四条指令

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

我们的g++版本就能迭代成最新的了

在这里插入图片描述

安装jsoncpp库

我们可以通过下面两个指令来安装jsoncpp库

sudo yum install epel-release
sudo yum install jsoncpp-devel

可以通过下面的指令来查看jsoncpp库是否安装成功

ls /usr/include/jsoncpp/json/ -1

下载bundle数据压缩库

在下载bundle数据压缩库之前我们首先要安装git

git的安装方法可以参考我的这篇博客 git

之后使用下面这段指令就可以下载

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

下载httplib库

使用下面这段指令就可以下载

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

第三方库认识

json认识

json 是一种数据交换格式 采用完全独立于编程语言的文本格式来存储和表示数据。

比如说我们要将小明同学的信息使用json存储

原来的信息如下

char name = "小明";
int age = 18;
float score[3] = {88.5, 99, 58};

转变为json存储之后如下

   {
        "姓名" : "小明",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
   }

json存储实际上是将原来的数据变成了一个字符串 (也就是说上面的代码实际上就是一个字符串)

json 数据类型:对象,数组,字符串,数字 介绍如下

  • 对象:使用花括号 {} 括起来的表示一个对象。
  • 字符串:使用常规双引号 “” 括起来的表示一个字符串
  • 数字:包括整形和浮点型,直接使用。
  • 数组:使用中括号 [] 括起来的表示一个数组。

也就是说 如果我们有多组同类型的数据 我们可以使用数组组织起来

代码表示如下

[
   {
        "姓名" : "小明",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
   },
   {
        "姓名" : "小黑",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
   }
]

jsoncpp – value类

jsoncpp 库用于实现 json 格式的序列化和反序列化,完成将多个数据对象组织成为 json 格式字符串,以及将 json格式字符串解析得到多个数据对象的功能

这其中最重要的是三个类 它们分别是value writer reader

value类的主要函数以及作用如下

//Json数据对象类
class Json::Value{
    Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
    Value& operator[](const std::string& key);//简单的方式完成 val["姓名"] = "小明";
    Value& operator[](const char* key);
    Value removeMember(const char* key);//移除元素
    const Value& operator[](ArrayIndex index) const; //val["成绩"][0]
    Value& append(const Value& value);//添加数组元素val["成绩"].append(88); 
    ArrayIndex size() const;//获取数组元素个数 val["成绩"].size();
    std::string asString() const;//转string string name = val["name"].asString();
    const char* asCString() const;//转char*   char *name = val["name"].asCString();
    Int asInt() const;//转int int age = val["age"].asInt();
    float asFloat() const;//转float
    bool asBool() const;//转 bool
};

jsoncpp – writer类

//json序列化类,低版本用这个更简单
class JSON_API Writer {
  virtual std::string write(const Value& root) = 0;
}
class JSON_API FastWriter : public Writer {
  virtual std::string write(const Value& root);
  }
class JSON_API StyledWriter : public Writer {
  virtual std::string write(const Value& root);
}
//json序列化类,高版本推荐,如果用低版本的接口可能会有警告

class JSON_API StreamWriter {
    virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
    virtual StreamWriter* newStreamWriter() const;
}

一般来说json序列化有两个类 一个低版本一个高版本

它们之间的区别在于 低版本的类直接返回一个字符串 而高版本的类返回一个文件描述符

jsoncpp – reader类

//json反序列化类,低版本用起来更简单
class JSON_API Reader {
 bool parse(const std::string& document, Value& root, bool collectComments = true);
}
//json反序列化类,高版本更推荐
class JSON_API CharReader {
    virtual bool parse(char const* beginDoc, char const* endDoc, 
                       Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
    virtual CharReader* newCharReader() const;
}

reader类也分为低版本和高版本

其中低版本的reader类需要我们传入字符串 value对象 还有一个bool值

高版本的reader类需要我们传入字符串的起始位置 字符串的终止位置 value对象 还有一个string对象

jsoncpp – 实现序列化

比如说我们目前拥有以下的数据

   {
        "name" : "xiaoming",
        "age" : 18,
        "score" : [88.5, 99, 58]
   }

现在我们要将这些输出实现序列化的话 我们要分为以下两步走

  1. 将所有的数据存放在json::Value中
  2. 使用json::StreamWriter进行序列化

将所有的数据存放在json::Value中

代码表示如下

  const char* name = "xiaoming";    
  int age = 18;    
  float score[] = {77.5 , 88 , 93.6};    
    
  Json::Value root;    
  root["name"] = name;    
  root["age"] = age;    
  root["score"].append(score[0]);                                                                                     
  root["score"].append(score[1]);    
  root["score"].append(score[2]);    
    
    
  Json::StreamWriterBuilder swb;    
  std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());    
  std::stringstream ss;    
  sw->write(root , &ss); 

序列化后结果如下

在这里插入图片描述

jsoncpp – 实现反序列化

比如说我们目前拥有以下的jason字符串类格式类型的数据

  string str = R"({"name":"xiaohei" , "age":19 , "score":[58.5 , 66 , 35,5]})";    

这里介绍下 C++11中 R()语法
使用该语法之后括号内将为原生字符串 不受任何特殊字符的影响
举个例子 \n本来应该是换行符 可如果在()内的话 它就是两个字符组成的字符串“\n“

反序列化的本质其实就是将json格式的字符串转化为一个value对象

所以说实现反序列化也是分两步走

  1. 定义一个value对象
  2. 使用json::CharReader进行反序列化

代码表示如下

  // deserialize     
  string str = R"({"name":"xiaohei" , "age":19 , "score":[58.5 , 66 , 35,5]})";    
  Json::Value root; // instantiate a value objict                                                                     
  Json::CharReaderBuilder crb;  // subclass    
  std::unique_ptr<Json::CharReader> cr(crb.newCharReader());                       
  string err;                                         
  bool set = cr->parse(str.c_str() , str.c_str()+str.size() , &root , &err);    
                                                                
  if (set == false)    
  {                                                                             
    std::cerr << "json parse error!" << std::endl;    
  }                    
       
  std::cout << root["name"].asString() << std::endl;    
  std::cout << root["age"].asString() << std::endl;    

bundle库认识

Bundle 是一个嵌入式压缩库 支持23种压缩算法和两种存档格式 使用的之后只需要加入两个头文件 bundle.cppbundle.h 即可

需要注意的是 这里的嵌入式和我们所熟知的嵌入式工程不同 它的意思是bundle库可以不需要编译 直接在我们的代码种使用

下面是bundle库的一些方法 大家简单浏览下即可

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库实现文件压缩

我们使用bundle库进行文件压缩实际上只需要用到一句代码

string pack(bundle::LZIP , string& body);

返回值说明:

它会返回一个string对象

参数说明:

  • 第一个参数是压缩的格式
  • 第二个参数是一个strting对象

实现bundle库文件压缩的代码如下

#include <iostream>
#include <fstream>
#include <string>
#include "bundle.h"
using std::cout;
using std::endl;


int main(int argc , char* argv[])
{
  if (argc < 3)
  {
    cout << "no enough arguements" << endl;
    return -1; 
  }

  std::string istreamstring = argv[1];
  std::string ostreamstring = argv[2];
    
  // open and copy the content of istreamstring     
  std::ifstream ifs;                                                                                                  
  ifs.open(istreamstring.c_str() , std::ios::binary);    
    // get the size of istreamstring     
  ifs.seekg(0 , std::ios::end);
  int size = ifs.tellg();    
  ifs.seekg(0 , std::ios::beg);  
      // get end 
  std::string body;
  body.resize(size);
  ifs.read(&body[0] , size); // &body[0] is a wonderful code  because if we use c_str we just get a const str but &body[0] can give us a str whihout const 

  std::string packed = bundle::pack(bundle::LZIP , body); // compress in lzip format 

  std::ofstream ofs;
  ofs.open(ostreamstring , std::ios::binary);
  ofs.write(&packed[0] , packed.size());

  ifs.close();
  ofs.close();
  return 0;
}

我们编译运行之后可以发现

在这里插入图片描述
文件压缩后的大小确实比文件压缩前的大小要小不少

bundle库实现文件解压缩

我们要证明压缩后的文件确实是源文件的压缩的话 就需要解压之后对比两个文件的md5值即可

于是乎我们下面就来实现一个文件解压缩的代码

bundle库解压缩的代码如下

string unpack(string body); 

返回值说明:

它会返回一个string对象

参数说明:

它的参数只有一个 是一个string对象 里面是被压缩的文件内容

实现bundle库文件解压缩的代码如下

#include <iostream>    
using std::cout;    
using std::endl;    
#include <string>    
using std::string;    
#include <fstream>    
#include "bundle.h"    
    
int main(int argc , char* argv[])    
{    
  if (argc < 3)    
  {    
    cout << "no enough arguements" << endl;    
    return -1;    
  }    
    
  string ifilestring = argv[1];    
  string ofilestring = argv[2];    
    
  // open and copy the file     
  std::ifstream ifs;                                                                                                  
  ifs.open(ifilestring , std::ios::binary);    
  ifs.seekg(0 , std::ios::end);    
  size_t size = ifs.tellg();    
  ifs.seekg(0 , std::ios::beg);    
  string body; 
  body.resize(size);
  ifs.read(&body[0] , size);

  // uncompress                                                                                                       
  string unpacked = bundle::unpack(body);

  // write to ofilestring 
  std::ofstream ofs;
  ofs.open(ofilestring , std::ios::binary);
  ofs.write(&unpacked[0] , unpacked.size());

  // close stream 
  ifs.close();
  ofs.close();
  return 0;
}

编译运行之后我们可以发现 MD5值和大小一模一样 所以说压缩和解压缩功能正常 在这里插入图片描述

httplib库认识

httplib 库 一个 C++11 单文件头的跨平台 HTTP/HTTPS 库 安装起来非常容易 只需包含 httplib.h 在你的代码中即可

httplib 库实际上是用于搭建一个简单的 http 服务器或者客户端的库 这种第三方网络库 可以让我们免去搭建服务器或客户端的时间 把更多的精力投入到具体的业务处理中 提高开发效率

博主自己其实也实现过一个简单的Http服务器 如果大家有兴趣可以参考博客

自主实现http服务器

http服务器代码

httplib 库搭建简单服务器

下面介绍的内容是如何使用httplib库来搭建一个简单的服务器

首先我们需要利用httplib里面的Sever类实例化一个对象

httplib::Server server; // instantiate a sever object 

之后调用这个对象的函数去针对请求的各种资源注册各种函数

 server.Get("/hi" , Hello); // register a function method for get request resource hi 
  void Hello(const httplib::Request& req , httplib::Response& res)    
  {                                                                                                                   
    res.body = "Hello";    
    res.status = 200;    
  }   

之后让这个对象去监听服务器的网卡

server.listen("0.0.0.0" , 8081); 

最后让服务器运行起来之后我们使用浏览器访问特定资源便可以收到特定的回复
在这里插入图片描述

httplib 库搭建简单客户端

首先我们需要利用httplib库里面的Client类实例化一个对象

httplib::Client client(SEVER_IP, SEVER_PORT);  // ip  port   

上面的IP和端口均为服务器的

紧接着我们就可以使用这个对象去调用函数来得到一个指针

使用该指针可以获得我们想要的各种数据

  auto res = client.Get("/hi");    
    
  cout << res->status << endl;    
  cout << res->body << endl;   

而如果我们要请求的是post方法 我们则需要进行下面的操作

			// 2. send data to client
			httplib::Client client(SERVER_ADDR, SERVER_PORT); 
			httplib::MultipartFormData item;
			item.content = body;
			item.filename = fu.FileName();
			item.name = "file"; // mark
			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;
			}

我们需要定义一个item 并且将数据填充完毕之后添加到items中 zu欧维一个Post函数的参数上传

项目编写

工具类设计

我们在实际做项目的使用会用到许多文件操作 我们如果是用到的时候才去写就会导致效率较低并且代码冗余复用率低 因此我们可以在项目编写之前就设计出一个工具类 当我们需要使用该工具类的时候只需要定义一个对象即可

对于该工具类的函数声明如下

/*util.hpp*/
class FileUtil{
 private:
   std::string _name;
    public:
   FileUtil(const std::string &name);
   size_t FileSize();
   time_t LastATime();
   time_t LastMTime();
   std::string FileName();
   bool GetPosLen(std::string *content, size_t pos, size_t len);
   bool GetContent(std::string *content);
   bool SetContent(std::strint *content);
   bool Compress(const std::string &packname);
   bool UnCompress(const std::string &filename);
   bool Exists();
   bool CreateDirectory();
   bool ScanDirectory(std::vector<std::string> *arry);
};

下面我会带大家完成整个工具类

属性和名称获取

首先我们定义的函数名称可能会和系统库或者第三方库的名称冲突 所以说我们在完成工具类的时候最好是在一个命名域里面操作

我们在查找文件的各种属性的时候使用的一个系统函数叫做stat

它在windows和Linux平台下都存在 所以说我们不用担心跨平台移植性

stat函数

函数原型如下

int stat(const char* path , struct stat *buf)

参数说明:

  • const char* path 是我们要寻找的路径(是一个字符串)
  • struct stat *buf 这是一个结构体 我们通过该结构体来查看文件的信息

返回值说明:

如果找到该文件返回0 如果没找到返回-1

文件大小获取

      int64_t FileSize()                                            
      {                                                             
        struct stat st;                                             
        int ret = stat(_filename.c_str(), &st);                     
        if (ret < 0)                                                
        {                                                           
          std::cout << "Get file size failed!" << std::endl;        
          return -1;                                                
        }    
    
        return st.st_size;    
      }

文件最后修改时间获取

      time_t LastModifyTime()
      {
        struct stat st;
        int ret = stat(_filename.c_str() , &st);
        if (ret < 0)
        {
          std::cout << "Get lastmodifytime failed!" << std::endl;
          return -1;
        }

        return st.st_mtime;
      }   

文件最后访问时间获取

      time_t LastAccessTime()                                   
      {                                                         
        struct stat st;                                         
                                                                
        int ret = stat(_filename.c_str() , &st);                
        if (ret < 0)                                            
        {                                                       
          std::cout << "Get lastaccesstime failed!"  << std::endl;
          return -1;
        }

        return st.st_atime;
      }   

文件名获取

为什么要要获取文件名呢

因为用户在上传文件名的时候可能连带着路径一起上传了 可是我们只需要一个文件名

一般来说Linux下路径的格式如下 xxx/yyy/test.c

如果我们要想获取最后这个文件名我们需要以最右边的 / 的后一个字符开始往后截取所有内容

代码表示如下

      std::string FileName()    
      {    
        size_t pos = _filename.rfind("/");    
    
        if (pos == std::string::npos)    
        {    
          return _filename;    
        }    
    
        return _filename.substr(pos+1);                                                                               
      } 

功能测试

这里要跟同学们强调的一点是 我们在自主开发一个项目的时候一定要写一段测试一段

不然到最后可能会出现bug比代码行数都多的现象 这样子调试起来就很头疼了

下面编写一段代码来测试下上面实现的功能

#include "Util.hpp"    
    
    
void TestUtil(const std::string& filename)    
{    
  shy::FileUtil fu(filename);    
  std::cout << fu.FileName() << std::endl;    
  std::cout << fu.FileSize() << std::endl;    
  std::cout << fu.LastAccessTime() << std::endl;    
  std::cout << fu.LastModifyTime() << std::endl;    
  return;    
}    
    
void Useage()    
{    
  std::cout << "./main + FileName" << std::endl;    
  return;    
}    
    
int main(int argc , char* argv[])    
{                                                                                                                     
  if (argc != 2)    
  {    
    Useage();    
    return -1;    
  }  
  std::string filename = argv[1];
  TestUtil(filename);
  return 0;
}

测试效果如下

在这里插入图片描述
我们发现各函数使用效果正常

文件读写操作

获取指定位置往后指定长度的数据

      bool GetPosLen(std::string& body , size_t pos , size_t len)
      {
        size_t fsize = this->FileSize();    
        if (pos + len < fsize)    
        {
          std::cout << "Get file len failed!" << std::endl;    
          return false;    
        }                                                                                                             
    
        std::ifstream ifs;    
        ifs.open(_filename.c_str() , std::ios::binary);    
        if (ifs.is_open() == false)    
        {    
          std::cout << "open file failed!" <<  std::endl;    
          return false;    
        }  
        ifs.seekg(pos , std::ios::beg);    
        body.resize(len);    
        ifs.read(&body[0] , len);    
        if (ifs.good() == false)    
        {    
          std::cout << "read file failed!" << std::endl;    
          return false;    
        }    
        ifs.close();
        return true;
      }

获取所有文件数据

我们直接复用上面的函数即可

      bool GetContent(std::string& body)
      {
        size_t fsize = this->FileSize();
        return this->GetPosLen(body , 0 , fsize);
      }    

写入文件数据

      bool SetContent(std::string& body)    
      {        
        std::ofstream ofs;    
        ofs.open(_filename.c_str() , std::ios::binary);    
        if (ofs.is_open() == false)    
        {      
          std::cout << "open ofs failed" << std::endl;    
          return false;    
        }      
               
        ofs.write(&body[0] , body.size());    
        if (ofs.good() == false)    
        {      
          std::cout << "ofs write failed!" << std::endl;    
          return false;    
        }                                                                                                             
        ofs.close();    
        return true;                                                                            
      }    

接下来我们测试上面所写的函数

  std::string body;    
  fu.GetContent(body);    
    
  shy::FileUtil nfu("test.txt");    
  nfu.SetContent(body);    

我们在当前文件下创建一个叫做test.txt的文件 如果说我们的读写操作没问题 那么我们的源文件里面的内容应该和body里面的内容一致

测试结果如下

在这里插入图片描述

结果符合预期

文件压缩和解压缩

文件压缩和文件解压我们直接使用Bundle库中的函数即可

代码表示如下

压缩

      bool Compress(const std::string &packname)
      {
        // 1. get source date 
        std::string body;
        if (this->GetContent(body) == false)
        {
          std::cout << "compress get file content failed" << std::endl;
          return false;
        }

        // 2. compress date 
        std::string packed = bundle::pack(bundle::LZIP , body);

        // 3 write comress date to packname 
        FileUtil fu(packname);
        if (fu.SetContent(packed) == false)                                                                           
        {
          std::cout << "write compress date failed" << std::endl;
          return false;
        }

        return true;
      }

解压缩

      bool uncompress(std::string& unpackname)
      {
        // 1. get compress file name 
        std::string body;                                                                                             
        if (this->GetContent(body) == false)
        {
          std::cout << "get Compress file content failed" << std::endl;
          return false;
        }

        // 2 . uncompress file 
        std::string unpacked = bundle::unpack(body);

        // 3. write unpacked file 
        FileUtil fu(unpackname); 
        if (fu.SetContent(unpacked) == false)
        {
          std::cout << "uncompress write unpacked file failed" << std::endl;
          return false;
        }

        return true;
      }

演示效果如下

在这里插入图片描述

我们发现md5值一样 也就是说压缩和解压缩功能正常

目录操作

在C++17版本中 给我们提供了一个很方便的文件操作库

我们可以使用这个库来进行一些文件目录操作

他的头文件为 <experimental/filesystem>

为了简化我们后面的操作 我们将其命名域重新定义

namespace fs = std::experimental::filesystem;

目录是否存在

检查目录是否存在的函数为

bool exists( const path& p );

返回值说明:

返回值是一个bool类型的数据 如果存在返回true 失败返回false

参数说明:

我们传入一个路径名以判断该路径下的最后一个文件是否存在

使用代码表示如下

      bool Exists()    
      {                                                                                                               
        return fs::exists(_filename);    
      }  

创建目录

创建目录的函数为

bool create_directories( const path& p );

返回值说明:

如果创建成功 则直接返回true 否则返回false

参数说明:

传入一个路径名即可

代码表示如下

      bool CreateDirectory()         
      {                              
        if (this -> Exists())        
        {                            
          return true;               
        }                            
                                                                                                                      
        return fs::create_directories(_filename);
      } 

查看目录

我们可以使用迭代器来遍历一个目录 之后查看该目录里面的文件

具体代码表示如下

      bool ScanDirectory(std::vector<std::string>& arry)  
      {  
        for (auto& p : fs::directory_iterator(_filename))  
        {  
          if (fs::is_directory(p) == true)  
          {  
            continue;  
          }  
                                                                                                                      
          // return relative path
          arry.push_back(fs::path(p).relative_path().string());
        }  
        return true;  
      } 

上面的代码中还有几处小细节

  1. 目录中还有可能存在目录 如果是目录则我们不查看
  2. 迭代器本身不是string类型 我们需要通过类型转换之后才能够放进arry数组中

功能测试

在编译之前我们要连接stdc++fs

我们以当面目录下的文件作为测试对象

测试代码如下

void TestUtilDirectory(std::string& filename)
{
  shy::FileUtil fu(filename);
  fu.CreateDirectory();
  if (fu.Exists())
  {
    std::vector<std::string> arry;
    fu.ScanDirectory(arry);

    for (auto& fn : arry)    
    {    
      std::cout << fn << std::endl;    
    }    
  }    
                                                                                                                      
}  

测试结果如下

在这里插入图片描述
确实打印出了除目录以外的所有文件路径

json实用工具类设计

我们之前使用json类的时候又是智能指针 又是定义对象的 十分繁琐

为了简化操作 我们这里设计一个json使用工具类

大体格式如下

/*util.hpp*/
class JsonUtil{
 public:
 static bool Serialize(const Json::Value &root, std::string *str);
   static bool UnSerialize(const std::string &str, Json::Value *root);
};

序列化代码如下

      static bool Serialize(const Json::Value& root , std::string& str)    
      {    
         Json::StreamWriterBuilder swb;        
         std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());           
         std::stringstream ss;         
         sw->write(root , &ss);     
         str = ss.str();    
         return true;    
      }   

反序列化代码如下

      static bool UnSerialize(const std::string& str , Json::Value& root)    
      {    
           Json::CharReaderBuilder crb;  // subclass                                                                  
           std::unique_ptr<Json::CharReader> cr(crb.newCharReader());         
           std::string err;        
           bool set = cr->parse(str.c_str() , str.c_str()+str.size() , &root , &err);       
           if (set == false)    
           {    
             return false;
           }

           return true;
      }

测试代码如下

void TestSerialize()    
{    
  const char* name = "xiaoming";    
  int age = 18;    
  float score[3] = {90 , 91 , 98};    
  Json::Value root;    
  root["name"] = name;    
  root["age"] = age;     
  root["score"].append(score[0]);    
  root["score"].append(score[1]);    
  root["score"].append(score[2]);    
  std::string json_str;    
  shy::jsonUtil::Serialize(root , json_str);    
  std::cout << json_str << std::endl;    
                                                                                                                      
} 

配置文件模块

系统配置信息

我们可以将系统需要的一些配置信息加载到文件当中

如果我们这样子做的话 想要修改一些信息我们就只需要修改文件中的信息然后重启服务员即可

这样子就让我们的配置信息变得灵活起来

下面是我们的一些配置信息

热点管理时间

在我们的测试样本中 我们将30s没有访问的文件标志为一个非热点文件 可是在现实中 一个非热点文件可能要在一两天没有被访问之后才会被标记 所以说这是一个多变的数据

文件下载的url前缀路径

url的格式一般是这样子的 http://192.168.1.1:9090/path

而我们这里所说的path实际上是一个相对的根目录 一般是wwwroot

所以说假如我们访问的是 /test.txt的话 我们实际上访问的路径是 .../wwwroot/test.txt

那么当我们要访问一个文件资源是listshow的时候 系统要怎么分别 我是要下载listshow这个文件 还是说我要执行listshow指令来查看服务器上有多少文件呢?

这个时候我们的前缀路径就能很好的解决这个问题 如果说我们访问的前缀加上/download/listshow 就是要下载这个文件 如果没有加则说明我们要执行listshow指令

压缩包后缀名

我们可以根据压缩的规则在文件最后加上后缀 比如说 .zip .lz 等等

上传文件存放路径

它标志着我们的文件存放在哪里 别人请求之后应该到哪里找

压缩文件存放路径

压缩文件存放路径和上传文件存放路径肯定是不能放在一起的 因为别人的文件有可能就是压缩文件

服务端备份信息存放地址

当我们将文件压缩之后如果客户端像我们发送请求 要求我们列出文件信息 显然 我们列出压缩后的信息是很不合理的

所以说 在文件压缩之前 我们应该将文件的信息保存起来 以便于客户端随时查询

服务器的IP地址和端口

当我们要将该服务器部署到另外一台主机的时候 我们只需要修改下配置文件中的IP地址和端口就可以了

单例配置类的设计

我们采用懒汉模式来设计该单例类 所以说会有一些线程安全的问题

所以 除了我们之前的那些数据之外我们还要额外加锁保证线程安全

关于单例模式还不了解的同学可以参考我的这篇博客

特殊类的设计

我们的配置文件是使用json格式书写的 代码如下

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

接下来是模块类的设计

#pragma once     
#include "Util.hpp"    
#include <mutex>    
using namespace shy;    
    
namespace cloud    
{    
    
#define CONFIG_FILE "./cloud.conf"    
  class Config    
  {    
    private:    
      Config()    
      {    
        ReadConfigFile();    
      }    
      static Config* _instance;    
      static std::mutex _mutex;                                                                                                                                                                                                                                                                                                                                                                                                                                       
    private:    
      int _hot_time;    
      int _setver_port;    
      std::string _server_ip;    
      std::string _download_prefix;    
      std::string _packfile_suffix;    
      std::string _pack_dir;    
      std::string _back_dir;    
      std::string _backup_file;    
      bool ReadConfigFile()    
      {    
         FileUtil fu(CONFIG_FILE);    
         std::string body;    
         if (fu.GetContent(body) == false)    
         {    
           std::cout << "load Config file failed" << std::endl;    
           return false;    
         }    
    
         Json::Value root;    
         if (jsonUtil::UnSerialize(body , root) == false)    
         {    
           return false;    
         }    
    
         _hot_time = root["hot_time"].asInt();    
         _setver_port = root["setver_port"].asInt();    
         _server_ip = root["server_ip"].asString();    
         _download_prefix = root["download_prefix"].asString();    
         _pack_dir = root["pack_dir"].asString();    
         _back_dir = root["back_dir"].asString();    
         _backup_file = root["backup_file"].asString();    
         return true;    
      }    
    public:    
      static Config* GetInstance()    
      {    
        if (_instance == nullptr)    
        {    
          _mutex.lock();    
          if (_instance == nullptr)    
          {    
            _instance = new Config();    
          }    
          _mutex.unlock();    
         }    
        return _instance;    
      }    
    
      int GetHotTime()    
      {    
        return _hot_time;    
      }    
    
     int GetSetverPort()    
     {    
       return _setver_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;    
    }    
  };    
  Config* Config::_instance = nullptr;    
  std::mutex Config::_mutex;    
}  

单例配置类的测试

测试的代码也很简单 我们直接使用我们写的cloud类 看看能不能获取各个类型的数据就可以了

  void ConfigTest()
  {
    cloud::Config* config = cloud::Config::GetInstance();                         
    std::cout << config->GetHotTime() << std::endl;
    std::cout << config->GetServerIP() << std::endl;
    std::cout << config->GetSetverPort() << std::endl;
    std::cout << config->GetPackfileSuffix() << std::endl;
  } 

数据管理模块

数据管理模块信息

我们首先要知道我们为什么要设计数据管理模块

对于一些数据来说 我们接收到之后使用一次就可以丢了 那么我们就可以不需要将它管理起来

如果说一个数据我们以后要经常用 或者说必须要用 我们就必须要将它管理起来了

所以说我们设计数据管理模块的根本原因是我们之后要用到这些数据

我们要存放的数据有哪些呢?

  1. 文件的实际存储路径:当客户端需要下载文件的时候 服务器从这个路径读取数据并进行响应
  2. 文件压缩包存放路径名:如果文件是一个非热点文件 那么这个文件就会被压缩 压缩后的文件路径 就是文件压缩包存放路径名 此外如果客户要下载一个压缩文件 我们会先解压缩 并且发送给客户端
  3. 文件是否被压缩的标志位
  4. 文件大小
  5. 文件最后一次修改时间
  6. 文件最后一次访问时间
  7. 文件访问的URL路径 (其实就是我们平时见到的下载路径)

如何管理数据

  1. 用于数据信息访问 :使用hash表在内存中管理数据 以url的path作为key值
  2. 信息的持久化管理 :使用json序列化将所有的数据保存在文件中

数据管理模块设计

我们这里要介绍到一个新的锁的种类 读写锁

pthread_rwlock_t_rwlock 

这个锁的特点是 读共享 写互斥

我们这里之所以不用互斥锁的原因是互斥锁是要串行化的 它的效率特别低

代码表示如下

namespace cloud    
{    
  using namespace shy;    
  typedef struct BackupInfo_t    
  {    
    bool pack_flag;    
    size_t fsize;    
    time_t mtime;    
    time_t atime;    
    std::string real_path;    
    std::string pack_path;    
    std::string url;    
    
    public:    
    void NewBackupInfo(const std::string& realpath)    
    {    
      Config* config = Config::GetInstance();                                                                                                                                                                                                                   
      std::string packdir = config->GetPackDir();    
      std::string packsuffix = config->GetPackfileSuffix();    
      std::string download_prefix = config->GetDownloadPrefix();    
      FileUtil fu(realpath);    
      this->pack_flag = false;    
      this->fsize = fu.FileSize();    
      this->mtime = fu.LastModifyTime();    
      this->atime = fu.LastAccessTime();    
      this->real_path = realpath;    
      this->pack_path = packdir + fu.FileName() + packsuffix;    
      this->url = download_prefix + fu.FileName();    
    }    
  }BackupInfo;    
}  

当然 写完一段代码之后我们最重要的就是测试了 我们在main函数中使用该类并且依次打印其所有信息看看是否有错漏

在此之前 因为每次都要编译bundle.cpp库都需要花费很多的时间 我们可以将其生成一个静态库来使用

具体生成静态库的方法可以参考我的这篇博客 动静态库

测试代码如下

void DataTest(const std::string& filename)                           
{    
  cloud::BackupInfo info;    
  info.NewBackupInfo(filename);    
  std::cout << info.pack_flag << std::endl;    
  std::cout << info.fsize << std::endl;    
  std::cout << info.mtime << std::endl;    
  std::cout << info.atime << std::endl;    
  std::cout << info.real_path << std::endl;    
  std::cout << info.pack_path << std::endl;    
  std::cout << info.url << std::endl;    
}   

测试结果如下

在这里插入图片描述

数据管理类的实现

数据管理类中要使用哈希表存储数据 并且要使用读写锁

      std::string _backup_file;
      pthread_rwlock_t _rwlock;
      std::unordered_map<std::string,BackupInfo> _table;

在数据管理类中 我们要实现 插入 更新 读取操作 实现函数如下

      bool Insert(const BackupInfo& info)
      {
        pthread_rwlock_wrlock(&_rwlock);
        _table[info.url] = info;
        pthread_rwlock_unlock(&_rwlock);
        return true;
      }
      bool Update(const BackupInfo& info)
      {
        pthread_rwlock_wrlock(&_rwlock);                                                       
        _table[info.url] = info;    
        pthread_rwlock_unlock(&_rwlock);    
        return true;    
      }
      bool GetOneByURL(const std::string& url , BackupInfo* info)                              
      {
        pthread_rwlock_rdlock(&_rwlock);
        auto it = _table.find(url);
        if (it == _table.end())
        {
          return false;
        }
        *info = it->second;
        pthread_rwlock_unlock(&_rwlock);
        return true;
      }
      bool GetOneByRealPath(const std::string& realpath, BackupInfo* info)
      {
        pthread_rwlock_wrlock(&_rwlock);                                                       
        auto it = _table.begin();
        while (it != _table.end())
        {
          if (it->second.real_path == realpath)
          {
            *info = it->second;
            pthread_rwlock_unlock(&_rwlock);
            return true;
          }
          it++;
        }
        pthread_rwlock_unlock(&_rwlock);
        return false;
      }

      bool GetAll(std::vector<BackupInfo>* array)
      {
        pthread_rwlock_wrlock(&_rwlock);
        auto it = _table.begin();
        while (it != _table.end())
        {
          array->push_back(it->second);
        }
        pthread_rwlock_unlock(&_rwlock);
        return true;
      }

数据管理类的测试

接下来我们在主函数中对于数据管理类进行测试

代码表示如下

void DataTest(const std::string& filename)    
{    
  cloud::BackupInfo info;    
  info.NewBackupInfo(filename);    
  cloud::DataManager data;    
  data.Insert(info);    
    
  cloud::BackupInfo tmp;    
  data.GetOneByURL("/download/bundle.h", &tmp);    
  std::cout << "test getonebyrul" << std::endl;    
  std::cout << tmp.pack_flag << std::endl;    
  std::cout << tmp.fsize << std::endl;    
  std::cout << tmp.mtime << std::endl;    
  std::cout << tmp.atime << std::endl;    
  std::cout << tmp.real_path << std::endl;    
  std::cout << tmp.pack_path << std::endl;    
  std::cout << tmp.url << std::endl;    
    
  info.pack_flag = true;    
  data.Update(info);    
  std::vector<cloud::BackupInfo> arry;    
  data.GetAll(&arry);    
  std::cout << "test getall" << std::endl;    
  for (auto x : arry)    
  {    
    std::cout << x.pack_flag << std::endl;    
    std::cout << x.fsize << std::endl;    
    std::cout << x.mtime << std::endl;    
    std::cout << x.atime << std::endl;    
    std::cout << x.real_path << std::endl;    
    std::cout << x.pack_path << std::endl;    
    std::cout << x.url << std::endl;    
  }    
    
  data.GetOneByRealPath(tmp.real_path ,&tmp);    
  std::cout << "test getonebyrealpath" << std::endl;    
  std::cout << "--------------------------" << std::endl;    
  std::cout << tmp.pack_flag << std::endl;    
  std::cout << tmp.fsize << std::endl;    
  std::cout << tmp.mtime << std::endl;    
  std::cout << tmp.atime << std::endl;    
  std::cout << tmp.real_path << std::endl;    
  std::cout << tmp.pack_path << std::endl;                                                                                                                                                                                                                                                                                                                            
  std::cout << tmp.url << std::endl;    
}    

测试结果如下

在这里插入图片描述

可以正常运行

数据的持久化存储

我们的程序有可能关机 但是在下次开机的时候我们可能还是会需要这些数据 所以说我们需要将这些数据进行持久化存储

在前面的第一个项目httpsever中 我们使用mysql存储数据

那么在这个项目中 我们就使用文件来存储数据

持久化存储分为四步

  • 获取所有数据
  • 使用json格式存储
  • 序列化数据
  • 写入文件
        bool Storage()    
        {    
          // 1. get all date    
          std::vector<BackupInfo> arry;    
          this->GetAll(&arry);    
          // 2. add to json::value    
          Json::Value items;    
W>        for (int i = 0; i < arry.size(); i++)    
          {    
            Json::Value root;    
            root["pack_flag"] = arry[i].pack_flag;    
            root["fszie"] = std::to_string(arry[i].fsize);    
            root["atime"] = std::to_string(arry[i].atime);    
            root["mtime"] = std::to_string(arry[i].mtime);    
            root["real_path"] = arry[i].real_path;    
            root["pack_path"] = arry[i].pack_path;    
            root["url"] = arry[i].url;    
            items.append(root); // append array elements                                                                                                                           
          }    
      
          // 3. serialize the items     
          std::string body;    
          jsonUtil::Serialize(items , body);    
          // 4. write the file     
          FileUtil fu(_backup_file);    
          fu.SetContent(body);    
          return true;    
        }   

数据的持久化存储测试

当我们在测试函数内部调用该函数的时候 就会生成一个cloud.dat文件用来作为持久化存储

文件如下

在这里插入图片描述

初始化从文件中读取数据

从文件中读取数据分为三步

  • 从cloud.dat中读取数据
  • 反序列化
  • 添加值到table中
        bool InitLoad()
        {
          // 1 get data from cloud.dat 
          FileUtil fu(_backup_file);
          if (fu.Exists() == false)                                                                                                                  
          {
            return true;
          }
          std::string body;
          fu.GetContent(body);
          // 2 unserialize 
          Json::Value root;
          jsonUtil::UnSerialize(body , root);
          // 3 add value to table 
W>        for (int i = 0; i < root.size(); i++)
          {
            BackupInfo info;
            info.pack_flag = root[i]["pack_flag"].asBool();
            info.fsize = root[i]["fszie"].asInt();
            info.mtime = root[i]["mtime"].asInt();
            info.atime = root[i]["atime"].asInt();
            info.pack_path = root[i]["pack_path"].asString();
            info.real_path = root[i]["real_path"].asString();
            info.url = root[i]["url"].asString();
            Insert(info);
          }
        }

这里我们需要注意的是 如果出现类型转化的错误 我们可以使用强制类型转换来解决

测试结果如下

在这里插入图片描述

cloud.dat 中的数据相同

在这里插入图片描述

热点管理模块

热点管理模块的设计

首先我们要理解热点管理模块的作用是什么

为了节省磁盘空间
对于服务器上备份的文件 如果长时间没有被访问 则认为是非热点文件 我们就将其压缩 以节省磁盘空间

实现思路:

  • 我们遍历所有文件 使用文件的最后一次访问时间减去当前时间 我们就能得到一个差值 如果这个差值大于等于我们设定好的一个阈值 则对其进行压缩 存放到压缩路径中并且删除源文件

其中 我们遍历所有文件有两种方式

  • 从数据管理模块中遍历所有的文件备份信息
  • 遍历备份文件夹 获取所有的文件信息

我们选择第二种方式来遍历 这是因为如果使用第一种方式有可能有文件上传成功但是并没有加载到数据管理模块中 有可能会导致数据的缺失

并且 我们文件的最后一次访问时间不能以数据管理模块中的最后一次访问时间进行判断 因为这个时间如果我们没有更新 它就是长时间保持不变的

热点管理流程

  1. 获取备份目录下的所有文件
  2. 逐个判断文件是否为非热点文件
  3. 非热点文件压缩处理
  4. 删除源文件 修改备份信息

设计热点管理类

私有成员有

  • 备份文件路径
  • 压缩文件路径
  • 压缩包后缀名
  • 热点时间

成员函数有

  • 构造函数
  • 运行模块 (实现热点管理流程)

热点管理模块的实现

extern函数的作用

当我们使用extern关键字修饰一个变量的时候 它的意思是告诉编译器 虽然你现在没有找到关于这个变量的定义 但是这个变量在其他文件中是一个全局变量 你现在直接放行即可(不报错)

remove函数的作用

在Linux中 我们可以使用remove函数来删除一个文件

例如 remove(filename)

如果成功会返回0 如果失败会返回-1 同时错误码将被设置

整体代码如下

extern cloud::DataManager* _data;
namespace cloud
{
  class HotManager
  {
    private:
      std::string _back_dir;
      std::string _pack_dir;
      std::string _pack_suffix;
      int _hot_time;
    private:
      bool HotJudge(const std::string &filename)
      {
        // hot return false; 
        // non hot return true; 
        FileUtil fu(filename);
        time_t last_atime = fu.LastAccessTime();
        time_t cur_time = time(nullptr);
        if (cur_time - last_atime > _hot_time)
        {
          return true;    
        }    
        return false;
      }    
    public:    
      HotManager()                                                                                                                                                                                                                                              
      {    
        Config* config = Config::GetInstance();    
        _pack_dir = config->GetBackDir();    
        _back_dir = config->GetPackDir();    
        _pack_suffix = config->GetPackfileSuffix();    
        _hot_time = config->GetHotTime();    
      }    
      bool RunModule()    
      {    
        while (1)    
        {    
            // 1. traverse the backup directory to get all file names     
            FileUtil fu(_back_dir);    
            std::vector<std::string> arry;    
            fu.ScanDirectory(arry);    
    
            // 2. judge if the file is a non hotspot file    
            for (auto a : arry)    
            {    
              if (HotJudge(a) == false)    
              {    
                continue;    
              }    
                             BackupInfo bi;    
               if(_data -> GetOneByRealPath(a , &bi) == false)    
               {    
                 // we have a file but not recorded    
                 bi.NewBackupInfo(a); // set a new BackupInfo 
               }
               // 3. compress the non hotspot file
               FileUtil tmp(a);
               tmp.Compress(bi.pack_path);
               // 4. delete source file and update the infomation
               tmp.Remove();
               bi.pack_flag = true;
               _data->Update(bi);
            }
            usleep(1000);
        }
     }
  };
}

热点管理模块的测试

我们可以试验将一个文件放到backdir中 当一段时间(hot_time)后

backdir中的文件就会自动压缩并且存放到packdir中

在这里插入图片描述

服务端处理模块

服务端处理模块思路

我们想要处理服务端发送过来的请求 首先就要和服务端建立连接 (这里我们通过httplib库完成)

步骤如下

  1. 搭建网络通信服务器 通过httplib完成
  2. 业务处理请求
    其中 业务处理请求有如下三种
  • 文件上传请求: 备份客户端上传的文件 响应上传成功
  • 文件列表请求: 客户端浏览器请求一个备份文件的展示页面 响应页面
  • 文件下载请求: 通过展示页面 点击下载 响应客户端要下载的文件数据

网络通信接口设计

其实设计网络通信接口就是约定好 客户端发送什么样的请求 我们就给予什么样的响应

上传请求

当客户端使用post方法的/update请求的时候 我们认为这是一个文件上传请求

服务器于是对于该请求进行解析 获取文件名和文件内容 并且在服务器上对于该内容进行保存

而响应的话我们只需要返回一个成功就可以 比如说

HTTP/1.1 200 OK

展示页面

如果客户端使用GET方法请求 /listshow 则我们此时返回给客户端一个html页面

html页面的内容就是我们所有的文件信息

文件下载

如果客户端使用GET方法请求 /download/xxx 则我们此时通过正文给客户端响应该文件的所有信息

业务处理类设计

业务处理类的设计实际上就是服务类的设计 因为服务类需要和客户端保持通信 所以说其私有成员如下

  • 端口号
  • IP地址
  • 下载前缀
  • httplib库中的sever对象

成员函数如下

  • 构造函数
  • 运行模块
  • 三个业务处理函数

代码表示如下

        Service()    
        {    
          Config* config = Config::GetInstance();    
          _server_port = config->GetSetverPort();    
          _server_ip = config->GetServerIP();    
          _download_prefix = config->GetDownloadPrefix();    
        }                                                                                                                       
      
        bool RunModule()    
        {    
          _server.Post("/upload" , Upload);    
          _server.Get("/listshow" , ListShow);    
          _server.Get("/" , ListShow);    
          std::string download_url = _download_prefix + "(.*)";    
          _server.Get(download_url , Download);    
          _server.listen(_server_ip , _server_port);    
          return true;    
        }  

上传/展示/下载函数编写

上传函数编写

        static void Upload(const httplib::Request &req, httplib::Response &rsp)    
        {    
          // post /upload     
          auto ret = req.has_file("file"); // judge it if there is a file update area     
          if (ret == false)    
          {    
            rsp.status = 400;    
            return;    
          }    
          const auto& file = req.get_file_value("file");    
          std::string back_dir = Config::GetInstance()->GetBackDir();    
          std::string realpath = back_dir + FileUtil(file.filename).FileName();    
          FileUtil fu(realpath);    
          std::string content = file.content;    
          fu.SetContent(content); // write data to file     
          BackupInfo info;    
          info.NewBackupInfo(realpath);    
          _data->Insert(info);                                                                                                  
          return ;    
        }   

展示函数编写

对于展示函数来说 我们首先要获取所有备份文件信息 其次组织下html文件数据即可

时间转化函数

string ctime(time_t* t)

它要求我们传入一个时间戳 我们会返回一个字符串的时间数据

代码表示如下

        static void ListShow(const httplib::Request &req , httplib::Response &rsp)    
        {                
          // 1. get all file backup info     
          std::vector<BackupInfo> arry;                                                                                                   
             
          _data->GetAll(&arry);              
      
          // 2. create html pages based on file data     
          std::stringstream ss;    
          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 << "'>" << 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;                 
        } 

运行结果如下

**加粗样式**

下载函数编写


HTTP的ETag字段

这个字段中存储了一个资源的唯一标识

客户端第一次下载文件的时候会收到这个响应信息

第二次下载的时候就会将这个信息发送给服务器 想要让服务器根据这个唯一标识判断这个资源有没有被修改过 如果没有被修改过直接使用原先缓存的数据 不用重新下载

而HTTP协议本身对于etag中是什么数据并不关心 只要你服务端能够自己标识就行

因此我们的etag就使用 “文件名 - 文件大小 - 最后一次修改时间” 组成

HTTP协议的Accept-Ranges字段

用于告诉客户端服务器支持断点续传 并且以字节作为单位


代码表示如下

        static void Download(const httplib::Request &req, httplib::Response &rsp)    
        {    
          // 1.  get client Request path     
          // 2.  get backup info     
          BackupInfo info;    
          _data->GetOneByURL(req.path , &info);    
          // 3.  uncompress if compressed    
          if (info.pack_flag == true)    
          {    
            FileUtil fu(info.pack_path);    
            fu.uncompress(info.real_path);    
            // 4.  delete packup info     
            fu.Remove();    
            info.pack_flag = false;    
            _data->Update(info);    
          }    
          // 5.  read file info     
          FileUtil fu(info.real_path);                                                                                                    
          fu.GetContent(rsp.body);    
          // 6.  set ETag accept-ranges     
          rsp.set_header("Accept-Ranges" , "bytes");    
          rsp.set_header("ETag" , GetETag(info));    
          rsp.status = 200;    
        }

如果我们在刚刚的 html 页面中点击超链接 我们就会发现什么都不会发生

这是因为我们没有设置响应报头中 Content-Type 字段是什么

实际上 Content-Type 字段是一个非常重要的属性 它告诉了接收/响应方 正文是什么格式的 是html呢?还是一个文本呢?还是一个下载连接呢?

在我们加上 Content-Type字段 application/octet-stream 之后 我们就可以告诉浏览器 这是一个二进制流文件(通常标识为一个可下载文件)

之后点击我们的文件名就可以下载了

在这里插入图片描述

断点续传

为什么要实现断点续传

当我们在网络传输的过程中 如果因为某种异常而下载中断而需要从头开始传输的话效率较低 因为这相当于把已经传输的文件再次传输一遍

所以说 我们要实现断点续传功能来避免这种情况

如果出现异常中断 我们只需要从中断的位置继续传输即可

实现思想和需要考虑的问题

客户端在下载文件的时候 每次接收到数据写入文件的时候记录下自己当前下载的数据量

当异常下载中断的时候 下次断点续传的时候 只需要将自己需要数据的区间告诉服务器

之后服务器回传客户端需要的数据即可

需要注意的问题:

如果上次下载文件之后 文件在服务器上被修改了 那么此时我们不应该进行断点续传 而应该重新进行文件的下载操作

http协议中 断点续传的实现

实现断点续传最重要的两个点

  1. 告诉服务器需要下载的范围
  2. 检测上一次下载后该文件有没有被修改

HTTP协议实现

http协议服务器中有两个字段 分别是

  • etag
  • accept-ranges

第一个字段是唯一标识一份资源 第二个字段是告诉客户端自己是否支持断点续传

http协议客户端中有两个字段 分别是

  • ifrange
  • range 10~1000

ifrange表明了 服务器是否支持断点续传

而range字段表明了 需要从服务器下载的 10到1000个字节

206状态码

206状态码表示请求成功 返回一部分内容

断点续传实现

其实我们的httplib库已经实现了断点续传功能 就算我们不改上面的代码 服务器也是支持这个功能的

但是我们还是要理解断点续传是怎么实现的

结构代码大体如下

            old_etag = req.get_header_value("If-Range");    
            if (old_etag == GetETag(info))    
            {    
              retrans = true;    
            }    
          }    
          if (retrans == false)    
          {    
            // 6.  set ETag accept-ranges     
            rsp.set_header("Accept-Ranges" , "bytes");    
            rsp.set_header("Content-Type" , "application/octet-stream");    
            rsp.set_header("ETag" , GetETag(info));    
            rsp.status = 200;    
          }    
          else    
          {    
            // httplib has achieced resume    
            fu.GetContent(rsp.body);    
            rsp.set_header("Accept-Ranges" , "bytes");    
            rsp.set_header("ETag" , GetETag(info));    
            rsp.status = 206; // range resquest responce status code 206                                                                  
          } 

我们会拿出客户端的ETag 和 文件当前的ETag进行判断

如果不同 则不进行断点续传 进行正常的下载操作

如果不同 则我们进行断点续传

我们要用客户端的request请求中获取range字段 获取起始位置 终止位置

并且从文件中读取好这些位置的信息 返回给客户端即可

客户端编写

客户端要实现的功能: 自动对指定文件夹中的文件进行备份

也就是说要实现下面三个模块

  • 数据管理模块:管理备份的文件信息
  • 目录遍历模块:获取指定文件夹中的所有文件路径名
  • 文件备份模块:将需要备份的文件上传备份到服务器

由于客户端和服务器的文件操作基本相同 所以我们将我们服务器文件操作的代码复制一份即可

数据管理类设计

我们的客户端只需要有一个文件的唯一标识 因为我们客户端只需要判断该文件是否要被上传即可

一个唯一标识就能很好的做到这一点

此外 该类还应该具有以下函数

  • 初始化(从文件中读取数据)
  • 持久化存储
  • 插入数据
  • 更新
  • 查找到一个数据

代码标识如下

namespace cloud
{
	class DataManager
	{
	private:
		std::string _backup_file; 
		std::unordered_map<std::string, std::string> _table;
	public:
		DataManager(const std::string& backup_file)
			:_backup_file(backup_file)
		{}

		bool Storage()
		{
			std::stringstream ss;
			// 1. get all backup info 
			auto it = _table.begin();
			while (it != _table.end())
			{
				// 2. key val\n  key val\n  key val\n
				ss << it->first << " " << it->second << std::endl;
			}
			// 3. persistence storage
			FileUtil fu(_backup_file);
			fu.SetContent(ss.str());
			return true;
		}

		int Split(const std::string& str, const std::string& sep, std::vector<std::string>& arry)
		{
			int count = 0;
			size_t pos = 0;
			size_t idx = 0;
			while (1)
			{
				pos = str.find(sep, idx);
				if (pos == std::string::npos)
				{
					break;
				}
				if (pos == idx)
				{
					idx = pos + sep.size();
					continue;
				}
				std::string tmp = str.substr(idx, pos - idx);
				arry.push_back(tmp);
				count++;
				idx = pos + sep.size();
			}

			if (idx < str.size())
			{
				arry.push_back(str.substr(idx));
				count++;
			}

			return count;
		}

		bool InitLoad()
		{
			// 1. get all data from file 
			FileUtil fu(_backup_file);
			std::string body;
			fu.GetContent(body);
			// 2. analysis the string and add info to table
			std::vector<std::string> arry;
			for (auto a : arry)
			{
				std::vector<std::string> tmp;
				Split(a, " ", tmp);
				if (tmp.size() != 2)
				{
					continue;
				}
				_table[tmp[0]] = tmp[1];
			}
			return true;
		}


		bool Insert(const std::string& key, const std::string& val)
		{
			_table[key] = val;
			return true;
		}
		bool Update(const std::string& key, const std::string& val)
		{
			_table[key] = val;
			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;
		}

	};
}

数据管理类测试

我们在写完数据管理类之后在main函数中写出下面的代码

	cloud::FileUtil fu("./");
	std::vector<std::string> arry;
	fu.ScanDirectory(arry);
	cloud::DataManager data(BACKUP_FILE);
	for (auto x : arry)
	{
		data.Insert(x, "adadd");
	}

这段代码的含义是 在 ./ 目录中 创建一个BACKUP_FILE 文件 并且将arry中的所有数据保存到这个文件中

我们打开目录即可看到 backup.dat 文件 文件信息如下

在这里插入图片描述

文件备份类设计

我们需要自动将指定文件夹中的文件备份到服务器中

所以我们要做以下流程

  • 遍历指定文件夹 获取文件信息
  • 逐一判断文件是否需要备份
  • 需要备份的文件进行上传备份

所以说需要的私有成员如下

  • 要监控的文件夹
  • 数据管理类

需要的成员函数如下

  • 构造函数
  • 运行模块
  • 得到文件唯一标识
  • 上传文件

文件的唯一标识

我们使用文件名+大小+最后一次修改时间作为文件的唯一标识

整体函数如下

namespace cloud
{
#define SERVER_ADDR "43.143.132.22"
#define SERVER_PORT 8080
	class Backup
	{
	private:
		std::string _back_dir;
		DataManager* _data;
	public:
		Backup(const std::string& backup_dir, const std::string& back_file)
		{
			_back_dir = backup_dir;
			_data = new DataManager(back_file);
		}

		std::string GetFileIdentifier(std::string filename)
		{
			// a.txt-fsize-mtime;
			FileUtil fu(filename);
			std::stringstream ss;
			ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastModifyTime();
			return ss.str();
		}

		bool Upload(const std::string& filename)
		{
			// 1. get all data
			FileUtil fu(filename);
			std::string body;
			fu.GetContent(body);

			// 2. send data to client
			httplib::Client client(SERVER_ADDR, SERVER_PORT); 
			httplib::MultipartFormData item;
			item.content = body;
			item.filename = fu.FileName();
			item.name = "file"; // mark
			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 IsNeedUpload(const std::string& filename)
		{
			// we have two condition to judge if the file is new
			// 1. we don have the info in the table
			// 2. the unicode change
			std::string id;
			if (_data->GetOneByKey(filename, id) == false)
			{
				return true;
			}

			std::string new_id = GetFileIdentifier(filename);
			if (new_id == id)
			{
				return false;
			}
			// copy need time 
			// so if the file has big content we will continue upload it 
			FileUtil fu(filename);
			if (time(NULL) - fu.LastModifyTime() < 3)
			{
				return false;
			}
			return true;
		}

		bool Runmodule()
		{
			while (1)
			{

				FileUtil fu(_back_dir);
				std::vector<std::string> arry;
				fu.ScanDirectory(arry);
				for (auto a : arry)
				{
					if (IsNeedUpload(a) == false)
					{
						continue;
					}

					if (Upload(a) == true)
					{
						_data->Insert(a, GetFileIdentifier(a));
					}
				}
				// judge if the file need upload

				Sleep(100);
			}
		}
	};
}

接下来我们的客户端就完成了

运行客户端和服务器之后 客户端就会实时检测backup目录下的文件 检测到未备份的文件便会自动上传客户端

此外在最后运行代码的时候我遇到了个小问题

Linux中的目录分隔符和Windows中的目录分隔符是不一样的

所以说我们可以使用C++17文件管理的获取文件名来完成

        std::string FileName()
        {
            std::experimental::filesystem::path p(_filename);
            return p.filename().string();
        }

项目总结

项目名称:云备份系统

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

开发环境: centos7.6/vim、g++、gdb、makefile 以及 windows10/vs2017

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

项目模块:
服务端:

  1. 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
  2. 业务处理模块:搭建 http 服务器与客户端进行通信处理客户端的上传,下载,查看请求,并支持断点续传
  3. 热点管理模块:对备份的文件进行热点管理,将长时间无访问文件进行压缩存储,节省磁盘空间。 客户端
  4. 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
  5. 文件检索模块:基于 c++17 文件系统库,遍历获取指定文件夹下所有文件。
  6. 文件备份模块:搭建 http 客户端上传备份文件。

项目扩展

  1. 给客户端开发一个好看的界面,让监控目录可以选择
  2. 内存中的管理的数据也可以采用热点管理
  3. 压缩模块也可以使用线程池实现
  4. 实现用户管理,不同的用户分文件夹存储以及查看
  5. 实现断点上传
  6. 客户端限速,收费则放开

项目常见问题

  1. 介绍下你的项目

项目介绍参考 上面的总结即可

  1. 项目中的某个技术点是怎么实现的

本文中对于使用到的技术都详细介绍了

  1. 服务器为什么不自己搭建

可以参考博主的另外一篇博客 自主实现http服务器

  1. 多个客户端同时上传文件如何处理

本项目实现的单个目录存储 所以说多个客户端同时上传文件也会放到同个目录下

  1. 断点续传怎么实现

本文中有详细的介绍

  1. 云备份的传输速度有多少

因为博主自己使用的服务器带宽是2mb 所以传输速度也就是200多kb

大家可以参考自己的云服务器参考下

  1. 服务器支持多少个客户端

博主使用多线程测试了下 大概能支持二十多个客户端 并且此时的速度明显变慢了

如果面试官问到listen的第二个参数相关 可以参考我的这篇博客 详解TCP

项目地址

云备份项目地址

更多推荐

19.组合模式(Composite)

意图:将对象组成树状结构以表示“部分-整体”的层次结构,使得Client对单个对象和组合对象的使用具有一致性。上下文:在树型结构的问题中,Client必须以不同的方式处理单个对象和组合对象。能否提供一种封装,统一简单元素和复杂元素的概念,让对象容器自己来实现自身的复杂结构,让Client可以像处理简单元素一样来处理复杂

leetcode 332. Reconstruct Itinerary(重构行程)

有一些票tickets,tickets[i]=[from,to],每个出发到达城市名字都是3个大写英文字母,同一个出发城市时,优先去字母顺序较小的到达城市。必须先从“JFK”出发。每个ticket必须用且只用一次,所有ticket一定会形成至少一个有效的行程(出发至到达的一路上遍历所有城市)按顺序记录行程上的城市并返回

应用商店优化之关键词优化指南2

在寻找良好关键词的方法中,分析竞争对手是寻找关键词的最佳且最可靠的方法。查找排名最高的关键词或者是获得最多下载量的关键词,这样就可以准确的瞄准那些带来最大价值的关键词,从而带来更好的优化效果。1、选择正确的关键词。搜索分数用来衡量关键词流行度的分数。该分数是一个从1到100的数字,分数越高,对该关键词进行的搜索就越多。

MySQL索引,Explain,事务,锁与MVCC

MySQL的索引为什么不能为二叉树假如为二叉树,索引值插入顺序为1,2,3,4,5,6,那么形成的索引结构如图:搜索效率并不高。此时可以优化为红黑树(二叉平衡树),如图:但是红黑树也有问题,就是树的高度,如果数据过多,红黑树过高也会影响效率。为了控制高度,可以给每一个节点分配大一点的空间,例如上面的0002节点可以存储

css自学框架之图片懒加载

首先解释一下什么叫图片懒加载。图片懒加载是一种在页面加载时,延迟加载图片资源的技术,也就是说图片资源在需要的时候才会加载,就是在屏幕显示范围内加载图片,屏幕显示范围外图片不加载。一、关键函数用到的关键函数:globalThis.IntersectionObserver。varobserver=newIntersecti

亚马逊应该怎么快速提升排名,获取review?

跨境电商做久了,卖家都会陷入一个困境,到底是该坚持慢慢做好,还是要测评?现在跨境电商平台人人都在刷,不刷单想成功真的很难,不是没可能,但是选品要非常好,而且你的listing也要做好,推广要求又高你看那些大卖的评论长篇大论,图片视频样样都有,说是国外人写的,我估计都没人信,只是你还不会测评而已,会的话你就不会惊讶了但老

MyBatis 分页插件 PageHelper

文章目录前言PageHelper应用实现原理剖析应用场景分析前言分页插件PageHelper是我们使用Mybatis到的比较多的插件应用,适用于任何复杂的单表、多表分页查询操作。本文介绍PageHelper的使用及原理。PageHelper应用添加依赖<dependency><groupId>com.github.pa

VMware Workstation Pro各版本下载安装教程

VMwareWorkstationPro下载打开浏览器,输入VMwareWorkstationPro找到VMwareWorkstationPro官网并点击进入,官网地址:https://www.vmware.com/cn/products/workstation-pro.html进入官网首页后可以下载最新版本的VMwa

DBAPI安装教程

安装教程请先下载安装包。默认账户admin/admin。为了便于您理解安装的时候需要配置的参数,请您先学习日志监控相关的功能设计本地部署单机版依赖java环境,先自行在服务器安装jdk8+,并配置环境变量下载安装包解压到需要安装的目录修改conf/application.properties文件中的以下配置#api访问

谷粒商城----rabbitmq

一、为什么要用MQ?三大好处,削峰,解耦,异步。削峰比如秒杀,或者高铁抢票,请求在某些时间点实在是太多了,服务器处理不过来,可以把请求放到MQ里面缓冲一下,把一秒内收到的1万个请求放到队列里面,花10分钟去消费队列里的请求。解耦比如有一个服务A每天都采集数据并计算各种数据,服务B需要调用服务A的接口获取数据,就在A开一

使用 PyTorch 的计算机视觉简介 (2/6)

一、说明在本单元中,我们从最简单的图像分类方法开始——一个全连接的神经网络,也称为感知器。我们将回顾一下PyTorch中定义神经网络的方式,以及训练算法的工作原理。二、数据加载的实践首先,我们使用pytorchcv助手来加载所有数据。!wgethttps://raw.githubusercontent.com/Micr

热文推荐