Go实现一个并发下载器

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

Idea

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

实现

探针

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

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
}

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

下载器

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

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请求完成后,数据将写入函数调用时传递的通道。
当下载开始后,不需要等待下载完成,可以直接开始合并分块文件,原理在于golang的channel本身就具有并发的属性。从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是一门天然支持并发的语言,利用该特性我们可以大大提升程序的效率。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/512421.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C++面向对象】C++考试题库管理系统(源码)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

构建第一个ArkTS应用(FA模型)

创建ArkTS工程 若首次打开DevEco Studio&#xff0c;请点击Create Project创建工程。如果已经打开了一个工程&#xff0c;请在菜单栏选择File > New > Create Project来创建一个新工程。选择Application应用开发&#xff08;本文以应用开发为例&#xff0c;Atomic Servi…

深入理解Java内存模型及其作用

目录 1.前言 2.为什么要有 Java 内存模型&#xff1f; 2.1 一致性问题 2.2 重排序问题 3.Java 内存模型的定义 4.规范内容 4.1 主内存和工作内存交互规范 4.2 什么是 happens-before 原则&#xff1f; 1.前言 当问到 Java 内存模型的时候&#xff0c;一定要注意&#…

【轻松上手】透明屏安装教程,一步到位,让您轻松享受透明视界

透明屏以其独特的视觉效果和广泛的应用场景&#xff0c;越来越受到人们的青睐。想要轻松享受透明视界&#xff0c;正确的安装步骤至关重要。下面&#xff0c;我们将为您提供一份简单明了的透明屏安装教程&#xff0c;让您一步到位&#xff0c;轻松上手。 第一步&#xff1a;准备…

STM32学习和实践笔记(4): 分析和理解GPIO_InitTypeDef GPIO_InitStructure (a)

深入分析及学习一下上面这一段代码的构成与含义。 首先&#xff0c;这个GPIO_InitTypeDef GPIO_InitStructure;其实与int a 是完全类似的语法格式以及含义。 GPIO_InitStructure就相当于a这样一个变量。不过从这个变量的名字可以知道&#xff0c;这是一个用于GPIO初始化的结构…

Java | Leetcode Java题解之第7题整数反转

题目&#xff1a; 题解&#xff1a; class Solution {public int reverse(int x) {int rev 0;while (x ! 0) {if (rev < Integer.MIN_VALUE / 10 || rev > Integer.MAX_VALUE / 10) {return 0;}int digit x % 10;x / 10;rev rev * 10 digit;}return rev;} }

MySQL函数大全

目录 一、数值类函数 1、ABS 2、SQRT 3、POW 4、MOD 5、CEIL 6、FLOOR 7、RAND 8、ROUND 9、SIGN 二、聚合函数 三、字符串函数 1、LENGTH 2、CHAR_LENGTH 3、CONCAT 4、INSERT 5、LOWER 6、UPPER 7、LEFT 8、RIGHT 9、TRIM 10、REPLACE 11、SUBSTRING …

正则表达式(1)

文章目录 专栏导读1、match2、匹配目标3、通用匹配4、常用匹配规则表格 专栏导读 ✍ 作者简介&#xff1a;i阿极&#xff0c;CSDN 数据分析领域优质创作者&#xff0c;专注于分享python数据分析领域知识。 ✍ 本文录入于《python网络爬虫实战教学》&#xff0c;本专栏针对大学生…

前端调试工具之Chrome Elements、Network、Sources、TimeLine调试

常用的调试工具有Chrome浏览器的调试工具&#xff0c;火狐浏览器的Firebug插件调试工具&#xff0c;IE的开发人员工具等。它们的功能与使用方法大致相似。Chrome浏览器简洁快速&#xff0c;功能强大这里主要介绍Chrome浏览器的调试工具。 打开 Google Chrome 浏览器&#xff0c…

Java多线程实战-从零手搓一个简易线程池(三)线程工厂,核心线程与非核心线程逻辑实现

&#x1f3f7;️个人主页&#xff1a;牵着猫散步的鼠鼠 &#x1f3f7;️系列专栏&#xff1a;Java全栈-专栏 &#x1f3f7;️本系列源码仓库&#xff1a;多线程并发编程学习的多个代码片段(github) &#x1f3f7;️个人学习笔记&#xff0c;若有缺误&#xff0c;欢迎评论区指正…

SWM341系列应用(上位机应用)

SWM341系列之上位机应用 1、分级图像和PNG、JPG的应用 现象&#xff1a;客户使用SWM34SVET6HMI_0.4.1版本上位机进行UI界面布局&#xff0c;反馈在模拟运行时&#xff08;PC端&#xff09;流畅&#xff0c;在Demo平台&#xff08;设备端&#xff09;运行卡顿。 分析及解决&…

基于SpringBoot+微信小程序的图书借阅管理系统(包运行调试)

介绍 系统介绍 是一套图书借阅管理系统&#xff0c;包括用户小程序以及后台管理系统。 前台商城系统包含用户注册登录、首页门户、图书查询、在线借阅、个人中心、我的信息、我的借阅、押金充值。 后台管理系统包含统计分析、用户管理、分类管理、图书管理、借阅管理、管理员…

Unknown redis exception; event execu tor terminated;解决

最近查看服务器日记是不是报发现有台服务器报错&#xff1a; rocessing failed; nested exception is org.springframework.data.redis.RedisSystemException: Unknown redis exception; nested exception is java.util.concurrent.RejectedExecutionException: event execu …

升降梯人数识别摄像机

升降梯人数识别摄像机是一种智能监测设备&#xff0c;主要用于实时识别和计算升降梯内乘客的数量。通过搭载先进的图像识别技术和人工智能算法&#xff0c;该设备可以准确监测乘客进出数量&#xff0c;提供重要数据支持和信息反馈&#xff0c;帮助管理人员有效管理升降梯运行&a…

经久耐用耐强腐蚀PFA材质气体洗涤瓶全氟烷氧基树脂尾气吸收瓶

PFA洗气瓶是一种常用于净化和干燥各种气体的实验室器皿&#xff0c;以去除其中的水分、油脂、颗粒物等杂质&#xff0c;从而使需要用到的气体满足实验要求。 PFA气体吸收瓶 PFA洗气瓶的工作原理&#xff1a; 主要是通过液体吸收、溶解或发生化学反应来去除气体中的杂质。在洗气…

【软件工程】详细设计(二)

这里是详细设计文档的第二部分。前一部分点这里 4. 学生端模块详细设计 学生端模块主要由几个组件构成&#xff1a;学生登录界面&#xff0c;成绩查询界面等界面。因为学生端的功能相对来说比较单一&#xff0c;因此这里只给出两个最重要的功能。 图4.1 学生端模块流程图 4.…

牛客网BC-71 三角形判断(操作符注意事项)

例题如下 这道题的编程很容易实现&#xff0c;但恰恰因为太简单导致容易忘记注意事项 代码如下 #include<stdio.h> int main() {int a 0,b 0,c 0;while(scanf("%d%d%d",&a,&b,&c)!EOF){if(ab>c&&ac>b&&bc>a){ //三…

零基础如何自学人工智能?推荐优秀的学习路径及方法

人工智能&#xff08;AI&#xff09;是一个广泛且复杂的领域&#xff0c;自学AI可能是一项艰巨的任务&#xff0c;但只要有兴趣和决心&#xff0c;这绝对是可能的。以下是一个零基础自学人工智能的学习路径&#xff0c;旨在帮助那些只有兴趣&#xff0c;但缺乏背景知识的人。 *…

C语言第三十九弹---预处理(上)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 预处理 1、预定义符号 2、#define定义常量 3、#define定义宏 4、带有副作用的宏参数 5、宏替换的规则 6、宏和函数的对比 总结 在C语言中&#xff0c;预处…

宝塔面板docker管理器安装后,返回docker菜单页,提示当前未安装docker或docker-compose 未安装,再次安装后,依然提示未安装。

宝塔面板docker管理器安装后&#xff0c;返回docker菜单页&#xff0c;提示当前未安装docker或docker-compose 未安装&#xff0c;再次安装后&#xff0c;依然提示未安装。 OS: debian 11 BT: 7.9.8 解答&#xff1a; 您好&#xff0c;服务器终端执行以下命令截图看一下命令…