您好,欢迎来到12图资源库!分享精神,快乐你我!我们只是素材的搬运工!!
  • 首 页
  • 当前位置:首页 > 开发 > WEB开发 >
    Go项目实战:一步步构建一个并发文件下载器(3)
    时间:2021-08-06 21:01 来源:网络整理 作者:网络 浏览:收藏 挑错 推荐 打印

        if resp.StatusCode == http.StatusOK && resp.Header.Get("Accept-Ranges") == "bytes" { 

            return d.multiDownload(strURL, filename, int(resp.ContentLength)) 

        } 

     

        return d.singleDownload(strURL, filename) 

     

    func (d *Downloader) multiDownload(strURL, filename string, contentLen int) error { 

        return nil 

     

    func (d *Downloader) singleDownload(strURL, filename string) error { 

      return nil 

    经过 Head 央求,判别能否支持部分央求。在原理部分曾经解说;

    假设不支持,就直接下载整个文件;

    当支持部分央求时,文件总大小经过 Head 央求的照应中的 ContentLength 可以取得。有了文件总大小和并发数,就可以知道每个部分的大小了。

    并发下载

    这部分第一个要点是如何发起部分央求:

    req, err := http.NewRequest("GET""https://apache.claz.org/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz", nil) 

    if err != nil { 

        return err 

    rangeStart := 2000 

    rangeStop := 3000 

    req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeStop)) 

     

    res, err := http.DefaultClient.Do(req) 

    我们可以将其封装成一个办法:

    func (d *Downloader) downloadPartial(strURL, filename string, rangeStart, rangeEnd, i int) { 

        if rangeStart >= rangeEnd { 

            return 

        } 

     

        req, err := http.NewRequest("GET", strURL, nil) 

        if err != nil { 

            log.Fatal(err) 

        } 

     

        req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeEnd)) 

        resp, err := http.DefaultClient.Do(req) 

        if err != nil { 

            log.Fatal(err) 

        } 

        defer resp.Body.Close() 

     

        flags := os.O_CREATE | os.O_WRONLY 

        partFile, err := os.OpenFile(d.getPartFilename(filename, i), flags, 0666

        if err != nil { 

            log.Fatal(err) 

        } 

        defer partFile.Close() 

     

        buf := make([]byte32*1024

        _, err = io.CopyBuffer(partFile, resp.Body, buf) 

        if err != nil { 

            if err == io.EOF { 

                return 

            } 

            log.Fatal(err) 

        } 

     

    // getPartDir 部分文件寄存的目录 

    func (d *Downloader) getPartDir(filename string) string { 

        return strings.SplitN(filename, "."2)[0

     

    // getPartFilename 结构部分文件的名字 

    func (d *Downloader) getPartFilename(filename string, partNum int) string { 

        partDir := d.getPartDir(filename) 

        return fmt.Sprintf("%s/%s-%d", partDir, filename, partNum) 

    经过发起 Range 央求后,将央求的内容写入本地文件中;

    为了方便后续兼并,文件名加上了序号,这就是 downloadPartial 最后一个参数的作用;

    rangeStart 和 rangeEnd 辨别表示 Range 的末尾和完毕;

    然后就是 multiDownload 办法中怎样分部分,这和并发央求多个 URL 很相似,运用 sync.WaitGroup 停止控制:

    func (d *Downloader) multiDownload(strURL, filename string, contentLen int) error { 

        partSize := contentLen / d.concurrency 

     

      // 创立部分文件的寄存目录 

        partDir := d.getPartDir(filename) 

        os.Mkdir(partDir, 0777

        defer os.RemoveAll(partDir) 

     

        var wg sync.WaitGroup 

        wg.Add(d.concurrency) 

     

        rangeStart := 0 

     

        for i := 0; i < d.concurrency; i++ { 

        // 并发央求 

            go func(i, rangeStart int) { 

                defer wg.Done() 

     

                rangeEnd := rangeStart + partSize 

          // 最后一部分,总长度不能超过 ContentLength 

                if i == d.concurrency-1 { 

                    rangeEnd = contentLen 

                } 

     

                d.downloadPartial(strURL, filename, rangeStart, rangeEnd, i) 

     

            }(i, rangeStart) 

     

            rangeStart += partSize + 1 

        } 

     

        wg.Wait() 

       

      // 兼并文件 

        d.merge(filename) 

     

        return nil 

     

    func (d *Downloader) merge(filename string) error { 

        return nil 

    计算出每个部分的大小;

    经过 sync.WaitGroup 协调并发央求;

    留意每个部分的 rangeStart 和 rangeEnd 的计算规则,特别留意最后一部分;

    一切部分都央求完成后,需求停止兼并;

    由于把每部分独自保存为文件了,所以兼并只需求按照顺序处置这些文件即可:

    func (d *Downloader) merge(filename string) error { 

        destFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666

        if err != nil { 

            return err 

        } 

        defer destFile.Close() 

     

        for i := 0; i < d.concurrency; i++ { 

            partFileName := d.getPartFilename(filename, i) 

            partFile, err := os.Open(partFileName) 

            if err != nil { 

                return err 

            } 

            io.Copy(destFile, partFile) 

            partFile.Close() 

            os.Remove(partFileName) 

        } 

     

        return nil 

    衔接顺序

    到这里,顺序的中心部分曾经完成。接上去该在 main.go 中的 Action 作如下处置:

    Action: func(c *cli.Context) error { 

      strURL := c.String("url"

      filename := c.String("output"

      concurrency := c.Int("concurrency"

      return NewDownloader(concurrency).Download(strURL, filename) 

    }, 

    到这里可以运转测试下:

    go run . --url https://apache.claz.org/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bin.tar.gz 

    不出不测的话文件会下载成功。

    03 总结

    完成了基本功用,读者冤家们可以进一步做优化、完善。比如:

    看到下载进程,体验更友好,可以参加 github.com/schollz/progressbar 库;

    可以暂停下载,然后继续下载。即端点续传;

    不支持并发下载的,支持单个下载,即完成 singleDownload 办法;

    相似下面这样:

    Go项目实战:一步步构建一个并发文件下载器

    这个完成的残缺代码我放在了 GitHub: https://github.com/polaris1119/downloader 。

    还有两点大家可以留意下:

    并发下载并不一定总是比复杂下载快,普通文件越大,并发下载的优势才能表现。不过,并发下载可以端点续传;

    并发下载可以进一步优化,毕竟写文件,再翻开文件兼并,是需求时间的;

    最后,再提示一次,记得本人入手完成一个哦。

    【编辑引荐】

    vim基础与提升(第2季):运用插件定制本人的IDE开发环境

    Scrum矫捷开发运用实战课程

    徒弟带徒弟学Python:项目实战4:开发Python版QQ2006聊天工具视频课程

    技巧分享:多 Goroutine 如何优雅处置错误?

    优化 Golang 散布式行情推送的功用瓶颈

    (责任编辑:admin)