Go 實戰(zhàn) :如何實現(xiàn) HTTP 斷點續(xù)傳多線程下載?

1. HTTP斷點續(xù)傳多線程下載
一個比較常見的場景,就是斷點續(xù)傳/下載,在網(wǎng)絡(luò)情況不好的時候,可以在斷開連接以后,僅繼續(xù)獲取部分內(nèi)容. 例如在網(wǎng)上下載軟件,已經(jīng)下載了 95% 了,此時網(wǎng)絡(luò)斷了,如果不支持范圍請求,那就只有被迫重頭開始下載.但是如果有范圍請求的加持,就只需要下載最后 5% 的資源,避免重新下載.
另一個場景就是多線程下載,對大型文件,開啟多個線程, 每個線程下載其中的某一段,最后下載完成之后, 在本地拼接成一個完整的文件,可以更有效的利用資源.
一圖勝千言

2. Range & Content-Range
HTTP1.1 協(xié)議(RFC2616)開始支持獲取文件的部分內(nèi)容,這為并行下載以及斷點續(xù)傳提供了技術(shù)支持. 它通過在 Header 里兩個參數(shù)實現(xiàn)的,客戶端發(fā)請求時對應(yīng)的是 Range ,服務(wù)器端響應(yīng)時對應(yīng)的是 Content-Range.
$?curl?--location?--head?'https://download.jetbrains.com/go/goland-2020.2.2.exe'
date:?Sat,?15?Aug?2020?02:44:09?GMT
content-type:?text/html
content-length:?138
location:?https://download-cf.jetbrains.com/go/goland-2020.2.2.exe
server:?nginx
strict-transport-security:?max-age=31536000;?includeSubdomains;
x-frame-options:?DENY
x-content-type-options:?nosniff
x-xss-protection:?1;?mode=block;
x-geocountry:?United?States
x-geocode:?US
HTTP/1.1?200?OK
Content-Type:?binary/octet-stream
Content-Length:?338589968
Connection:?keep-alive
x-amz-replication-status:?COMPLETED
Last-Modified:?Wed,?12?Aug?2020?13:01:03?GMT
x-amz-version-id:?p7a4LsL6K1MJ7UioW7HIz_..LaZptIUP
Accept-Ranges:?bytes
Server:?AmazonS3
Date:?Fri,?14?Aug?2020?21:27:08?GMT
ETag:?"1312fd0956b8cd529df1100d5e01837f-41"
X-Cache:?Hit?from?cloudfront
Via:?1.1?8de6b68254cf659df39a819631940126.cloudfront.net?(CloudFront)
X-Amz-Cf-Pop:?PHX50-C1
X-Amz-Cf-Id:?LF_ZIrTnDKrYwXHxaOrWQbbaL58uW9Y5n993ewQpMZih0zmYi9JdIQ==
Age:?19023
Range
The Range 是一個請求首部,告知服務(wù)器返回文件的哪一部分. 在一個 Range 首部中,可以一次性請求多個部分,服務(wù)器會以 multipart 文件的形式將其返回. 如果服務(wù)器返回的是范圍響應(yīng),需要使用 206 Partial Content 狀態(tài)碼. 假如所請求的范圍不合法,那么服務(wù)器會返回 416 Range Not Satisfiable 狀態(tài)碼,表示客戶端錯誤. 服務(wù)器允許忽略 Range 首部,從而返回整個文件,狀態(tài)碼用 200 .Range:(unit=first byte pos)-[last byte pos]
Range 頭部的格式有以下幾種情況:
Range:?=-
Range:?=-
Range:?=-,?-
Range:?=-,?-,?-
Content-Range
假如在響應(yīng)中存在 Accept-Ranges 首部(并且它的值不為 “none”),那么表示該服務(wù)器支持范圍請求(支持斷點續(xù)傳). 例如,您可以使用 cURL 發(fā)送一個 HEAD 請求來進行檢測.curl -I http://i.imgur.com/z4d4kWk.jpg
HTTP/1.1?200?OK
...
Accept-Ranges:?bytes
Content-Length:?146515
在上面的響應(yīng)中, Accept-Ranges: bytes 表示界定范圍的單位是 bytes . 這里 Content-Length 也是有效信息,因為它提供了要檢索的圖片的完整大小.
如果站點未發(fā)送 Accept-Ranges 首部,那么它們有可能不支持范圍請求.一些站點會明確將其值設(shè)置為 “none”,以此來表明不支持.在這種情況下,某些應(yīng)用的下載管理器會將暫停按鈕禁用.
3. Golang代碼實現(xiàn)HTTP斷點續(xù)傳多線程下載
通過以下代碼您可以了解到多線程下載的原理, 同時給您突破百度網(wǎng)盤下載提供思路.
package?main
import?(
?"crypto/sha256"
?"encoding/hex"
?"errors"
?"fmt"
?"io/ioutil"
?"log"
?"mime"
?"net/http"
?"os"
?"path/filepath"
?"strconv"
?"sync"
?"time"
)
func?parseFileInfoFrom(resp?*http.Response)?string?{
?contentDisposition?:=?resp.Header.Get("Content-Disposition")
?if?contentDisposition?!=?""?{
??_,?params,?err?:=?mime.ParseMediaType(contentDisposition)
??if?err?!=?nil?{
???panic(err)
??}
??return?params["filename"]
?}
?filename?:=?filepath.Base(resp.Request.URL.Path)
?return?filename
}
//FileDownloader?文件下載器
type?FileDownloader?struct?{
?fileSize???????int
?url????????????string
?outputFileName?string
?totalPart??????int?//下載線程
?outputDir??????string
?doneFilePart???[]filePart
}
//NewFileDownloader?.
func?NewFileDownloader(url,?outputFileName,?outputDir?string,?totalPart?int)?*FileDownloader?{
?if?outputDir?==?""?{
??wd,?err?:=?os.Getwd()?//獲取當前工作目錄
??if?err?!=?nil?{
???log.Println(err)
??}
??outputDir?=?wd
?}
?return?&FileDownloader{
??fileSize:???????0,
??url:????????????url,
??outputFileName:?outputFileName,
??outputDir:??????outputDir,
??totalPart:??????totalPart,
??doneFilePart:???make([]filePart,?totalPart),
?}
}
//filePart?文件分片
type?filePart?struct?{
?Index?int????//文件分片的序號
?From??int????//開始byte
?To????int????//解決byte
?Data??[]byte?//http下載得到的文件內(nèi)容
}
func?main()?{
?startTime?:=?time.Now()
?var?url?string?//下載文件的地址
?url?=?"https://download.jetbrains.com/go/goland-2020.2.2.dmg"
?downloader?:=?NewFileDownloader(url,?"",?"",?10)
?if?err?:=?downloader.Run();?err?!=?nil?{
??//?fmt.Printf("\n%s",?err)
??log.Fatal(err)
?}
?fmt.Printf("\n?文件下載完成耗時:?%f?second\n",?time.Now().Sub(startTime).Seconds())
}
//head?獲取要下載的文件的基本信息(header)?使用HTTP?Method?Head
func?(d?*FileDownloader)?head()?(int,?error)?{
?r,?err?:=?d.getNewRequest("HEAD")
?if?err?!=?nil?{
??return?0,?err
?}
?resp,?err?:=?http.DefaultClient.Do(r)
?if?err?!=?nil?{
??return?0,?err
?}
?if?resp.StatusCode?>?299?{
??return?0,?errors.New(fmt.Sprintf("Can't?process,?response?is?%v",?resp.StatusCode))
?}
?//檢查是否支持?斷點續(xù)傳
?//https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
?if?resp.Header.Get("Accept-Ranges")?!=?"bytes"?{
??return?0,?errors.New("服務(wù)器不支持文件斷點續(xù)傳")
?}
?d.outputFileName?=?parseFileInfoFrom(resp)
?//https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
?return?strconv.Atoi(resp.Header.Get("Content-Length"))
}
//Run?開始下載任務(wù)
func?(d?*FileDownloader)?Run()?error?{
?fileTotalSize,?err?:=?d.head()
?if?err?!=?nil?{
??return?err
?}
?d.fileSize?=?fileTotalSize
?jobs?:=?make([]filePart,?d.totalPart)
?eachSize?:=?fileTotalSize?/?d.totalPart
?for?i?:=?range?jobs?{
??jobs[i].Index?=?i
??if?i?==?0?{
???jobs[i].From?=?0
??}?else?{
???jobs[i].From?=?jobs[i-1].To?+?1
??}
??if?i?-1?{
???jobs[i].To?=?jobs[i].From?+?eachSize
??}?else?{
???//the?last?filePart
???jobs[i].To?=?fileTotalSize?-?1
??}
?}
?var?wg?sync.WaitGroup
?for?_,?j?:=?range?jobs?{
??wg.Add(1)
??go?func(job?filePart)?{
???defer?wg.Done()
???err?:=?d.downloadPart(job)
???if?err?!=?nil?{
????log.Println("下載文件失敗:",?err,?job)
???}
??}(j)
?}
?wg.Wait()
?return?d.mergeFileParts()
}
//下載分片
func?(d?FileDownloader)?downloadPart(c?filePart)?error?{
?r,?err?:=?d.getNewRequest("GET")
?if?err?!=?nil?{
??return?err
?}
?log.Printf("開始[%d]下載from:%d?to:%d\n",?c.Index,?c.From,?c.To)
?r.Header.Set("Range",?fmt.Sprintf("bytes=%v-%v",?c.From,?c.To))
?resp,?err?:=?http.DefaultClient.Do(r)
?if?err?!=?nil?{
??return?err
?}
?if?resp.StatusCode?>?299?{
??return?errors.New(fmt.Sprintf("服務(wù)器錯誤狀態(tài)碼:?%v",?resp.StatusCode))
?}
?defer?resp.Body.Close()
?bs,?err?:=?ioutil.ReadAll(resp.Body)
?if?err?!=?nil?{
??return?err
?}
?if?len(bs)?!=?(c.To?-?c.From?+?1)?{
??return?errors.New("下載文件分片長度錯誤")
?}
?c.Data?=?bs
?d.doneFilePart[c.Index]?=?c
?return?nil
}
//?getNewRequest?創(chuàng)建一個request
func?(d?FileDownloader)?getNewRequest(method?string)?(*http.Request,?error)?{
?r,?err?:=?http.NewRequest(
??method,
??d.url,
??nil,
?)
?if?err?!=?nil?{
??return?nil,?err
?}
?r.Header.Set("User-Agent",?"mojocn")
?return?r,?nil
}
//mergeFileParts?合并下載的文件
func?(d?FileDownloader)?mergeFileParts()?error?{
?log.Println("開始合并文件")
?path?:=?filepath.Join(d.outputDir,?d.outputFileName)
?mergedFile,?err?:=?os.Create(path)
?if?err?!=?nil?{
??return?err
?}
?defer?mergedFile.Close()
?hash?:=?sha256.New()
?totalSize?:=?0
?for?_,?s?:=?range?d.doneFilePart?{
??mergedFile.Write(s.Data)
??hash.Write(s.Data)
??totalSize?+=?len(s.Data)
?}
?if?totalSize?!=?d.fileSize?{
??return?errors.New("文件不完整")
?}
?//https://download.jetbrains.com/go/goland-2020.2.2.dmg.sha256?_ga=2.223142619.1968990594.1597453229-1195436307.1493100134
?if?hex.EncodeToString(hash.Sum(nil))?!=?"3af4660ef22f805008e6773ac25f9edbc17c2014af18019b7374afbed63d4744"?{
??return?errors.New("文件損壞")
?}?else?{
??log.Println("文件SHA-256校驗成功")
?}
?return?nil
}
Github Action 運行結(jié)果
Github Action Run 日志[1]
Run?go?run?main.go
2020/08/15?02:15:31?開始[9]下載from:376446150?to:418273495
2020/08/15?02:15:31?開始[0]下載from:0?to:41827349
2020/08/15?02:15:31?開始[1]下載from:41827350?to:83654699
2020/08/15?02:15:31?開始[5]下載from:209136750?to:250964099
2020/08/15?02:15:31?開始[6]下載from:250964100?to:292791449
2020/08/15?02:15:31?開始[7]下載from:292791450?to:334618799
2020/08/15?02:15:31?開始[2]下載from:83654700?to:125482049
2020/08/15?02:15:31?開始[8]下載from:334618800?to:376446149
2020/08/15?02:15:31?開始[4]下載from:167309400?to:209136749
2020/08/15?02:15:31?開始[3]下載from:125482050?to:167309399
2020/08/15?02:15:36?開始合并文件
2020/08/15?02:15:38?文件SHA-256校驗成功
?文件下載完成耗時:?7.169149?second
4. 附錄
源碼[2] GithubAction 運行日志[3] HTTP/Headers/Range[4] HTTP/Range_requests[5]
本文作者:Eric Zhou
原文鏈接:https://mojotv.cn/go/go-range-download
本文作者:Eric Zhou
原文鏈接:https://mojotv.cn/go/go-range-download
Github Action Run 日志: https://github.com/mojocn/flash/runs/987304235?check_suite_focus=true
[2]源碼: https://github.com/mojocn/flash
[3]GithubAction 運行日志: https://github.com/mojocn/flash/runs/987304235?check_suite_focus=true
[4]HTTP/Headers/Range: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Range
[5]HTTP/Range_requests: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Range_requests
推薦閱讀

