云备份的C/C++实现

云备份的C/C++实现

一、开发环境

centos7.6/vim、g++、gdb、makefile
windows10/vs2017
对于文件操作,后续会使用到experimental/filesystem,它需要C++17的支持

二、项目概述

自动将用户机指定文件夹中需要备份的文件自动上传到服务器中实现备份。并且能够通过浏览器进行查看下载,其中下载支持断点续传功能功能,而且服务器也会对上传文件实现热点管理,将非热点文件进行压缩,节省磁盘空间

三、实现目标

达成两端程序,一是部署在用户机的客户端程序,上传需要备份的文件,以及再服务器上的服务端程序,实现文件的备份存储和管理,两端合作,实现云备份的目的。

四、服务端程序负责的功能

1.对客户端上传的文件进行备份存储

2.支持客户端通过浏览器访问文件列表

3.支持客户端通过浏览器进行下载,并且支持断点续传功能

4.能够对备份文件进行热点管理,节省磁盘空间

五、服务端功能模块划分

1.网络通信模块——搭建网络通信服务器,实现与客户端的通信

2.业务处理模块——对不同的请求提供不同的处理方法

3.数据管理模块——负责服务器上备份文件的信息管理

4.热点管理模块——负责文件的热点判断,对非热点文件进行压缩

六、客户端程序负责的功能

1.自动检测客户端指定文件,判断文件是否需要上传

2.将需要备份的文件进行上传

七、客户端功能模块划分

1.网络通信模块——搭建网络通信客户端,实现与服务器的通信

2.文件检索模块——遍历获取指定文件夹的所有文件信息和路径

3.数据管理模块——负责客户端文件信息的管理,通过这些信息判断文件是否需要备份

八、环境的搭建

1.gcc升级7.3版本

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库

sudo yum install epel-release
sudo yum install jsoncpp-devel
[dev@localhost ~]$ 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

这里有可能因为centos版本不同,安装的头文件内容也会发生变化,这里需要根据自己的版本来寻找。

3.安装boudle数据压缩库

sudo yum install git
git clone https://github.com/r-lyeh-archived/bundle.gi

这个是从github中找到的,下载失败的话可以去github中下载
下载连接:
Github下载地址

4.下载httplib库

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

这个库的主要作用是为了以后搭建网络通信可以简单一点,因为时间有限,并且自己写的可能并没有别人那么完美,但是还是建议大家去看一下源代码,学习别人的代码思路,理解代码的底层实现,这样才是最好的学习方式。
下载链接:
Github下载地址

九、json库的认识与使用

1.json的认识

json是一个数据交换格式,将对象中的数据转换为字符串,这样就可以很简单的再函数中传递这些数据,而且格式易于人的阅读和编写以及机器的解析和生成

string name = "小明";
int age = 18;
float score[3] = {77.5,88,99.3};

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

json将数据进行组织后:
[
   {
        "姓名" : "小明",
        "年龄" : 18,
        "成绩" : [77.5, 88, 99.3]
   },
   {
        "姓名" : "小黑",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
   }
]

json数据类型:对象,数组,字符串,数字
对象:用 {} 括起来的表示一个对象
数组:用 [] 括起来的表示一个数组
字符串:使用 “” 括起来的表示一个字符串
数字:整型和浮点型都可以,直接使用

2.jsoncpp库的使用

首先需要将jsoncpp库拷贝到工作目录下,然后将其当作头文件引入即可

#include "jsoncpp/json"

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

完成序列化,反序列化的功能,需要借助库中三个类以及部分成员函数来完成

//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
};

这里面主要是符号的重载以及类型的转化和获取,主要是用于将数据存入Json::Value这个类实例化的对象中,然后通过序列化类和反序列化类对Json::Value的对象进行序列化和反序列化,操作很简单。

//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反序列化类
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}


//这两个类的主要使用方法就是
StreamWriterBuilder swb;
StreamWriter* writer = swb.newStreamWriter();
std::stringstream ss;
writer->write(val,ss);//将Json::value实例化的对象的内容写入字符串流中
std::string body;
body = ss.str();//ss并不是一个字符串,所以要用str()将其中的内容转化为字符串
delete writer  //使用完不要忘记释放,new出来的对象不释放会造成内存泄漏
//这样我们就完成了对val中存储的数据的序列化

CharReaderBuilder crb;
CharReader* reader = crb.newStreamReader();  
std::string str;
/*这一步是完成对body中存储的json序列化后的格式的数据进行解析然
后存入val对象中,之后就可以通过val来访问数据成员,拿到我们想要的数据*/
reader->parse(body.c_str(),body.c_str()+body.size(),val,&str);
delete reader;


//将数据成员添加到Json::Value实例化的的对象中
const char *name = "小明";
int age = 18;
float score[] = {88.5, 98, 58};
Json::Value val;
val["姓名"] = name;
val["年龄"] = age;
val["成绩"].append(score[0]);
val["成绩"].append(score[1]);
val["成绩"].append(score[2]);

//读取val对象中存储的数据
std::cout<<val["姓名"].asString()<<std::endl;
std::cout<<val["年龄"].asInt()<<std::endl;
int sz = val["成绩"].size();
//这里也可以使用迭代器
for (auto it = val["成绩"].begin(); it != val["成绩"].end(); it++)
 	std::cout << it->asFloat() << std::endl; 
for(int i = 0;i < sz;i++)
	std::cout<<val["成绩"][i].asFloat()<<std::endl;

经过这些练习,应该对json的使用已经熟悉,这样就可以开始学习剩下的库的使用

十、bundle库的认识和使用

1.bundle的认识

BundleBundle 是一个嵌入式压缩库,支持23种压缩算法和2种存档格式。使用的时候只需要加入两个文件 bundle.h 和 bundle.cpp 即可

#include "bundle.h"
namespace bundle 
{ 
 在该项目中我们只需要使用这两个接口
 T pack( unsigned Q, T ); //T为存放将要被压缩的内容的string,Q为bundle提供的压缩格式这里我们用bundle::LZIP;
 //string body;
 //read(&body);  //这里是一个我们自己写的文件读取函数,后面我们需要自己实现一个,这里举例使用,将文件内容读入body中
 //string pack_body = bundle::pack(bundle::LZIP,body);//将body中内容进行压缩,将被压缩之后的内容存入pack_body中
 //wrtie(&pack_body)//将压缩后的内容写入指定文件当中完成压缩操作
 T unpack( T ); 
 //unpack操作同上,只是调用的函数的不同eg:bundle::unpack(body);
} 

2.bundle库的使用

 T pack( unsigned Q, T ); //T为存放将要被压缩的内容的string,Q为bundle提供的压缩格式这里我们用bundle::LZIP;
 string body;
 read(&body);  //这里是一个我们自己写的文件读取函数,后面我们需要自己实现一个,这里举例使用,将文件内容读入body中
 string pack_body = bundle::pack(bundle::LZIP,body);//将body中内容进行压缩,将被压缩之后的内容存入pack_body中
 wrtie(&pack_body)//将压缩后的内容写入指定文件当中完成压缩
 //之后删除被压缩文件即可
 remove_all(filename);


 T unpack( T ); 
 //unpack操作同上,只是调用的函数的不同eg:boudle::unpack(body);

这里主要的困难就是文件的读写操作,这里将在后面的工具类中写到。压缩和解压缩操作并不困难,学会使用即可。

十一、httplib库的认识和使用

1.httplib库的认识

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

#include "httplib.h"

namespace httplib
{ 
 struct MultipartFormData  //这里是html中的东西
 { 
	 std::string name;   //区域名称标识
	 std::string content; //文件正文
	 std::string filename;  //文件名
	 std::string content_type;  //文件传输格式,一般都是二进制传输
 }; 
 //这里面保存了一个vector数组,里面每一个元素是一个MultipartFormData
 //应为有可能一次传输多个文件,所以要将每个文件对应格式的MultipartFormData创建队对应的区域然后进行传输
 using MultipartFormDataItems = std::vector<MultipartFormData>; 
 struct Request 
 { 
	 std::string method; //请求方法
	 std::string path; //URL中的资源路径
	 Headers headers; //map<string,string>存放头部字段
	 std::string body;  //存放正文
	 // for server 
	 std::string version; 
	 Params params;   //URL中的查询字符串
	 /*MultipartFormDataMap的内部其实是一个multimap<std::string,MultipartFormData>*/
	 MultipartFormDataMap files;
	 Ranges ranges;//用于实现断点续传的数据请求范围区间
	 bool has_header(const char *key) const;  //判断是都具有该头部字段
	 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; //判断有没有name字段值是file的标识区域
	 MultipartFormData get_file_value(const char *key) const; //获取解析后的区域数据
 }; 
 struct Response  //响应类
 { 
 	 std::string version; //协议版本
	 int status = -1; //响应状态码默认是-1;
	 std::string reason;
	 Headers headers; //map<string,string>存放头部字段
	 std::string body; //响应正文
	 std::string location; // Redirect location 
	 void set_header(const char *key, const char *val); //设置头部字段
	 void set_content(const std::string &s, const char *content_type); //写入正文,和对应的格式
 }; 
 class Server   //服务器类
 { 
	 using Handler = std::function<void(const Request &, Response &)>; //函数指针类型
	 using Handlers = std::vector<std::pair<std::regex, Handler>>; //请求-处理函数映射信息表
	 std::function<TaskQueue *(void)> new_task_queue; //线程池
	 Server &Get(const std::string &pattern, Handler handler); //建立请求与对应处理函数的映射关系
	 Server &Post(const std::string &pattern, Handler handler); 
	 Server &Put(const std::string &pattern, Handler handler); 
	 Server &Patch(const std::string &pattern, Handler handler); 
	 Server &Delete(const std::string &pattern, Handler handler); 
	 Server &Options(const std::string &pattern, Handler handler); 
	 bool listen(const char *host, int port, int socket_flags = 0); //启动服务器开始监听
 }; 
 class Client   //客户端类
 { 
	 Client(const std::string &host, int port); //服务器IP地址,服务器端口号
	 Result Get(const char *path, const Headers &headers); 
	 Result Post(const char *path, const char *body, size_t content_length,const char *content_type); 
	 Result Post(const char *path, const MultipartFormDataItems &items); 
 } 
} 

这个红框框就是一个name区域标识符对应的区域,里面包含了MultipartFormData中的四个数据
在这里插入图片描述

2.httplib的使用

//简单服务器的搭建
#include "httplib.h"
bool Upload(const httplib::Request &req,httplib::Response &rsp)
{}
bool List(const httplib::Request &req,httplib::Response &rsp)
{}
bool Download(const httplib::Request &req,httplib::Response &rsp)
{}
int main(void) 
{ 
 std::string _download_prefix = "/download/";
 using namespace httplib; 
 Server svr; 
 svr.Post("upload",Upload);   //Post(请求的资源路径,对应的业务处理函数)
 svr.Get("list",List);
 string download_path = _download_prefix + (".*");//使用正则表达式获取文件名
 svr.Get(download_path,Download);
 svr.listen("0.0.0.0", 9090); //运行服务器
 return 0; 
} 

十二、项目开发

服务器开发

服务器建立思想

目标功能:
能够通过浏览器进行查看下载,其中下载支持断点续传功能功能,而且服务器也会对上传文件实现热点管理,将非热点文件进行压缩,节省磁盘空间

思想:
1.接收请求数据并进行解析,将解析得到的请求数据存放到了一个Request结构体变量中

2.根据请求信息(请求方法&资源路径),到映射表中查找有没有对应的资源,如果没有则返回404

3.如果有对应的映射表信息,则调用对应的业务处理函数,将解析得到的req传入接口中,并传入一个空的Response结构体变量rsp

4.处理函数中会根据req中的请求信息进行对应的业务处理,并且在处理的过程中向rsp结构体中添加响应信息

5.等处理函数执行完毕,则httplib得到了一个填充了响应信息的Respose结构体变量rsp

6.根据rsp中的信息,组织一个http格式的响应,回复给客户端

7.如果是短连接则关闭连接处理下一个,如果是长连接,则等待请求,超时则关闭处理下一个

服务器模块划分:

1.网络通信模块——搭建网络通信服务器,实现与客户端的通信
2.业务处理模块——对不同的请求提供不同的处理方法
3.数据管理模块——负责服务器上备份文件的信息管理
4.热点管理模块——负责文件的热点判断,对非热点文件进行压缩

FileTools——文件工具类

#ifndef __MY_TOOLS__
#define __MY_TOOLS__

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
#include <sstream>
#include <fstream>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
#include <cstdint>
#include "bundle.h"

namespace fs = std::experimental::filesystem;

namespace cloud
{
  class FileTool
  {
    private:
      std::string _name;
    public:
      FileTool(const std::string name):_name(name)
      {}
      std::string Name()
      {
        return fs::path(_name).filename().string();
      }
      bool Exist()  //文件是否存在
      {
        return fs::exists(_name);
      }
      size_t Size() //文件大小
      {
        if(!Exist())
          return 0;
        return fs::file_size(_name);
      }
      time_t LastChange_Tm()  //最后一次修改时间
      {
        if(!Exist())
          return 0;
        auto ftime = fs::last_write_time(_name);
        std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
        return cftime;
      }
      time_t LastVisit_Tm()   //最后一次访问时间
      {
        if(!Exist())
          return 0;
        struct stat st;
        stat(_name.c_str(),&st);
        return st.st_atime;

      }
      bool Write(const std::string &body)  //文件写入
      {
        if(!Exist())
          CreateDirectory();
        std::ofstream writer;
        writer.open(_name,std::ios::binary);  //以二进制格式打开文件
        if(!writer.is_open())
        {
          std::cout<<"writer open file false"<<std::endl;
          writer.close();
          return false;
        }
        writer.write(body.c_str(),body.size());
        if(!writer.good())
        {
          std::cout<<"writer write false"<<std::endl;
          writer.close();
          return false;
        }
        writer.close();
        return true;
      }
      bool Read(std::string* body)  //文件读取
      {

        if(!Exist())
          return 0;
        std::ifstream reader;
        reader.open(_name,std::ios::binary);
        if(!reader.is_open())
        {
          std::cout<<"reader open file false"<<std::endl;
          reader.close();
          return false;
        }
        size_t fsize = Size();
        body->resize(fsize);    //给body开辟_name文件大小的空间
        reader.read(&(*body)[0],fsize);  //将_name中数据读取到body中
        if(!reader.good())
        {
          std::cout<<"reader read false"<<std::endl;
          reader.close();
          return false;
        }
        reader.close();
        return true;
      }
      bool CreateDirectory() //创建目录
      {
        if(!Exist())
          return 0;
        fs::create_directories(_name);
        return true;
      }
      bool ScanDirectory(std::vector<std::string>& arry) //遍历目录
      {
        if(!Exist())
          return 0;
        for(auto &a : fs::directory_iterator(_name))
        {
          if(fs::is_directory(a) == true)
            continue;   //当前文件是一个文件夹,则不处理,只遍历普通文件
          //std::string filename = fs::path(a).filename().string();   //纯文件名
          std::string pathname = fs::path(a).relative_path().string(); //带路径的文件名
          arry.push_back(pathname);
        }
        return true;
      }
      bool Compress(const std::string &packname) //压缩文件
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"compress read file false"<<std::endl;
          return false;
        }
        std::string packbody = bundle::pack(bundle::LZIP,body);
        if(FileTool(packname).Write(packbody) == false)
        {
          std::cout<<"compress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name); //删除被压缩了的源文件
        return true;
      }
      bool Uncompress(const std::string &filename) //解压缩
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"Uncompress read file false"<<std::endl;
          return false;
        }
        std::string unpack_body = bundle::unpack(body);
        if(!FileTool(filename).Write(unpack_body))
        {
          std::cout<<"Uncompress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
      bool Remove()
      {
        if(!Exist())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
  };
  
  //序列化和反序列化
  class Json_Tool
  {
    public:
      static bool Serialize(const Json::Value &val, std::string *body)   //序列化
      {
        Json::StreamWriterBuilder swb;
        Json::StreamWriter *sw = swb.newStreamWriter();
        std::stringstream ss;
        int ret = sw->write(val,&ss);
        if(ret != 0)
        {
          std::cout<<"serialize false"<<std::endl;
          delete sw;
          return false;
        }
        *body = ss.str();
        delete sw;
        return true;
      }

      static bool UnSerialize(const std::string& body,Json::Value *val)
      {
        Json::CharReaderBuilder crb;
        Json::CharReader *cr = crb.newCharReader();
        std::string str;
        bool ret = cr->parse(body.c_str(),body.c_str() + body.size(),val,&str);
        if(!ret)
        {
          std::cout<<"unserialize false"<<std::endl;
          delete cr;
          return false;
        }
        delete cr;
        return true;
      }
    };
}


#endif
文件读写操作
class FileTool
  {
    private:
      std::string _name;
    public:
      FileTool(const std::string name):_name(name)
      {}
      std::string Name()
      {
        return fs::path(_name).filename().string();
      }
      bool Exist()  //文件是否存在
      {
        return fs::exists(_name);
      }
      size_t Size() //文件大小
      {
        if(!Exist())
          return 0;
        return fs::file_size(_name);
      }
      time_t LastChange_Tm()  //最后一次修改时间
      {
        if(!Exist())
          return 0;
        auto ftime = fs::last_write_time(_name);
        std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
        return cftime;
      }
      time_t LastVisit_Tm()   //最后一次访问时间
      {
        if(!Exist())
          return 0;
        struct stat st;
        stat(_name.c_str(),&st);
        return st.st_atime;

      }
      bool Write(const std::string &body)  //文件写入
      {
        if(!Exist())
          CreateDirectory();
        std::ofstream writer;
        writer.open(_name,std::ios::binary);  //以二进制格式打开文件
        if(!writer.is_open())
        {
          std::cout<<"writer open file false"<<std::endl;
          writer.close();
          return false;
        }
        writer.write(body.c_str(),body.size());//从body起始到结束
        if(!writer.good())
        {
          std::cout<<"writer write false"<<std::endl;
          writer.close();
          return false;
        }
        writer.close();
        return true;
      }
      bool Read(std::string* body)  //文件读取
      {

        if(!Exist())
          return 0;
        std::ifstream reader;
        reader.open(_name,std::ios::binary);
        if(!reader.is_open())
        {
          std::cout<<"reader open file false"<<std::endl;
          reader.close();
          return false;
        }
        size_t fsize = Size();
        body->resize(fsize);    //给body开辟_name文件大小的空间
        reader.read(&(*body)[0],fsize);  //将_name中数据读取到body中
        if(!reader.good())
        {
          std::cout<<"reader read false"<<std::endl;
          reader.close();
          return false;
        }
        reader.close();
        return true;
      }
 };

首先说明,这里的操作基本都都是C++文件操作库中的接口调用,所以需要注意的就是文件操作接口的正确调用,以及保证自己的编译器要支持C++17,不然会报错。

两个标准库IO流操作:

Read接口ifstream类实例化文件输入流对象,通过该对象,调用open接口,以二进制格式打开文件,打开成功后,将文件内容读取到body中,但是这里需要注意一点,要先给body开辟对应文件大小的空间,保证接收内容完整;其次是应为写入要从头写入,所以需要获取body空间的首地址,又因为我们传入进来的是个指针,所以需要代码中所示方法(这里是因为vim报了特殊的错误,所以采用这种方式解决)读取成功则关闭文件,失败则报错,然后关闭文件。

Write接口ifstream类实例化文件输出流对象,将body中内容写入,关闭文件等操作。与读取不同的地方在于,如果没有对应的文件,要创建一个新的文件,将内容写入。

获取文件最后访问时间,和获取文件最后修改时间,以及文件是否存在,这三个操作,就只是调用库中提供的接口,但是要注意一点就是他们返回的都是时间戳,所以还需要借助其中的一些操作将其转换成我们需要的时间。

#include <iostream>
#include <chrono>
#include <iomanip>
#include <fstream>
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
using namespace std::chrono_literals;
int main()
{
    fs::path p = fs::current_path() / "example.bin";
    std::ofstream(p.c_str()).put('a'); // create file
    auto ftime = fs::last_write_time(p);
    //下面的操作就可以将时间戳,转换为可读的年月日时分秒
    std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
}
文件目录创建和遍历
bool CreateDirectory() //创建目录
{
  if(!Exist())
    return 0;
  fs::create_directories(_name);
  return true;
}
bool ScanDirectory(std::vector<std::string>& arry) //遍历目录
{
  if(!Exist())
    return 0;
  for(auto &a : fs::directory_iterator(_name))
  {
    if(fs::is_directory(a) == true)
      continue;   //当前文件是一个文件夹,则不处理,只遍历普通文件
    //std::string filename = fs::path(a).filename().string();   //纯文件名
    std::string pathname = fs::path(a).relative_path().string(); //带路径的文件名
    arry.push_back(pathname);
  }
  return true;
}

对于这里,要说的就是目录遍历时,可能其中还有目录,所以我们只遍历当前目录下的文件,如果要遍历所有文件,这里可以选择一个简单的递归调用,就能解决这个问题。

string>>& arry**,这是用来将遍历到的文件名进行存储,等遍历完成之后就可以从中访问,所以是引用传参,直接修改外部的arry文件目录数组

文件压缩和解压缩
      bool Compress(const std::string &packname) //压缩文件
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"compress read file false"<<std::endl;
          return false;
        }
        std::string packbody = bundle::pack(bundle::LZIP,body);
        if(FileTool(packname).Write(packbody) == false)
        {
          std::cout<<"compress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name); //删除被压缩了的源文件
        return true;
      }
      bool Uncompress(const std::string &filename) //解压缩
      {
        if(!Exist())
          return false;
        std::string body;
        if(!Read(&body))
        {
          std::cout<<"Uncompress read file false"<<std::endl;
          return false;
        }
        std::string unpack_body = bundle::unpack(body);
        if(!FileTool(filename).Write(unpack_body))
        {
          std::cout<<"Uncompress write file false"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
      bool Remove()
      {
        if(!Exist())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        fs::remove_all(_name);
        return true;
      }
  };

文件压缩和解压缩,这里使用的是bondle库,实现该功能的主要思想是:
压缩:
1.读取文件内容
2.将其中内容用bonldle中所支持的压缩方式进行压缩,该项目中选择的是LZIP,这个压缩的压缩率是很高的 (pack(压缩格式,string &body))
3.将压缩内容写入压缩文件
4.删除压缩前的文件,只保留一个压缩包,需要时再解压缩,节省磁盘空间

解压缩:
1.读取压缩包文件内容
2.将其中内容解压缩(unpack(string &body)
3.将解压缩内容写入文件
4.删除源压缩包

文件持久化存储
class Json_Tool
{
  public:
    static bool Serialize(const Json::Value &val, std::string *body)   //序列化
    {
      Json::StreamWriterBuilder swb;
      Json::StreamWriter *sw = swb.newStreamWriter();
      std::stringstream ss;
      int ret = sw->write(val,&ss);
      if(ret != 0)
      {
        std::cout<<"serialize false"<<std::endl;
        delete sw;
        return false;
      }
      *body = ss.str();
      delete sw;
      return true;
    }

    static bool UnSerialize(const std::string& body,Json::Value *val)
    {
      Json::CharReaderBuilder crb;
      Json::CharReader *cr = crb.newCharReader();
      std::string str;
      bool ret = cr->parse(body.c_str(),body.c_str() + body.size(),val,&str);
      if(!ret)
      {
        std::cout<<"unserialize false"<<std::endl;
        delete cr;
        return false;
      }
      delete cr;
      return true;
    }
  };

Json的使用方法再上面已经说过了,所以这里不再多说。

使用Json目的,是为了实现文件的持久化存储,并且可以在进程间传递。可以让数据的访问更加方便。

使用stringstream字节流对象存储被序列化的数据,然后通过stringstream字节流对象对string body赋值,但是这里需要将其内部序列化数据转化成string格式才能赋值,所以出现了ss.str()。

部分总结

这个类,大部分工作其实都已经通过库函数完成了,我们只是进行库函数的调用,所以可以展现的东西并不多,需要思考的就是怎么调用,他给你接口了又该怎么实现自己想要的功能。

Data_Management——数据管理类

#ifndef __MY_DATA__
#define __MY_DATA__

#include "file_tools.hpp"
#include <unordered_map>
#include <vector>

namespace cloud
{
  struct _FileInfo
  {
    std::string filename;   //文件名称
    std::string url_path;   //文件下载路径
    std::string real_path;  //文件真实存储路径
    size_t file_size;       //文件大小
    time_t back_time;       //最后备份时间
    bool pack_flag;         //压缩标志
    std::string pack_path;  //压缩包路径
  };

  class Data_Management
  {
    private:
      std::string _file_back_path = "./back_dir/";   //备份文件夹
      std::string _file_pack_path = "./pack_dir/";   //压缩包文件夹
      std::string _download_prefix = "/download/";  //下载链接前缀
      std::string _pack_suffix = ".zip";   //压缩包后缀名
      std::string _back_info_file = "./back_info.dat";   //存储备份信息的文件
      std::unordered_map<std::string , _FileInfo> _back_info;  //备份信息
    public:
      Data_Management()
      {
        FileTool(_file_back_path).CreateDirectory();
        FileTool(_file_pack_path).CreateDirectory();
        if(FileTool(_back_info_file).Exist())
        {
          InitLoad();
        }
      }
      bool InitLoad()
      {
        std::string body;
        bool ret = FileTool(_back_info_file).Read(&body);
        if(ret == false)
        {
          std::cout<<"InitLoad read file filed"<<std::endl;
          return false;
        }
        Json::Value infos;
        ret = Json_Tool::UnSerialize(body,&infos);
        if(ret == false)
        {
          std::cout<<"InitLoad UnSerialize filed"<<std::endl;
          return false;
        }
        for(int i = 0; i < infos.size();i++)
        {
          _FileInfo info;
          info.filename = infos[i]["filename"].asString();
          info.url_path = infos[i]["url_path"].asString();
          info.real_path = infos[i]["real_path"].asString();
          info.file_size = infos[i]["file_size"].asInt64();
          info.back_time = infos[i]["back_time"].asInt64();
          info.pack_flag = infos[i]["pack_flag"].asBool();
          info.pack_path = infos[i]["pack_path"].asString();

          _back_info[info.url_path] = info;
        }
        return true;
      }
      bool Storage()
      {
        Json::Value infos;
        std::vector<_FileInfo> arry;
        Select_All(arry);
        for(auto &e : arry)
        {
          Json::Value info;
          info["filename"] = e.filename;
          info["url_path"] = e.url_path;
          info["real_path"] = e.real_path;
          info["file_size"] = (Json::UInt64)e.file_size;
          info["back_time"] = (Json::UInt64)e.back_time;
          info["pack_flag"] = e.pack_flag;
          info["pack_path"] = e.pack_path;

          infos.append(info);
        }
        std::string body;
        Json_Tool::Serialize(infos,&body);
        FileTool(_back_info_file).Write(body);

        return true;
      }
      bool Insert(const std::string &filename)
      {
        if(FileTool(filename).Exist() == false)
        {
          std::cout<<"Inset file is not exist"<<std::endl;
          return false;
        }
        _FileInfo info;
        info.filename = FileTool(filename).Name();
        info.url_path = _download_prefix + info.filename;
        info.real_path = filename;
        info.file_size = FileTool(filename).Size();
        info.back_time = FileTool(filename).LastChange_Tm();
        info.pack_flag = false;
        info.pack_path = _file_pack_path + info.filename + _pack_suffix;
        
        _back_info[info.url_path] = info;
        Storage();
        return true;
      }
      bool Update_packflag(const std::string &filename,bool status)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }

        it->second.pack_flag = status;
        Storage();
        return true;
      }
      bool Select_All(std::vector<_FileInfo> &v)
      {
        for(auto e = _back_info.begin();e != _back_info.end();e++)
          v.push_back(e->second);
        return true;
      }
      bool Select_One(const std::string &filename,_FileInfo *info)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        *info = it->second;
        return true;
      }
      bool Delete(const std::string &filename)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
          std::cout<<"not have this file"<<std::endl;

        _back_info.erase(url_path); 
        Storage();
        return true;
      }
  };
}

数据管理类,是我们整个项目中,最重要的类,任何操作,都不能避免数据的访问,那么就需要有人管理这些数据,对数据进行组织,这样程序工作才能井然有序。

_FileInfo——文件信息结构体
struct _FileInfo
  {
    std::string filename;   //文件名称
    std::string url_path;   //文件下载路径
    std::string real_path;  //文件真实存储路径
    size_t file_size;       //文件大小
    time_t back_time;       //最后备份时间
    bool pack_flag;         //压缩标志
    std::string pack_path;  //压缩包路径
  };

要管理,那就需要赋予文件特定的标识,没有标识,又怎么能分辨出来那个文件是哪个文件呢,文件名字是必须的。既然是备份,那用户就需要下载,那么我们就要给一个用户下载时需要的路径,来找到文件,但是这里我们其实就再后面实现下载功能时,就可以通过该路径确定是哪个文件需要下载,文件真实存储路径,就是我们在服务器主机硬盘中备份文件的位置,那再进行热点管理的时候,经常访问的文件不会压缩,不经常访问的文件需要被压缩,那是不是就得有一个东西,标识它是否被压缩。那么经常访问和不经常访问,又要根据什么来判断呢,这里就有一个最后的备份时间,也就是用户最后修改文件的时间,如果它大于某一个限定,就将其压缩。

 class Data_Management
  {
    private:
      std::string _file_back_path = "./back_dir/";   //备份文件夹
      std::string _file_pack_path = "./pack_dir/";   //压缩包文件夹
      std::string _download_prefix = "/download/";  //下载链接前缀
      std::string _pack_suffix = ".zip";   //压缩包后缀名
      std::string _back_info_file = "./back_info.dat";   //存储备份信息的文件
      std::unordered_map<std::string , _FileInfo> _back_info;  //备份信息
    public:
      Data_Management()
      {
        FileTool(_file_back_path).CreateDirectory();
        FileTool(_file_pack_path).CreateDirectory();
        if(FileTool(_back_info_file).Exist())
        {
          InitLoad();
        }
      }
      bool InitLoad()
      {
        std::string body;
        bool ret = FileTool(_back_info_file).Read(&body);
        if(ret == false)
        {
          std::cout<<"InitLoad read file filed"<<std::endl;
          return false;
        }
        Json::Value infos;
        ret = Json_Tool::UnSerialize(body,&infos);
        if(ret == false)
        {
          std::cout<<"InitLoad UnSerialize filed"<<std::endl;
          return false;
        }
        for(int i = 0; i < infos.size();i++)
        {
          _FileInfo info;
          info.filename = infos[i]["filename"].asString();
          info.url_path = infos[i]["url_path"].asString();
          info.real_path = infos[i]["real_path"].asString();
          info.file_size = infos[i]["file_size"].asInt64();
          info.back_time = infos[i]["back_time"].asInt64();
          info.pack_flag = infos[i]["pack_flag"].asBool();
          info.pack_path = infos[i]["pack_path"].asString();

          _back_info[info.url_path] = info;
        }
        return true;
      }
      bool Storage()
      {
        Json::Value infos;
        std::vector<_FileInfo> arry;
        Select_All(arry);
        for(auto &e : arry)
        {
          Json::Value info;
          info["filename"] = e.filename;
          info["url_path"] = e.url_path;
          info["real_path"] = e.real_path;
          info["file_size"] = (Json::UInt64)e.file_size;
          info["back_time"] = (Json::UInt64)e.back_time;
          info["pack_flag"] = e.pack_flag;
          info["pack_path"] = e.pack_path;

          infos.append(info);
        }
        std::string body;
        Json_Tool::Serialize(infos,&body);
        FileTool(_back_info_file).Write(body);

        return true;
      }
      bool Insert(const std::string &filename)
      {
        if(FileTool(filename).Exist() == false)
        {
          std::cout<<"Inset file is not exist"<<std::endl;
          return false;
        }
        _FileInfo info;
        info.filename = FileTool(filename).Name();
        info.url_path = _download_prefix + info.filename;
        info.real_path = filename;
        info.file_size = FileTool(filename).Size();
        info.back_time = FileTool(filename).LastChange_Tm();
        info.pack_flag = false;
        info.pack_path = _file_pack_path + info.filename + _pack_suffix;
        
        _back_info[info.url_path] = info;
        Storage();
        return true;
      }
      bool Update_packflag(const std::string &filename,bool status)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }

        it->second.pack_flag = status;
        Storage();
        return true;
      }
      bool Select_All(std::vector<_FileInfo> &v)
      {
        for(auto e = _back_info.begin();e != _back_info.end();e++)
          v.push_back(e->second);
        return true;
      }
      bool Select_One(const std::string &filename,_FileInfo *info)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
        {
          std::cout<<"not have this file"<<std::endl;
          return false;
        }
        *info = it->second;
        return true;
      }
      bool Delete(const std::string &filename)
      {
        std::string url_path = _download_prefix + FileTool(filename).Name();
        auto it = _back_info.find(url_path);
        if(it == _back_info.end())
          std::cout<<"not have this file"<<std::endl;

        _back_info.erase(url_path); 
        Storage();
        return true;
      }
  };
}
Insert——数据的插入
std::unordered_map<std::string , _FileInfo> _back_info;  //备份信息
bool Insert(const std::string &filename)
{
  if(FileTool(filename).Exist() == false)
  {
    std::cout<<"Inset file is not exist"<<std::endl;
    return false;
  }
  _FileInfo info;
  info.filename = FileTool(filename).Name();
  info.url_path = _download_prefix + info.filename;
  info.real_path = filename;
  info.file_size = FileTool(filename).Size();
  info.back_time = FileTool(filename).LastChange_Tm();
  info.pack_flag = false;
  info.pack_path = _file_pack_path + info.filename + _pack_suffix;
  
  _back_info[info.url_path] = info;
  Storage();
  return true;
}

这里的插入数据,是将保存的数据插入到内存当中,内存中使用的存储结构我们使用的是查找效率较高的unordered_map结构,使用Key=Val键值对,Key为我们的定义的url_path,也就是所谓的下载地址,Val是文件信息,这样可以通过Key快速找到对应的文件,可以方便后续功能的开发。对于文件信息的序列化,在Json的认识当中已经说过啦,可以回去看看。但是这里插入的文件信息,都是已经备份了的文件,不存在的文件,是没办法插入的。其实就是实现了一个内存中的文件管理。

Update_packflag——更新文件信息
bool Update_packflag(const std::string &filename,bool status)
{
  std::string url_path = _download_prefix + FileTool(filename).Name();
  auto it = _back_info.find(url_path);
  if(it == _back_info.end())
  {
    std::cout<<"not have this file"<<std::endl;
    return false;
  }

  it->second.pack_flag = status;
  Storage();
  return true;
}

文件压缩的时候默认是没有压缩,因为是刚上传上来,还没有超出系统设定的热点和非热点时间,所以默认是false没有压缩,但是压缩之后,所以需要将其设定为true,其实最后也就更新了一个压缩标志而已

Select_All——查找所有数据
bool Select_All(std::vector<_FileInfo> &v)
{
  for(auto e = _back_info.begin();e != _back_info.end();e++)
    v.push_back(e->second);
  return true;
}

外界传入一个数组,里面每个元素都是_FileInfo类型,用来保存遍历到的文件信息。

Select_One——查找单个数据
bool Select_One(const std::string &filename,_FileInfo *info)
{
  std::string url_path = _download_prefix + FileTool(filename).Name();
  auto it = _back_info.find(url_path);
  if(it == _back_info.end())
  {
    std::cout<<"not have this file"<<std::endl;
    return false;
  }
  *info = it->second;
  return true;
}

因为我们插入数时插入的是url_path,它的组成方式是_download_prefix + info.filename,也就是一个/download/前缀加文件名,所以我们查找时,需要组合出来该Key值,进行查找。

三个接口搭配使用——初始化管理
std::string _back_info_file = "./back_info.dat";   //存储备份信息的文件
 
Data_Management()
      {
        FileTool(_file_back_path).CreateDirectory();
        FileTool(_file_pack_path).CreateDirectory();
        if(FileTool(_back_info_file).Exist())
        {
          InitLoad();
        }
      }
      bool InitLoad()
      {
        std::string body;
        bool ret = FileTool(_back_info_file).Read(&body);
        if(ret == false)
        {
          std::cout<<"InitLoad read file filed"<<std::endl;
          return false;
        }
        Json::Value infos;
        ret = Json_Tool::UnSerialize(body,&infos);
        if(ret == false)
        {
          std::cout<<"InitLoad UnSerialize filed"<<std::endl;
          return false;
        }
        for(int i = 0; i < infos.size();i++)
        {
          _FileInfo info;
          info.filename = infos[i]["filename"].asString();
          info.url_path = infos[i]["url_path"].asString();
          info.real_path = infos[i]["real_path"].asString();
          info.file_size = infos[i]["file_size"].asInt64();
          info.back_time = infos[i]["back_time"].asInt64();
          info.pack_flag = infos[i]["pack_flag"].asBool();
          info.pack_path = infos[i]["pack_path"].asString();

          _back_info[info.url_path] = info;
        }
        return true;
      }
      bool Storage()
      {
        Json::Value infos;
        std::vector<_FileInfo> arry;
        Select_All(arry);  //读取全部备份文件,更新历史备份信息
        for(auto &e : arry)
        {
          Json::Value info;
          info["filename"] = e.filename;
          info["url_path"] = e.url_path;
          info["real_path"] = e.real_path;
          info["file_size"] = (Json::UInt64)e.file_size;
          info["back_time"] = (Json::UInt64)e.back_time;
          info["pack_flag"] = e.pack_flag;
          info["pack_path"] = e.pack_path;

          infos.append(info);
        }
        std::string body;
        Json_Tool::Serialize(infos,&body);
        FileTool(_back_info_file).Write(body);

        return true;
      }

在每次加再程序的同时,需要将历史备份信息表也加载出来,不然服务器怎么知道它备份过什么文件呢,所以就有了InitLoad接口,每次在服务器运行时,如果有备份信息,就需要将其读取到内存中,判断新上传的文件是新文件还是已经备份过得修改的文件。起到了一个初始化的作用。

Stroage接口,是用来更新历史备份信息,如果历史备份信息发生变化,那么就需要将其更新,历史备份信息文件就需要重新写入,所以这里,只要会导致数据发生变化的操作,都需要调用该函数,来保证备份信息的实时性。

因为一切都需要在程序运行时同时加再,所以需要将初始化接口在构造函数中调用,只要程序的运行起来,数据管理模块初始化的对象就会将信息读入,等待数据的到来。

部分总结

这个类主要完成了对数据的管理和组织,完成后台数据的存储和更新。后面有一个删除接口没有说,其实这个完全不需要,服务器不可能会自主删除用户的数据,所以只是一个扩展项。

hot——热点管理类

class HotManager
{
  private:
    time_t _hot_time = 30;
    std::string _back_dir = "./back_dir";
  public:
    HotManager()
    {}
    bool _Is_Hot(const std::string filename)
    {
      time_t _last_visit_time = FileTool(filename).LastVisit_Tm();  //文件最后访问时间
      time_t _now_time = time(NULL);  //本机实际时间
      if((_now_time - _last_visit_time) > _hot_time)
        return false;
      return true;
    }
    bool RunModule()
    {
      while(1)
      {
      //1.遍历目录
        std::vector<std::string> arry;
        FileTool(_back_dir).ScanDirectory(arry);
      //2.遍历信息
        for(auto &filename : arry)
        {
          //3.获取文件时间属性,进行热点判断
          if(!_Is_Hot(filename))
          {
            _FileInfo info;
            if(!_data->Select_One(filename,&info))
            {
              //4.1 文件备份信息不存在则直接删除文件
              FileTool(filename).Remove();
              continue;
              //4.1 文件备份信息不存在则创建
              //FileTool(filename).
            }
          //5.非热点文件则压缩
            FileTool(filename).Compress(info.pack_path);
            std::cout<<filename<<"已经被压缩"<<std::endl;
          //6.压缩后修改文件备份信息
            _data->Update_packflag(filename,true);
          }
        }
        usleep(10000);
      }
      return true;
    }
};

我们的hot管理思想如下:
1.遍历目录,获取所有文件信息
2.遍历文件信息
3.获取文件最后备份时间,判断是否需要压缩
——(1)找到了需要压缩的文件
——1.1)如果只在硬盘中存在,并没有存在于历史备份信息中,说明文件异常,直接删除
——(2)对非热点文件进行压缩
——(3)修改文件备份标志
4.重复上述操作

Server——服务器类

extern cloud::Data_Management *_data;
class Server
{
  private:
    int _srv_port = 9090;   //服务器绑定的端口号
    std::string _url_prefix = "/download/";
    std::string _back_dir = "./back_dir/";  //文件上传之后的备份路径
    httplib::Server _srv;
  private:
    static void Upload(const httplib::Request &req,httplib::Response &rsp)
    {
      std::string _back_dir = "./back_dir/";
      if(req.has_file("file") == false)   //判断有没有对应标识的文件上传区域数据
      {
        std::cout<<"upload file data format error"<<std::endl;
        rsp.status = 400;    
        return;
      }
      //获取解析后的区域数据
      httplib::MultipartFormData data = req.get_file_value("file"); 
      //组织文件的实际存储路径名
      std::string real_path = _back_dir + data.filename;
      //备份文件
      if(FileTool(real_path).Write(data.content) == false)
      {
        std::cout<<"back file filed"<<std::endl;
        return;
      }
      //新增备份信息
      if(_data->Insert(real_path) == false)
      {
        std::cout<<"Insert back info filed"<<std::endl;
        rsp.status = 500;
        return;
      }
      rsp.status = 200;
      return;
    }
    static void List(const httplib::Request &req,httplib::Response &rsp)
    {
      std::vector<_FileInfo> arry;
      if(_data->Select_All(arry) == false)
      {
        std::cout<<"List Select_All filed";
        rsp.status = 400;
        return;
      }

      std::stringstream ss;
      ss<< "<html>";
      ss<<  "<head>";
      ss<<      "<meta charset='utf-8' />";
      ss<<      "<meta http-equiv='Content-Type'>"; 
      ss<<      "<meta content='text/html'>";
      ss<<      "<title>Download</title>";
      ss<<  "</head>";
      ss<<  "<body>";
      ss<<      "<h1>Download</h1>";
      ss<<      "<table>";
      for(auto &e : arry)
      {
                ss<<  "<tr>";
                ss<<    "<td><a href='"<<e.url_path<<"'download=>"<<e.filename<<" </a></td>";
                ss<<    "<td align='right'>"<<std::asctime(std::localtime(&e.back_time))<<"</td>";
                ss<<    "<td align='rgiht'>"<<e.file_size / 1024<<"KB </td>";
                ss<<  "</tr>";
      }
      ss<<      "</table>";
      ss<<  "</body>";
      ss<< "</html>";

      rsp.set_content(ss.str(),"text/html");
      rsp.status = 200;
      return;
    }
    static std::string StrEtag(const _FileInfo info)
    {
      time_t last_time = info.back_time;
      size_t fsize = info.file_size;
      std::stringstream ss;
      ss<<fsize<<"-"<<last_time;
      return ss.str();
    }
    static void Download(const httplib::Request &req,httplib::Response &rsp)
    {
      _FileInfo info;
      if(_data->Select_One(req.path,&info) == false)
      {
        std::cout<<"not have this file"<<std::endl;
        rsp.status = 404;
        return;
      }  
      if(info.pack_flag == true)
      {
        FileTool(info.pack_path).Uncompress(info.real_path);
        _data->Update_packflag(info.filename,false);
        std::cout<<info.filename<<"已经解压缩"<<std::endl;
      }
      if(req.has_header("If-Range"))
      {
        std::string old_etag = req.get_header_value("If-Range");
        std::string now_etag = StrEtag(info);
        if(old_etag == now_etag)
        {
          //Range:bytes = 200-1000
          //size_t start = req.Ranges[0].first;
          //size_t end = req.Ranges[0].second; 如果没有end数字,则表示到文件末尾,httplib将second设置为-1
          FileTool(info.real_path).Read(&rsp.body);
          //rsp.set_header("Content-Type","appliaction/octet stream");
          rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
          //rsp.set_header("Content-Type","byte start-end/fsize"); //httplib会自动设置
          rsp.status = 206;
          return;
        }
      }
      FileTool(info.real_path).Read(&rsp.body);
      //rsp.set_header("Content-Type","application/octet-stream"); //设置正文类型为二进制流
      rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
      rsp.set_header("ETag",StrEtag(info));
      rsp.status = 200;
      return;
    }
  public:
    Server()
    {
      FileTool(_back_dir).CreateDirectory();
    }
    bool RunModule()
    {
      _srv.Post("/upload",Upload);  //Post(请求的资源路径,对应的业务处理函数)
      _srv.Get("/list",List);
      std::string _download_path = _url_prefix + "(.*)";
      _srv.Get(_download_path,Download);
      //运行服务器
      _srv.listen("0.0.0.0",_srv_port);
      return true;
    }
};
Upload——上传业务处理
static void Upload(const httplib::Request &req,httplib::Response &rsp)
{
  std::string _back_dir = "./back_dir/";
  if(req.has_file("file") == false)   //判断有没有对应标识的文件上传区域数据
  {
    std::cout<<"upload file data format error"<<std::endl;
    rsp.status = 400;    
    return;
  }
  //获取解析后的区域数据
  httplib::MultipartFormData data = req.get_file_value("file"); 
  //组织文件的实际存储路径名
  std::string real_path = _back_dir + data.filename;
  //备份文件
  if(FileTool(real_path).Write(data.content) == false)
  {
    std::cout<<"back file filed"<<std::endl;
    return;
  }
  //新增备份信息
  if(_data->Insert(real_path) == false)
  {
    std::cout<<"Insert back info filed"<<std::endl;
    rsp.status = 500;
    return;
  }
  rsp.status = 200;
  return;
}

//拿过来方便看
struct MultipartFormData  //这里是html中的东西
 { 
	 std::string name;   //区域名称标识
	 std::string content; //文件正文
	 std::string filename;  //文件名
	 std::string content_type;  //文件传输格式,一般都是二进制传输
 }; 

先解释为什么要设置为静态函数,因为库函数在封装的时候,只有两个参数,但是要知道,类内函数,都不是你看到的样子,除了友元和静态函数,其他都含有一个隐藏的this指针,所以如果不声明称静态函数,编译的时候就会报错的。

实现思想:
1.检查name标识区域是否有file标识的文件上传区域
2.解析区域内内容,将其存入MultipartFormData中对应的数据中
3.组织文件实际存储路径
4.备份文件(在存储备份文件的文件夹中创建对应文件,写入文件内容,完成备份)

List——用户使用客户端浏览历史备份记录
 static void List(const httplib::Request &req,httplib::Response &rsp)
 {
   std::vector<_FileInfo> arry;
   if(_data->Select_All(arry) == false)
   {
     std::cout<<"List Select_All filed";
     rsp.status = 400;
     return;
   }

   std::stringstream ss;
   ss<< "<html>";
   ss<<  "<head>";
   ss<<      "<meta charset='utf-8' />";
   ss<<      "<meta http-equiv='Content-Type'>"; 
   ss<<      "<meta content='text/html'>";
   ss<<      "<title>Download</title>";
   ss<<  "</head>";
   ss<<  "<body>";
   ss<<      "<h1>Download</h1>";
   ss<<      "<table>";
   for(auto &e : arry)
   {
             ss<<  "<tr>";
             ss<<    "<td><a href='"<<e.url_path<<"'download=>"<<e.filename<<" </a></td>";
             ss<<    "<td align='right'>"<<std::asctime(std::localtime(&e.back_time))<<"</td>";
             ss<<    "<td align='rgiht'>"<<e.file_size / 1024<<"KB </td>";
             ss<<  "</tr>";
   }
   ss<<      "</table>";
   ss<<  "</body>";
   ss<< "</html>";

   rsp.set_content(ss.str(),"text/html");
   rsp.status = 200;
   return;
 }

实现思想:

1.遍历历史备份信息
2.通过html语言完成一个页面,在<<a href= " " -->>标签中,加入下载连接,就可以实现下载文件的功能,传递过来的Get请求会被Download接口匹配,然后调用对应的Download业务处理函数
3.设置正文是text/html,这一个超文本格式,也就是返回了一个浏览界面
4.设置服务器状态码

Download——下载业务处理
static void Download(const httplib::Request &req,httplib::Response &rsp)
{
  _FileInfo info;
  if(_data->Select_One(req.path,&info) == false)
  {
    std::cout<<"not have this file"<<std::endl;
    rsp.status = 404;
    return;
  }  
  if(info.pack_flag == true)
  {
    FileTool(info.pack_path).Uncompress(info.real_path);
    _data->Update_packflag(info.filename,false);
    std::cout<<info.filename<<"已经解压缩"<<std::endl;
  }
  if(req.has_header("If-Range"))
  {
    std::string old_etag = req.get_header_value("If-Range");
    std::string now_etag = StrEtag(info);
    if(old_etag == now_etag)
    {
      //Range:bytes = 200-1000
      //size_t start = req.Ranges[0].first;
      //size_t end = req.Ranges[0].second; 如果没有end数字,则表示到文件末尾,httplib将second设置为-1
      FileTool(info.real_path).Read(&rsp.body);
      //rsp.set_header("Content-Type","appliaction/octet stream");
      rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
      //rsp.set_header("Content-Type","byte start-end/fsize"); //httplib会自动设置
      rsp.status = 206;
      return;
    }
  }
  FileTool(info.real_path).Read(&rsp.body);
  //rsp.set_header("Content-Type","application/octet-stream"); //设置正文类型为二进制流
  rsp.set_header("Accept-Ranges","Bytes");//告诉客户端支持断点续传
  rsp.set_header("ETag",StrEtag(info));
  rsp.status = 200;
  return;
}

实现思想:

1.通过传递过来的下来路径,在哈希表中寻找对应的文件,将文件信息存储到info中
2.判断文件是否被压缩,压缩则解压缩,修改压缩标志
3.判断是否有头不字段是“If-Range”,如果有说明需要断点续传
4.判断下载文件ETag是否跟备份文件ETag值相同,相同则是相同文件,可以执行断点续传,如果不同,则是备份文件被修改,需要重新下载。
5.将文件内容读取到响应正文中
6.设置头部字段(“Accept-Ranges”,“Bytes”),告诉客户端支持断点续传
7.设置头部字段(set_header(“ETag”,StrEtag(info)))设置ETag值,由于断点续传中的判断
8.设置响应状态码,如果是断点续传则返回206,如果是普通下载则返回200.

主要需要说一下断点续传的实现,虽然大部分功能都由httplib实现了,但是主要思想还是需要理解,服务器在执行下载业务时,会在头部字段中设置ETag值和支持断点续传的头部字段,如果客户端因为种种原因中断了下载,那么下次服务器重启,或者客户端重新下载时,客户端发送的请求中就有If-Range头部字段,其后跟的就是文件ETag值,如果值相同,就是相同的文件,可以断点续传,如果不同,则代表文件在期间发生了修改,所以需要重新下载。并且请求头中还有一个Range:bytes,后面跟随的就是文件还需要下载的大小,举个例子,假如长度为1000的文件,第一次只下载了200,那么它后面就会跟着一个200-999或者200-,这时httplib就会在后面补一个 -1 ,-1 就代表到文件末尾,如果有 - 右边有数字,那么请求的文件长度就是999 - 200 + 1 也就是800这里时包括900的,如果时 - 1,那么就是到文件末尾也就是999,所以只需要999-200就可以了。那么影响正确的时候就会在响应头部添加Content-Range:bytes:200-1000/文件大小。正文长度就是剩下的长度。

Listen——服务器监听接口
int _srv_port = 9090;   //服务器绑定的端口号
_srv.listen("0.0.0.0",_srv_port);

服务器开始监听才可以接收客户端的请求,0.0.0.0IP地址表示监听当前主机下所有IP地址,然后绑定一个端口号即可

部分总结

服务器的实现主要是借助了httplib,因为时间的限制,所以没办法自己完成一个完整的服务器,但是需要理解其中的思想的

客户端开发

客户端建立思想

目标功能: 对客户端主机上指定文件夹中的文件进行检测,哪个文件需要备份,则将其上传到服务器备份起来

思想:

  1. 目录检索遍历指定文件夹,获取所有文件路径名
  2. 获取所有文件的属性,判断该文件是否需要备份
    2.1.新增文件——如果根据文件路径找不到对应的文件备份信息,则说明是新增文件,需要备份
    2.2.备份过但是被修改的文件——有历史备份信息,但是唯一标识与备份中的不同,说明是修改过的文件,需要备份
    2.3.如果文件持续被修改,那么每次检索都会造成一次备份
    实际处理——当前文件没有被任何进程占用,则进行备份操作
    简单处理——当前文件在指定修改时间间隔内都没有进行修改,则进行上传
  3. 如果一个文件需要备份,则创建一个http客户端,进行上传
  4. 上传文件后,添加历史备份信息

模块划分

数据管理模块——管理客户端的备份信息
目录检索模块——获取指定文件夹下的所有文件
网络通信模块——将所需要备份的文件通过http客户端进行上传

FileTools——文件工具类

#include <iostream>
#include <string>
#include <sstream>
#include <fstream>
#include <vector>
#include <experimental/filesystem>
#include <sys/stat.h>
#include <cstdint>

namespace fs = std::experimental::filesystem;

namespace cloud
{
    class FileTool
    {
    private:
        std::string _name;
    public:
        FileTool(const std::string name) :_name(name)
        {}
        std::string Name()
        {
            return fs::path(_name).filename().string();
        }
        bool Exist()
        {
            return fs::exists(_name);
        }
        size_t Size()
        {
            if (!Exist())
                return 0;
            return fs::file_size(_name);
        }
        time_t LastChange_Tm()
        {
            if (!Exist())
                return 0;
            auto ftime = fs::last_write_time(_name);
            std::time_t cftime = decltype(ftime)::clock::to_time_t(ftime);
            return cftime;
        }
        time_t LastVisit_Tm()
        {
            if (!Exist())
                return 0;
            struct stat st;
            stat(_name.c_str(), &st);
            return st.st_atime;

        }
        bool Write(const std::string& body)
        {
            if (!Exist())
                my_CreateDirectory();
            std::ofstream writer;
            writer.open(_name, std::ios::binary); 
            if (!writer.is_open())
            {
                std::cout << "writer open file false" << std::endl;
                writer.close();
                return false;
            }
            writer.write(body.c_str(), body.size());
            if (!writer.good())
            {
                std::cout << "writer write false" << std::endl;
                writer.close();
                return false;
            }
            writer.close();
            return true;
        }
        bool Read(std::string* body)  
        {

            if (!Exist())
                return 0;
            std::ifstream reader;
            reader.open(_name, std::ios::binary);
            if (!reader.is_open())
            {
                std::cout << "reader open file false" << std::endl;
                reader.close();
                return false;
            }
            size_t fsize = Size();
            body->resize(fsize);
            reader.read(&(*body)[0], fsize);  
            if (!reader.good())
            {
                std::cout << "reader read false" << std::endl;
                reader.close();
                return false;
            }
            reader.close();
            return true;
        }
        bool my_CreateDirectory()
        {
            if (Exist() == true)
                return 0;
            fs::create_directories(_name);
            return true;
        }
        bool ScanDirectory(std::vector<std::string>& arry) 
        {
            if (!Exist())
                return 0;
            for (auto& a : fs::directory_iterator(_name))
            {
                if (fs::is_directory(a) == true)
                    continue; 
                 
                std::string pathname = fs::path(a).relative_path().string(); 
                arry.push_back(pathname);
            }
            return true;
        }
        bool Remove()
        {
            if (!Exist())
            {
                std::cout << "not have this file" << std::endl;
                return false;
            }
            fs::remove_all(_name);
            return true;
        }
    };
}
   

其实服务器和客户端的文件管理工具类并没有区别,因为本来就是使用的是可以跨平台的库来实现的,所以直接留下需要的接口,实现了代码复用

Data_Management——数据管理模块


#include "file_tools.hpp"
#include <unordered_map>
#include <vector>

class Data_Management
{
  private:
      std::string _backup_dat = "./backup.dat";
      std::unordered_map<std::string, std::string> _back_info;
  public:
    Data_Management()
    {
        FileTool(_backup_dat).my_CreateDirectory();
        InitLoad();
    }
    static int Split(const std::string body,std::string key,std::vector<std::string> &arry)
    {
        int count = 0;
        int pr = 0, pos = 0;
        while (pos < body.size())
        {
            pos = body.find(key, pr);
            if (pos == std::string::npos) //找不到分隔符
                break;
            if (pos == pr)    //防止分隔符连在一起
            {
                pr += key.size();
                continue;
            }
            std::string val = body.substr(pr,pos);
            arry.push_back(val);
            pr = pos + key.size();   
            count++;
        }
        if (pos < body.size() - 1)
        {
            std::string val = body.substr(pr);
            arry.push_back(val);
            count++;
        }
        return count;
    }
    bool InitLoad()
    {
        std::string body;
        if (FileTool(_backup_dat).Read(&body) == false)
            return false;
        std::vector<std::string> arry;
        int count = Split(body, "\n", arry);
        for (int i = 0; i < arry.size(); i++)
        {
            int key = arry[i].find("=");
            if (key == std::string::npos)
                continue;
            _back_info[arry[i].substr(0, key)] = arry[i].substr(key + 1);
        }
        return true;
    }
    bool Storage()
    {
        std::stringstream body;
        for (auto it = _back_info.begin(); it != _back_info.end(); it++)
            body << it->first << "=" << it->second;
        FileTool(_backup_dat).Write(body.str());
        return true;
    }
    std::string FileETag(const std::string& filename)
    {
        size_t fsize = FileTool(filename).Size();
        time_t ftime = FileTool(filename).LastChange_Tm();
        std::stringstream ss;
        ss << fsize << "-" << ftime;
        return ss.str();
    }
    bool Insert(const std::string &filename)
    {
        std::string etag = FileETag(filename);
        _back_info[filename] = etag;
        Storage();
        return true;
    }
    bool Updata(const std::string &filename)
    {
        std::string ETag = FileETag(filename);
        _back_info[filename] = ETag;
        Storage();
        return true;
    }
    bool Select_All(std::vector<std::pair<std::string,std::string>> &infos)
    {
        for (auto it = _back_info.begin(); it != _back_info.end(); it++)
        {
            std::pair<std::string, std::string> info;
            info.first = it->first;
            info.second = it->second;
            infos.push_back(info);
        }
        return true;
    }
    bool Select_One(const std::string &filename,std::string &ETag)
    {
        auto it = _back_info.find(filename);
        if (it == _back_info.end())
            return false;
        ETag = it->second;
        return true;
    }
    bool Delete(const std::string &filename)
    {
        auto it = _back_info.find(filename);
        if (it == _back_info.end())
            return false;
        _back_info.erase(it);
        Storage();
        return true;
    }
};

建立一个信息映射表,根据文件路径名,查找对应文件属性。与服务器不同的是,Updata接口更新的是文件属性也就是自己定义的ETag,该字符串是由文件大小以及文件最后修改时间来确定,也是用来确定文件是否被修改的唯一标识。其他都跟服务器思想相同,只不过查找和保存的信息发生了变化

Client——客户端类

#include "data.hpp"
#include "httplib.h"
#include <Windows.h>

namespace cloud
{
class Client
{
private:
	std::string _srv_ip;
	int _srv_port;
	std::string _back_dir = "./backup.dir";
	Data_Management* data = nullptr;
protected:
	std::string FileETag(const std::string& filename)
	{
		size_t fsize = FileTool(filename).Size();
		time_t ftime = FileTool(filename).LastChange_Tm();
		std::stringstream ss;
		ss << fsize << "-" << ftime;
		return ss.str();
	}
	bool Is_NeedBackup(const std::string filename)
	{
		std::string old_ETag;
		if (data->Select_One(filename, old_ETag) == false)
			return true;
		std::string new_ETag = FileETag(filename);
		if (new_ETag != old_ETag)
		{
			time_t ftime = FileTool(filename).LastChange_Tm();
			time_t ntime = time(NULL);
			if (ntime - ftime > 5)
				return true;
		}
		return false;
	}
	bool Upload(const std::string filename)
	{   //添加name标识区域信息
		httplib::Client client(_srv_ip, _srv_port);
		httplib::MultipartFormDataItems items;
		httplib::MultipartFormData item;
		item.name = "file";
		item.filename = FileTool(filename).Name();
		FileTool(filename).Read(&item.content);
		item.content_type = "accplication/octet-stream";
		items.push_back(item);

		auto res = client.Post("/upload", items);
		if (res && res->status != 200)
			return false;
		return true;
	}
public:
	Client(const std::string srv_ip, int srv_port)
		:_srv_ip(srv_ip),_srv_port(srv_port)
	{}
	void RunMoudle()
	{
		//1.初始化,初始化数据管理对象,创建管理目录
		FileTool(_back_dir).my_CreateDirectory();
		data = new Data_Management();
		while (1)
		{
			//2.遍历目录,获取目录下文件
			std::vector<std::string> arry;
			FileTool(_back_dir).ScanDirectory(arry);
			//3.根据历史备份信息,判断是否需要备份
			for (auto& e : arry)
			{
				if (Is_NeedBackup(e) == false)
					continue;
				//4.需要备份则进行上传
				std::cout << e << "need backup!" << std::endl;
				if (Upload(e))
					std::cout << e << "upload success!" << std::endl;
				//5.添加备份信息
				data->Insert(e);
			}
			Sleep(10);
		}
	}
};

实现思想:
1.初始化,将历史备份信息读入内存中的映射表中
2.遍历目录,将目录中的文件路径进行存储
3.根据文件路径,寻找历史备份信息中的ETag标识,判断是否需要备份
4.需要备份的文件则进行上传,否则继续遍历
5.上传备份后,添加备份信息

Upload——上传函数
bool Upload(const std::string filename)
	{   //添加name标识区域信息
		httplib::Client client(_srv_ip, _srv_port);
		httplib::MultipartFormDataItems items;
		httplib::MultipartFormData item;
		item.name = "file";
		item.filename = FileTool(filename).Name();
		FileTool(filename).Read(&item.content);
		item.content_type = "accplication/octet-stream";
		items.push_back(item);

		auto res = client.Post("/upload", items);
		if (res && res->status != 200)
			return false;
		return true;
	}

实现思想:
1.创建http客户端
2.填充请求中name标识区域的数据信息
3.使用POST请求上传文件

Is_NeedBackup——是否需要备份判断函数
bool Is_NeedBackup(const std::string filename)
	{
		std::string old_ETag;
		if (data->Select_One(filename, old_ETag) == false)
			return true;
		std::string new_ETag = FileETag(filename);
		if (new_ETag != old_ETag)
		{
			time_t ftime = FileTool(filename).LastChange_Tm();
			time_t ntime = time(NULL);
			if (ntime - ftime > 5)
				return true;
		}
		return false;
	}

实现思想:
1.查找是否由该文件的历史备份信息,如果有则往下走,没有则是新文件,需要备份
2.有历史备份信息,计算当先ETag值,与文件历史备份信息中的ETag进行比较,如果不同,则说明文件被修改,需要备份

十三、项目总结

项目名称: 云备份——CloudBackup

项目简介: 本项目实现了云备份服务器与客户端,其中客户端部署在用户机上,实现对指定文件夹中文件进行自动备份;服务器端提供客户端的文件备份存储,并且在服务器端进行压缩操作,节省磁盘空间,并且支持客户端通过浏览器获取自己的文件信息目录,并进行下载,下载支持断点续传功能

开发环境: centos7/g++,gdb,makefile,vim && Windows10/vs2020

技术特点: http服务器,文件压缩,热点管理,断点续传,json序列化

项目实现:
服务器:
1.网络通信模块——搭建网络通信服务器,实现与客户端的通信

2.业务处理模块——对不同的请求提供不同的处理方法

3.数据管理模块——负责服务器上备份文件的信息管理

4.热点管理模块——负责文件的热点判断,对非热点文件进行压缩

客户端:
1.网络通信模块——搭建网络通信客户端,实现与服务器的通信

2.文件检索模块——遍历获取指定文件夹的所有文件信息和路径

3.数据管理模块——负责客户端文件信息的管理,通过这些信息判断文件是否需要备份

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
WEBCRAWLER 网络爬虫实训项目 1 WEBCRAWLER 网 络 爬 虫 实 训 项 目 文档版本: 1.0.0.1 编写单位: 达内IT培训集团 C++教学研发部 编写人员: 闵卫 定稿日期: 2015年11月20日 星期五WEBCRAWLER 网络爬虫实训项目 2 1. 项目概述 互联网产品形形色色,有产品导向的,有营销导向的,也有技术导向的,但是 以技术见长的互联网产品比例相对小些。搜索引擎是目前互联网产品中最具技 术含量的产品,如果不是唯一,至少也是其中之一。 经过十几年的发展,搜索引擎已经成为互联网的重要入口之一,Twitter联合创 始人埃文•威廉姆斯提出了“域名已死论”,好记的域名不再重要,因为人们会 通过搜索进入网站。搜索引擎排名对于中小网站流量来说至关重要。了解搜索 引擎简单界面背后的技术原理其实对每一个希望在互联网行业有所建树的信息 技术人员都很重要。 1.1. 搜索引擎 作为互联网应用中最具技术含量的应用之一,优秀的搜索引擎需要复杂的架构 和算法,以此来支撑对海量数据的获取、 存储,以及对用户查询的快速而准确 地响应。 从架构层面,搜索引擎需要能够对以百亿计的海量网页进行获取、 存 储、 处理的能力,同时要保证搜索结果的质量。 如何获取、 存储并计算如此海WEBCRAWLER 网络爬虫实训项目 3 量的数据?如何快速响应用户的查询?如何使得搜索结果尽可能满足用户对信 息的需求?这些都是搜索引擎的设计者不得不面对的技术挑战。 下图展示了一个通用搜索引擎的基本结构。商业级别的搜索引擎通常由很多相 互独立的模块组成,各个模块只负责搜索引擎的一部分功能,相互配合组成完 整的搜索引擎: 搜索引擎的信息源来自于互联网网页,通过“网络爬虫” 将整个“互联网” 的 信息获取到本地,因为互联网页面中有相当大比例的内容是完全相同或者近似 重复的,“网页去重”模块会对此做出检测,并去除重复内容。 在此之后,搜索引擎会对网页进行解析,抽取网页主体内容,以及页面中包含 的指向其它页面的所谓超链接。 为了加快用户查询的响应速度,网页内容通过 “倒排索引”这种高效查询数据结构来保存,而网页之间的链接关系也会予以 保存。之所以要保存链接关系,是因为这种关系在网页相关性排序阶段是可利 用的,通过“链接分析”可以判断页面的相对重要性,对于为用户提供准确的 搜索结果帮助很大。 由于网页数量太多,搜索引擎不仅需要保存网页的原始信息,还要保存一些中 间处理结果,使用单台或者少量的计算机明显是不现实的。 Google等商业搜索 引擎提供商,为此开发了一整套存储与计算平台,使用数以万计的普通PCWEBCRAWLER 网络爬虫实训项目 4 搭建了海量信息的可靠存储与计算架构,以此作为搜索引擎及其相关应用的基 础支撑。优秀的存储与计算平台已经成为大型商业搜索引擎的核心竞争 力。 以上所述是搜索引擎如何获取并存储海量的网页相关信息。这些功能因为不需 要实时计算,所以可以被看作是搜索引擎的后台计算系统。搜索引擎的首要目 标当然是为用户提供准确而全面的搜索结果,因此响应用户查询并实时提供准 确结果便构成了搜索引擎的前台计算系统。 当搜索引擎接收到用户的查询请求后,首先需要对查询词进行分析,通过与用 户信息的结合,正确推导出用户的真实搜索意图。 此后,先在“Cache系统” 所维护的缓存中查找。搜索引擎的缓存存储了不同的搜索意图及其相对应的搜 索结果。如果在缓存中找到满足用户需求的信息,则直接将搜索结果返回给用 户。这样既省掉了重复计算对资源的消耗,又加快了整个搜索过程的响应速 度。而如果在缓存中没有找到满足用户需求的信息,则需要通过“网页排 序”,根据用户的搜索意图,实时计算哪些网页是满足用户需求的,并排序输 出作为搜索结果。 而网页排序最重要的两个参考因素,一个是“内容相似 性”,即哪些网页是和用户的搜索意图密切相关的;一个是网页重要性,即哪 些网页是质量较好或相对重要的,而这往往可以从“链接分析”的结果中获 得。综合以上两种考虑,前台系统对网页进行排序,作为搜索的最终结果。 除了上述功能模块,搜索引擎的“反作弊”模块近年来越来越受到重视。搜索 引擎作为互联网用户上网的入口,对于网络流量的引导和分流至关重要,甚至 可以说起着决定性的作用。因此,各种“作弊”方式也逐渐流行起来,通过各 种手段将网页的搜索排名提前到与其网页质量不相称的位置,这会严重影响用 户的搜索体验。所以,如何自动发现作弊网页并对其给于相应的惩罚,就成了 搜索引擎非常重要的功能之一。 1.2. 网络爬虫 通用搜索引擎的处理对象是互联网网页,截至目前的网页数量数以百万计,所 以搜索引擎首先面临的问题就是如何能够设计出高效的下载系统,将如此海量 的网页数据传送到本地,在本地形成互联网网页的镜像备份。 网络爬虫即扮演 如此角色。 它是搜索引擎中及其关键的基础构件。WEBCRAWLER 网络爬虫实训项目 5 网络爬虫的一般工作原理如下图所示:  从互联网网页中选择部分网页的链接作为“种子URL”,放入“待抓取URL 队列”;  爬虫从“待抓取URL队列”中依次“读取URL”;  爬虫通过“DNS解析” 将读到的URL转换为网站服务器的IP地址;  爬虫将网站服务器的IP地址、通信端口、网页路径等信息交给“网页下载” 器;  “网页下载”器负责从“互联网”上下载网页内容;  对于已经下载到本地的网页内容,一方面将其存储到“下载页面库” 中,等 待建立索引等后续处理,另一方面将其URL放入“已抓取URL队列”,后者显 然是为了避免网页被重复抓取;  对于刚刚下载到本地的网页内容,还需要从中“抽取URL”;  在“已抓取URL队列”中检查所抽取的URL是否已被抓取过;  如果所抽取的URL没有被抓取过,则将其排入“待抓取URL队列” 末尾,在 之后的抓取调度中重复第步,下载这个URL所对应的网页。 如此这般,形成WEBCRAWLER 网络爬虫实训项目 6 循环,直到“待抓取URL队列”空,这表示爬虫已将所有能够被抓取的网页尽 数抓完,完成一轮完整的抓取过程。 以上所述仅仅是网络爬虫的一般性原理,具体实现过程中还可以有很多优化的 空间,比如将“网页下载”以多线索(进程或线程)并发的方式实现,甚至将 “DNS解析”也处理为并发的过程,以避免爬虫系统的I/O吞吐率受到网站服 务器和域名解析服务器的限制。而对于“已抓取URL队列”则可以采用布隆排 重表的方式加以优化,以降低其时间和空间复杂度。 2. 总体架构 本项目总体架构如下图所示: 配置器 Configurator 超文本传输协议响应 HttpResponse 日志 Log 主线程 main 多路输入输出 MultiIo 插件管理器 PluginMngr 套接字 Socket 字符串工具包 StrKit 统一资源定位符队列 UrlQueues 网络爬虫 WebCrawler 原始统一资源定位符 RawUrl 超文本传输协议响应包头 HttpHeader 域名解析线程 DnsThread 解析统一资源定位符 DnsUrl 接收线程 RecvThread 布隆过滤器 BloomFilter 哈希器 Hash 最大深度插件 MaxDepth 域名限制插件 DomainLimit 超文本传输协议响应包头过滤器插件 HeaderFilter 超文本标记语言文件存储插件 SaveHTMLToFile 图像文件存储插件 SaveImageToFile 发送线程 SendThreadWEBCRAWLER 网络爬虫实训项目 7 2.1. 基础设施 2.1.1. 字符串工具包(StrKit) 常用字符串处理函数。 2.1.2. 日志(Log) 分等级,带格式的日志文件打印。 2.1.3. 配置器(Configurator) 从指定的配置文件中加载配置信息。 2.1.4. 多路输入输出(MultiIo) 封装epoll多路I/O系统调用,提供增加、删除和等待操作接口。 2.1.5. 插件管理器(PluginMngr) 加载插件并接受其注册,维护插件对象容器并提供调用其处理函数的外部接 口。 2.2. 网络通信 2.2.1. 哈希器(Hash) 封装各种哈希算法函数。 2.2.2. 布隆过滤器(BloomFilter) 基于布隆算法,对欲加入队列的原始统一资源定位符进行过滤,以防止已被抓 取过的URL再次入队,降低冗余开销同时避免无限循环。 2.2.3. 原始统一资源定位符(RawUrl) 提供原始形态的统一资源定位符字符串的简单包装,以及规格化等辅助支持。 2.2.4. 解析统一资源定位符(DnsUrl) 将原始形态的统一资源定位符字符串,解析为服务器域名、资源路径、服务器 IP地址,乃至服务器通信端口等。WEBCRAWLER 网络爬虫实训项目 8 2.2.5. 统一资源定位符队列(UrlQueues) 封装原始统一资源定位符队列和解析统一资源定位符队列,提供线程安全的入 队、出队操作,通过统一资源定位符过滤器排重,同时支持基于正则表达式的 统一资源定位符抽取功能。 2.2.6. 套接字(Socket) 发送/接收超文本传输协议请求/响应,发送成功将套接字描述符加入多路I/O, 接收成功抽取统一资源定位符压入队列。 2.2.7. 超文本传输协议响应包头(HttpHeader) 状态码和内容类型等关键信息。 2.2.8. 超文本传输协议响应(HttpResponse) 服务器统一资源定位符和超文本传输协议包头、包体及长度的简单封装。 2.3. 流程控制 2.3.1. 域名解析线程(DnsThread) 从原始统一资源定位符队列中弹出RawUrl对象,借助域名解析系统(DNS)获 取服务器的IP地址,构造DnsUrl对象压入解析统一资源定位符队列。 2.3.2. 发送线程(SendThread) 通过WebCrawler对象启动新的抓取任务,从解析统一资源定位符队列中弹出 DnsUrl对象,向HTTP服务器发送HTTP请求,并将套接字描述符放入MultiIo 对象。 2.3.3. 接收线程(RecvThread) 由WebCrawler对象在从MultiIo对象中等到套接字描述符可读时动态创建,通 过Socket对象接收超文本传输协议响应。WEBCRAWLER 网络爬虫实训项目 9 2.3.4. 网络爬虫(WebCrawler) 代表整个应用程序的逻辑对象,构建并维护包括日志、配置器、多路I/O、插件 管理器、统一资源定位符队列、域名解析线程等在内的多个底层设施,提供诸 如初始化、执行多路输入输出循环、启动抓取任务等外部接口。 2.3.5. 主线程(main) 主函数,处理命令行参数,初始化应用程序对象,进入多路I/O循环。 2.4. 外围扩展 2.4.1. 最大深度插件(MaxDepth) 根据配置文件的MAX_DEPTH配置项,对被抓取超链接的最大递归深度进行限 制。 2.4.2. 域名限制插件(DomainLimit) 根据配置文件的INCLUDE_PREFIXES和EXCLUDE_PREFIXES配置项,对被抓取 超链接的前缀进行限制。 2.4.3. 超文本传输协议响应包头过滤器插件(HeaderFilter) 根据配置文件的ACCEPT_TYPE配置项,对超文本传输协议响应的内容类型进行 限制。 2.4.4. 超文本标记语言文件存储插件(SaveHTMLToFile) 将用超文本标记语言描述的页面内容保存到磁盘文件中。 2.4.5. 图像文件存储插件(SaveImageToFile) 将页面内容中引用的图像资源保存到磁盘文件中。 3. 工作流程 3.1. 主事件流 进程入口函数在进行必要的命令行参数处理和系统初始化以后,进入网络爬虫 的多路输入输出循环,一旦发现某个与服务器相连的套接字有数据可读,即创WEBCRAWLER 网络爬虫实训项目 10 建接收线程,后者负责抓取页面内容,而前者继续于多路输入输出循环中等待 其它套接字上的I/O事件。 3.2. 解析事件流 独立的域名解析线程实时监视原始统一资源定位符队列的变化,并将其中的每 一条新近加入的原始统一资源定位符,借助域名解析系统转换为解析统一资源 定位符,并压入解析统一资源定位符队列。 3.3. 发送事件流 不断从解析统一资源定位符队列弹出解析统一资源定位符,创建套接字,根据 服务器的IP地址和通信端口发起连接请求,建立TCP连接,发送超文本传输协 议请求包,并将套接字放入多路输入输出对象,由主事件流等待其数据到达事 件。 3.4. 接收事件流 每个超文本传输线程通过已明确有数据可读的套接字接收来自服务器的超文本 传输协议响应,并交由统一资源定位符队列进行超链接抽取和布隆排重过滤, 直至压入原始统一资源定位符队列。在压入原始统一资源定位符队列之前,以 及接收到超文本传输协议包头和包体之后,分别执行统一资源定位符插件、超 文本传输协议包头插件和超文本标记语言插件的处理过程。 以上四个事件流,需要平行且独立地并发运行,并在共享资源和执行步调上保 持适度的同步。 4. 目录结构 本项目的目录结构如下所示: WebCrawler/ ├── bin/ │ ├── WebCrawler │ ├── WebCrawler.cfg │ └── WebCrawler.scr ├── docs/ │ ├── 概要设计.pdfWEBCRAWLER 网络爬虫实训项目 11 │ └── 详细设计.pdf ├── download/ ├── plugins/ │ ├── DomainLimit.cpp │ ├── DomainLimit.h │ ├── DomainLimit.mak │ ├── DomainLimit.so │ ├── HeaderFilter.cpp │ ├── HeaderFilter.h │ ├── HeaderFilter.mak │ ├── HeaderFilter.so │ ├── MaxDepth.cpp │ ├── MaxDepth.h │ ├── MaxDepth.mak │ ├── MaxDepth.so │ ├── SaveHTMLToFile.cpp │ ├── SaveHTMLToFile.h │ ├── SaveHTMLToFile.mak │ ├── SaveHTMLToFile.so │ ├── SaveImageToFile.cpp │ ├── SaveImageToFile.h │ ├── SaveImageToFile.mak │ ├── SaveImageToFile.so │ └── mkall └── src/ ├── BloomFilter.cpp ├── BloomFilter.h ├── Configurator.cpp ├── Configurator.h ├── DnsThread.cpp ├── DnsThread.h ├── Hash.cpp ├── Hash.h ├── Http.h ├── Log.cpp ├── Log.h ├── Main.cpp ├── Makefile ├── MultiIo.cpp ├── MultiIo.h ├── Plugin.h ├── PluginMngr.cpp ├── PluginMngr.h ├── Precompile.h ├── RecvThread.cpp ├── RecvThread.h ├── SendThread.cpp ├── SendThread.h ├── Socket.cpp ├── Socket.hWEBCRAWLER 网络爬虫实训项目 12 ├── StrKit.cpp ├── StrKit.h ├── Thread.cpp ├── Thread.h ├── Url.cpp ├── Url.h ├── UrlFilter.h ├── UrlQueues.cpp ├── UrlQueues.h ├── WebCrawler.cpp └── WebCrawler.h 其中bin目录存放可执行程序文件、启动画面文件和配置文件,docs目录存放 项目文档,download目录存放爬虫下载的网页文件和图像文件,plugins目录 存放扩展插件的源代码和共享库文件,src目录存放项目主体部分的源代码文 件。 在教学环境下,以上目录结构可分别放在teacher和student两个子目录中。其 中teacher目录包含完整的程序源码和资料文档,以为学生开发时提供参考和借 鉴。 student目录中的源代码是不完整的,部分类或者函数的实现只给出了基 本框架,但代码中的注释和teacher目录下对应的部分完全相同,其中缺失的内 容,需要学生在理解整体设计思路和上下文逻辑的前提下予以补全。需要学生 参与补全的源代码文件详见开发计划。 5. 开发计划 本项目拟在四个工作日内完成: 工作日 模块 子模块 代码文件 第一天 基础设施 预编译头 Precompile Precompile.h 字符串工具包 StrKit StrKit.h StrKit.cpp 日志 Log Log.h Log.cpp 配置器 Configurator Configurator.h Configurator.cppWEBCRAWLER 网络爬虫实训项目 13 多路输入输出 MultiIo MultiIo.h MultiIo.cpp 插件接口 Plugin Plugin.h 插件管理器 PluginMngr PluginMngr.h PluginMngr.cpp 第二天 网络通信 哈希器 Hash Hash.h Hash.cpp 统一资源定位 符过滤器接口 UrlFilter UrlFilter.h 布隆过滤器 BloomFilter BloomFilter.h BloomFilter.cpp 原始统一资源定位符 RawUrl Url.h 解析统一资源定位符 DnsUrl Url.cpp 统一资源定位符队列 UrlQueues UrlQueues.h UrlQueues.cpp 套接字 Socket Socket.h Socket.cpp 超文本传输协 议响应包头 HttpHeader Http.h 超文本传输协议响应 HttpResponse 第三天 流程控制 线程 Thread Thread.h Thread.cpp 域名解析线程 DnsThread DnsThread.h DnsThread.cpp 发送线程 SendThread SendThread.h SendThread.cppWEBCRAWLER 网络爬虫实训项目 14 接收线程 RecvThread RecvThread.h RecvThread.cpp 网络爬虫 WebCrawler WebCrawler.h WebCrawler.cpp 主线程 main Main.cpp 构建脚本 Makefile Makefile 第四天 外围扩展 最大深度插件 MaxDepth MaxDepth.h MaxDepth.cpp MaxDepth.mak 域名限制插件 DomainLimit DomainLimit.h DomainLimit.cpp DomainLimit.mak 超文本传输协议响 应包头过滤器插件 HeaderFilter HeaderFilter.h HeaderFilter.cpp HeaderFilter.mak 超文本标记语言 文件存储插件 SaveHTMLToFile SaveHTMLToFile.h SaveHTMLToFile.cpp SaveHTMLToFile.mak 图像文件存储插件 SaveImageToFile SaveImageToFile.h SaveImageToFile.cpp SaveImageToFile.cpp 构建脚本 mkall mkall 其中被突出显示的代码文件中,包含需要学生添加的内容,注意源文件中形 如“// 此处添加代码”的注释。WEBCRAWLER 网络爬虫实训项目 15 6. 知识扩展 为了能在实训环节,进一步强化学生独立思考、独立解决问题的能力,本项目 有意涵盖了一些前期课程中不曾涉及或只作为一般性了解的知识和技巧。具体 包括:  预编译头文件  std::string  变长参数表  基于epoll的多路I/O  哈希算法和布隆表  URL、 DNS、 HTTP和HTML  正则表达式  线程封装  精灵进程和I/O重定向  Makefile 对于上述内容,建议项目指导教师根据学生的接受能力,结合项目中的具体应 用,在项目正式启动之前,先做概要性介绍,同时提供进一步详细学习和研究 的线索,包括man手册、参考书、网络链接或其它媒体资源,尽量让学生通过 自己的实践和探索找到解决问题的方法,这才是项目实训的意义所在!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值