技术标签: Golang G Golang并发下载\上传 分块下载 并发下载 计算机
大家都用过迅雷等下载工具,特点就是支持并发下载,断点续传。
主要讲三个方面,如何HTTP的并发下载、通过Golang进行多协程开发、如何断点续传。
想要并发下载,就是把下载内容分块,然后并行下载这些块。这就要求服务器能够支持分块获取数据。大迅雷、电驴这种都有自己的协议,thunder://
这种,我们只研究原理,就说说HTTP协议对于并发的支持。
HTTP头 | 对应值 | 含义 | ||||
Content-Length | 14247 | HTTP响应的Body大小,下载的时候,Body就是文件,也可以认为是文件大小,单位是比特 | ||||
Content-Disposition | inline; filename=”bryce.jpg” | 是MIME协议的扩展,MIME协议指示MIME用户代理如何显示附加的文件。当浏览器接收到头时,它会激活文件下载。这里还包含了文件名 | ||||
Accept-Ranges | bytes | 允许客户端以bytes的形式获取文件 | ||||
Range | bytes=0-511 | 分块获取数据,这里表示获取第0到第511的数据,共512字节 |
Nginx官网ngx_http_slice_module: Module ngx_http_slice_module
Nginx的ngx_http_slice_module模块是用来支持Range回源的。 ngx_http_slice_module从Nginx的1.9.8版本开始有的。启用ngx_http_slice_module模块需要在编译Nginx时,加参数--with-http_slice_module。
location / {
slice 1m;
proxy_cache cache;
proxy_cache_key $uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 1h;
proxy_pass http://localhost:8000;
}
Module ngx_http_slice_module 缓存配置文件
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
keepalive_timeout 65;
#cache
proxy_cache_path /data/cache
keys_zone=cache_my:100m
levels=1:1
inactive=12d
max_size=200m;
server {
listen 80;
server_name localhost;
location / {
#slice
slice 1k;
proxy_cache cache_my;
proxy_cache_key $uri$is_args$args$slice_range;
add_header X-Cache-Status $upstream_cache_status;
proxy_set_header Range $slice_range;
proxy_cache_valid 200 206 3h;
proxy_pass http://192.168.1.10:80;
proxy_cache_purge PURGE from 127.0.0.1;
}
}
}
三、运行结果
查看的是源站的日志
index.html文件大小为5196
curl www.*****.com/index.html -r 0-1024
四 GO实例代码
package main
import (
"flag"
"fmt"
"io"
"log"
"math"
"mime"
"net/http"
"net/http/httputil"
"os"
"strings"
"sync"
"time"
)
const (
DEFAULT_DOWNLOAD_BLOCK int64 = 4096
)
type GoGet struct {
Url string
Cnt int
DownloadBlock int64
CostomCnt int
Latch int
Header http.Header
MediaType string
MediaParams map[string]string
FilePath string // 包括路径和文件名
GetClient *http.Client
ContentLength int64
DownloadRange [][]int64
File *os.File
TempFiles []*os.File
WG sync.WaitGroup
}
func NewGoGet() *GoGet {
get := new(GoGet)
get.FilePath = "./"
get.GetClient = new(http.Client)
flag.Parse()
get.Url = *urlFlag
get.DownloadBlock = DEFAULT_DOWNLOAD_BLOCK
return get
}
var urlFlag = flag.String("u", "http://7b1h1l.com1.z0.glb.clouddn.com/bryce.jpg", "Fetch file url")
// var cntFlag = flag.Int("c", 1, "Fetch concurrently counts")
func main() {
get := NewGoGet()
download_start := time.Now()
req, err := http.NewRequest("HEAD", get.Url, nil)
resp, err := get.GetClient.Do(req)
get.Header = resp.Header
if err != nil {
log.Panicf("Get %s error %v.\n", get.Url, err)
}
get.MediaType, get.MediaParams, _ = mime.ParseMediaType(get.Header.Get("Content-Disposition"))
get.ContentLength = resp.ContentLength
get.Cnt = int(math.Ceil(float64(get.ContentLength / get.DownloadBlock)))
if strings.HasSuffix(get.FilePath, "/") {
get.FilePath += get.MediaParams["filename"]
}
get.File, err = os.Create(get.FilePath)
if err != nil {
log.Panicf("Create file %s error %v.\n", get.FilePath, err)
}
log.Printf("Get %s MediaType:%s, Filename:%s, Size %d.\n", get.Url, get.MediaType, get.MediaParams["filename"], get.ContentLength)
if get.Header.Get("Accept-Ranges") != "" {
log.Printf("Server %s support Range by %s.\n", get.Header.Get("Server"), get.Header.Get("Accept-Ranges"))
} else {
log.Printf("Server %s doesn't support Range.\n", get.Header.Get("Server"))
}
log.Printf("Start to download %s with %d thread.\n", get.MediaParams["filename"], get.Cnt)
var range_start int64 = 0
for i := 0; i < get.Cnt; i++ {
if i != get.Cnt-1 {
get.DownloadRange = append(get.DownloadRange, []int64{range_start, range_start + get.DownloadBlock - 1})
} else {
// 最后一块
get.DownloadRange = append(get.DownloadRange, []int64{range_start, get.ContentLength - 1})
}
range_start += get.DownloadBlock
}
// Check if the download has paused.
for i := 0; i < len(get.DownloadRange); i++ {
range_i := fmt.Sprintf("%d-%d", get.DownloadRange[i][0], get.DownloadRange[i][1])
temp_file, err := os.OpenFile(get.FilePath+"."+range_i, os.O_RDONLY|os.O_APPEND, 0)
if err != nil {
temp_file, _ = os.Create(get.FilePath + "." + range_i)
} else {
fi, err := temp_file.Stat()
if err == nil {
get.DownloadRange[i][0] += fi.Size()
}
}
get.TempFiles = append(get.TempFiles, temp_file)
}
go get.Watch()
get.Latch = get.Cnt
for i, _ := range get.DownloadRange {
get.WG.Add(1)
go get.Download(i)
}
get.WG.Wait()
for i := 0; i < len(get.TempFiles); i++ {
temp_file, _ := os.Open(get.TempFiles[i].Name())
cnt, err := io.Copy(get.File, temp_file)
if cnt <= 0 || err != nil {
log.Printf("Download #%d error %v.\n", i, err)
}
temp_file.Close()
}
get.File.Close()
log.Printf("Download complete and store file %s with %v.\n", get.FilePath, time.Now().Sub(download_start))
defer func() {
for i := 0; i < len(get.TempFiles); i++ {
err := os.Remove(get.TempFiles[i].Name())
if err != nil {
log.Printf("Remove temp file %s error %v.\n", get.TempFiles[i].Name(), err)
} else {
log.Printf("Remove temp file %s.\n", get.TempFiles[i].Name())
}
}
}()
}
func (get *GoGet) Download(i int) {
defer get.WG.Done()
if get.DownloadRange[i][0] > get.DownloadRange[i][1] {
return
}
range_i := fmt.Sprintf("%d-%d", get.DownloadRange[i][0], get.DownloadRange[i][1])
log.Printf("Download #%d bytes %s.\n", i, range_i)
defer get.TempFiles[i].Close()
req, err := http.NewRequest("GET", get.Url, nil)
req.Header.Set("Range", "bytes="+range_i)
resp, err := get.GetClient.Do(req)
defer resp.Body.Close()
if err != nil {
log.Printf("Download #%d error %v.\n", i, err)
} else {
cnt, err := io.Copy(get.TempFiles[i], resp.Body)
if cnt == int64(get.DownloadRange[i][1]-get.DownloadRange[i][0]+1) {
log.Printf("Download #%d complete.\n", i)
} else {
req_dump, _ := httputil.DumpRequest(req, false)
resp_dump, _ := httputil.DumpResponse(resp, true)
log.Panicf("Download error %d %v, expect %d-%d, but got %d.\nRequest: %s\nResponse: %s\n", resp.StatusCode, err, get.DownloadRange[i][0], get.DownloadRange[i][1], cnt, string(req_dump), string(resp_dump))
}
}
}
// http://stackoverflow.com/questions/15714126/how-to-update-command-line-output
func (get *GoGet) Watch() {
fmt.Printf("[=================>]\n")
}
单元测试
package tests
import (
"fmt"
"testing"
"github.com/mnhkahn/go_code/goget"
)
func TestProcess(t *testing.T) {
schedule := goget.NewGoGetSchedules(2)
schedule.SetDownloadBlock(1)
job := schedule.NextJob()
fmt.Println(job)
schedule.FinishJob(job)
job = schedule.NextJob()
fmt.Println(job)
schedule.FinishJob(job)
}
判断是否支持 Accept-Ranges : bytes
curl -i -X HEAD https://img-blog.csdnimg.cn/30f7496053534e77a036730ac40da861.png
CSDN 文件存储
阿里OSS
七牛存储
参考资料:
ngx_http_slice_module (ngx_http_slice_module) - Nginx 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云
CDN如何使用nginx负载均衡实现回源请求_BigChen_up的博客-程序员宅基地_nginx 回源
文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib
文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang
文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些
文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器
文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距
文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器
文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn
文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios
文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql
文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...
文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120
文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数