基于Go语言实现一个并发下载器

 更新时间:2023年10月22日 15:51:18   作者:surzia  
这篇文章主要为大家详细介绍了如何利用GO语言实现一个并发的文件下载器,可以在不重新启动整个下载的情况下处理错误,感兴趣的小伙伴可以了解一下

本文将实现一个并发的文件下载器,可以在不重新启动整个下载的情况下处理错误。这将通过分块下载文件来实现。

Idea

首先从发出下载的HTTP请求开始,当采用HEAD option来请求要下载的文件时,在某些服务器上,返回的标头之一是Content-Length。此标头以字节为单位指定文件的大小。知道文件大小后,分派多个Goroutine,每个Goroutine都分配有一个要下载的数据范围。Goroutine发送GET请求来执行下载,该请求将具有标头Range,此标头将告诉服务器要返回多少文件。Goroutine完成下载后,数据将通过通道发回。一旦所有的Goroutines完成,将加入数据并写出文件。

实现

探针

probe模块主要负责探针功能,侦测要下载的文件是否包含Content-LengthHTTP头部。如果存在,那么会返回分块下载的文件大小,具体代码如下:

package probe

import (
	"fmt"
	"log"
	"net/http"
	"strconv"
)

type Probe struct {
	workers int
	url     string
}

func NewProbe(worker int, url string) *Probe {
	return &Probe{
		workers: worker,
		url:     url,
	}
}

func (p *Probe) GetFileSize() (int, error) {
	var size = -1

	client := &http.Client{}
	req, err := http.NewRequest("HEAD", p.url, nil)
	if err != nil {
		log.Fatal(err)
	}
	resp, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}

	if header, ok := resp.Header["Content-Length"]; ok {
		fileSize, err := strconv.Atoi(header[0])
		if err != nil {
			log.Fatal("File size could not be determined : ", err)
		}

		size = fileSize / p.workers
	} else {
		log.Fatal("File size was not provided!")
		return size, fmt.Errorf("file size was not provided.")
	}
	return size, nil
}

通过发送一条HEADHTTP请求来拿到目标文件的大小,从而确定并发下载的分块大小。

下载器

接下来是下载器部分,首先定义下载器的结构体

type Downloader struct {
	result  chan Part
	size    int
	workers int
}

下载器包括了一个由文件分块组成的channel,它的定义如下

type Part struct {
	Data  []byte
	Index int
}

包含了文件分块的数据流以及对应索引顺序。同时下载器也定义了分块下载的大小,并发数量。

func (d *Downloader) Download(index int, url string) {
	client := &http.Client{}

	// calculate offset by multiplying
	// index with size
	start := index * d.size

	// Write data range in correct format
	// I'm reducing one from the end size to account for
	// the next chunk starting there
	dataRange := fmt.Sprintf("bytes=%d-%d", start, start+d.size-1)

	// if this is downloading the last chunk
	// rewrite the header. It's an easy way to specify
	// getting the rest of the file
	if index == d.workers-1 {
		dataRange = fmt.Sprintf("bytes=%d-", start)
	}
	log.Println(dataRange)

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		// TODO: restart download
		return
	}
	req.Header.Add("Range", dataRange)
	resp, err := client.Do(req)
	if err != nil {
		// TODO: restart download
		return
	}

	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		// TODO: restart download
		return
	}

	d.result <- Part{Index: index, Data: body}
}

当执行下载操作时,该方法将向下载请求添加标头Range。此标头将指定要获取文件的哪些部分。HTTP请求完成后,数据将写入函数调用时传递的通道。

当下载开始后,不需要等待下载完成,可以直接开始合并分块文件,原理在于golangchannel本身就具有并发的属性。从channel中持续读取已经下载好的分块文件,然后根据索引顺序写入本地文件中。

func (d *Downloader) Merge(filename string) error {
	log.Println("start to merge data")

	parts := make([][]byte, d.workers)
	counter := 0
	for part := range d.result {
		counter++
		parts[part.Index] = part.Data
		if counter == d.workers {
			break
		}
	}
	log.Println("sort data as original order")

	file := []byte{}
	for _, part := range parts {
		file = append(file, part...)
	}
	log.Println("write data into buffer array")
	err := ioutil.WriteFile(filename, file, 0777)
	return err
}

运行

至此,我们可以编写一个main函数来测试并发下载器。下载的目标文件是http://212.183.159.230/512MB.zip ,大小为512MB,我们控制并发数为5,测试下载到本地的时间。

package main

import (
	"flag"
	"log"
	"time"

	"go-store/applications/downloader/download"
	"go-store/applications/downloader/probe"
)

var (
	// to test internet
	url = flag.String("url", "http://212.183.159.230/512MB.zip", "download url")
	// number of goroutines to spawn for download.
	workers = flag.Int("worker", 5, "concurrent downloader number")
	// filename for downloaded file
	filename = flag.String("file", "data.zip", "downloaded filename")
)

func main() {
	flag.Parse()
	start := time.Now()
	probe := probe.NewProbe(*workers, *url)
	size, err := probe.GetFileSize()
	if err != nil {
		panic(err)
	}

	results := make(chan download.Part, *workers)
	downloader := download.NewDownloader(results, size, *workers)
	for i := 0; i < *workers; i++ {
		go downloader.Download(i, *url)
	}

	err = downloader.Merge(*filename)
	end := time.Now()
	if err != nil {
		panic(err)
	}

	log.Println("cost time: ", end.Sub(start))
}

结果如下

song@ubuntu20-04:~/go/src/github.com/surzia/go-store/applications/downloader$ go build main.go 
song@ubuntu20-04:~/go/src/github.com/surzia/go-store/applications/downloader$ ./main 
2023/02/26 12:13:59 bytes=429496728-
2023/02/26 12:13:59 bytes=107374182-214748363
2023/02/26 12:13:59 bytes=214748364-322122545
2023/02/26 12:13:59 bytes=322122546-429496727
2023/02/26 12:13:59 bytes=0-107374181
2023/02/26 12:14:21 start to merge data
2023/02/26 12:14:21 sort data as original order
2023/02/26 12:14:23 write data into buffer array
2023/02/26 12:14:23 cost time:  24.43482453s

用时约25s。对比直接下载该文件

song@ubuntu20-04:~/Downloads$ curl http://212.183.159.230/512MB.zip -o 512M.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  512M  100  512M    0     0  14.6M      0  0:00:34  0:00:34 --:--:-- 17.9M

用时34s,并发下载器的速度提升了10s左右。

结论

Go是一门天然支持并发的语言,利用该特性我们可以大大提升程序的效率。

完整代码见github

到此这篇关于基于Go语言实现一个并发下载器的文章就介绍到这了,更多相关Go并发下载器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用IDEA配置GO语言的开发环境备忘录

    使用IDEA配置GO语言的开发环境备忘录

    最近在配置idea开发go语言时碰到很多问题,想着很多人都可能会遇到,所以下面这篇文章主要给大家介绍了关于使用IDEA配置GO语言的开发环境,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2024-05-05
  • Go语言封装HTTP请求的Curl工具包详解

    Go语言封装HTTP请求的Curl工具包详解

    在 Go 语言开发中,与 HTTP 服务进行交互是非常常见的需求,本文将分享一个用 Go 语言封装的 Curl 工具包,它提供了简洁易用的接口来进行 HTTP 请求,需要的可以了解下
    2025-03-03
  • 使用Golang实现WebSocket心跳机制

    使用Golang实现WebSocket心跳机制

    WebSocket是一种在客户端和服务器之间实现全双工通信的协议,它允许实时地传输数据,并且比传统的HTTP请求更加高效,在使用Golang构建WebSocket应用程序时,一个重要的考虑因素是如何实现心跳机制,所以本文将探讨如何使用Golang实现WebSocket心跳
    2023-11-11
  • linux下通过go语言获得系统进程cpu使用情况的方法

    linux下通过go语言获得系统进程cpu使用情况的方法

    这篇文章主要介绍了linux下通过go语言获得系统进程cpu使用情况的方法,实例分析了Go语言使用linux的系统命令ps来分析cpu使用情况的技巧,需要的朋友可以参考下
    2015-03-03
  • Go语言占位符的使用

    Go语言占位符的使用

    本文主要介绍了Go语言占位符的使用,字符串占位符在fmt包的各种打印函数中使用,下面就一起来介绍一下,感兴趣的可以了解一下
    2024-08-08
  • go语言中的udp协议及TCP通讯实现示例

    go语言中的udp协议及TCP通讯实现示例

    这篇文章主要为大家介绍了go语言中的udp协议及TCP通讯的实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • golang中使用匿名结构体的方法

    golang中使用匿名结构体的方法

    这篇文章主要介绍了golang中使用匿名结构体,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08
  • 手把手教你如何在Goland中创建和运行项目

    手把手教你如何在Goland中创建和运行项目

    欢迎来到本指南!我们将手把手地教您在Goland中如何创建、配置并运行项目,通过简单的步骤,您将迅速上手这款强大的集成开发环境(IDE),轻松实现您的编程梦想,让我们一起开启这段精彩的旅程吧!
    2024-02-02
  • 解决老版本goland无法调试新版本go的问题

    解决老版本goland无法调试新版本go的问题

    这篇文章主要给大家介绍了如何解决老版本goland无法调试新版本go的问题,文中通过代码示例给大家讲解的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2023-11-11
  • golang sync.Cond同步机制运用及实现

    golang sync.Cond同步机制运用及实现

    在 Go 里有专门为同步通信而生的 channel,所以较少看到 sync.Cond 的使用,不过它也是并发控制手段里的一种,今天我们就来认识下它的相关实现,加深对同步机制的运用
    2023-09-09

最新评论