goroutine&waitgroup下载文件
0.1、索引
https://blog.waterflow.link/articles/1663078266267
当我们下载一个大文件的时候,会因为下载时间太久而超时或者出错。那么我么我们可以利用goroutine的特性并发分段的去请求下载资源。
1、Accept-Ranges
首先下载链接需要在响应中返回Accept-Ranges,并且它的值不为 “none”,那么该服务器支持范围请求。比如我们可以利用HEAD请求来进行检测
...// head请求获取url的header
head, err := http.Head(url)
if err != nil {
return err
}// 判断url是否支持指定范围请求及哪种类型的分段请求
if head.Header.Get("Accept-Ranges") != "bytes" {
return errors.New("not support range download")
}...
我们可以使用
curl
命令看下head头curl -I https://agritrop.cirad.fr/584726/1/Rapport.pdf
HTTP/1.1 200 OK
Date: Tue, 13 Sep 2022 13:52:08 GMT
Server: HTTPD
Strict-Transport-Security: max-age=63072000
X-Content-Type-Options: nosniff
X-Frame-Options: sameorigin
Content-MD5: K4j+rsagurPwGP/5cm8k8Q==
Last-Modified: Tue, 04 Jul 2017 08:26:16 GMT
Expires: Wed, 13 Sep 2023 13:52:08 GMT
Content-Disposition: inline;
filename=Rapport.pdf
Accept-Ranges: bytes # 允许范围请求,单位是字节
Content-Length: 6659798 # 文件的完整大小
Content-Type: application/pdf
X-XSS-Protection: 1;
mode=block
X-Permitted-Cross-Domain-Policies: none
Cache-Control: public
其中,
Accept-Ranges: bytes
表示界定范围的单位是 bytes 。这里 Content-Length也是有效信息,因为它提供了文件的完整大小。2、Range
【goroutine&waitgroup下载文件】假如服务器支持范围请求的话,你可以使用 Range 首部来生成该类请求。该首部指示服务器应该返回文件的哪一或哪几部分。
...
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Println("初始化request失败:", err)
return
}rangeL := fmt.Sprintf("bytes=%d-%d", start, end)
fmt.Println("字符范围:", rangeL)
// 获取制定范围的数据
req.Header.Add("Range", rangeL)
res, err := client.Do(req)
...
单一范围
我们可以请求资源的某一部分。这次我们依然用 cURL 来进行测试。"-H" 选项可以在请求中追加一个首部行,在这个例子中,是用 Range 首部来请求图片文件的前 1024 个字节。
curl https://agritrop.cirad.fr/584726/1/Rapport.pdf -i -H "Range: bytes=0-1023"
HTTP/1.1 206 Partial Content
Date: Tue, 13 Sep 2022 14:00:47 GMT
Server: HTTPD
Strict-Transport-Security: max-age=63072000
X-Content-Type-Options: nosniff
X-Frame-Options: sameorigin
Content-MD5: K4j+rsagurPwGP/5cm8k8Q==
Last-Modified: Tue, 04 Jul 2017 08:26:16 GMT
Expires: Wed, 13 Sep 2023 14:00:47 GMT
Content-Disposition: inline;
filename=Rapport.pdf
Accept-Ranges: bytes
Content-Range: bytes 0-1023/6659798 # 返回指定的字节
Content-Length: 1024
Content-Type: application/pdf
X-XSS-Protection: 1;
mode=block
X-Permitted-Cross-Domain-Policies: none
Cache-Control: public
Content-Range表示请求的资源在整个资源中的位置,这个时候Content-Length就不是表示整个资源的大小,而是请求资源的大小。
多重范围
我们也可以请求多个范围,只需要在Range中指定多个即可
curl https://agritrop.cirad.fr/584726/1/Rapport.pdf -i -H "Range: bytes=0-50, 100-150"
HTTP/1.1 206 Partial Content
Date: Tue, 13 Sep 2022 14:04:53 GMT
Server: HTTPD
Strict-Transport-Security: max-age=63072000
X-Content-Type-Options: nosniff
X-Frame-Options: sameorigin
Content-MD5: K4j+rsagurPwGP/5cm8k8Q==
Last-Modified: Tue, 04 Jul 2017 08:26:16 GMT
Expires: Wed, 13 Sep 2023 14:04:53 GMT
Content-Disposition: inline;
filename=Rapport.pdf
Accept-Ranges: bytes
Content-Length: 312
Content-Type: multipart/byteranges;
boundary=4876db1cd4aa85af6
X-XSS-Protection: 1;
mode=block
X-Permitted-Cross-Domain-Policies: none
Cache-Control: public--4876db1cd4aa85af6
Content-type: application/pdf
Content-range: bytes 0-50/6659798内容
--4876db1cd4aa85af6
Content-type: application/pdf
Content-range: bytes 100-150/6659798内容
--4876db1cd4aa85af6--
服务器返回 206 Partial Content 状态码和 Content-Type:multipart/byteranges; boundary=3d6b6a416f9b5 头部,Content-Type:multipart/byteranges 表示这个响应有多个 byterange。每一部分 byterange 都有他自己的 Content-type 头部和 Content-Range,并且使用 boundary 参数对 body 进行划分。
3、goroutine
我们代码中通过获取Contetn-Length总大小,和spPart分成了3部分,通过goroutine进行并行的单一范围请求。然后把最终请求的结果保存在临时文件。之后再把这3部分内容统一保存到最终的文件中
具体代码如下:
package mainimport (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"strconv"
"strings"
"sync"
)// 通过Content-Length分成3部分并发执行
var spPart = 3// 任务编排控制
var wg sync.WaitGroupfunc main() {
url := "https://agritrop.cirad.fr/584726/1/Rapport.pdf"err := DownloadFile(url, "rapport.pdf")
if err != nil {
panic(err)
}
}func DownloadFile(url string, filename string) error {
if strings.TrimSpace(url) == "" {
return nil
}// head请求获取url的header
head, err := http.Head(url)
if err != nil {
return err
}// 判断url是否支持指定范围请求及哪种类型的分段请求
if head.Header.Get("Accept-Ranges") != "bytes" {
return errors.New("not support range download")
}contentLen, err := strconv.Atoi(head.Header.Get("Content-Length"))
if err != nil {
return err
}offset := contentLen / spPartfor i := 0;
i < spPart;
i++ {
wg.Add(1)
start := offset * i
end := offset * (i + 1)
name := fmt.Sprintf("part%d", i)go rangeDownload(url, name, start, end)
}wg.Wait()out, err := os.Create(filename)
if err != nil {
return err
}
defer out.Close()for i := 0;
i < spPart;
i++ {
name := fmt.Sprintf("part%d", i)
file, err := ioutil.ReadFile(name)
if err != nil {
return err
}
out.WriteAt(file, int64(i*offset))if err := os.Remove(name);
err != nil {
return err
}
}return nil}func rangeDownload(url string, name string, start int, end int) {
defer wg.Done()client := http.Client{}
file, err := os.Create(name)
if err != nil {
fmt.Println("创建文件失败:", err)
return
}defer file.Close()req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
fmt.Println("初始化request失败:", err)
return
}rangeL := fmt.Sprintf("bytes=%d-%d", start, end)
fmt.Println("字符范围:", rangeL)
// 获取制定范围的数据
req.Header.Add("Range", rangeL)
res, err := client.Do(req)if err != nil {
fmt.Println("发起http请求失败:", err)
return
}defer res.Body.Close()body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println("读取返回体失败:", err)
return
}_, err = file.Write(body)
if err != nil {
fmt.Println("写入文件失败:", err)
return
}
}
推荐阅读
- #|基于改进的蚂蚁群算法求解最短路径问题、二次分配问题、背包问题【Matlab&Python代码实现】
- #|无线电信号调制【小波去噪】(Matlab&Python代码实现)
- vue.js|Vue路由&nodejs环境搭建
- goroutine调度
- 1.1|1.1 & 1.2 战略定义与角色
- Envoy|Envoy & Istio 性能指标与原理初探
- 弱隔离级别 & 事务并发问题
- 强化学习|[强化学习实战]出租车调度-Q learning & SARSA
- 生活记录&DIY|3D打印机DIY之一------Prusa i3的材料清单和总体结构组装
- HTML5|猿创征文 | 基于H5实现跨文档通信 & websocket