1. 目标网址

https://www.ucg.ac.me/skladiste/blog_44233/objava_64433/fajlovi/Computer%20Networking%20_%20A%20Top%20Down%20Approach,%207th,%20converted.pdf
wget
wget https://www.ucg.ac.me/skladiste/blog_44233/objava_64433/fajlovi/Computer%20Networking%20_%20A%20Top%20Down%20Approach,%207th,%20converted.pdf

2. go 实现

  • 原理:
Content-Typeapplication/pdf Content-Length18304347
18304347

2.1 单个协程实现下载功能

0. 需要用到的库

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
)


main
  1. 目标下载网址:
 	strURL := "https://www.ucg.ac.me/skladiste/blog_44233/objava_64433/fajlovi/Computer%20Networking%20_%20A%20Top%20Down%20Approach,%207th,%20converted.pdf"   


  1. 获取要下载文件大小及构建 http 请求:
	// 先获取目标网址的头部信息
	resp, err := http.Head(strURL)
	if err != nil {
		fmt.Println("resp, err := http.Head(strURL)  报错: strURL = ", strURL)
		log.Fatalln(err)
	}

	// 文件长度,int64 类型转换为 int 类型,方便处理
	fileLength := int(resp.ContentLength)

	// 创建 get 请求,并设置请求数据主体的范围
	req, err := http.NewRequest("GET", strURL, nil)
	if err != nil {
		fmt.Println(err)
		log.Fatal(err)
	}
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", 0, fileLength))


2. 发送请求,并获取目标文件的主体

	// 获取文件主体,存入内存
	resp, err = http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("http.DefaultClient.Do(req)", "error")
		log.Fatal(err)
	}
	defer resp.Body.Close()


3. 将内存内的数据写到硬盘上

	// 创建文件
	// 可以自定义名字
	filename := path.Base(strURL)
	flags := os.O_CREATE | os.O_WRONLY
	f, err := os.OpenFile(filename, flags, 0666)
	if err != nil {
		fmt.Println("创建文件失败")
		log.Fatal("err")
	}
	defer f.Close()

	// 写入数据到硬盘文件
	buf := make([]byte, 16*1024)
	_, err = io.CopyBuffer(f, resp.Body, buf)
	if err != nil {
		if err == io.EOF {
			fmt.Println("io.EOF")
			return
		}
		fmt.Println(err)
		log.Fatal(err)
	}


  • 代码汇总(可以将它封装成一个函数):
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path"
)

func main(){
	// 测试的 URL
	strURL := "https://www.ucg.ac.me/skladiste/blog_44233/objava_64433/fajlovi/Computer%20Networking%20_%20A%20Top%20Down%20Approach,%207th,%20converted.pdf"

	resp, err := http.Head(strURL)
	if err != nil {
		fmt.Println("resp, err := http.Head(strURL)  报错: strURL = ", strURL)
		log.Fatalln(err)
	}

	// fmt.Printf("%#v\n", resp)
	fileLength := int(resp.ContentLength)

	req, err := http.NewRequest("GET", strURL, nil)
	if err != nil {
		fmt.Println(err)
		log.Fatal(err)
	}
	req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", 0, fileLength))
	// fmt.Printf("%#v", req)

	resp, err = http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("http.DefaultClient.Do(req)", "error")
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// 创建文件
	filename := path.Base(strURL)
	flags := os.O_CREATE | os.O_WRONLY
	f, err := os.OpenFile(filename, flags, 0666)
	if err != nil {
		fmt.Println("创建文件失败")
		log.Fatal("err")
	}
	defer f.Close()

	// 写入数据
	buf := make([]byte, 16*1024)
	_, err = io.CopyBuffer(f, resp.Body, buf)
	if err != nil {
		if err == io.EOF {
			fmt.Println("io.EOF")
			return
		}
		fmt.Println(err)
		log.Fatal(err)
	}

}



main.go
Content-Length18304347

打开,没有问题。功能初步实现。




2.2 多个协程实现下载功能

Accept-Ranges: None
Accept-Ranges: bytes

范围请求的使用:

Range

具体语法(单位一般为 bytes):

  • 注意:前闭后闭——[range_start, range_end]
// 

// [range_start, 到超文本末尾
Range: <unit>=<range-start>-
// 单个闭区间
// 从 range-start 到 range-end ,闭区间
Range: <unit>=<range-start>-<range-end>
// 多个闭区间
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
Content-Length1830434788
Range: <unit>=<range-start>-<range-end>

第 0 个协程(核):Range: 0-2288043                  存到硬盘上的文件 filename_0
第 1 个协程(核):Range: 2288044-4576087            存到硬盘上的文件 filename_1
第 2 个协程(核):Range: 4576088-6864131            存到硬盘上的文件 filename_2
第 3 个协程(核):Range: 6864132-9152175            存到硬盘上的文件 filename_3
第 4 个协程(核):Range: 9152176-11440219           存到硬盘上的文件 filename_4
第 5 个协程(核):Range: 11440220-13728263          存到硬盘上的文件 filename_5
第 6 个协程(核):Range: 13728264-16016307          存到硬盘上的文件 filename_6
// 第 7 个协程右边界不写,直接默认到末尾
第 7 个协程(核):Range: 16016308-                  存到硬盘上的文件 filename_7
i
// 一个协程范围下载 strURL 的目标文件
// 每次下载范围 [range_start, range_end]
// 并存到文件名为 filename 的文件中
func downloadOneFile(strURL string, filename string, range_start, range_end int) {

	// 设置 http 请求头的 Range 字段
	var Range string
	if range_end >= range_start {
		Range = fmt.Sprintf("bytes=%d-%d", range_start, range_end)
	} else if range_end == 0 {
		// 默认是从 range_start 到末尾
		Range = fmt.Sprintf("bytes=%d-", range_start)
	} else if range_start > range_end{
		fmt.Println("Range error")
		return
	}

	// 创建 http 请求头
	req, err := http.NewRequest("GET", strURL, nil)
	if err != nil {
		fmt.Println("http GET error")
		log.Fatal(err)
	}
	req.Header.Set("Range", Range)
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("请求出错")
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// 创建硬盘文件,名字为 源文件 + 第 number 个协程
	f, err := os.OpenFile(filename, os.O_WRONLY | os.O_CREATE, 0666)
	if err != nil {
		fmt.Println("创建文件失败")
		log.Fatal(err)
	}
	defer f.Close()

	// 将 http 响应体写进硬盘的文件
	buf := make([]byte, 16*1024)
	_, err = io.CopyBuffer(f, resp.Body, buf)
	if err != nil {
		if err == io.EOF {
			return
		}
		log.Fatal(err)
	}
}


然后第二个地方,也是比较核心的地方,就是将目标下载文件的大小按自己电脑核数进行分块

代码:

package setting

import (
	"os"
	"runtime"
)


// 全局变量
var (
	// 计算机核数
	Cores 		= runtime.NumCPU()
	// 分块文件数量
	FileNumbers = Cores
	// 临时存放下载文件的目录
	TemplateDir = GetDir() + "/templates"
)

// 获取运行目录
func GetDir() string {
	myDir, _ := os.Getwd()
	return myDir
}


func Mutil_downloader(strURL, filename string, fileLength int) error {
	// 按电脑核数将目标下载文件分块
	everyFileLength := fileLength / setting.FileNumbers
	println(everyFileLength)

	// 临时存放目录
	os.Mkdir(setting.TemplateDir, 0777)
	defer os.Remove(setting.TemplateDir)

	// 控制并发
	var wg sync.WaitGroup
	wg.Add(setting.FileNumbers)

	// 起始边界
	range_start := 0

	for i := 0; i < setting.FileNumbers; i++ {
		go func(i, range_start int) {
			defer wg.Done()

			range_end := range_start + everyFileLength
			// 最后一个协程范围:Range: range_left-
			if i == setting.FileNumbers - 1 {
				range_end = 0
			}

			// 当前的协程存到硬盘的文件名
			TemplateFileName := getOneTemplateFileName(filename, i)

			downloadOneFile(strURL, TemplateFileName, range_start, range_end)
		}(i, range_start)

		range_start += everyFileLength + 1
	}

	wg.Wait()

	mergeFiles(filename)

	return nil
}

注:以上的代码是不完善的,只是将核心的部分写出来,

完整的代码在: