乐趣区

关于goroutine:goroutinewaitgroup下载文件

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

如果服务器反对范畴申请的话,你能够应用 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 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
    }
}
退出移动版