【云备份】服务端业务处理模块设计与实现

目录

一. 业务处理模块的任务

二. 网络通信接口设计

2.1.文件上传

2.2.展示页面获取

2.3.文件下载

三.业务处理类设计

3.1.业务处理类的代码框架编写 

3.2.文件上传代码编写

3.3.展示页面的获取代码编写

3.4.文件下载代码编写——下载篇

3.4.文件下载代码编写——断点续传篇

四.服务端功能联调


一. 业务处理模块的任务

云备份项目中,业务处理模块是针对客户端的业务请求进行处理,并最终给与响应。

而整个过程中包含以下要实现的功能:

  1. 借助网络通信模块httplib库搭建http服务器与客户端进行网络通信
  2. 针对收到的请求进行对应的业务处理并进行响应(文件上传,列表查看,文件下载(包含断点续传))

仔细一看,就像是将网络通信模块和业务处理模块进行了合并

因为我们可以借助httplib库快速完成完成http服务器的搭建,所以干脆就和业务处理模块合并到一起

服务端业务管理模块的基本任务就是下面这些

一. 搭建网络通信服务器:借助httplib库完成;

二. 业务处理请求:

  1. 文件上传请求:客户端上传需要备份的文件——服务端响应上传文件成功;
  2. 文件列表请求:客户端浏览器请求一个备份文件的展示页面——服务端响应该页面;
  3. 文件下载请求:客户端通过展示页面,点击下载文件——服务端响应客户端要下载的文件数据。

二. 网络通信接口设计

什么叫网络通信接口设计?

业务处理模块要对客户端的请求进行处理,那么我们就需要提前定义好客户端与服务端的通信,明确客户端发送什么样的请求,服务端处理后应该给与什么样的响应,而这就是网络通信接口的设计.

我们的客户端请求就是只有文件上传,展示页面,文件下载这3种

2.1.文件上传

我们看个文件上传的例子,我们从客户端上传一个a.txt,它的内容就是hello world,我们在服务端收到的http请求报文就是下面这样子的

POST /upload HTTP/1.1
Content-Type:multipart/form-data;boundary= ----WebKitFormBoundary+16字节随机字符

....此处省略

------WebKitFormBoundary
Content-Disposition:form-data;name="file";filename="a.txt";
Content-Type:text/plain

hello world
------WebKitFormBoundary--

我们怎么获取正文内容——hello world呢?我们是不是看到了3个------WebKitFormBoundary啊?

我们就根据这个来分割报文,就能获得正文。

但事实上,这个活不用我们来干,httplib库会帮我们干好,这里只是为了帮助大家理解而已。

所以最重要的部分其实就是下面这句

POST /upload HTTP/1.1

当服务器收到一个POST方法的/upload请求,则我们认为这是一个文件上传请求,我们就应该解析请求,获得文件数据,将数据写进文件里面,这个文件就备份成功了。

成功之后,我们还需要进行响应,这个很简单,往客户端返回下面这个即可(先别想太多,目前就只考虑成功的情况)

HTTP/1.1 200 OK
Content-Length: 0

2.2.展示页面获取

客户端往服务端发生下面这种请求时

GET /list HTTP/1.1
Content-Length: 0

我们只关心请求方法GET和资源路径/list。

这个时候服务器就应该返回一个html界面,来展示已经上传的文件。

这个时候,我们服务器返回的响应报文就应该类似于是下面这样子的

HTTP/1.1 200 OK
Content-Length:
Content-Type: text/html
...
<html>
	这里是html界面
</html>

我知道大家可能不太了解html,不了解也没有关心,我们完全可以去别人的官网上面看看,它们是怎么实现的,然后随便复制一些下来就行,我这里就简单的copy了一些,就是为了让大家简单的看看一下我们大概的html的界面

<html>
	<head>
	 	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<title>Page of Download</title>
	</head>
	<body>
		<h1>Download</h1>
		<table>
			<tr>
				 <td><a href="/download/a.txt"> a.txt </a></td>
				 <td align="right"> 1994-07-08 03:00 </td>
				 <td align="right"> 27K </td>
			</tr>
		</table>
	</body>
</html>

我们可以把这个放到记事本里面去,

然后修改文件后缀名为html即可

然后我们打开就是下面这样子

我们点击a.txt就能实现下载,我们后面的html界面就基于上面这个界面改就OK了,不要把大量时间放到前端设计上。

2.3.文件下载

当客户端发来下面这种请求报文,就是想要下载文件了

GET /download/a.txt http/1.1
Content-Length: 0

我们是需要关注GET和/download两个东西的

我们服务端如何去响应呢?

大概是下面这样子

HTTP/1.1 200 OK
Content-Length: 文件长度

正文就是文件数据

至于断点续传,我们要等到我们基本功能都设计完成了之后,我们再来讲 

三.业务处理类设计

我们这里是需要借助httplib库的,如果说大家对httplib库还是不太了解的,可以去:【云备份】httplib库-CSDN博客

3.1.业务处理类的代码框架编写 

我们创建一个service.hpp来编写我们的业务处理的代码

然后,我们先把我们的这个业务处理类的框架写出来

service.hpp

#ifndef __MY_SERVICE__
#define __MY_SERVICE__

#include "data.hpp" //我们上传文件,需要把数据放进去,需要数据管理模块
#include"httplib.h"

extern cloud::DataManager *_data;//全局的数据管理模块
namespace cloud
{
    class Service
    {
    public:
        Service()//初始化
        {
            //我们那些东西都是从配置文件里面获取的,这些东西配置文件都有
            Config* config = Config::GetInstance();
            _server_port = config->GetServerPort();
            _server_ip = config->GetSeverIp();
            _download_prefix = config->GetDownloadPrefix();
        }
        bool RunModule() // 主逻辑执行函数——搭建服务器
        {
            //这个就是httplib库的使用了
            _server.Post("/upload", Upload);//文件上传——对于POST方法和/upload的请求
            _server.Get("/listshow", ListShow);//文件列表请求
            //注意一件事情,当我们在浏览器输入12.34.56.78:9090,浏览器默认会在后面加一个/,也就是12.34.56.78:9090/
            _server.Get("/", ListShow);//文件列表请求
            _server.Get("/download/(.*)", Download);//文件下载——(.*)是正则表达式,可以匹配任意一个字符串,不会的去网上搜索一下
            _server.listen(_server_ip.c_str(), _server_port);//一定要监听服务器的端口
            return true; 
        }
    private:
        // 文件上传请求处理函数
        static void Upload(const httplib::Request &req, httplib::Response &rsp);
        // 展示页面请求处理函数
        static void ListShow(const httplib::Request &req, httplib::Response &rsp);
        // 文件下载请求处理函数
        static void Download(const httplib::Request &req, httplib::Response &rsp);

    private:
        int _server_port;             // 服务器端口
        std::string _server_ip;       // 服务器IP
        std::string _download_prefix; // 文件下载请求前缀
        httplib::Server _server;      // Server类对象用于搭建服务器
    };

}
#endif

这里使用了正则表达式,大家可以去网上搜索一下即可

结构解析

  1. ()
    表示一个捕获组(capture group),用于提取匹配的内容。例如,匹配到的文本可以被后续代码引用(如 $1 或 \1)。

  2. .
    匹配任意单个字符(默认不包括换行符 \n,除非开启单行模式)。

  3. *
    表示前面的元素(此处是.)可以出现 0 次或多次(贪婪匹配,尽可能多匹配)。

整体行为

  • (.*) 会匹配任意长度的字符串(包括空字符串),并将其捕获到第一个分组中。

  • 如果用于全局匹配(如 /g 标志),它会从当前位置匹配到行尾(或符合后续模式的位置)。

3.2.文件上传代码编写

#ifndef __MY_SERVICE__
#define __MY_SERVICE__

#include "data.hpp" //我们上传文件,需要把数据放进去,需要数据管理模块
#include "httplib.h"

extern cloud::DataManager *_data;//全局的数据管理模块
namespace cloud
{
    class Service
    {
    public:
        Service()//初始化
        {
            //我们那些东西都是从配置文件里面获取的,这些东西配置文件都有
            Config* config = Config::GetInstance();
            _server_port = config->GetServerPort();
            _server_ip = config->GetSeverIp();
            _download_prefix = config->GetDownloadPrefix();
        }
        bool RunModule() // 主逻辑执行函数——搭建服务器
        {
            //这个就是httplib库的使用了
            //对于12.34.56.78:9090/upload,我们就上传文件
            _server.Post("/upload", Upload);//文件上传——对于POST方法和/upload的请求

            //对于12.34.56.78:9090/listshow,我们就返回一个html界面
            _server.Get("/listshow", ListShow);//文件列表请求
            //注意一件事情,当我们在浏览器输入12.34.56.78:9090,浏览器默认会在后面加一个/,也就是12.34.56.78:9090/
            _server.Get("/", ListShow);//文件列表请求

            _server.Get("/download/(.*)", Download);//文件下载——(.*)是正则表达式,可以匹配任意一个字符串,不会的去网上搜索一下

            _server.listen(_server_ip.c_str(), _server_port);//一定要监听服务器的端口
            return true; 
        }
    private:
        // 文件上传请求处理函数
        static void Upload(const httplib::Request &req, httplib::Response &rsp)
        {
            //只有请求方法是POST,url是/upload时,才能进行文件上传
            //不过要注意的是,客户端发来的http请求报文里面的正文内容并不全是数据,但是全部数据都在正文里面
            //这个实现很复杂,所以我们借助httplib库
            auto ret=req.has_file("file");//用于检查HTTP请求中是否包含名为 "file" 的文件上传字段。
            //req.has_file("file") 中的 "file" 参数是客户端上传文件时使用的字段名称(即 HTML 表单中 <input type="file"> 的 name` 属性),并非固定死的。
            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);//文件管理类
            fu.SetContent(file.content); // 将数据写入文件中
            
            BackupInfo info;//数据管理模块
            info.NewBackupInfo(realpath); // 组织备份的文件信息

            _data->Insert(info); // 向全局的数据管理模块添加备份的文件信息
            return;
        }
        // 展示页面请求处理函数
        static void ListShow(const httplib::Request &req, httplib::Response &rsp){}
        // 文件下载请求处理函数
        static void Download(const httplib::Request &req, httplib::Response &rsp){}

    private:
        int _server_port;             // 服务器端口
        std::string _server_ip;       // 服务器IP
        std::string _download_prefix; // 文件下载请求前缀
        httplib::Server _server;      // Server类对象用于搭建服务器
    };

}
#endif

首先我们需要将我们当前目录下的packdir和backdir目录里面的文件清理干净,然后还有删除cloud.dat,此外,我们还需要将httplib.h拷贝到当前目录来

cp cpp-httplib/httplib.h .

接着我们编写测试函数

#include "util.hpp"
#include "conf.hpp"
#include "data.hpp"
#include"hot.hpp"
#include"service.hpp"

cloud::DataManager *_data;//全局的数据管理模块
void Servicetest()
{
	cloud::Service svr;
	svr.RunModule();
}
int main(int argc, char *argv[])
{
	Servicetest();
}

编译即可

接着我们可以叫deepseek来生成一个html界面

<!DOCTYPE html>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
    <form action="http://117.72.80.239:9090/upload" method="post" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit" value="上传">
    </form>
</body>
</html>

这里的主机名和端口号一定要填写我们服务器的ip和那个端口号

然后我们创建一个html界面即可

我们再创建一个www.txt,里面只写了一句话yunbeifen,等会我们就把这个文件上传到我们的服务器里面去

确保服务器防火墙和安全组的9090端口都开放了。

然后我们打开我们创建的那个html文件,点击上传,这一步是最关键的。

点击上传之后,会显示出下面这个界面

我们回我们的服务器上看一下

很好,我们成功了

3.3.展示页面的获取代码编写

这个的过程其实也挺简单的

  • 1.获取所有文件的备份信息——>都存放在cloud.dat里面
  • 2.根据所有备份信息,组织html文件数据

cloud.dat

注意:我上传了两次www.txt。

此外我们的html界面应该是和下面类似的

<html>
	<head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<title>Download</title>
	</head>
	<body>
		<h1>Download</h1>
		<table>
			<tr>
				 <td><a href="/download/a.txt"> a.txt </a></td>
				 <td align="right"> 1994-07-08 03:00 </td>
				 <td align="right"> 27K </td>
			</tr>
		</table>
	</body>
</html>

实际上我们可以把它写成一行,展示的效果是一样的

<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>Page of Download</title></head><body><h1>Download</h1><table><tr><td><a href="/download/a.txt"> a.txt </a></td><td align="right"> 1994-07-08 03:00 </td><td align="right"> 27K </td></tr></table></body></html>

那我们的代码就很好写了 

注意:下面这行应该交给httlib库来写,而不能直接写进我们的html文件里面

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

所以我们真正写进我们的html文件里面的是

<html>
	<head>
		<title>Download</title>
	</head>
	<body>
		<h1>Download</h1>
		<table>
			<tr>
				 <td><a href="/download/a.txt"> a.txt </a></td>
				 <td align="right"> 1994-07-08 03:00 </td>
				 <td align="right"> 27K </td>
			</tr>
		</table>
	</body>
</html>

好,我们的代码如下

service.hpp 

//传入time_t,传出string
        static std::string TimetoStr(time_t t)
        {
            std::string tmp = std::ctime(&t);
            return tmp;
        }
        // 展示页面请求处理函数
        static void ListShow(const httplib::Request &req, httplib::Response &rsp) 
        {
            // 1.获取所有文件的备份信息
            std::vector<BackupInfo> array;
            _data->GetAll(&array);//通过全局数据
            // 2.根据所有备份信息,组织html文件数据
            std::stringstream ss;
            
            ss << "<html><head><title>Download</title></head>";
            ss << "<body><h1>Download</h1><table>";
            for(auto &a : array)
            {
                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 << "</table></body></html>";
            rsp.body = ss.str();
            rsp.set_header("Content-Type", "text/html");
            rsp.status = 200;
        }

现在我们来测试一下

cloud.cc

#include "util.hpp"
#include "conf.hpp"
#include "data.hpp"
#include"hot.hpp"
#include"service.hpp"

cloud::DataManager *_data;//全局的数据管理模块
void Servicetest()
{
	cloud::Service svr;
	svr.RunModule();
}
int main(int argc, char *argv[])
{
	Servicetest();
}

我们编译运行

我们现在就看到了我们之前上传的文件

现在我们再次上传一个文件看看

是不是挺顺利的啊?

3.4.文件下载代码编写——下载篇

首先我们这里不搞断点续传,先把下载的基本功能搞定了再说

这里需要对上面我们设计的文件下载过程的内容进行补充:

  • Etag字段

HTTP的Etag字段是标识文件的唯一标识

其中客户端第一次下载文件的时候,服务器自动生成能唯一标识文件的Etag首部字段,然后将这个Etag字段加入到HTTP响应信息返还给客户端。

第二次下载的时候客户端就会把Etag首部字段发给服务器,而服务器则根据这个Etag判断这个资源有没有被修改过(即Etag值不同),如果没有被修改过,直接使用原先缓存的数据,不用重新下载。

事实上,这个Etag是什么东西,HTTP协议并没有说明,只需要服务端和客户端都认识即可。

为了方便,我们将Etag设置为“文件名-文件大小-最后一次的修改时间”,这样子也能保证能标识唯一一个文件。

而Etag不仅仅是缓存用的到,还有就是后面的断点续传的实现也能用到。

断点续传也需要保证文件没有被修改过。事实上我们可以看看《图解HTTP》上是怎么描述这个字段的

我们看看《图解HTTP》是怎么描述这个字段的。

ETag: "82e22293907ce725faf67773957acd12"

首部字段 ETag 能告知客户端实体标识。

它是一种可将资源以字符串 形式做唯一性标识的方式。

服务器会为每份资源分配对应的 ETag 值。 

另外,当资源更新时,ETag 值也需要更新。

生成 ETag 值时,并没有 统一的算法规则,而仅仅是由服务器来分配。

资源被缓存时,就会被分配唯一性标识。

例如,当使用中文版的浏览 器访问 http://www.google.com/ 时,就会返回中文版对应的资源,而 使用英文版的浏览器访问时,则会返回英文版对应的资源。

两者的 URI 是相同的,所以仅凭 URI 指定缓存的资源是相当困难的。

若在下 载过程中出现连接中断、再连接的情况,都会依照 ETag 值来指定资 源。

资源被缓存时,就会被分配唯一性标识。

例如,当使用中文版的浏览 器访问 http://www.google.com/ 时,就会返回中文版对应的资源,而 使用英文版的浏览器访问时,则会返回英文版对应的资源。

两者的 URI 是相同的,所以仅凭 URI 指定缓存的资源是相当困难的。

若在下 载过程中出现连接中断、再连接的情况,都会依照 ETag 值来指定资 源。


  • Accept-Ranges字段

这个用于告诉客户端,我服务器支持断点续传,并且数据单位以字节为单位。

也就是说,我们的服务端返回的HTTP响应报文应该是下面这样子的

HTTP/1.1 200 OK
Content-Length: 100000
ETag: "一个能够唯一标识文件的数据"
Accept-Ranges: bytes
文件数据

我们看看《图解HTTP》是怎么描述的

 

图:当不能处理范围请求时,Accept-Ranges: none

Accept-Ranges: bytes

首部字段 Accept-Ranges 是用来告知客户端服务器是否能处理范围请 求,以指定获取服务器端某个部分的资源。

可指定的字段值有两种,可处理范围请求时指定其为 bytes,反之则 指定其为 none。


我们写出的代码如下:

service.hpp

// 生成Etag字段:filename-size-mtime
        static std::string GetETag(const BackupInfo &info)
        {
            // etag: filename-fsize-mtime
            FileUtil fu(info.real_path);
            std::string etag = fu.FileName();
            etag += '-';
            etag += std::to_string(info.fsize);
            etag += '-';
            etag += std::to_string(info.mtime);
            return etag;
        }
        // 文件下载请求处理函数
        static void Download(const httplib::Request &req, httplib::Response &rsp)
        {
            // 1.获取客户端请求的路径资源,如果被压缩,要先解压缩
            // 2.根据资源路径,获取文件备份信息
            BackupInfo info;
            _data->GetOneByURL(req.path, &info);
            // 3.判断文件是否被压缩,如果被压缩,要先解压缩
            if (info.pack_flag == true)
            {
                FileUtil fu(info.pack_path);
                fu.UnCompress(info.real_path); // 将文件解压到备份目录下
                // 4.删除压缩包,修改备份信息(已经没有被压缩)
                fu.Remove();
                info.pack_flag = false;
                _data->Updata(info);
            }

            FileUtil fu(info.real_path);
            fu.GetContent(&rsp.body);//读取文件,把文件内容放到rsp.body里面去
            // 5.设置相应头部字段:Etag, Accept-Ranges: bytes
            rsp.set_header("Accept-Ranges", "bytes");
            rsp.set_header("ETag", GetETag(info));
            rsp.status = 200;
        }

我们随机点击那个蓝色的,可是我们点进去,它却不是给我们下载,而是直接给我们看这个文件里面有什么

这个时候我就需要讲讲这个Content-Type字段的重要性了

  • Content-Type
Content-Type: text/html; charset=UTF-8

首部字段 Content-Type 说明了实体主体内对象的媒体类型,字段值用 type/subtype 形式赋值

Content-Type决定了浏览器怎么处理这个数据。我们并没有设置Content-Type,所以我们就需要去设置一下,我们在原代码上面加上这一句

rsp.set_header("Content-Type", "application/octet-stream");

"application/octet-stream" 是一种通用的MIME类型,表示二进制数据流

现在我们编译运行

回到下面这个界面,随便点击一个蓝色的

 

点击之后,立马下载了

打开一看

那我们怎么判断这个hhhh.txt文件和我们之前上传的文件是一样的呢?

我们借助md5这个工具就行,我们打开powershell

Get-FileHash -Path "文件路径" -Algorithm MD5

 我们发现就是一模一样的。

3.4.文件下载代码编写——断点续传篇

先来理解一下这个断点续传的原理是什么

在文件下载过程中,因为某种异常而中断,如果再次进行从头下载,那么效率比较低下,因为需要将之前已经传输给的数据再次传输一遍。

因此断点续传就是从上次下载断开的位置,重新下载即可,之前已经传输过的数据将不再进行重新传输

实现思想:

客户端在下载文件的时候,需要每次接收到数据写入文件后记录自己当前下载的数据量,当异常下载中断时,下次断点续传的时候,只需将要重新下载的数据区间(下载起始位置,下载结束位置)告诉服务器,这个时候服务器只需要回传客户端需要的区间数据即可。

需要考虑的一个问题:

如果上次下载文件之后,这个文件在服务器上被修改了,那这个时候不能断点续传,必须重新下载整个文件

主要关键点就是

  1. 客户端能告诉服务器,文件下载区间范围
  2. 服务器能够检测上一次下载这个文件后,这个文件是否被修改过

那HTTP是怎么实现断点续传的呢?

首先HTTP有一个Accept-Ranges字段,这个用于告诉客户端,我服务器支持断点续传,并且数据单位以字节为单位。

其次服务器会发给客户端一个Etag值,客户端会保存起来。

接着断点续传的时候,客户端会把上次下载时服务端发来的Etag值发回给这个服务端,这个时候服务端就会根据这个Etag值来判断这个要下载的文件在上次下载之后有没有被修改过。

此外在断点续传的时候,客户端发给服务器的HTTP请求报文里面还会包含下面两个首部字段

If-Range字段和 Range字段


  • If-Range字段

这个字段是客户端发给服务器的,不是服务器发给客户端的!!!

 首部字段 If-Range 属于附带条件之一。

它告知服务器若指定的 If Range 字段值(ETag 值或者时间)和请求资源的 ETag 值或时间相一 致时,则作为范围请求处理。

反之,则返回全体资源。 

  •  Range
Range: bytes=5001-10000

对于只需获取部分资源的范围请求,包含首部字段 Range 即可告知服 务器资源的指定范围。

上面的示例表示请求获取从第 5001 字节至第 10000 字节的资源。

接收到附带 Range 首部字段请求的服务器,会在处理请求之后返回状 态码为 206 Partial Content 的响应。

无法处理该范围请求时,则会返 回状态码 200 OK 的响应及全部资源。

也就是说,客户端发来的HTTP请求一般就会包含下面这些字段

GET /download/a.txt http/1.1
Content-Length:0
If-Range:“文件唯一标识"
Range:bytes=89-999

对于断点续传,除了客户端发来的HTTP报文的首部字段有点不同,我们服务端的HTTP响应也是有一点不同的

我们HTTP响应需要添加Content-Range字段

  • Content-Range


针对范围请求,返回响应时使用的首部字段 Content-Range,能告知客 户端作为响应返回的实体的哪个部分符合范围请求。

字段值以字节为 单位,表示当前发送部分及整个实体大小。

Content-Range: bytes 5001-10000/10000
  • bytes 5001-10000:表示当前响应中返回的数据是资源的 第 5001 字节到第 10000 字节(闭区间)。
  • /10000:表示资源的总大小为 10000 字节。

 除此之外,如果断点续传成功之后,我们的服务器需要返回206状态码

  • 206状态码

该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求。

响应报文中包含由 Content-Range 指定范围的实体内容。

也就是说我们的服务端的HTTP响应报文应该包含下面这些字段

HTTP/1.1 206 Partial content
Content-Length:
content-Range:bytes 89-999/100000
Content-Type:application/octet-stream
ETag:"inode-size-mtime一个能够唯一标识文件的数据
Accept-Ranges:bytes

好了,我们现在就来实现我们的断点续传

// 生成Etag字段:filename-size-mtime
        static std::string GetETag(const BackupInfo &info)
        {
            // etag: filename-fsize-mtime
            FileUtil fu(info.real_path);
            std::string etag = fu.FileName();
            etag += '-';
            etag += std::to_string(info.fsize);
            etag += '-';
            etag += std::to_string(info.mtime);
            return etag;
        }
        // 文件下载请求处理函数
        static void Download(const httplib::Request &req, httplib::Response &rsp)
        {
            // 1.获取客户端请求的路径资源,如果被压缩,要先解压缩
            // 2.根据资源路径,获取文件备份信息
            BackupInfo info;
            _data->GetOneByURL(req.path, &info);
            // 3.判断文件是否被压缩,如果被压缩,要先解压缩
            if(info.pack_flag == true)
            {
                FileUtil fu(info.pack_path);
                fu.UnCompress(info.real_path); // 将文件解压到备份目录下
                // 4.删除压缩包,修改备份信息(已经没有被压缩)
                fu.Remove();
                info.pack_flag = false;
                _data->Updata(info);
            }
            //现在需要来判断有没有断点续传这个需要
            bool retrans = false;//这个表示我们需不需要断点续传
            std::string old_etag;
            if(req.has_header("If-Range"))//如果客户端发来的报文里面有If-Range这个头部字段,表示客户端在请求断点续传
            {
                old_etag = req.get_header_value("If-Range");//获取If-Range的值——Etag值
                // 有If-Range字段且这个字段的值与请求文件的最新Etag一致则符合断点续传,
                //不一致则表示在上一次下载之后这个文件没有被修改过,可以进行断点续传
                if(old_etag == GetETag(info))
                {
                    retrans = true;
                }
            }
            // 如果没有If-Range字段则是正常下载,或者如果有这个字段,但是
            // 它的值与当前文件的etag不一致,则必须重新返回全部数据
            // 5.读取文件数据,放入rsp.body中
            FileUtil fu(info.real_path);
            if(retrans == false)//客户端没有断点续传的需求或者在上一次下载之后这个文件被修改过,那不进行断点续传
            {
                fu.GetContent(&rsp.body);
                // 6.设置相应头部字段:Etag, Accept-Ranges: bytes
                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("ETag", GetETag(info));
                rsp.set_header("Content-Type", "application/octet-stream");
                rsp.status = 200;
            }
            else//断点续传
            {
                // httplib库内部实现了对于区间请求也就是断点续传请求的处理
                // 只需要我们用户将文件所有数据读取到rsp.body中,它内部会自动根据请求区间
                // 从body中取出指定区间数据进行响应
                // 也就是说,我们不需要写std::string range = req.get_header_value("Range"); bytes=starts-end
                fu.GetContent(&rsp.body);
                rsp.set_header("Accept-Ranges", "bytes");
                rsp.set_header("ETag", GetETag(info));
                // rsq.set_header("Content-Range", "bytes start-end/fsize");//这个httplib库实现了,我们就不写了
                rsp.status = 206;
            }
        }

我们编译运行一下

 

我们打开我们上传的那个界面:

我们上传一个比较大的文件,然后在客户端下载文件过程中,我们关闭服务器

注意文件名字不要有中文,要不然就会出现下面这个

 

上面那些乱码的文件都是我的实验品,请忽略

我们点击第一个即可,点完之后立马关闭服务器

接着我们立马关闭服务器

我们点击恢复,我们发现是从上次下载的位置继续下载的

这就是我们的断点续传。

接着我们看看这两个文件是不是一样的啊

    是一样的啊!!!


     好了啊,在这里,我们就把源码给你们

    service.hpp

    #ifndef __MY_SERVICE__
    #define __MY_SERVICE__
    
    #include "data.hpp" //我们上传文件,需要把数据放进去,需要数据管理模块
    #include "httplib.h"
    
    extern cloud::DataManager *_data; // 全局的数据管理模块
    namespace cloud
    {
        class Service
        {
        public:
            Service() // 初始化
            {
                // 我们那些东西都是从配置文件里面获取的,这些东西配置文件都有
                Config *config = Config::GetInstance();
                _server_port = config->GetServerPort();
                _server_ip = config->GetSeverIp();
                _download_prefix = config->GetDownloadPrefix();
            }
            bool RunModule() // 主逻辑执行函数——搭建服务器
            {
                // 这个就是httplib库的使用了
                // 对于12.34.56.78:9090/upload,我们就上传文件
                _server.Post("/upload", Upload); // 文件上传——对于POST方法和/upload的请求
    
                // 对于12.34.56.78:9090/listshow,我们就返回一个html界面
                _server.Get("/listshow", ListShow); // 文件列表请求
                // 注意一件事情,当我们在浏览器输入12.34.56.78:9090,浏览器默认会在后面加一个/,也就是12.34.56.78:9090/
                _server.Get("/", ListShow); // 文件列表请求
    
                _server.Get("/download/(.*)", Download); // 文件下载——(.*)是正则表达式,可以匹配任意一个字符串,不会的去网上搜索一下
    
                _server.listen(_server_ip.c_str(), _server_port); // 一定要监听服务器的端口
    
                // 检查服务器是否成功启动
                if (!_server.listen(_server_ip.c_str(), _server_port))
                {
                    std::cerr << "服务器启动失败!" << std::endl;
                    return false;
                }
                return true;
            }
    
        private:
            // 文件上传请求处理函数
            static void Upload(const httplib::Request &req, httplib::Response &rsp)
            {
                // 只有请求方法是POST,url是/upload时,才能进行文件上传
                // 不过要注意的是,客户端发来的http请求报文里面的正文内容并不全是数据,但是全部数据都在正文里面
                // 这个实现很复杂,所以我们借助httplib库
                auto ret = req.has_file("file"); // 用于检查HTTP请求中是否包含名为 "file" 的文件上传字段。
                // req.has_file("file") 中的 "file" 参数是客户端上传文件时使用的字段名称(即 HTML 表单中 <input type="file"> 的 name` 属性),并非固定死的。
                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);       // 文件管理类
                fu.SetContent(file.content); // 将数据写入文件中
    
                BackupInfo info;              // 数据管理模块
                info.NewBackupInfo(realpath); // 组织备份的文件信息
    
                _data->Insert(info); // 向全局的数据管理模块添加备份的文件信息
                return;
            }
            // 传入time_t,传出string
            static std::string TimetoStr(time_t t)
            {
                std::string tmp = std::ctime(&t);
                return tmp;
            }
            // 展示页面请求处理函数
            static void ListShow(const httplib::Request &req, httplib::Response &rsp)
            {
                // 1.获取所有文件的备份信息
                std::vector<BackupInfo> array;
                _data->GetAll(&array); // 通过全局数据
                // 2.根据所有备份信息,组织html文件数据
                std::stringstream ss;
    
                ss << "<html><head><title>Download</title></head>";
                ss << "<body><h1>Download</h1><table>";
                for (auto &a : array)
                {
                    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 << "</table></body></html>";
                rsp.body = ss.str();
                rsp.set_header("Content-Type", "text/html");
                rsp.status = 200;
            }
            // 生成Etag字段:filename-size-mtime
            static std::string GetETag(const BackupInfo &info)
            {
                // etag: filename-fsize-mtime
                FileUtil fu(info.real_path);
                std::string etag = fu.FileName();
                etag += '-';
                etag += std::to_string(info.fsize);
                etag += '-';
                etag += std::to_string(info.mtime);
                return etag;
            }
            // 文件下载请求处理函数
            static void Download(const httplib::Request &req, httplib::Response &rsp)
            {
                // 1.获取客户端请求的路径资源,如果被压缩,要先解压缩
                // 2.根据资源路径,获取文件备份信息
                BackupInfo info;
                _data->GetOneByURL(req.path, &info);
                // 3.判断文件是否被压缩,如果被压缩,要先解压缩
                if(info.pack_flag == true)
                {
                    FileUtil fu(info.pack_path);
                    fu.UnCompress(info.real_path); // 将文件解压到备份目录下
                    // 4.删除压缩包,修改备份信息(已经没有被压缩)
                    fu.Remove();
                    info.pack_flag = false;
                    _data->Updata(info);
                }
                //现在需要来判断有没有断点续传这个需要
                bool retrans = false;//这个表示我们需不需要断点续传
                std::string old_etag;
                if(req.has_header("If-Range"))//如果客户端发来的报文里面有If-Range这个头部字段,表示客户端在请求断点续传
                {
                    old_etag = req.get_header_value("If-Range");//获取If-Range的值——Etag值
                    // 有If-Range字段且这个字段的值与请求文件的最新Etag一致则符合断点续传,
                    //不一致则表示在上一次下载之后这个文件没有被修改过,可以进行断点续传
                    if(old_etag == GetETag(info))
                    {
                        retrans = true;
                    }
                }
                // 如果没有If-Range字段则是正常下载,或者如果有这个字段,但是
                // 它的值与当前文件的etag不一致,则必须重新返回全部数据
                // 5.读取文件数据,放入rsp.body中
                FileUtil fu(info.real_path);
                if(retrans == false)//客户端没有断点续传的需求或者在上一次下载之后这个文件被修改过,那不进行断点续传
                {
                    fu.GetContent(&rsp.body);
                    // 6.设置相应头部字段:Etag, Accept-Ranges: bytes
                    rsp.set_header("Accept-Ranges", "bytes");
                    rsp.set_header("ETag", GetETag(info));
                    rsp.set_header("Content-Type", "application/octet-stream");
                    rsp.status = 200;
                }
                else//断点续传
                {
                    // httplib库内部实现了对于区间请求也就是断点续传请求的处理
                    // 只需要我们用户将文件所有数据读取到rsp.body中,它内部会自动根据请求区间
                    // 从body中取出指定区间数据进行响应
                    // 也就是说,我们不需要写std::string range = req.get_header_value("Range"); bytes=starts-end
                    fu.GetContent(&rsp.body);
                    rsp.set_header("Accept-Ranges", "bytes");
                    rsp.set_header("ETag", GetETag(info));
                    // rsq.set_header("Content-Range", "bytes start-end/fsize");//这个httplib库实现了,我们就不写了
                    rsp.status = 206;
                }
            }
    
        private:
            int _server_port;             // 服务器端口
            std::string _server_ip;       // 服务器IP
            std::string _download_prefix; // 文件下载请求前缀
            httplib::Server _server;      // Server类对象用于搭建服务器
        };
    
    }
    #endif

    我们git一下

     

    四.服务端功能联调

     到这里我们的服务端就算是写完了。我们得让这个服务器运行起来

    我们这个热点管理模块是一个死循环,我们的业务处理模块也是一个死循环

    两个死循环,那我们只能使用多线程了

    cloud.cc

    #include "util.hpp"
    #include "conf.hpp"
    #include "data.hpp"
    #include"hot.hpp"
    #include"service.hpp"
    #include<thread>
    
    cloud::DataManager *_data;//全局的数据管理模块
    void Servicetest()
    {
    	cloud::Service svr;
    	svr.RunModule();
    }
    void HotTest()
    {
    	_data=new cloud::DataManager();
    	cloud::HotManager hot;
    	hot.RunModule();
     
    }
    int main(int argc, char *argv[])
    {
    	_data=new cloud::DataManager();
    	//C++多线程模块
    	std::thread thread_hot_manager(HotTest);
    	std::thread thread_service(Servicetest);
    	//等待线程退出
    	thread_hot_manager.join();
    	thread_service.join();
    	Servicetest();
    
    }
    

     

    我们打开这个网站

    我们上传一个文件上去

    这个时候我们等待30s,这个是我们的热点管理时间

    等待30s后,我们发现下面这个情况

    这个时候展示界面是没有变化的

    这个时候我们点击下载,发现是下面这种情况

    这个时候我们再等30s,发现是下面这个

    这个就很完美了

    评论 1
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值