Go 实战 :如何实现 HTTP 断点续传多线程下载?

Posted Go语言中文网

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go 实战 :如何实现 HTTP 断点续传多线程下载?相关的知识,希望对你有一定的参考价值。

点击上方蓝色“ Go语言中文网 ”关注, 回复「电子书」领全套Go资料
$ 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: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
HTTP/1.1 200 OK
...
Accept-Ranges: bytes
Content-Length: 146515
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下载得到的文件内容
}

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(" %s", err)
  log.Fatal(err)
 }
 fmt.Printf("  文件下载完成耗时: %f second ", 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))
 }
 //检查是否支持 断点续传
 //https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
 if resp.Header.Get("Accept-Ranges") != "bytes" {
  return 0, errors.New("服务器不支持文件断点续传")
 }

 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 开始下载任务
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 < d.totalPart-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 ", 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("服务器错误状态码: %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 创建一个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

}
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


推荐阅读



福利

我为大家整理了一份从入门到进阶的Go学习资料礼包(下图只是部分),同时还包含学习建议:入门看什么,进阶看什么。
关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。


以上是关于Go 实战 :如何实现 HTTP 断点续传多线程下载?的主要内容,如果未能解决你的问题,请参考以下文章