需求
开发一个下载服务,如果能够显示下载进度,这对使用者来说是很友好的,这样的下载进度功能的实现可以基于 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
}
}
当err
是io.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
}
}
}
COMMENTS | NOTHING