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