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([]byte, 32*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 办法;
相似下面这样:
这个完成的残缺代码我放在了 GitHub: https://github.com/polaris1119/downloader 。
还有两点大家可以留意下:
并发下载并不一定总是比复杂下载快,普通文件越大,并发下载的优势才能表现。不过,并发下载可以端点续传;
并发下载可以进一步优化,毕竟写文件,再翻开文件兼并,是需求时间的;
最后,再提示一次,记得本人入手完成一个哦。
【编辑引荐】
vim基础与提升(第2季):运用插件定制本人的IDE开发环境
Scrum矫捷开发运用实战课程
徒弟带徒弟学Python:项目实战4:开发Python版QQ2006聊天工具视频课程
技巧分享:多 Goroutine 如何优雅处置错误?
优化 Golang 散布式行情推送的功用瓶颈
(责任编辑:admin)