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 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 / 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 }}