需求分析

选择的项目是下载器设计,内容是实现一个支持主流下载协议(https,ftp等协议)的下载器,需要支持多线程下载,断点下载,垃圾回收,磁盘检测等基本机制,以及可用性,得有一个UI。根据以上软件需求,结合自己已有的技术栈,打算使用QT实现UI部分的设计,然后下载部分的实现打算使用一个叫libcurl的C库,然后封装使其支持以上下载需求。

软件设计思路

基于以上的需求分析可知,UI只需要负责显示以及用户信息输入,下载器只负责下载部分的逻辑处理,因此可用使用MVP模式(Model-View-Presenter)。使用此模型能使单元测试更为方便。

确定了软件设计模式,接下来则是UI设计和下载器设计。整体框架设计图

原理实现

在选定libcurl作为底层的下载库之后,我们得弄清楚其主要的功能原理。首先我们下载一个文件需要知道哪些数据呢?首先是文件链接,文件链接是由用户进行输入的,之后是文件名和文件存储路径,文件路径也是由用户指定的,文件名我们可用通过对用户输入的url进行解析,以url的后缀参数作为文件名。有了以上参数,像迅雷,chrome等软件就能进行文件下载了,因此我们设计的下载器设计思路也是这样的,尽量减少使用者的操作。但以上只是用户输入的参数,确定文件来源以及文件去处,还不能进行文件下载,文件下载部分还是由我们自己实现的。

首先我们从用户输入的url入手,看看通过url我们能获取到什么。我们以网页输入url为例(实际上libcurl也是这个过程,但是这个过程curl帮我们封装好了,原理我们还是要大致了解一下)。首先是我们像浏览器输入

1
https://www.baidu.com

在我们像目标url发起请求后,游览器首先会解析这个域名,首先它会查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。

即将用户可读的url翻译成用于不可读但是机器可读的ipv4(ipv6)地址。

假如本地的host中并没有这个[key,value]的映射,我们得发送一个DNS(Domain name system)请求到本地的DNS服务器。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。

查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。

根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。这种过程是迭代的过程。

当拿到这个[url,ipv4]的映射关系后,会将其缓存在本地的host中,以备下次查询使用。

到此,我们已经拿到了[url,ipv4]的映射了,可以正式像目标服务器发起资源请求了。

访问网页和下载文件发起的都是tcp链接请求,这里我们在复习一下tcp建立链接的过程。

tcp建立链接的过程,这里以Linux阻塞IO为例

既然都说到这里了,我们顺便把数据帧由网卡到应用程序缕一缕。

数据帧由网卡到应用程序

还是以Linux kernel为例

首先我们将TCP/IP协议按照iso分层大致分为物理层、链路层、网络层,传输层和应用层。

网络协议栈分层

内核和网络设备驱动是通过中断的方式来处理的。当设备上有数据到达的时候,会给CPU的相关引脚上触发一个电压变化,以通知CPU来处理数据。对于网络模块来说,由于处理过程比较复杂和耗时,如果在中断函数中完成所有的处理,将会导致中断处理函数(优先级过高)将过度占据CPU,将导致CPU无法响应其它设备,例如鼠标和键盘的消息。因此Linux中断处理函数是分上半部和下半部的。上半部是只进行最简单的工作,快速处理然后释放CPU,接着CPU就可以允许其它中断进来。剩下将绝大部分的工作都放到下半部中,可以慢慢从容处理。以下是一个网络包收发的过程。

image-20210720105919972

首先当数据帧从网线到达网卡上的时候,第一站是网卡的接收队列。网卡在分配给自己的RingBuffer中寻找可用的内存位置,找到后DMA引擎会把数据DMA到网卡之前关联的内存里,这个时候CPU都是无感的。当DMA操作完成以后,网卡会像CPU发起一个硬中断,通知CPU有数据到达。注意,如果数据到达后网卡的Ring Buffer已经满了,这是网卡会丢弃刚刚到达的数据。ifconfig查看网卡的时候,可以里面有个overruns,表示因为环形队列满被丢弃的包。如果发现有丢包,可能需要通过ethtool命令来加大环形队列的长度。

image-20210720110242507

此时数据已经经过了物理层和链路层,到达了网络层,网络协议栈要先经过IP ICMP等协议处理。

image-20210720110551919

此时通过ip协议解析后会根据包类型分布发送至tcp_rcv(),udp_rcv()中去,在通过用户的skb接收列队发送至对应的用户进程socket中。

补充:TCP三次握手对应的Linux接口函数

image-20210720154346012

首先是服务器创建服务套接字,之后调用bind函数绑定服务套接字,绑定本机的IP和端口号,协议族等信息。

之后是调用listen函数开始监听,此时listen会创建俩个队列,全连接队列(accept queue)和半连接队列(syns queue)。

在三次握手中,俩个队列是以如下形式工作的

第一步,server收到client的syn后,把相关信息放到半连接队列中

第二步,回复syn+ack给client;

第三步,server收到client的ack,如果这时全连接队列没满,那么从半连接队列拿出相关信息放入到全连接队列中,否则按tcp_abort_on_overflow指示的执行。全连接队列满了并且tcp_abort_on_overflow是0的话,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),如果client超时等待比较短,就很容易异常了。

image-20210720155818969

半连接队列

半连接队列的大小由/proc/sys/net/ipv4/tcp_max_syn_backlog控制,Linux的默认是1024。

当服务端发送SYN_ACK后将会开启一个定时器,如果超时没有收到客户端的ACK,将会重发SYN_ACK包。重传的次数由/proc/sys/net/ipv4/tcp_synack_retries控制,默认是5次。

全连接队列

全连接队列的大小通过/proc/sys/net/core/somaxconn指定,在使用listen函数时,内核会根据传入的backlog参数与系统参数somaxconn,取二者的较小值。

1
int listen(int sockfd, int backlog)

Nginx和Redis默认的backlog值等于511,Linux默认的backlog 为 128,Java默认的backlog等于50

默认情况下,全连接队列满以后,服务端会忽略客户端的 ACK,随后会重传SYN+ACK,也可以修改这种行为,这个值由/proc/sys/net/ipv4/tcp_abort_on_overflow决定。

tcp_abort_on_overflow为0表示三次握手最后一步全连接队列满以后服务端会丢掉客户端发过来的ACK,服务端随后会进行重传SYN+ACK。tcp_abort_on_overflow为1表示全连接队列满以后服务端发送RST给客户端,直接释放资源。

sync flood攻击

syn floods 攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,导致server上这个队列满其它正常请求无法进来。

为了预防这个问题,提出了SYN Cookie技术,它可以让服务器在收到客户端的SYN报文时,不分配资源保存客户端信息,而是将这些信息保存在SYN+ACK的初始序号和时间戳中。对正常的连接,这些信息会随着ACK报文被带回来。

补充结束,回到主题。

如果没有找到,则发送一个目标不可达的icmp包。此时数据已经到达了socket,更具阻塞io模型可知

image-20210720112219943

此时处于假设数据已经从tcp_rev队列转移到socket缓存队列,此时kernel将冲no dataready 转移至dataready状态。在数据copy完成后returnok时会返回用户调用的recvfrom,此时app完成了一次tcp数据接收。

image-20210720113229958

数据由服务器到用户

回到主题,此时我们已经拿到了服务器的响应头了

1
2
3
4
5
6
7
8
9
Accept-Ranges: bytes
Cache-Control: max-age=315360000
Content-Length: 196475
Content-Type: image/png
Date: Thu, 01 Jul 2021 02:47:46 GMT
Etag: "2ff7b-5c591376103f6"
Expires: Sun, 29 Jun 2031 02:47:46 GMT
Last-Modified: Fri, 25 Jun 2021 06:26:13 GMT
Server: Apache

不出意外会返回以下信息

1
2
3
4
5
6
7
8
9
Accept-Ranges: bytes
Cache-Control: max-age=315360000
Content-Length: 196475
Content-Type: image/png
Date: Thu, 01 Jul 2021 02:47:46 GMT
Etag: "2ff7b-5c591376103f6"
Expires: Sun, 29 Jun 2031 02:47:46 GMT
Last-Modified: Fri, 25 Jun 2021 06:26:13 GMT
Server: Apache

其中Content-Length会标识文件大小Content-Type会标识文件类型,Last-Modified会标识文件最后更新时间,Etag是文件条目标识符。

根据以上提到的三个信息,能确定唯一服务器上的唯一文件,这是libcurl文件的主要标识。当然返回状态码也很关键,例如不出意外会返回200状态码,如果返回302这类状态码libcurl也提供了类型重定位的参数设置。

此时数据已经到达了浏览器,可以开始准备页面渲染了。

我们来回顾一下用户输入url到用户页面显示的全过程。

image-20210720121731029

对比一下下载文件和网页请求的区别。

image-20210720121707373

我们只需要将页面渲染改为存储文件即可。

此时我们已经解决了文件由服务器到用户应用程序的过程,接下来我们讨论如何接收数据已经如何多线程接收数据。

文件下载

前文中我看可以看见在服务器返回的响应头中是由标识文件长度的(Content-Length),那么要想实现多线程文件下载,我们就必须是每个线程下载文件的一部分,最后再把整个文件拼起来。在libcurl中也为我们提供了相应的设置参数。

image-20210720141107123

任务指派

在确定了多线程分段下载文件后,我们该如何指派下载任务已经文件拼凑呢?

我们可以在文件下载信息中指定文件分段的起始和结束位置,这样在文件下载时我们只需要下载确认分段的文件即可,之后是文件合并,我们也可以在文件下载时指定文件的分段索引,在文件合并时按照索引顺序合并即可。那假如下载到一般用户停止文件下载了呢,我们可以记录本地文件大小,下次下载时根据文件起始位置 + 本地文件大小 = 本次下载文件开始位置即可确定 本次要下载的文件大小。

image-20210720142004469

那如何实现线程间的文件下载信息同步呢?

此时就需要在所有下载信息包(Download Message)中共享一个原子类型,借助此原子类型来确定暂时还未完成下载的线程数,在所有线程都下载完成后在进行合并文件的函数回调,将文件合并即可。

image-20210720141824491

文件合并流程

image-20210720142413772

此时DownloadMessage中所包含的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DownloadMessage{
std::string _filePath; //记录文件路径
std::string _url; // 记录链接
std::string _filename; //记录文件名
long _localFileLength; //记录本地文件长度
long _segmentStart; //记录分断起始位置
long _segmentEnd; // 记录分段结束位置
long _serverFileSize; // 记录文件分段大小
long _segmentIndex; // 记录分段索引
std::autmic_int _work_count; // 记录剩余工作线程数
unsigned int _threadNum; // 记录此文件下载总共的线程数
int _percent; //记录文件下载的百分比
bool _cancel; //用于取消下载
std::function<void(DownloadMessage*)>_callback;// 回调指针
}

看起来参数很多,起始还远远不够,之后还会补充需要的参数。

我们来根据用户使用流程总结一下大致的软件逻辑。

首先是用户输入url然后选择文件的保存路径。之后根据情况勾线是否需要多线程下载。点击下载按钮之后开始目标文件的下载,同时会观察下载进度条来确定下载进度。以及根据情况是否要把下载内容移动到回收站,这部分是用户的操作逻辑。

由于每次用户下载都有可能会勾选多线程下载,我们不能频繁的创建和销毁线程,这会耗费太多不必要的资源,此时我们应该开辟线程池,在用户有下载需求时将任务导入线程池即可。

在考虑完大体的使用逻辑之后我们要进行具体的设计。

根据以上使用逻辑我们可知,必须有一个对象(MessageManager)根据用户输入的信息打包好具体的DownloadMessage供下载线程使用,由于使用的是线程池下载,还必须有一个对象(DownloadManager)来管理线程池,在Message Manager打包好DownloadMessage之后传递给DownloadManager,DownloadManager根据其中的线程数来分配具体的下载分片任务,之后将分好片的DownloadMessage导入具体的工作线程进行下载,在单个下载任务结束后会使其中的_work_count–,当_work_count == 0时会合并所有被下载的分段。

image-20210720144910423

那我们该如何显示下载进度呢,此时我们有俩种做法,第一种是在DownloadMessage中添加一个_callback,在每次下载更新时在libcurl的进度回调函数中都回调用户UI的progressBar来更新进度。另一种是在当前分段下载完成后_work_count–,此时根据剩余未完成的下载线程数 / 总下载线程数来获取当前的下载进度。然后UI中使用一个QTimer来定时检查更新即可。最后设计采用的是后者,虽然前者更为精确,但是不同线程之间的平凡回调开销较大也不好控制。

libcurl的使用

libcurl函数分类

libcurl主要分为如下几类函数,资源管理类函数curl_global_init(),curl_global_cleanup()。这两个函数分别用于初始化和回收libcurl资源。

之后是最重要的(本设计中)curl_easy_setopt(CURL *handle, CURLoption option, parameter)。以下是其相关参数设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
1.	CURLOPT_URL							设置访问URL
2. CURLOPT_WRITEFUNCTION 设置写数据的回调函数
CURLOPT_WRITEDATA 设置回调函数的第四次参数的指针
如果不设置回调函数且设置此参数数据会直接写入这个*FILE fp
3. CURLOPT_HEADERFUNCTION 设置http头部处理函数
CURLOPT_HEADERDATA 设置头部指针
4. CURLOPT_READFUNCTION 设置上传数据函数
CURLOPT_READDATA 设置上传数据函数指针
5. CURLOPT_NOPROGRESS 设置进度条,此参数给false
CURLOPT_PROGRESSFUNCTION 进度条回调函数
CURLOPT_PROGRESSDATA 进度条回调函数参数第一个参数
6. CURLOPT_TIMEOUT 由于设置传输超时时间
CURLOPT_CONNECTIONTIMEOUT 设置链接等待时间

7. CURLOPT_FOLLOWLOCATION 设置重定位URL

8. CURLOPT_RANGE 设置一个数据范围
如"0-" 表示第一个字节到最后一个字节
"0-500"标识[0,500)字节
"999-"标识[999,end)999到最后一个字节

CURLOPT_RESUME_FROM 传递一个long参数给libcurl,指定你希望开始传递的 偏移量
9. CURLOPT_USERPWD 设置用户名与密码
curl_easy_setopt(easy_handle, CURLOPT_USERPWD, "user_name:password");

在完成了参数设置后才能正式开启下载任务curl_easy_perform();这个函数会返回一些状态码,根据状态码即可得知下载过程出现的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
几乎所有的“easy”界面函数都返回一个CURLcode错误代码。无论什么,使用curl_easy_setopt选项CURLOPT_ERRORBUFFER是一个好主意,因为它会给你一个人类可读的错误字符串,可以提供更多的错误原因的细节,而不仅仅是错误代码。curl_easy_strerror可以被调用来从给定的CURLcode号获取错误字符串。

CURLcode是以下之一:

CURLE_OK(0)

一切都好 照常进行。

CURLE_UNSUPPORTED_PROTOCOL(1)

您传递给libcurl的URL使用此libcurl不支持的协议。支持可能是您没有使用的编译时选项,它可能是拼写错误的协议字符串,或只是一个协议libcurl没有代码。

CURLE_FAILED_INIT(2)

非常早的初始化代码失败。这可能是一个内部错误或问题,或资源问题,在初始时间无法完成某些基本的事情。

CURLE_URL_MALFORMAT(3)

网址格式不正确

CURLE_NOT_BUILT_IN(4)

由于构建时间决定,在此libcurl中内置了一个请求的功能,协议或选项。这意味着在构建libcurl时,功能或选项未启用或显式禁用,为了使其功能得以重建libcurl。

CURLE_COULDNT_RESOLVE_PROXY(5)

无法解析代理。给定的代理主机无法解决。

CURLE_COULDNT_RESOLVE_HOST(6)

无法解析主机。给定的远程主机未解决。

CURLE_COULDNT_CONNECT(7)

无法连接()到主机或代理。

CURLE_FTP_WEIRD_SERVER_REPLY(8)

服务器发送的数据libcurl无法解析。此错误代码是用于不仅仅是FTP更多的别名是CURLE_WEIRD_SERVER_REPLY自7.51.0。

CURLE_REMOTE_ACCESS_DENIED(9)

我们被拒绝访问URL中给出的资源。对于FTP,尝试更改为远程目录时会发生这种情况。

CURLE_FTP_ACCEPT_FAILED(10)

当使用活动的FTP会话等待服务器连接时,通过控制连接或类似的方式发送错误代码。

CURLE_FTP_WEIRD_PASS_REPLY(11)

将FTP密码发送到服务器后,libcurl会对此进行适当的回复。此错误代码表示返回了意外的代码。

CURLE_FTP_ACCEPT_TIMEOUT(12)

在等待服务器连接的活动FTP会话期间,CURLOPT_ACCEPTTIMEOUT_MS(或内部默认)超时过期。

CURLE_FTP_WEIRD_PASV_REPLY(13)

libcurl无法从服务器获得明智的结果,作为对PASV或EPSV命令的响应。服务器有缺陷。

CURLE_FTP_WEIRD_227_FORMAT(14)

FTP服务器作为对PASV命令的响应返回227行。如果libcurl无法解析该行,则返回此代码。

CURLE_FTP_CANT_GET_HOST(15)

查找用于新连接的主机的内部故障。

CURLE_HTTP2(16)

在HTTP2框架层中检测到问题。这有点通用,可以是几个问题中的一个,有关详细信息,请参阅错误缓冲区。

CURLE_FTP_COULDNT_SET_TYPE(17)

尝试将传输模式设置为二进制或ASCII时收到错误。

CURLE_PARTIAL_FILE(18)

文件传输比预期更短或更大。当服务器首先报告预期的传输大小,然后传送与以前给定的大小不匹配的数据时,会发生这种情况。

CURLE_FTP_COULDNT_RETR_FILE(19)

这是对“RETR”命令或零字节传输完成的奇怪回复。

CURLE_QUOTE_ERROR(21)

向远程服务器发送自定义“QUOTE”命令时,其中一个命令返回的错误代码为400或更高(对于FTP)或以其他方式指示命令不成功完成。

CURLE_HTTP_RETURNED_ERROR(22)

如果CURLOPT_FAILONERROR设置为TRUE并返回错误代码> = 400,则返回此值。

CURLE_WRITE_ERROR(23)

将接收的数据写入本地文件时发生错误,或者从写入回调将错误返回给libcurl。

CURLE_UPLOAD_FAILED(25)

无法启动上传。对于FTP,服务器通常会拒绝STOR命令。错误缓冲区通常包含服务器的解释。

CURLE_READ_ERROR(26)

读取本地文件或读回读返回的错误有问题。

CURLE_OUT_OF_MEMORY(27)

内存分配请求失败。这是严重的坏事,如果发生这种情况,事情就会严重瘫痪。

CURLE_OPERATION_TIMEDOUT(28)

操作超时 根据条件达到指定的超时期限。

CURLE_FTP_PORT_FAILED(30)

FTP PORT命令返回错误。这主要发生在你没有指定一个足够好的libcurl使用的地址。请参阅CURLOPT_FTPPORT。

CURLE_FTP_COULDNT_USE_REST(31)

FTP REST命令返回错误。如果服务器是合理的,这绝对不会发生。

CURLE_RANGE_ERROR(33)

服务器不支持或接受范围请求。

CURLE_HTTP_POST_ERROR(34)

这是一个奇怪的错误,主要是由于内部的混乱造成的。

CURLE_SSL_CONNECT_ERROR(35)

SSL / TLS握手中的某个地方出现问题。你真的想要错误缓冲区,并在那里读取消息,因为它更明确地指出了问题。可以是证书(文件格式,路径,权限),密码等。

CURLE_BAD_DOWNLOAD_RESUME(36)

由于指定的偏移超出文件边界,所以无法恢复下载。

CURLE_FILE_COULDNT_READ_FILE(37)

FILE://提供的文件无法打开。最可能的原因是文件路径不能识别现有的文件。你是否检查文件权限?

CURLE_LDAP_CANNOT_BIND(38)

LDAP无法绑定。LDAP绑定操作失败。

CURLE_LDAP_SEARCH_FAILED(39)

LDAP搜索失败。

CURLE_FUNCTION_NOT_FOUND(41)

找不到功能 没有找到所需的zlib函数。

CURLE_ABORTED_BY_CALLBACK(42)

通过回调中止。回调返回到libcurl“abort”。

CURLE_BAD_FUNCTION_ARGUMENT(43)

内部错误。一个函数调用了一个坏的参数。

CURLE_INTERFACE_FAILED(45)

接口错误。指定的出站界面无法使用。使用CURLOPT_INTERFACE设置要用于传出连接的源IP地址的接口。

CURLE_TOO_MANY_REDIRECTS(47)

重定向太多 当以下重定向时,libcurl命中最大数量。用CURLOPT_MAXREDIRS设置你的限制。

CURLE_UNKNOWN_OPTION(48)

传递给libcurl的选项不被识别/已知。请参阅相应的文档。这很可能是程序中使用libcurl的问题。错误缓冲区可能包含更多关于哪个确切选项的具体信息。

CURLE_TELNET_OPTION_SYNTAX(49)

telnet选项字符串被非法格式化。

CURLE_PEER_FAILED_VERIFICATION(51)

远程服务器的SSL证书或SSH md5指纹被认为不正常。

CURLE_GOT_NOTHING(52)

没有从服务器返回任何东西,在这种情况下,没有任何东西被认为是错误。

CURLE_SSL_ENGINE_NOTFOUND(53)

未找到指定的加密引擎。

CURLE_SSL_ENGINE_SETFAILED(54)

默认设置所选的SSL加密引擎失败!

CURLE_SEND_ERROR(55)

发送网络数据失败

CURLE_RECV_ERROR(56)

接收网络数据失败。

CURLE_SSL_CERTPROBLEM(58)

本地客户端证书出现问题。

CURLE_SSL_CIPHER(59)

无法使用指定的密码。

CURLE_SSL_CACERT(60)

对等证书无法通过已知的CA证书进行身份验证。

CURLE_BAD_CONTENT_ENCODING(61)

无法识别的传输编码。

CURLE_LDAP_INVALID_URL(62)

无效的LDAP网址

CURLE_FILESIZE_EXCEEDED(63)

超过最大文件大小。

CURLE_USE_SSL_FAILED(64)

请求的FTP SSL级别失败。

CURLE_SEND_FAIL_REWIND(65)

当发送操作卷曲不得不倒带数据重发时,倒带操作失败。

CURLE_SSL_ENGINE_INITFAILED(66)

启动SSL引擎失败。

CURLE_LOGIN_DENIED(67)

远程服务器拒绝卷曲登录(7.13.1中添加)

CURLE_TFTP_NOTFOUND(68)

在TFTP服务器上找不到文件。

CURLE_TFTP_PERM(69)

TFTP服务器上的权限问题

CURLE_REMOTE_DISK_FULL(70)

超出服务器上的磁盘空间。

CURLE_TFTP_ILLEGAL(71)

非法TFTP操作。

CURLE_TFTP_UNKNOWNID(72)

未知的TFTP传输ID。

CURLE_REMOTE_FILE_EXISTS(73)

文件已存在,不会被覆盖。

CURLE_TFTP_NOSUCHUSER(74)

TFTP服务器不应该返回此错误。

CURLE_CONV_FAILED(75)

字符转换失败。

CURLE_CONV_REQD(76)

来电者必须注册转换回调。

CURLE_SSL_CACERT_BADFILE(77)

阅读SSL CA证书(路径?访问权限?)的问题?

CURLE_REMOTE_FILE_NOT_FOUND(78)

URL中引用的资源不存在。

CURLE_SSH(79)

在SSH会话期间发生未指定的错误。

CURLE_SSL_SHUTDOWN_FAILED(80)

无法关闭SSL连接。

CURLE_AGAIN(81)

Socket还没有准备好发送/ recv等待,直到它准备好,然后再试一次。此返回代码仅从curl_easy_recv和curl_easy_send(在7.18.2中添加) 返回,

CURLE_SSL_CRL_BADFILE(82)

无法加载CRL文件(在7.19.0中添加)

CURLE_SSL_ISSUER_ERROR(83)

发行人检查失败(7.19.0中添加)

CURLE_FTP_PRET_FAILED(84)

FTP服务器根本不了解PRET命令,也不支持给定的参数。使用CURLOPT_CUSTOMREQUEST时要小心,在PASV之前也会使用自定义LIST命令与PRET CMD一起发送。(在7.20.0中添加)

CURLE_RTSP_CSEQ_ERROR(85)

RTSP CSeq号码不匹配。

CURLE_RTSP_SESSION_ERROR(86)

RTSP会话标识符不匹配。

CURLE_FTP_BAD_FILE_LIST(87)

无法解析FTP文件列表(在FTP通配符下载期间)。

CURLE_CHUNK_FAILED(88)

块回调报告错误。

CURLE_NO_CONNECTION_AVAILABLE(89)

(仅供内部使用,永远不会由libcurl返回)无连接可用,会话将排队。(加入7.30.0)

CURLE_SSL_PINNEDPUBKEYNOTMATCH(90)

无法匹配使用CURLOPT_PINNEDPUBLICKEY指定的固定密钥。

CURLE_SSL_INVALIDCERTSTATUS(91)

使用CURLOPT_SSL_VERIFYSTATUS询问状态时返回失败。

CURLE_HTTP2_STREAM(92)

HTTP / 2框架层中的流错误。

CURLE_OBSOLETE *

这些错误代码将永远不会被返回。它们在旧的libcurl版本中使用,目前未使用。

遇到的问题

多线程下载设计问题。

一开始设计时并没有采用上面所说的分段模型来完成多线程下载,一开始使用的模型是当遇到多线程下载任务时,零时开辟多条线程进行文件下载,使用std::mutex进行并发控制,在下载完成后写入文件。

image-20210720150337576

这个模型设计有好几个bug,首先是零时开辟下载线程会耗费很资源,其次是文件写入时使用互斥锁进行并发控制会造成大量阻塞,最后也是最重要的他不支持断点下载,是的,由于是同时写入同一个文件并不能根据文件大小来确定本次已下载的文件大小,如果中途停止了文件下载只能重新开始文件下载。

之后根据观察者模式设计了一套观察者回调模型,这套模型的主要思路是为每一个文件下载任务分配一个下载观察者,观察者会将文件分为N段,之后在将N段文件分为M片,每次使用M个线程下载M片文件,当M片文件都下载完成时回调此文件的观察者,观察者将M片文件合并成一段并写入文件,写入文件后在开始下一段文件的下载。如此循环回调,在最后一次文件下载完成后会掉Download Manager销毁观察者。

image-20210720151753360

这套模型的优点是支持分段下载,同时还支持多文件同时多线程下载。缺点是观察者的回调次数过多,逻辑写起来很复杂,容易dump不好调试。

资源管理问题

由于一开始使用的是curl提供的进度条回调函数(不支持智能指针),导致多线程回调的资源管理异常麻烦,加上一开始还不熟悉函数包装器,使用的是函数指针进行的回调(十分的恶心)。导致程序有因为这些崩掉。

其他问题

在加入UI之前做好了命令行下的断点下载,测试时没有问题,在加入UI后跑不出预期的结果而且程序可能会莫名其妙的崩掉,现在还没解决这个问题,打算在重构一遍代码后在梳理此问题。

参考资料

[1]https://curl.se/libcurl/c/

[2]https://mp.weixin.qq.com/s/GoYDsfy9m0wRoXi_NCfCmg

[3]https://www.bookstack.cn/books/qter-qt5-basic

[4]https://www.bookstack.cn/books/Cpp_Concurrency_In_Action(C++ Concurrency In Action))

[5]https://www.jianshu.com/p/6a0fcb1008d6