golang 基于 Web 和 CLI 的下载管理器 一

发表于 2021-02-28  1.49k 次阅读


需求

开发一个下载服务,如果能够显示下载进度,这对使用者来说是很友好的,这样的下载进度功能的实现可以基于 Web 或 CLI。

尝试自己进行需求分析和设计。例如,如何从互联网读取文件,然后将文件写入本地磁盘。通过执行这两个步骤,大体上,你就实现了一个下载器。你可以一步步添加和增强相关功能来改进它。

通过练习这个项目,你能学到:

使用 HTTP 相关包/库从网路读取文件;
使用 os 包写文件到磁盘,已经恢复功能(断点续传);
通过 goroutine 管理多个连接同时写一个文件;
使用 CLI I/O 分析输入参数并显示适当的进度条;
使用与 Web 相关的函数/库创建 Web 界面;

需求分析

1、输入下载链接
2、获取需要下载的链接
3、请求这个文件参数,比如名称,类型,大小
3.5、判断这个文件是否存在
不存在
->4、创建临时文件,占位作用,之后的获取的内容向这个文件写
存在
->4 获取文件,计算已下载量,继续下载 (断点续传)
5、开始下载,网络请求下载(单进程,多协程)
6、写入文件,显示进程
7、下载完成,结束
完成的流程差不多是这样子的,根据流程可以先做一个简单版本 单进程版 之后可以在根据单进程版修改成并发版本 在修改断点续传并发版本
单进程版 -> 断点续传单进程版 -> 并发版 -> 并发断点续传版

简单单进程版

请求这个文件参数,比如名称,类型,大小

首先获取需要下载的文件,可以是zip,exe,jpg等,需要判断这个文件是否可以下载
我们只需要需要下载的文件信息,使用HEAD请求就可以了(其实http.Get方法也可以,只要我们不获取消息体也是不下载消息提的,但这里我们严谨一些)

url := "http://ybook.iapi.im/header-leg.jpg"
request, err := http.NewRequest("HEAD", url, nil) //自定义请求
if err != nil{
    panic(err)
}

client := http.Client{}
response, err := client.Do(request)  //请求
if err != nil{
    panic(err)
}

fmt.Println(response.Header)  //消息头
//map[Accept-Ranges:[bytes] Cache-Control:[max-age=2592000] Connection:[keep-alive] Content-Length:[572356] Content-Type:[image/jpeg] Date:[Tue, 23 Feb 2021 12:00:57 GMT] Etag:["6032775a-8bbc4"] Ex
//pires:[Thu, 25 Mar 2021 12:00:57 GMT] Last-Modified:[Sun, 21 Feb 2021 15:08:10 GMT] Server:[nginx]]

这里我们通过消息头中
Accept-Ranges 可以得知文件可以断点续传
Content-Length 可以得知文件大小
Content-Type 可以得知文件类型
因为我们这个功能最终需要做到断点续传,就不去管不能断点续传的文件了,至此我们就能获取需要下载的文件,文件大小了,因为response.Header.Get获取的内容都是string内容,这里我们需要转一下

if response.Header.Get("Accept-Ranges") != "bytes" {
    panic(errors.New("当前文件不支持下载"))
}

fileName := path.Base(url)
fileSize, _ := strconv.Atoi(response.Header.Get("Content-Length"))

fmt.Printf("文件名称:%s, 文件大小:%d \n", fileName, fileSize)
//文件名称:header-leg.jpg, 文件大小:572356

创建临时文件,占位

根据文件名和大小就可以通过os.Create创建一个文件了

downloadFile, err := os.Create("./" + fileName)
if err != nil {
    panic(err)
}
defer downloadFile.Close()

开始下载内容

文件已经创建好了,开始下载

response, err = http.Get(url)
if err != nil {
    panic(err)
}

defer response.Body.Close()
content := make([]byte, 1024 * 4)

for {
    read, err := response.Body.Read(content)
    if read > 0 {
        downloadFile.Write(content[0:read])
    }

    if err != nil {
        if err != io.EOF {
            panic(err)
        }
        break
    }
}

errio.EOF就说明读取结束了
运行后,可以下载了,下载完成后退出,打开文件也可以正常打开
但这样我们不知道下载到多少了,盯着感觉很傻,可以简单加一个进度

response, err = http.Get(url)
if err != nil {
    panic(err)
}

defer response.Body.Close()
content := make([]byte, 1024 * 4)

downloadedSize := 0
for {
    read, err := response.Body.Read(content)
    if read > 0 {
        downloadFile.Write(content[0:read])
        downloadedSize += read
        fmt.Printf("\r当前下载量 %d/%d", downloadedSize, fileSize)
    }

    if err != nil {
        if err != io.EOF {
            panic(err)
        }
        break
    }
}

这样就好多了,写入文件的时候,打印当前下载进度
这样我们的 简单单进程版 下载器就完成了

断点续传单进程版

通过一次次启动程序,我们看到下载的内容正常下载,但如果我们在下载进行是关闭了程序,在执行的时候,会把原来的文件删除,重新下载,之前的下载就白费了
读取当前的文件大小,判断是否可以再次续传,再通过Range指定开始下载节点下载,下载内容写入到文件中

downloadFile, err := os.OpenFile("./" + fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
    panic(err)
}

fileInfo, err := downloadFile.Stat()
if err != nil {
    panic(err)
}

if fileInfo.Size() == int64(fileSize) {
    panic(errors.New("当前文件已经下载完成"))
}

request, err = http.NewRequest("GET", url, nil)
if err != nil {
    panic(err)
}
request.Header.Add("Range", fmt.Sprintf("bytes=%d-", fileInfo.Size()))
response, err = client.Do(request)
if err != nil {
    panic(err)
}

defer response.Body.Close()
content := make([]byte, 1024 * 4)

downloadedSize := int(fileInfo.Size())
for {
    read, err := response.Body.Read(content)
    if read > 0 {
        downloadFile.Write(content[0:read])
        downloadedSize += read
        fmt.Printf("\r当前下载量 %d/%d", downloadedSize, fileSize)
    }

    if err != nil {
        if err != io.EOF {
            panic(err)
        }
        break
    }
}

测试多次执行停止,可以正常下载内容并可以正常打开,断点续传功能完成

单进程版完整代码

package main

import (
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "path"
    "strconv"
)

func main() {
    //需要下载的文件
    url := "http://ybook.iapi.im/header-leg.jpg"

    //获取文件基础信息
    request, err := http.NewRequest("HEAD", url, nil)  
    if err != nil{
        panic(err)
    }

    client := http.Client{}
    response, err := client.Do(request)
    if err != nil{
        panic(err)
    }

    fmt.Println(response.Header)
    //map[Accept-Ranges:[bytes] Cache-Control:[max-age=2592000] Connection:[keep-alive] Content-Length:[572356] Content-Type:[image/jpeg] Date:[Tue, 23 Feb 2021 12:00:57 GMT] Etag:["6032775a-8bbc4"] Ex
    //pires:[Thu, 25 Mar 2021 12:00:57 GMT] Last-Modified:[Sun, 21 Feb 2021 15:08:10 GMT] Server:[nginx]]
    if response.Header.Get("Accept-Ranges") != "bytes" {
        panic(errors.New("当前文件不支持下载"))
    }

    fileName := path.Base(url)
    fileSize, _ := strconv.Atoi(response.Header.Get("Content-Length"))

    fmt.Printf("文件名称:%s, 文件大小:%d \n", fileName, fileSize)

    //创建临时文集 || 加载已下载文件
    downloadFile, err := os.OpenFile("./" + fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)  
    if err != nil {
        panic(err)
    }

    //获取文件信息
    fileInfo, err := downloadFile.Stat()  
    if err != nil {
        panic(err)
    }

    if fileInfo.Size() == int64(fileSize) {
        panic(errors.New("当前文件已经下载完成"))
    }

    //下载文件剩余内容
    request, err = http.NewRequest("GET", url, nil)  
    if err != nil {
        panic(err)
    }
    request.Header.Add("Range", fmt.Sprintf("bytes=%d-", fileInfo.Size()))
    response, err = client.Do(request)
    if err != nil {
        panic(err)
    }

    defer response.Body.Close()
    content := make([]byte, 1024 * 4)

    downloadedSize := int(fileInfo.Size())
    for {
        //读取消息体
        read, err := response.Body.Read(content)
        if read > 0 {
            //写入文件
            downloadFile.Write(content[0:read])
            downloadedSize += read
            fmt.Printf("\r当前下载量 %d/%d", downloadedSize, fileSize)
        }

        if err != nil {
            if err != io.EOF {
                panic(err)
            }
            break
        }
    }
}

本站文章基于国际协议BY-NA-SA 4.0协议共享;
如未特殊说明,本站文章皆为原创文章,请规范转载。

2

一盏灯 一座城 找一人 一路的颠沛流离