0.1、索引
blog.waterflow.link/articles/16630...
當我們下載一個大檔案的時候,會因為下載時間太久而超時或者出錯。那麼我麼我們可以利用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
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結