Go源码实现使用多线程并发下载大文件的功能

摘要:Go语言编码实现了使用多线程并发下载文件的功能。

1. 代码流程介绍

  1. 获取系统的CPU核心数量,并将其作为线程数的参考值,并打印出来。

  2. 定义要下载的文件的URL、线程数和输出文件名。

  3. 使用`getFileSize()`函数获取文件大小,并打印出来。

  4. 根据文件大小和线程数计算文件块大小,如果是最后一次线程的结尾设置为文件结尾,确保文件下载的完整性。

  5. 创建一个等待组(`sync.WaitGroup`),用于确保所有下载完成后再合并文件。

  6. 创建一个通道(`chunkPaths`),用于接收下载完成的文件块路径。

  7. 启动多个goroutine并发下载文件块,每个goroutine负责下载指定范围的文件块。

  8. 每个goroutine使用`downloadChunk()`函数下载文件块,并将下载完成的文件块路径发送到通道。

  9. 等待所有下载完成,然后关闭通道,表示所有文件块都已下载完成。

  10. 创建一个输出文件。

  11. 使用`mergeChunk()`函数将下载的文件块合并到输出文件中,并在合并过程中打印合并成功或失败的信息。

  12. 在合并完成后,删除临时的文件块。

  13. 打印文件下载完成的消息。

源码通过并发下载文件块,利用多线程来加快文件下载的速度。每个线程负责下载文件的一个部分,下载完成后将文件块合并到最终的输出文件中。通过合理设置线程数,可以充分利用可用的CPU资源,提高下载效率。

请注意,代码中使用了`http`和`os`包来进行文件下载和操作,需要保证网络连接正常,并且有足够的权限来创建和删除文件。

2. Go完整源码

Go 复制代码
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"runtime"
	"sync"
)

const (
	fileURL = "http://example.com/large-file.zip" // 要下载的文件URL
	// threads    = 5                                                                                                // 并发下载的线程数
	outputFile = "output.zip" // 下载完成后的输出文件名
)

func main() {

	// CPU数量作为线程数量
    numCPU := runtime.NumCPU()
	fmt.Println("CPU核心数量:", numCPU)
	threads := numCPU
	fmt.Println("多线程数量:", threads)
	fmt.Println("开始下载文件...")

	// 获取文件大小
	fileSize, err := getFileSize(fileURL)
	if err != nil {
		fmt.Println("无法获取文件大小:", err)
		return
	}
	fmt.Println("文件大小:", fileSize, "bytes")

	// 计算文件块大小
	chunkSize := fileSize / int64(threads)

	// 创建等待组,确保所有下载完成后再合并文件
	var wg sync.WaitGroup
	wg.Add(threads)

	// 创建一个通道用于接收下载完成的文件块路径
	chunkPaths := make(chan string, threads)

	// 启动多个 goroutine 并发下载文件块
	for i := 0; i < threads; i++ {
		go func(index int) {
			defer wg.Done()

			start := int64(index) * chunkSize
			end := start + chunkSize - 1
			// 如果是最后一次线程的结尾设置为文件结尾,确保文件下载的完整性

			if index == threads-1 {
				end = fileSize - 1
			}

			fmt.Printf("线程 %d 开始下载:%d-%d\n", index, start, end)
			chunkPath, err := downloadChunk(fileURL, start, end)
			if err != nil {
				fmt.Printf("线程 %d 下载失败:%v\n", index, err)
			} else {
				fmt.Printf("线程 %d 下载完成:%d-%d\n", index, start, end)
				chunkPaths <- chunkPath // 将下载完成的文件块路径发送到通道
			}
		}(i)
	}

	// 等待所有下载完成
	wg.Wait()
	close(chunkPaths) // 关闭通道,表示所有文件块都已下载完成

	// 创建一个输出文件
	output, err := os.Create(outputFile)
	if err != nil {
		fmt.Println("无法创建输出文件:", err)
		return
	}
	defer output.Close()

	// 合并下载的文件块到输出文件
	for chunkPath := range chunkPaths {
		fmt.Println("合并文件块:", chunkPath)
		err := mergeChunk(chunkPath, output)
		if err != nil {
			fmt.Printf("合并文件块失败:%v\n", err)
		} else {
			fmt.Printf("合并文件块成功:%s\n", chunkPath)
		}

		// 删除临时文件块
		err = os.Remove(chunkPath)
		if err != nil {
			fmt.Printf("删除文件块失败:%v\n", err)
		}
	}

	fmt.Println("文件下载完成。")
}

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

	if resp.StatusCode != http.StatusOK {
		return 0, fmt.Errorf("服务器返回错误: %v", resp.Status)
	}

	return resp.ContentLength, nil
}

// 下载文件块
func downloadChunk(url string, start, end int64) (string, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusPartialContent {
		return "", fmt.Errorf("服务器不支持分块下载:%v", resp.Status)
	}

	// 创建一个临时文件用于保存下载的文件块
	chunkPath := fmt.Sprintf("chunk_%d_%d.tmp", start, end)
	chunkFile, err := os.Create(chunkPath)
	if err != nil {
		return "", err
	}
	defer chunkFile.Close()

	_, err = io.Copy(chunkFile, resp.Body)
	if err != nil {
		return "", err
	}

	return chunkPath, nil
}

// 合并文件块
func mergeChunk(chunkPath string, output *os.File) error {
	chunkFile, err := os.Open(chunkPath)
	if err != nil {
		return err
	}
	defer chunkFile.Close()

	_, err = io.Copy(output, chunkFile)
	if err != nil {
		return err
	}

	return nil
}

3. 执行结果

> go run .\largefile_download_goroutine.go

CPU核心数量: 8

多线程数量: 8

开始下载文件...

文件大小: 28057414 bytes

线程 7 开始下载:24550232-28057413

线程 1 开始下载:3507176-7014351

线程 0 开始下载:0-3507175

线程 4 开始下载:14028704-17535879

线程 3 开始下载:10521528-14028703

线程 5 开始下载:17535880-21043055

线程 6 开始下载:21043056-24550231

线程 2 开始下载:7014352-10521527

线程 0 下载完成:0-3507175

线程 7 下载完成:24550232-28057413

线程 3 下载完成:10521528-14028703

线程 1 下载完成:3507176-7014351

线程 2 下载完成:7014352-10521527

线程 4 下载完成:14028704-17535879

线程 6 下载完成:21043056-24550231

线程 5 下载完成:17535880-21043055

合并文件块: chunk_0_3507175.tmp

合并文件块成功:chunk_0_3507175.tmp

合并文件块: chunk_24550232_28057413.tmp

合并文件块成功:chunk_24550232_28057413.tmp

合并文件块: chunk_10521528_14028703.tmp

合并文件块成功:chunk_10521528_14028703.tmp

合并文件块: chunk_3507176_7014351.tmp

合并文件块成功:chunk_3507176_7014351.tmp

合并文件块: chunk_7014352_10521527.tmp

合并文件块成功:chunk_7014352_10521527.tmp

合并文件块: chunk_14028704_17535879.tmp

合并文件块成功:chunk_14028704_17535879.tmp

合并文件块: chunk_21043056_24550231.tmp

合并文件块成功:chunk_21043056_24550231.tmp

合并文件块: chunk_17535880_21043055.tmp

合并文件块成功:chunk_17535880_21043055.tmp

文件下载完成。

相关推荐
Rust研习社6 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒6 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro7 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax7 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH7 小时前
Koa和Express的区别
后端
MariaH7 小时前
Koa框架的使用
后端
luckdewei9 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某10 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy10 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom10 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github