前言

多线程下载,顾名思义就是对一个文件进行切片访问,等待所有的文件下载完成后在本地进行拼接成一个整体文件的过程。

因此可以利用 golang 的多协程对每个分片同步下载,之后再合并且进行md5校验或者总长度校验。

请求资源

下载文件的本质就是从服务器获取数据,更笼统地说就是向服务器发送 GET请求。

http1.1协议

HTTP1.1 协议(RFC2616)开始支持获取文件的部分内容,这为并行下载以及断点续传提供了技术支持:Range\Content-Range。Range参数是本地发往服务器的http头参数;Content-Range是远程服务器发往本地http头参数。

Range\Content-Range

range: (unit=first byte pos)-[last byte pos] : 指定第一个字节位置和最后一个字节位置。

例子说明:

  • range: bytes=0-1300 : 表示第0-1300字节范围的内容发往远程服务器。
  • range: bytes=1301-23041: 表示第1201-23041字节范围的内容发往远程服务器。

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]

例子说明:

  • content-Range: bytes 0-797/1024000 : 表示0-797字节范围内容从服务器响应到客户端,1024000是文件总大小。

完成http响应后,http状态码返回:206 表示使用断掉续传方式,而一般200表示不使用断掉续传方式。

比如:

(base) luliang@shenjian ~ % curl --location --head ‘https://download.jetbrains.com/go/goland-2020.2.2.exe’
HTTP/2 302
date: Sat, 06 May 2023 11:52:42 GMT
content-type: text/html
content-length: 138
location: https://download.jetbrains.com.cn/go/goland-2020.2.2.exe
server: nginx
strict-transport-security: max-age=31536000; includeSubdomains;
x-frame-options: DENY
x-content-type-options: nosniff
x-xss-protection: 1; mode=block;
x-geocountry: China
x-geocode: CN
x-geocity: Taiyigong
HTTP/2 200
content-type: binary/octet-stream
content-length: 338589968
date: Sat, 06 May 2023 11:51:35 GMT
last-modified: Tue, 30 Mar 2021 14:16:56 GMT
etag: “548422fa12ec990979c847cfda85a068-65”
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 f7c361bc042484d244950f166c4f320c.cloudfront.net (CloudFront)
x-amz-cf-pop: PVG52-E1
x-amz-cf-id: xkbWvLoSgdyhCV-gXgANy7pq_P4ndAHEBCznYtxiOIAuvEm5ew9Qlw==
age: 72

如果在响应的Header中存在Accept-Ranges首部(并且它的值不为 “none”),那么表示该服务器支持范围请求(支持断点续传)。

可以使用 curl 发送一个 HEADER 请求来进行检测:

(base) luliang@shenjian ~ % curl -I https://download.jetbrains.com.cn/go/goland-2020.2.2.exe
HTTP/2 200
content-type: binary/octet-stream
content-length: 338589968
date: Sat, 06 May 2023 11:55:58 GMT
last-modified: Tue, 30 Mar 2021 14:16:56 GMT
etag: “548422fa12ec990979c847cfda85a068-65”
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 cf7a8587fc03d8367e313c3f45e5b454.cloudfront.net (CloudFront)
x-amz-cf-pop: BJS9-E1
x-amz-cf-id: UDJvsOsiddSrXUF9CzkUKucO9ClpNrFrj2m-M9S4LYJADs34pMn8wA==

在上面的响应中, Accept-Ranges: bytes 表示界定范围的单位是 bytes,这里 Content-Length 也是很有用的信息,因为它提供了要检索的图片的完整大小!

如果站点返回的Header中不包括Accept-Ranges,那么它有可能不支持范围请求。一些站点会明确将其值设置为 “none”,以此来表明不支持。在这种情况下,某些应用的下载管理器可能会将暂停按钮禁用!

Last-Modified\If-Modified-Since

利用HTTP协议头Last-Modified\If-Modified-Since参数存储文件最后修改日期,每次通信文件要判断与上一次文件最后修改日期是否相同,如果不同就从0开始重新接收文件,相同则继续。Last-Modified 是由服务器往客户端发送的 HTTP 头,而If-Modified-Since 则是由客户端往服务器发送的头。

例如:

  • Last-Modified: Fri, 22 Feb 2023 03:45:06 GMT : 服务器端返回客户端HTTP头信息。
  • If-Modified-Since: Fri, 22 Feb 2013 03:45:02 GMT : 客户端通过 If-Modified-Since HTTP头将上一次服务器端发过来的 Last-Modified 时间戳发送回服务器端进行比较验证。

NewRequest()

该NewRequest()函数的定义为:

*Request

http.DefaultClient.Do()

该函数定义为:

而函数 do()也返回一个 *Response,Response的结构体定义如下:

可以看到,Response 中有StatusCode 、Header 、Body等我们想要的信息。

因此可以打一套组合拳将Response得到:

用函数实现就是:

获取文件名

我们先看看 Hear 上定义的方法:

里面有一个 get方法,它传入一个 key,返回一个值。我们可以传入一个想要的键从而得到想要的信息。

如果我们可以传入一个"Content-Disposition",得到 fileName。Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名。

下载文件

两个重要的结构体:

其中一个是定义的下载器,这个下载器定义了源地址、总文件大小、文件名、文件存储地址、md5 校验等;另一个定义了一个分片,这个分片定义了分片的身份(编号),文件开始点、结束点以及一个存储数据的Data。

接下来就可以初始化下载器了,填充一些基本的信息:

下载分片

这个思路就是就把 Body 存储起来,那就是有效数据。之后就可以把所有的 数据合成成一个完整文件。

合成文件

该函数合成了新文件还对文件完整性、MD5 做了校验。

多线程下载

该函数将文件总长度信息获取之后,进行了等分的分片,然后开启协程进行并发请求。

之后,我们在 main()函数中填上目标链接以及 md5值就可以下载了。

运行效果:

2023/05/07 19:56:48 开始[7]下载from:365989316 to:418273495
2023/05/07 19:56:48 开始[0]下载from:0 to:52284187
2023/05/07 19:56:48 开始[5]下载from:261420940 to:313705127
2023/05/07 19:56:48 开始[4]下载from:209136752 to:261420939
2023/05/07 19:56:48 开始[3]下载from:156852564 to:209136751
2023/05/07 19:56:48 开始[1]下载from:52284188 to:104568375
2023/05/07 19:56:48 开始[6]下载from:313705128 to:365989315
2023/05/07 19:56:48 开始[2]下载from:104568376 to:156852563
…………

总结

该程序的流程简单,和爬虫相比,更简单,毕竟不用使用各种选择器+正则表达式来获取特定元素。本质上来说,就是在获取 GET 请求,只是绕的弯比较多。

另外这里有一个获取某个文件 md5 值的方法: