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
- 目标下载网址:
strURL := "https://www.ucg.ac.me/skladiste/blog_44233/objava_64433/fajlovi/Computer%20Networking%20_%20A%20Top%20Down%20Approach,%207th,%20converted.pdf"
- 获取要下载文件大小及构建 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
}
注:以上的代码是不完善的,只是将核心的部分写出来,
完整的代码在: