Golang多線程下載器實現(xiàn)高效快速地下載大文件
前言
多線程下載,顧名思義就是對一個文件進行切片訪問,等待所有的文件下載完成后在本地進行拼接成一個整體文件的過程。
因此可以利用 golang 的多協(xié)程對每個分片同步下載,之后再合并且進行md5校驗或者總長度校驗。
請求資源
下載文件的本質就是從服務器獲取數(shù)據(jù),更籠統(tǒng)地說就是向服務器發(fā)送 GET請求。
http1.1協(xié)議
HTTP1.1 協(xié)議(RFC2616)開始支持獲取文件的部分內(nèi)容,這為并行下載以及斷點續(xù)傳提供了技術支持:Range\Content-Range。Range參數(shù)是本地發(fā)往服務器的http頭參數(shù);Content-Range是遠程服務器發(fā)往本地http頭參數(shù)。
Range\Content-Range
range: (unit=first byte pos)-[last byte pos] : 指定第一個字節(jié)位置和最后一個字節(jié)位置。
例子說明:
- range: bytes=0-1300 : 表示第0-1300字節(jié)范圍的內(nèi)容發(fā)往遠程服務器。
- range: bytes=1301-23041: 表示第1201-23041字節(jié)范圍的內(nèi)容發(fā)往遠程服務器。
Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
例子說明:
- content-Range: bytes 0-797/1024000 : 表示0-797字節(jié)范圍內(nèi)容從服務器響應到客戶端,1024000是文件總大小。
完成http響應后,http狀態(tài)碼返回:206 表示使用斷掉續(xù)傳方式,而一般200表示不使用斷掉續(xù)傳方式。
比如:
(base) luliang@shenjian ~ % curl --location --head ‘https://download.jetbrains.com/go/goland-2020.2.2.exe’
HTTP/2 302
date: Sat, 06 May 2023 11:52:42 GMT
content-type: text/html
content-length: 138
location: https://download.jetbrains.com.cn/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: China
x-geocode: CN
x-geocity: Taiyigong
HTTP/2 200
content-type: binary/octet-stream
content-length: 338589968
date: Sat, 06 May 2023 11:51:35 GMT
last-modified: Tue, 30 Mar 2021 14:16:56 GMT
etag: “548422fa12ec990979c847cfda85a068-65”
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 f7c361bc042484d244950f166c4f320c.cloudfront.net (CloudFront)
x-amz-cf-pop: PVG52-E1
x-amz-cf-id: xkbWvLoSgdyhCV-gXgANy7pq_P4ndAHEBCznYtxiOIAuvEm5ew9Qlw==
age: 72
如果在響應的Header中存在Accept-Ranges首部(并且它的值不為 “none”),那么表示該服務器支持范圍請求(支持斷點續(xù)傳)。
可以使用 curl 發(fā)送一個 HEADER 請求來進行檢測:
(base) luliang@shenjian ~ % curl -I https://download.jetbrains.com.cn/go/goland-2020.2.2.exe
HTTP/2 200
content-type: binary/octet-stream
content-length: 338589968
date: Sat, 06 May 2023 11:55:58 GMT
last-modified: Tue, 30 Mar 2021 14:16:56 GMT
etag: “548422fa12ec990979c847cfda85a068-65”
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 cf7a8587fc03d8367e313c3f45e5b454.cloudfront.net (CloudFront)
x-amz-cf-pop: BJS9-E1
x-amz-cf-id: UDJvsOsiddSrXUF9CzkUKucO9ClpNrFrj2m-M9S4LYJADs34pMn8wA==
在上面的響應中, Accept-Ranges: bytes 表示界定范圍的單位是 bytes,這里 Content-Length 也是很有用的信息,因為它提供了要檢索的圖片的完整大??!
如果站點返回的Header中不包括Accept-Ranges,那么它有可能不支持范圍請求。一些站點會明確將其值設置為 “none”,以此來表明不支持。在這種情況下,某些應用的下載管理器可能會將暫停按鈕禁用!
Last-Modified\If-Modified-Since
利用HTTP協(xié)議頭Last-Modified\If-Modified-Since參數(shù)存儲文件最后修改日期,每次通信文件要判斷與上一次文件最后修改日期是否相同,如果不同就從0開始重新接收文件,相同則繼續(xù)。Last-Modified 是由服務器往客戶端發(fā)送的 HTTP 頭,而If-Modified-Since 則是由客戶端往服務器發(fā)送的頭。
例如:
- Last-Modified: Fri, 22 Feb 2023 03:45:06 GMT : 服務器端返回客戶端HTTP頭信息。
- If-Modified-Since: Fri, 22 Feb 2013 03:45:02 GMT : 客戶端通過 If-Modified-Since HTTP頭將上一次服務器端發(fā)過來的 Last-Modified 時間戳發(fā)送回服務器端進行比較驗證。
NewRequest()
該NewRequest()函數(shù)的定義為:
func NewRequest(method string, url string, body io.Reader) (*Request, error)
返回一個*Request
,該結構體定義為:
type Request struct { Method string URL *url.URL Proto string // "HTTP/1.0" ProtoMajor int // 1 ProtoMinor int // 0 Header Header Body io.ReadCloser GetBody func() (io.ReadCloser, error) ContentLength int64 TransferEncoding []string Close bool Host string Form url.Values PostForm url.Values MultipartForm *multipart.Form Trailer Header RemoteAddr string RequestURI string TLS *tls.ConnectionState Cancel <-chan struct{} Response *Response ctx context.Context }
http.DefaultClient.Do()
該函數(shù)定義為:
func (c *Client) Do(req *Request) (*Response, error) { return c.do(req) }
而函數(shù) do()也返回一個 *Response,Response的結構體定義如下:
type Response struct { Status string // e.g. "200 OK" StatusCode int // e.g. 200 Proto string // e.g. "HTTP/1.0" ProtoMajor int // e.g. 1 ProtoMinor int // e.g. 0 Header Header Body io.ReadCloser ContentLength int64 TransferEncoding []string Close bool Uncompressed bool Trailer Header Request *Request TLS *tls.ConnectionState }
可以看到,Response 中有StatusCode 、Header 、Body等我們想要的信息。
因此可以打一套組合拳將Response得到:
用函數(shù)實現(xiàn)就是:
func (d *FileDownloader) getHeaderInfo() (int, error) { headers := map[string]string{ "User_Agent": userAgent, } req, err := getNewRequest(d.url, "HEADER", headers) // 得到一個 request resp, err := http.DefaultClient.Do(req) // 利用 req 發(fā)送請求,獲得一個請求 if err != nil { return 0, err } fmt.Println(req) fmt.Println(resp) fmt.Println(resp.StatusCode) // 對響應做出相應的處理 //信息響應 (100–199) //成功響應 (200–299) //重定向消息 (300–399) //客戶端錯誤響應 (400–499) //服務端錯誤響應 (500–599) if resp.StatusCode > 299 { // 如果出錯就直接返回 return 0, errors.New(fmt.Sprintf("Can't process, response is %v", resp.StatusCode)) } // 檢查是否支持斷點續(xù)傳 if resp.Header.Get("Accept-Ranges") != "bytes" { return 0, errors.New("服務器不支持文件斷點續(xù)傳") } // 支持斷點傳送時,獲取相應的信息 //獲取文件名 outputFileName, err := parseFileInfo(resp) if err != nil { return 0, errors.New(fmt.Sprintf("get file info err: %v", err)) } // 返回文件名 if d.outputFileName == "" { d.outputFileName = outputFileName } // 返回文件的長度 return strconv.Atoi(resp.Header.Get("Content-Length")) } // 返回一個 Request func getNewRequest(url, method string, headers map[string]string) (*http.Request, error) { r, err := http.NewRequest( method, url, nil, ) if err != nil { return nil, err } // 設置頭部信息,即 UserAgent 信息 for k, v := range headers { r.Header.Set(k, v) } return r, err }
獲取文件名
我們先看看 Hear 上定義的方法:
A Header represents the key-value pairs in an HTTP header. The keys should be in canonical form, as returned by CanonicalHeaderKey. Methods on (Header): Add(key string, value string) Set(key string, value string) Get(key string) string Values(key string) []string get(key string) string has(key string) bool Del(key string) Write(w io.Writer) error write(w io.Writer, trace *httptrace.ClientTrace) error Clone() http.Header sortedKeyValues(exclude map[string]bool) (kvs []http.keyValues, hs *http.headerSorter) WriteSubset(w io.Writer, exclude map[string]bool) error writeSubset(w io.Writer, exclude map[string]bool, trace *httptrace.ClientTrace) error `Header` on pkg.go.dev
里面有一個 get方法,它傳入一個 key,返回一個值。我們可以傳入一個想要的鍵從而得到想要的信息。
如果我們可以傳入一個"Content-Disposition",得到 fileName。Content-Disposition就是當用戶想把請求所得的內(nèi)容存為一個文件的時候提供一個默認的文件名。
// 或得 filename func parseFileInfo(resp *http.Response) (string, error) { contentDisposition := resp.Header.Get("Content-Disposition") if contentDisposition != "" { _, params, err := mime.ParseMediaType(contentDisposition) if err != nil { return "", err } return params["filename"], nil } filename := filepath.Base(resp.Request.URL.Path) return filename, nil }
下載文件
兩個重要的結構體:
// FileDownloader 定義下載器 type FileDownloader struct { // 待下載文件大小 fileSize int // 目標源連接 url string // 下載文件存儲名 outputFileName string // 文件切片的總數(shù) totalPart int // 文件存儲目錄 outputDir string // 已完成文件切片 doneFilePart []filePart // 文件校驗 md5 string } // 文件分片 type filePart struct { // 文件分片序號 Index int // 開始下載 byte 起點 From int // 結束byte To int // 下載得到的內(nèi)容 Data []byte }
其中一個是定義的下載器,這個下載器定義了源地址、總文件大小、文件名、文件存儲地址、md5 校驗等;另一個定義了一個分片,這個分片定義了分片的身份(編號),文件開始點、結束點以及一個存儲數(shù)據(jù)的Data。
接下來就可以初始化下載器了,填充一些基本的信息:
// NewFileDownloader 創(chuàng)建下載器(初始化) func NewFileDownloader(url, outputFileName, outputDir string, totalPart int, md5 string) *FileDownloader { if outputDir == "" { // 如果為空,就獲取當前目錄 wd, err := os.Getwd() if err != nil { log.Println(err) } outputDir = wd } return &FileDownloader{ fileSize: 0, url: url, outputFileName: outputFileName, totalPart: totalPart, doneFilePart: make([]filePart, totalPart), md5: md5, outputDir: outputDir, } }
下載分片
func (d *FileDownloader) downloadPart(c filePart) error { headers := map[string]string{ "User-Agent": userAgent, "Range": fmt.Sprintf("bytes=%v-%v", c.From, c.To), } // 或得一個 request r, err := getNewRequest(d.url, "GET", headers) if err != nil { return err } // 打印要下載的分片信息 log.Printf("開始[%d]下載from:%d to:%d\n", c.Index, c.From, c.To) resp, err := http.DefaultClient.Do(r) if resp.StatusCode > 299 { return errors.New(fmt.Sprintf("服務器錯誤狀態(tài)碼: %v", resp.StatusCode)) } // 最后關閉文件 defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { } }(resp.Body) // 讀取 Body 的響應數(shù)據(jù) bs, err := io.ReadAll(resp.Body) if err != nil { return err } if len(bs) != (c.To - c.From + 1) { return errors.New("下載文件分片長度錯誤") } c.Data = bs // c完成了后就加入到下載器中 d.doneFilePart[c.Index] = c return nil }
這個思路就是就把 Body 存儲起來,那就是有效數(shù)據(jù)。之后就可以把所有的 數(shù)據(jù)合成成一個完整文件。
合成文件
// 合并要下載的文件 func (d *FileDownloader) mergeFileParts() error { path := filepath.Join(d.outputDir, d.outputFileName) log.Println("開始合并文件") // 創(chuàng)建文件 mergedFile, err := os.Create(path) if err != nil { return err } // 最后關閉文件 defer func(mergedFile *os.File) { err := mergedFile.Close() if err != nil { } }(mergedFile) // sha256是一種密碼散列函數(shù),說白了它就是一個哈希函數(shù)。 //對于任意長度的消息,SHA256都會產(chǎn)生一個256bit長度的散列值, //稱為消息摘要,可以用一個長度為64的十六進制字符串表示。 fileMd5 := sha256.New() totalSize := 0 // 合并的工作 for _, s := range d.doneFilePart { _, err := mergedFile.Write(s.Data) if err != nil { fmt.Printf("error when merge file: %v\n", err) } fileMd5.Write(s.Data) // 更新哈希值 totalSize += len(s.Data) // 更新長度 } // 校驗文件完整性 if totalSize != d.fileSize { return errors.New("文件不完整") } // 檢驗 MD5 if d.md5 == "" { // 將整個文件進行了 Sum 運算, 該函數(shù)返回一個 16 進制串,轉成字符串之后, // 和 d.md5比較,起到了一個校驗的效果 if hex.EncodeToString(fileMd5.Sum(nil)) != d.md5 { return errors.New("文件損壞") } else { log.Println("文件SHA-256校驗成功") } } return nil }
該函數(shù)合成了新文件還對文件完整性、MD5 做了校驗。
多線程下載
func (d *FileDownloader) Run() error { // 獲取文件大小 fileTotalSize, err := d.getHeaderInfo() if err != nil { fmt.Printf("hello!!") return err } d.fileSize = fileTotalSize jobs := make([]filePart, d.totalPart) // 這里進行均分 eachSize := fileTotalSize / d.totalPart for i := range jobs { jobs[i].Index = i // 計算 form if i == 0 { jobs[i].From = 0 } else { jobs[i].From = jobs[i-1].To + 1 } // 計算 to if i < d.totalPart-1 { jobs[i].To = jobs[i].From + eachSize } else { // 最后一個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() }
該函數(shù)將文件總長度信息獲取之后,進行了等分的分片,然后開啟協(xié)程進行并發(fā)請求。
之后,我們在 main()函數(shù)中填上目標鏈接以及 md5值就可以下載了。
func main() { startTime := time.Now() url := "https://speed.hetzner.de/100MB.bin" md5 := "2f282b84e7e608d5852449ed940bfc51" downloader := NewFileDownloader(url, "", "", 8, md5) if err := downloader.Run(); err != nil { log.Fatal(err) } fmt.Printf("\n 文件下載完成耗時: %f second\n", time.Now().Sub(startTime).Seconds()) }
運行效果:
2023/05/07 19:56:48 開始[7]下載from:365989316 to:418273495
2023/05/07 19:56:48 開始[0]下載from:0 to:52284187
2023/05/07 19:56:48 開始[5]下載from:261420940 to:313705127
2023/05/07 19:56:48 開始[4]下載from:209136752 to:261420939
2023/05/07 19:56:48 開始[3]下載from:156852564 to:209136751
2023/05/07 19:56:48 開始[1]下載from:52284188 to:104568375
2023/05/07 19:56:48 開始[6]下載from:313705128 to:365989315
2023/05/07 19:56:48 開始[2]下載from:104568376 to:156852563
…………
總結
該程序的流程簡單,和爬蟲相比,更簡單,畢竟不用使用各種選擇器+正則表達式來獲取特定元素。本質上來說,就是在獲取 GET 請求,只是繞的彎比較多。
另外這里有一個獲取某個文件 md5 值的方法:
func getFileMd5(filename string) string { // 文件全路徑名 path := fmt.Sprintf("./%s", filename) pFile, err := os.Open(path) if err != nil { log.Println("打開文件失敗!") return "" } defer func(pFile *os.File) { err := pFile.Close() if err != nil { } }(pFile) md5h := md5.New() io.Copy(md5h, pFile) return hex.EncodeToString(md5h.Sum(nil)) } func main() { // 當前目錄的csv配置文件為例 fileName1 := "Tasks/Downloader/100MB.bin" fileName2 := "goland-2020.2.2.dmg" md5Val := getFileMd5(fileName2) md5Val1 := getFileMd5(fileName1) fmt.Println("配置文件的md5值:", md5Val, md5Val1) // 配置文件的md5值: 8c2e8bcad8f0612fb62c8d5bd21efb8f 2f282b84e7e608d5852449ed940bfc51 }
到此這篇關于Golang多線程下載器實現(xiàn)高效快速地下載大文件的文章就介紹到這了,更多相關Golang多線程下載器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解golang執(zhí)行Linux shell命令完整場景下的使用方法
本文主要介紹了golang執(zhí)行Linux shell命令完整場景下的使用方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2022-06-06golang語言如何將interface轉為int, string,slice,struct等類型
這篇文章主要介紹了golang語言如何將interface轉為int, string,slice,struct等類型,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12使用Golang創(chuàng)建單獨的WebSocket會話
WebSocket是一種在Web開發(fā)中非常常見的通信協(xié)議,它提供了雙向、持久的連接,適用于實時數(shù)據(jù)傳輸和實時通信場景,本文將介紹如何使用 Golang 創(chuàng)建單獨的 WebSocket 會話,包括建立連接、消息傳遞和關閉連接等操作,需要的朋友可以參考下2023-12-12