<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 7583字,需瀏覽 16分鐘

           ·

          2020-09-26 19:20

          點擊上方藍色“Go語言中文網(wǎng)”關(guān)注,回復(fù)「電子書」領(lǐng)全套Go資料
          HTTP斷點續(xù)傳多線程下載原理

          1. HTTP斷點續(xù)傳多線程下載

          一個比較常見的場景,就是斷點續(xù)傳/下載,在網(wǎng)絡(luò)情況不好的時候,可以在斷開連接以后,僅繼續(xù)獲取部分內(nèi)容. 例如在網(wǎng)上下載軟件,已經(jīng)下載了 95% 了,此時網(wǎng)絡(luò)斷了,如果不支持范圍請求,那就只有被迫重頭開始下載.但是如果有范圍請求的加持,就只需要下載最后 5% 的資源,避免重新下載.

          另一個場景就是多線程下載,對大型文件,開啟多個線程, 每個線程下載其中的某一段,最后下載完成之后, 在本地拼接成一個完整的文件,可以更有效的利用資源.

          一圖勝千言

          Golang HTTP Range Request

          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


          參考資料

          [1]

          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



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包(下圖只是部分),同時還包含學習建議:入門看什么,進階看什么。

          關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學習。


          瀏覽 54
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩无码第一页 | A片免费播放视频 | 人人做人人爱青青草视频 | 国产麻豆一区 | 日韩欧美成人在线 |