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

目录

一. 业务处理模块的任务

二. 网络通信接口设计

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/plainhello 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;       // 服务器IPstd::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;       // 服务器IPstd::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,传出stringstatic 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-mtimestatic std::string GetETag(const BackupInfo &info){// etag: filename-fsize-mtimeFileUtil 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: bytesrsp.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-mtimestatic std::string GetETag(const BackupInfo &info){// etag: filename-fsize-mtimeFileUtil 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: bytesrsp.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-endfu.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,传出stringstatic 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-mtimestatic std::string GetETag(const BackupInfo &info){// etag: filename-fsize-mtimeFileUtil 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: bytesrsp.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-endfu.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;       // 服务器IPstd::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,发现是下面这个

    这个就很完美了

    本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
    如若转载,请注明出处:http://www.pswp.cn/web/78603.shtml
    繁体地址,请注明出处:http://hk.pswp.cn/web/78603.shtml

    如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

    相关文章

    基于SpringBoot的漫画网站设计与实现

    1.1项目研究的背景 困扰管理层的许多问题当中,漫画信息管理一定是不敢忽视的一块。但是管理好漫画网站又面临很多麻烦需要解决,如何在工作琐碎,记录繁多的情况下将漫画网站的当前情况反应给相关部门决策等等。在此情况下开发一款漫画网站&#xff0c;于是乎变得非常合乎时宜。…

    学习记录:DAY22

    假日尾声&#xff1a;技术进阶与自我反思 前言 于是&#xff0c;假日迎来了它的尾声&#xff0c;把快乐和焦躁都留存在昨天。 我只觉情感的自相矛盾在加重&#xff0c;学习让我焦躁&#xff0c;纵欲无法填补空虚&#xff0c;于是我的心被拖入了无止尽的拉扯中。 我还没有找到必…

    Oracle OCP认证考试考点详解083系列07

    题记&#xff1a; 本系列主要讲解Oracle OCP认证考试考点&#xff08;题目&#xff09;&#xff0c;适用于19C/21C,跟着学OCP考试必过。 31. 第31题&#xff1a; 题目 解析及答案&#xff1a; 从 Oracle 19c 开始&#xff0c;数据库配置助手&#xff08;DBCA&#xff09;在克…

    专业课复习笔记 4

    前言 实际上对于我的考研来说&#xff0c;最重要的两门就是数学和专业课。所以从今天开始&#xff0c;我尽可能多花时间学习数学和专业课。把里面的知识和逻辑关系理解清楚&#xff0c;把常考的内容练习透彻。就这样。 寻址方式 立即数寻址 操作数在指令里面直接提供了。 …

    Go小技巧易错点100例(三十)

    本期分享&#xff1a; 1.切片共享底层数组 2.获取Go函数的注释 切片共享底层数组 在Go语言中&#xff0c;切片和数组是两种不同的元素&#xff0c;但是切片的底层是数组&#xff0c;并且还有一个比较重要的机制&#xff1a;切片共享底层数组。 下面这段代码演示了切片&…

    反转字符串2

    reverse函数的用法(reverse一般是左闭右开区间)&#xff1a; 1.反转数组&#xff1a; int arr[] {1, 2, 3, 4, 5}; int n sizeof(arr) / sizeof(arr[0]); // 反转数组arr的全部元素 reverse(arr, arr n); 2.反转字符串&#xff1a; string str "he…

    企业可用免费软件 | 7-Zip,压缩率比 WinZip 高10%!

    7-Zip是一款出色的文件压缩和存档工具&#xff0c;但实际上许多小伙伴们并不了解。它是一款开源的免费软件&#xff0c;目前支持87种语言&#xff0c;适用于所有系统&#xff0c;软件操作界面也十分简洁&#xff0c;大部分代码都在GNU LGPL许可下。除了免费无广告的优点之外&am…

    Gradio全解20——Streaming:流式传输的多模态应用(1)——Mistral-7B实现流式传输音频:魔力8号球

    Gradio全解20——Streaming&#xff1a;流式传输的多模态应用&#xff08;1&#xff09;——Mistral-7B实现流式传输音频&#xff1a;魔力8号球 前言本篇摘要20. Streaming&#xff1a;流式传输的多模态应用20.1 Mistral-7B实现流式传输音频&#xff1a;魔力8号球20.1.1 工作原…

    Qt实现网页内嵌

    文章目录 一、环境准备 二、代码实现 三、测试 一、环境准备 首先&#xff0c;确保你的Qt安装包含了QtWebEngine模块。我的Qt是5.12.9并且使用MSVC来编译项目。在项目文件中需要添加以下配置&#xff0c;其中在Qt中配置MSVC&#xff0c;建议去看看这位大佬的博客&#xff1a…

    conda管理python环境

    其他文章 服务容错治理框架resilience4j&sentinel基础应用---微服务的限流/熔断/降级解决方案-CSDN博客 conda管理python环境-CSDN博客 快速搭建对象存储服务 - Minio&#xff0c;并解决临时地址暴露ip、短链接请求改变浏览器地址等问题-CSDN博客 大模型LLMs的MCP入门-…

    Android工厂模式

    前言 工厂模式是创建型模式&#xff0c;使我们常用/常见的模式之一。多用于需要生成复杂对象的地方。用new就可以完成创建的对象就无需使用。工厂模式降低了对象之间的耦合度&#xff0c;由于工厂模式依赖抽象的架构&#xff0c;实例化的任务交由子类去完成&#xff0c;所以有…

    【AI面试准备】数据驱动测试思维与实践指南

    面试题&#xff1a;数据驱动思维 构建测试数据集&#xff1a;收集代码覆盖率、缺陷历史等数据。 模型训练优化&#xff1a;使用Jupyter Notebook分析特征重要性。 数据驱动思维是一种以数据为核心、基于数据分析结果进行决策的方法论。它强调通过量化分析、模式识别和预测建模…

    内存碎片深度剖析

    目录 什么是内存碎片 内部碎片的解决 malloc STL二级空间配置器 外部碎片的解决 伙伴系统算法 slab分配器 什么是内存碎片 内存碎片是指在内存中存在的一些不连续的、较小的空闲内存块&#xff0c;这些小块内存由于太小而无法被有效地分配给程序使用&#xff0c;从而导…

    flutter 专题 六十一 支持上拉加载更多的自定义横向滑动表格

    在股票软件中&#xff0c;经常会看到如下所示的效果&#xff08;ps&#xff1a;由于公司数据敏感&#xff0c;所以使用另一个朋友的一个图&#xff09;。 分析需要后&#xff0c;我先在网上找了下支持横向滑动的组件&#xff0c;最后找到了这个&#xff1a;flutter_horizontal…

    0-1背包问题基础概念

    一、问题描述 给定一个容量为 W 的背包和 n 个物品。每个物品有一个重量 w[i] 和价值 v[i]。每个物品只能选或不选&#xff08;即“0-1”&#xff09;&#xff0c;求在不超过背包容量的前提下&#xff0c;所能获得的最大总价值。 输入&#xff1a; 背包容量 W&#xff08;in…

    使用 Semantic Kernel 快速对接国产大模型实战指南(DeepSeek/Qwen/GLM)

    文章目录 使用 Semantic Kernel 快速对接国产大模型实战指南&#xff08;DeepSeek/Qwen/GLM&#xff09;一、引言二、环境准备2.1 开发环境2.2 模型服务配置 三、核心代码实现3.1 会话代码封装3.2 CurModelContext封装3.3 DeepSeek对接示例3.4 Qwen对接示例3.5 GLM对接示例 四、…

    Ai时代,运维人如何转型

    在AI时代,传统运维向智能运维(AIOps)的转型需要系统性重塑,以下是深度拆解的转型路线图和关键实施要素: 一、认知升级范式转变 1. 演进路线模型(三阶段) 被动响应阶段:人工巡检(→监控覆盖率<30%)主动防御阶段:规则引擎(→告警准确率70%~85%)预测自治阶段:深…

    windows鼠标按键自定义任意设置

    因为用惯了Linux的鼠标中键的复制黏贴&#xff0c;发现windows下有完全可以实现类似自定义功能的软件&#xff0c;推荐一下&#xff1a; X Mouse Button Control。 免费版足够好用。 软件简介&#xff1a; X Mouse Button Control是一款专业的重新映射鼠标按钮的软件工具&…

    怎么看户型好不好?

    看房型好不好可从以下方面判断&#xff1a; 空间布局 方正性&#xff1a;户型方正为佳 &#xff0c;此时进深与开间比例在1:1.5左右。方正户型空间利用率高&#xff0c;无采光死角。如手枪型、锯齿型等异形户型&#xff0c;易有拐角、长过道&#xff0c;空间浪费大。动静分区…

    基于WOA鲸鱼优化TCN-BiGRU注意力机制网络模型的时间序列预测算法matlab仿真

    目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a/matlab2024b 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频…