写点什么

goroutine&waitgroup 下载文件

作者:六月的
  • 2022-10-19
    上海
  • 本文字数:3203 字

    阅读完需:约 1 分钟

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.pdfHTTP/1.1 200 OKDate: Tue, 13 Sep 2022 13:52:08 GMTServer: HTTPDStrict-Transport-Security: max-age=63072000X-Content-Type-Options: nosniffX-Frame-Options: sameoriginContent-MD5: K4j+rsagurPwGP/5cm8k8Q==Last-Modified: Tue, 04 Jul 2017 08:26:16 GMTExpires: Wed, 13 Sep 2023 13:52:08 GMTContent-Disposition: inline; filename=Rapport.pdfAccept-Ranges: bytes # 允许范围请求,单位是字节Content-Length: 6659798 # 文件的完整大小Content-Type: application/pdfX-XSS-Protection: 1; mode=blockX-Permitted-Cross-Domain-Policies: noneCache-Control: public
复制代码


其中,Accept-Ranges: bytes 表示界定范围的单位是 bytes 。这里 Content-Length 也是有效信息,因为它提供了文件的完整大小。

2、Range

假如服务器支持范围请求的话,你可以使用 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 ContentDate: Tue, 13 Sep 2022 14:00:47 GMTServer: HTTPDStrict-Transport-Security: max-age=63072000X-Content-Type-Options: nosniffX-Frame-Options: sameoriginContent-MD5: K4j+rsagurPwGP/5cm8k8Q==Last-Modified: Tue, 04 Jul 2017 08:26:16 GMTExpires: Wed, 13 Sep 2023 14:00:47 GMTContent-Disposition: inline; filename=Rapport.pdfAccept-Ranges: bytesContent-Range: bytes 0-1023/6659798 # 返回指定的字节Content-Length: 1024Content-Type: application/pdfX-XSS-Protection: 1; mode=blockX-Permitted-Cross-Domain-Policies: noneCache-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 ContentDate: Tue, 13 Sep 2022 14:04:53 GMTServer: HTTPDStrict-Transport-Security: max-age=63072000X-Content-Type-Options: nosniffX-Frame-Options: sameoriginContent-MD5: K4j+rsagurPwGP/5cm8k8Q==Last-Modified: Tue, 04 Jul 2017 08:26:16 GMTExpires: Wed, 13 Sep 2023 14:04:53 GMTContent-Disposition: inline; filename=Rapport.pdfAccept-Ranges: bytesContent-Length: 312Content-Type: multipart/byteranges; boundary=4876db1cd4aa85af6X-XSS-Protection: 1; mode=blockX-Permitted-Cross-Domain-Policies: noneCache-Control: public

--4876db1cd4aa85af6Content-type: application/pdfContent-range: bytes 0-50/6659798
内容--4876db1cd4aa85af6Content-type: application/pdfContent-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 main
import ( "errors" "fmt" "io/ioutil" "net/http" "os" "strconv" "strings" "sync")
// 通过Content-Length分成3部分并发执行var spPart = 3
// 任务编排控制var wg sync.WaitGroup
func 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 / spPart
for 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 }}
复制代码


用户头像

六月的

关注

还未添加个人签名 2019-07-23 加入

还未添加个人简介

评论

发布
暂无评论
goroutine&waitgroup下载文件_goroutine_六月的_InfoQ写作社区