使用 go 实现多线程下载器的方法

时间:2022-06-01 18:45:38

本篇文章我们用Go实现一个简单的多线程下载器。

1.多线程下载原理

通过判断下载文件链接返回头信息中的 Accept-Ranges 字段,如果为 bytes 则表示支持断点续传。

然后在请求头中设置 Range 字段为 bytes=[start]-[end],以请求下载文件的分段部分,然后将所有分段合并为一个完整文件。

2.构造一个下载器

?
1
2
3
4
5
6
7
type HttpDownloader struct {
    url string
    filename string
    contentLength int   
    acceptRanges bool     // 是否支持断点续传
    numThreads int        // 同时下载线程数
}

2.1 为下载器提供初始化方法

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func New(url string, numThreads int) *HttpDownloader {
    var urlSplits []string = strings.Split(url, "/")
    var filename string = urlSplits[len(urlSplits)-1]
 
    res, err := http.Head(url)
    check(err)
 
    httpDownload := new(HttpDownloader)
    httpDownload.url = url
    httpDownload.contentLength = int(res.ContentLength)
    httpDownload.numThreads = numThreads
    httpDownload.filename = filename
 
    if len(res.Header["Accept-Ranges"]) != 0 && res.Header["Accept-Ranges"][0] == "bytes" {
        httpDownload.acceptRanges = true
    } else {
        httpDownload.acceptRanges = false
    }
    
    return httpDownload
}

3.实现下载综合调度逻辑

如果不支持多线程下载,就使用单线程下载。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (h *HttpDownloader) Download() {
    f, err := os.Create(h.filename)
    check(err)
    defer f.Close()
 
    if h.acceptRanges == false {
        fmt.Println("该文件不支持多线程下载,单线程下载中:")
        resp, err := http.Get(h.url)
        check(err)
        save2file(h.filename, 0, resp)
    } else {
        var wg sync.WaitGroup
        for _, ranges := range h.Split() {
            fmt.Printf("多线程下载中:%d-%d\n", ranges[0], ranges[1])
            wg.Add(1)
            go func(start, end int) {
                defer wg.Done()
                h.download(start, end)
            }(ranges[0], ranges[1])
        }
        wg.Wait()
    }
}

3.1 下载文件分段

?
1
2
3
4
5
6
7
8
9
10
11
12
13
func (h *HttpDownloader) Split() [][]int {
    ranges := [][]int{}
    blockSize := h.contentLength / h.numThreads
    for i:=0; i<h.numThreads; i++ {
        var start int = i * blockSize
        var end int = (i + 1) * blockSize - 1
        if i == h.numThreads - 1 {
            end = h.contentLength - 1
        }
        ranges = append(ranges, []int{start, end})
    }
    return ranges
}

3.2 子线程下载函数

?
1
2
3
4
5
6
7
8
9
10
11
12
func (h *HttpDownloader) download(start, end int) {
    req, err := http.NewRequest("GET", h.url, nil)
    check(err)
    req.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", start, end))
    req.Header.Set("User-Agent", userAgent)
    
    resp, err := http.DefaultClient.Do(req)
    check(err)
    defer resp.Body.Close()
 
    save2file(h.filename, int64(start), resp)
}

4. 保存下载文件函数

?
1
2
3
4
5
6
7
8
9
10
func save2file(filename string, offset int64, resp *http.Response) {
    f, err := os.OpenFile(filename, os.O_WRONLY, 0660)
    check(err)
    f.Seek(offset, 0)
    defer f.Close()
 
    content, err := ioutil.ReadAll(resp.Body)
    check(err) 
    f.Write(content)
}

5.完整代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package main
 
import (
    "fmt"
    "strings"
    "log"
    "os"
    "net/http"
    "sync"
    "io/ioutil"
)
 
const (
    userAgent = `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36`
)
 
type HttpDownloader struct {
    url string
    filename string
    contentLength int   
    acceptRanges bool     // 是否支持断点续传
    numThreads int        // 同时下载线程数
}
 
func check(e error) {
    if e != nil {
        log.Println(e)
        panic(e)
    }
}          
 
func New(url string, numThreads int) *HttpDownloader {
    var urlSplits []string = strings.Split(url, "/")
    var filename string = urlSplits[len(urlSplits)-1]
 
    res, err := http.Head(url)
    check(err)
 
    httpDownload := new(HttpDownloader)
    httpDownload.url = url
    httpDownload.contentLength = int(res.ContentLength)
    httpDownload.numThreads = numThreads
    httpDownload.filename = filename
 
    if len(res.Header["Accept-Ranges"]) != 0 && res.Header["Accept-Ranges"][0] == "bytes" {
        httpDownload.acceptRanges = true
    } else {
        httpDownload.acceptRanges = false
    }
    
    return httpDownload
}
 
// 下载综合调度
func (h *HttpDownloader) Download() {
    f, err := os.Create(h.filename)
    check(err)
    defer f.Close()
 
    if h.acceptRanges == false {
        fmt.Println("该文件不支持多线程下载,单线程下载中:")
        resp, err := http.Get(h.url)
        check(err)
        save2file(h.filename, 0, resp)
    } else {
        var wg sync.WaitGroup
        for _, ranges := range h.Split() {
            fmt.Printf("多线程下载中:%d-%d\n", ranges[0], ranges[1])
            wg.Add(1)
            go func(start, end int) {
                defer wg.Done()
                h.download(start, end)
            }(ranges[0], ranges[1])
        }
        wg.Wait()
    }
}
 
// 下载文件分段
func (h *HttpDownloader) Split() [][]int {
    ranges := [][]int{}
    blockSize := h.contentLength / h.numThreads
    for i:=0; i<h.numThreads; i++ {
        var start int = i * blockSize
        var end int = (i + 1) * blockSize - 1
        if i == h.numThreads - 1 {
            end = h.contentLength - 1
        }
        ranges = append(ranges, []int{start, end})
    }
    return ranges
}
 
// 多线程下载
func (h *HttpDownloader) download(start, end int) {
    req, err := http.NewRequest("GET", h.url, nil)
    check(err)
    req.Header.Set("Range", fmt.Sprintf("bytes=%v-%v", start, end))
    req.Header.Set("User-Agent", userAgent)
    
    resp, err := http.DefaultClient.Do(req)
    check(err)
    defer resp.Body.Close()
 
    save2file(h.filename, int64(start), resp)
}
 
// 保存文件
func save2file(filename string, offset int64, resp *http.Response) {
    f, err := os.OpenFile(filename, os.O_WRONLY, 0660)
    check(err)
    f.Seek(offset, 0)
    defer f.Close()
 
    content, err := ioutil.ReadAll(resp.Body)
    check(err) 
    f.Write(content)
}
 
 
func main() {
    var url string = "https://dl.softmgr.qq.com/original/im/QQ9.5.0.27852.exe"
    
    httpDownload := New(url, 4)
    fmt.Printf("Bool:%v\nContent:%d\n", httpDownload.acceptRanges, httpDownload.contentLength)
 
    httpDownload.Download()
}

到此这篇关于使用 go 实现多线程下载器的文章就介绍到这了,更多相关go多线程下载器内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://www.cnblogs.com/qxcheng/p/15378472.html