【毕业项目】 云备份(三)

本文涉及的产品
数据管理 DMS,安全协同 3个实例 3个月
推荐场景:
学生管理系统数据库
简介: 【毕业项目】 云备份(三)

热点管理模块

热点管理模块的设计

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

为了节省磁盘空间

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

实现思路:

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

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

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

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

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

热点管理流程

  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中

c5633a8983ba43d9ab271bbd9a3c5bdd.png

服务端处理模块

服务端处理模块思路

我们想要处理服务端发送过来的请求 首先就要和服务端建立连接 (这里我们通过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;                 
        } 

运行结果如下

b6aebb52de214aa7b0c152da60dbaac7.png

下载函数编写


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 之后 我们就可以告诉浏览器 这是一个二进制流文件(通常标识为一个可下载文件)

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

a3b11dbe217547b29d2051b39949612d.png

断点续传

为什么要实现断点续传

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

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

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

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

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

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

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

需要注意的问题:

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

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 文件 文件信息如下

88c152ee96054941b83e4d31d2f70b93.png

文件备份类设计

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

所以我们要做以下流程

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

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

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

需要的成员函数如下

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

文件的唯一标识

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

整体函数如下

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

项目地址

云备份项目地址

相关实践学习
MySQL基础-学生管理系统数据库设计
本场景介绍如何使用DMS工具连接RDS,并使用DMS图形化工具创建数据库表。
相关文章
|
存储 JSON 数据管理
【毕业项目】 云备份(二)
【毕业项目】 云备份(二)
70 0
|
存储 JSON 开发工具
【毕业项目】 云备份(一)
【毕业项目】 云备份
124 0
|
运维 关系型数据库 MySQL
企业运维训练营之数据库原理与实践—云数据库备份与恢复—备份恢复实战
企业运维训练营之数据库原理与实践—云数据库备份与恢复—备份恢复实战
129 0
|
SQL 运维 AliSQL
企业运维训练营之数据库原理与实践—云数据库备份与恢复—云上备份恢复能力与场景
企业运维训练营之数据库原理与实践—云数据库备份与恢复—云上备份恢复能力与场景
145 0
|
存储 Cloud Native 关系型数据库
【备考心得】教你如何顺利通过阿里云PolarDB开源人才培养考试
本次考试的经验与心得分享,含关键知识点、考点总结,助你顺利通过考试。
|
弹性计算 关系型数据库 MySQL
鹤酒全栈初入江湖,ECS服务器
鹤酒全栈初入江湖,ECS服务器
|
关系型数据库 MySQL Linux
上云第一课第一期部署MySQL数据库
主要讲述数据的部署以及使用
上云第一课第一期部署MySQL数据库
|
前端开发 Java 应用服务中间件
使用阿里云部署全栈项目
了解阿里云,学习部署项目
|
SQL 存储 安全
内附PPT下载 | 阿里云资深技术专家 陈长城:一站式数据管理DMS及最新解决方案解读
阿里云资深技术专家陈长城在“数聚云端·智驭未来”——阿里云数据库创新上云峰会上,做了一站式数据管理平台DMS以及解决方案的发布。议题包含企业数据管理当前的一些痛点、DMS一站式数据管理平台以及其核心技术、实时数仓解决方案以及相应的应用实践等。
1760 0
内附PPT下载 | 阿里云资深技术专家 陈长城:一站式数据管理DMS及最新解决方案解读
|
Web App开发 前端开发 JavaScript
致青春!一键上线你们专属的云上毕业纪念册
毕业不说再见,青春不散场!在云端,在一起!在问答https://developer.aliyun.com/ask/321737的留言区域晒出自己「线上环境」部署的毕业纪念册,在6月30号18点之前点赞数前10可以获得我们送出的毕业大礼包,阿里云的公仔盲盒一个以及10元的代金券一张,让你的青春永远在线!
致青春!一键上线你们专属的云上毕业纪念册