当前位置: 首页 > news >正文

Go语言实现多协程文件下载器

文章目录

  • 前言
  • 流程图
  • 主函数
  • 下载文件
  • 初始化分片下载worker
  • 分发下载任务
  • 获取下载文件的大小
  • 下载文件分片
  • 错误重试
  • 项目演示
  • 最后

前言

你好,我是醉墨居士,最近在开发文件传输相关的项目,然后顺手写了一个多协程文件下载器,代码非常精简,核心代码只有100行左右,适合分享给大家学习使用

流程图

在这里插入图片描述

主函数

func main() {fileURL := flag.String("u", "", "downloade url of the file")flag.Parse()if *fileURL == "" {log.Println("Please input a download url")flag.Usage()return}fileDir, err := os.Getwd()if err != nil {log.Println(err)return}// 下载文件保存路径filePath := filepath.Join(fileDir, filepath.Base(*fileURL))err = downloadFile(*fileURL, filePath)if err != nil {log.Println(err)return}log.Println("download file success:", filePath)
}

下载文件

// 下载文件
func downloadFile(fileURL string, filePath string) error {log.Println("downloading file:", fileURL, "to", filePath)taskCh := make(chan [2]int64, runtime.NumCPU())wg := new(sync.WaitGroup)// 创建执行下载任务的 workererr := initWorker(fileURL, filePath, taskCh, wg)if err != nil {return fmt.Errorf("init worker failed: %v", err)}// 分发下载任务err = dispatchTask(fileURL, taskCh)if err != nil {return fmt.Errorf("dispacth task failed: %v", err)}// 等待所有下载任务完成wg.Wait()return nil
}

初始化分片下载worker

// 初始化 下载 worker
func initWorker(url string, filePath string, taskCh chan [2]int64, wg *sync.WaitGroup) error {for i := 0; i < runtime.NumCPU(); i++ {// 打开文件句柄file, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, 0644)if err != nil {return err}wg.Add(1)go func(file *os.File, taskCh chan [2]int64) {defer wg.Done()defer file.Close()// 循环从 taskCh 中获取下载任务并下载for part := range taskCh {log.Printf("downloading part, start offset: %d, end offset: %d", part[0], part[1])// 重试下载,最大重试次数为 10 次,每次下载失败后等待 1 秒err := retryWithWaitTime(10, func() error {return downloadPart(url, file, part[0], part[1])}, time.Second)if err != nil {log.Printf("download part %d failed: %v", part, err)}}}(file, taskCh)}return nil
}

分发下载任务

// 分发下载任务
func dispatchTask(url string, taskCh chan [2]int64) error {defer close(taskCh)fileSize, err := getFileSize(url)if err != nil {return err}// 分片大小 1MBconst chunkSize = 1024 * 1024parts := fileSize / chunkSizelog.Println("file size:", fileSize, "parts:", parts, "chunk size:", chunkSize)for i := int64(0); i < parts; i++ {// 计算分片的起始和结束位置startOffset := i * chunkSizeendOffset := startOffset + chunkSize - 1// 发送下载任务taskCh <- [2]int64{startOffset, endOffset}}// 发送最后一个分片的下载任务if fileSize % chunkSize != 0 {taskCh <- [2]int64{parts * chunkSize, fileSize - 1}}return nil
}

获取下载文件的大小

// 获取文件大小
func getFileSize(url string) (int64, error) {resp, err := http.Head(url)if err != nil {return 0, err}defer resp.Body.Close()return resp.ContentLength, nil
}

下载文件分片

// 下载文件分片
func downloadPart(url string, file *os.File, startPos, endPos int64) error {req, err := http.NewRequest("GET", url, nil)if err != nil {return err}// 设置文件分片区间的请求头req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", startPos, endPos))resp, err := http.DefaultTransport.RoundTrip(req)if err != nil {return err}defer resp.Body.Close()// 如果服务器返回的状态码不是 206 Partial Content,则说明下载失败if resp.StatusCode != http.StatusPartialContent {data, err := io.ReadAll(resp.Body)if err != nil {return err}log.Println("unexpected data:", string(data))return fmt.Errorf("unexpected status code: %d", resp.StatusCode)}// 文件指针移动到分片的起始位置_, err = file.Seek(startPos, 0)if err != nil {return err}// 写入分片数据到文件_, err = io.Copy(file, resp.Body)if err != nil {return err}return nil
}

错误重试

// 重试函数
func retryWithWaitTime(retryCount int, fn func() error, waitTime time.Duration) error {var err errorfor i := 0; i < retryCount; i++ {e := fn()if e != nil {errors.Join(err, e)time.Sleep(waitTime)continue}return nil}return err
}

项目演示

在这里插入图片描述
在这里插入图片描述

最后

我是醉墨居士,如果这个项目对你有所帮助,希望你能多多支持,我们下期再见

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【Java 第六篇章】泛型
  • 聚鼎科技:装饰画怎么做盈利更快
  • C# 实现改造 GooFlow 流程图插件与数据库应用的结合
  • Linux系统性能调优实战:从基础到进阶的全方位指南
  • 【C++修炼之路 第七章】模拟实现 list 类模板
  • redis面试(四)ZSet数据结构
  • JavaScript输出数据的方法?
  • uniApp跳转外链
  • 密码学基础-数据加密
  • 【学术会议征稿】第八届力学、数学与应用物理学国际会议(ICMMAP 2024)
  • mysql 各种锁归纳总结
  • FLOW MATCHING FOR GENERATIVE MODELING 阅读笔记
  • C++ primer plus 第17 章 输入、输出和文件:用cout进行格式化
  • Hibernate Validator 数据校验框架
  • 【从零开始一步步学习VSOA开发】创建VSOA的client端
  • [ 一起学React系列 -- 8 ] React中的文件上传
  • CSS 专业技巧
  • Laravel核心解读--Facades
  • Leetcode 27 Remove Element
  • Python socket服务器端、客户端传送信息
  • Spring Cloud Alibaba迁移指南(一):一行代码从 Hystrix 迁移到 Sentinel
  • ViewService——一种保证客户端与服务端同步的方法
  • 编写高质量JavaScript代码之并发
  • 分享一份非常强势的Android面试题
  • 给第三方使用接口的 URL 签名实现
  • 看完九篇字体系列的文章,你还觉得我是在说字体?
  • 类orAPI - 收藏集 - 掘金
  • 深入浅出webpack学习(1)--核心概念
  • 一些css基础学习笔记
  • Android开发者必备:推荐一款助力开发的开源APP
  • $.ajax()参数及用法
  • (16)Reactor的测试——响应式Spring的道法术器
  • (C语言)fgets与fputs函数详解
  • (四)opengl函数加载和错误处理
  • (学习日记)2024.01.19
  • (一) springboot详细介绍
  • .cfg\.dat\.mak(持续补充)
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .net core使用RPC方式进行高效的HTTP服务访问
  • .net framework4与其client profile版本的区别
  • .NET Micro Framework初体验
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .net 反编译_.net反编译的相关问题
  • .NET 设计模式初探
  • .net 生成二级域名
  • .NET 中各种混淆(Obfuscation)的含义、原理、实际效果和不同级别的差异(使用 SmartAssembly)
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .NET/C# 编译期能确定的字符串会在字符串暂存池中不会被 GC 垃圾回收掉
  • .NET和.COM和.CN域名区别
  • .NET命令行(CLI)常用命令
  • .NET轻量级ORM组件Dapper葵花宝典
  • .NET值类型变量“活”在哪?
  • .NET中 MVC 工厂模式浅析
  • [04] Android逐帧动画(一)
  • [10] CUDA程序性能的提升 与 流