断点续传客户端实现主要参考了以下文章:

https://blog.csdn.net/binyao02123202/article/details/76599949

客户端实现续传的主要是一下几点

1.客户端的下载请求要包含“Range”头部

2.客户端通过 response 回来的头部判断是否包含“Content-Range”,“Accept-Ranges”来确认服务端是否支持断点续传,如果支持则分片取数据,否则读取整个流。

客户端的基本实现参照 文章前面提到的参考的文章即可,本文就不赘述了,秉着学习的态度,博主将这个客户端实现进行了功能的完善,实现了对下载进行可配置。实现了 暂停下载,重复下载,继续下载等封装,理论上还支持退出重新打开继续下载(需对配置信息进行保存,另外,之所以说理论,是因为本文并未实现这个配置的保存)。

直接看代码:

1.TaskInfo  主要是记录子线程信息,如果是支持断点续传的话,就会开多线程进行下载,每个线程取不同的片,甚至可以是不同来源的片。

    public class TaskInfo
{
/// <summary>
/// 请求方法
/// </summary>
public string method { get; set; }
public string downloadUrl { get; set; }
public string filePath { get; set; }
/// <summary>
/// 分片起点
/// </summary>
public long fromIndex { get; set; }
/// <summary>
/// 分片终点
/// </summary>
public long toIndex { get; set; }
/// <summary>
/// 分片的总大小
/// </summary>
public long count { get { return this.toIndex - this.fromIndex + 1; } }
}

2.DownloadService 根据 TaskInfo 实现线程初始化,机一个线程任务

    public class DownloadService
{
private string downloadUrl = "";//文件下载地址
private string filePath = "";//文件保存路径
private string method = "";//方法
private long fromIndex = 0;//开始下载的位置
private long toIndex = 0;//结束下载的位置
private long count = 0;//总大小
private long size = 524288;//每次下载大小 512kb
private bool isRun = false;//是否正在进行 public bool isFinish { get; private set; } = false;//是否已下载完成
public bool isStopped { get; private set; } = true;//是否已停止 public event Action OnStart;
public event Action OnDownload;
public event Action OnFinsh; public long GetDownloadedCount()
{
return this.count - this.toIndex + this.fromIndex - 1;
} public void Stop()
{
this.isRun = false;
}
public bool Start(TaskInfo info,bool isReStart)
{
this.downloadUrl = info.downloadUrl;
this.fromIndex = info.fromIndex;
this.toIndex = info.toIndex;
this.method = info.method;
this.filePath = info.filePath;
this.count = info.count;
this.isStopped = false;
if (File.Exists(this.filePath))
{
if(isReStart)
{
File.Delete(this.filePath);
File.Create(this.filePath).Close();
}
}
else
{
File.Create(this.filePath).Close();
}
using (var file = File.Open(this.filePath, FileMode.Open))
{
this.fromIndex = info.fromIndex+file.Length;
}
if(this.fromIndex>=this.toIndex)
{
OnFineshHandler();
this.isFinish = true;
this.isStopped = true;
return false;
}
OnStartHandler();
this.isRun = true;
new Action(() =>
{
WebResponse rsp;
while (this.fromIndex < this.toIndex && isRun)
{
long to;
if (this.fromIndex + this.size >= this.toIndex - 1)
to = this.toIndex - 1;
else
to = this.fromIndex + size;
using (rsp = HttpHelper.Download(this.downloadUrl, this.fromIndex, to, this.method))
{
Save(this.filePath, rsp.GetResponseStream());
}
}
if (!this.isRun) this.isStopped = true;
if (this.fromIndex >= this.toIndex)
{
this.isFinish = true;
this.isStopped = true;
OnFineshHandler();
} }).BeginInvoke(null, null);
return true;
} private void Save(string filePath, Stream stream)
{
try
{
using (var writer = File.Open(filePath, FileMode.Append))
{
using (stream)
{
var repeatTimes = 0;
byte[] buffer = new byte[1024];
var length = 0;
while ((length = stream.Read(buffer, 0, buffer.Length)) > 0 && this.isRun)
{
writer.Write(buffer, 0, length);
this.fromIndex += length;
if (repeatTimes % 5 == 0)
{
OnDownloadHandler();
}
repeatTimes++;
}
}
}
OnDownloadHandler();
}
catch (Exception)
{
//异常也不影响
}
} private void OnStartHandler()
{
new Action(() =>
{
this.OnStart?.Invoke();
}).BeginInvoke(null, null);
}
private void OnFineshHandler()
{
new Action(() =>
{
this.OnFinsh?.Invoke();
this.OnDownload?.Invoke();
}).BeginInvoke(null, null);
}
private void OnDownloadHandler()
{
new Action(() =>
{
this.OnDownload?.Invoke();
}).BeginInvoke(null, null);
}
}

3.DownloadInfo 保存着下载信息,在初始化完成后,可保存该类的对象信息,以实现退出重进下载

    public class DownloadInfo
{
/// <summary>
/// 子线程数量
/// </summary>
public int taskCount { get; set; } = 1;
/// <summary>
/// 缓存名,临时保存的文件名
/// </summary>
public string tempFileName { get; set; }
/// <summary>
/// 是否是新任务,如果不是新任务则通过配置去分配线程
/// 一开始要设为true,在初始化完成后会被设为true,此时可以对这个 DownloadInfo 进行序列化后保存,进而实现退出程序加载配置继续下载。
/// </summary>
public bool isNewTask { get; set; } = true;
/// <summary>
/// 是否重新下载
/// </summary>
public bool isReStart { get; set; } = false;
/// <summary>
/// 任务总大小
/// </summary>
public long count { get; set; }
/// <summary>
/// 保存的目录
/// </summary>
public string saveDir { get; set; }
/// <summary>
/// 请求方法
/// </summary>
public string method { get; set; } = "get";
public string fileName { get; set; }
/// <summary>
/// 下载地址,
/// 这里是列表形式,如果同一个文件有不同来源则可以通过不同来源取数据
/// 来源的有消息需另外判断
/// </summary>
public List<string> downloadUrlList { get; set; }
/// <summary>
/// 是否支持断点续传
/// 在任务开始后,如果需要暂停,应先通过这个判断是否支持
/// 默认设为false
/// </summary>
public bool IsSupportMultiThreading { get; set; } = false;
/// <summary>
/// 线程任务列表
/// </summary>
public List<TaskInfo> TaskInfoList { get; set; } }

4.DownloadManager 一个下载任务的管理,实现了 暂停,继续,重新下载,以及下载信息初始化等

    public class DownloadManager
{
private long fromIndex = 0;//开始下载的位置
private bool isRun = false;//是否正在进行
private DownloadInfo dlInfo; private List<DownloadService> dls = new List<DownloadService>(); public event Action OnStart;
public event Action OnStop;
public event Action<long,long> OnDownload;
public event Action OnFinsh; public DownloadManager(DownloadInfo dlInfo)
{
this.dlInfo = dlInfo;
}
public void Stop()
{
this.isRun = false;
dls.ForEach(dl => dl.Stop());
OnStopHandler();
} public void Start()
{
this.dlInfo.isReStart = false;
WorkStart();
}
public void ReStart()
{
this.dlInfo.isReStart = true;
WorkStart();
} private void WorkStart()
{
new Action(() =>
{
if (dlInfo.isReStart)
{
this.Stop();
} while (dls.Where(dl => !dl.isStopped).Count() > 0)
{
if (dlInfo.isReStart) Thread.Sleep(100);
else return;
} this.isRun = true;
OnStartHandler();
//首次任务或者不支持断点续传的进入
if (dlInfo.isNewTask||(!dlInfo.isNewTask&&!dlInfo.IsSupportMultiThreading))
{
//第一次请求获取一小块数据,根据返回的情况判断是否支持断点续传
using (var rsp = HttpHelper.Download(dlInfo.downloadUrlList[0], 0, 0, dlInfo.method))
{ //获取文件名,如果包含附件名称则取下附件,否则从url获取名称
var Disposition = rsp.Headers["Content-Disposition"];
if (Disposition != null) dlInfo.fileName = Disposition.Split('=')[1];
else dlInfo.fileName = Path.GetFileName(rsp.ResponseUri.AbsolutePath); //默认给流总数
dlInfo.count = rsp.ContentLength;
//尝试获取 Content-Range 头部,不为空说明支持断点续传
var contentRange = rsp.Headers["Content-Range"];
if (contentRange != null)
{
//支持断点续传的话,就取range 这里的总数
dlInfo.count = long.Parse(rsp.Headers["Content-Range"]?.Split('/')?[1]);
dlInfo.IsSupportMultiThreading = true; //生成一个临时文件名
var tempFileName = Convert.ToBase64String(Encoding.UTF8.GetBytes(dlInfo.fileName)).ToUpper();
tempFileName = tempFileName.Length > 32 ? tempFileName.Substring(0, 32) : tempFileName;
dlInfo.tempFileName = tempFileName + DateTime.Now.ToString("yyyyMMddHHmmssfff");
///创建线程信息
///
GetTaskInfo(dlInfo); }
else
{
//不支持断点续传则一开始就直接读完整流
Save(GetRealFileName(dlInfo), rsp.GetResponseStream());
OnFineshHandler();
}
}
dlInfo.isNewTask = false;
}
//如果支持断点续传采用这个
if(dlInfo.IsSupportMultiThreading)
{
StartTask(dlInfo); //等待合并
while (this.dls.Where(td => !td.isFinish).Count() > 0 && this.isRun)
{
Thread.Sleep(100);
}
if ((this.dls.Where(td => !td.isFinish).Count() == 0))
{ CombineFiles(dlInfo);
OnFineshHandler();
}
} }).BeginInvoke(null, null);
}
private void CombineFiles(DownloadInfo dlInfo)
{
string realFilePath = GetRealFileName(dlInfo); //合并数据
byte[] buffer = new Byte[2048];
int length = 0;
using (var fileStream = File.Open(realFilePath, FileMode.CreateNew))
{
for (int i = 0; i < dlInfo.TaskInfoList.Count; i++)
{
var tempFile = dlInfo.TaskInfoList[i].filePath;
using (var tempStream = File.Open(tempFile, FileMode.Open))
{
while ((length = tempStream.Read(buffer, 0, buffer.Length)) > 0)
{
fileStream.Write(buffer, 0, length);
}
tempStream.Flush();
}
//File.Delete(tempFile);
}
}
} private static string GetRealFileName(DownloadInfo dlInfo)
{
//创建正式文件名,如果已存在则加数字序号创建,避免覆盖
var fileIndex = 0;
var realFilePath = Path.Combine(dlInfo.saveDir, dlInfo.fileName);
while (File.Exists(realFilePath))
{
realFilePath = Path.Combine(dlInfo.saveDir, string.Format("{0}_{1}", fileIndex++, dlInfo.fileName));
} return realFilePath;
} private void StartTask(DownloadInfo dlInfo)
{
this.dls = new List<DownloadService>();
if (dlInfo.TaskInfoList != null)
{
foreach (var item in dlInfo.TaskInfoList)
{
var dl = new DownloadService();
dl.OnDownload += OnDownloadHandler;
dls.Add(dl);
dl.Start(item, dlInfo.isReStart);
}
}
} private void GetTaskInfo(DownloadInfo dlInfo)
{
var pieceSize = (dlInfo.count) / dlInfo.taskCount;
dlInfo.TaskInfoList = new List<TaskInfo>();
var rand = new Random();
var urlIndex = 0;
for (int i = 0; i <= dlInfo.taskCount + 1; i++)
{
var from = (i * pieceSize); if (from >= dlInfo.count) break;
var to = from + pieceSize;
if (to >= dlInfo.count) to = dlInfo.count; dlInfo.TaskInfoList.Add(
new TaskInfo
{
method = dlInfo.method,
downloadUrl = dlInfo.downloadUrlList[urlIndex++],
filePath = Path.Combine(dlInfo.saveDir, dlInfo.tempFileName + i + ".temp"),
fromIndex = from,
toIndex = to
});
if (urlIndex >= dlInfo.downloadUrlList.Count) urlIndex = 0;
}
} /// <summary>
/// 保存内容
/// </summary>
/// <param name="filePath"></param>
/// <param name="stream"></param>
private void Save(string filePath, Stream stream)
{
try
{
using (var writer = File.Open(filePath, FileMode.Append))
{
using (stream)
{
var repeatTimes = 0;
byte[] buffer = new byte[1024];
var length = 0;
while ((length = stream.Read(buffer, 0, buffer.Length)) > 0 && this.isRun)
{
writer.Write(buffer, 0, length);
this.fromIndex += length;
if (repeatTimes % 5 == 0)
{
writer.Flush();//一定大小就刷一次缓冲区
OnDownloadHandler();
}
repeatTimes++;
}
writer.Flush();
OnDownloadHandler();
}
}
}
catch (Exception)
{
//异常也不影响
}
} private void OnStartHandler()
{
new Action(() =>
{
this.OnStart?.Invoke();
}).BeginInvoke(null, null);
}
private void OnStopHandler()
{
new Action(() =>
{
this.OnStop?.Invoke();
}).BeginInvoke(null, null);
}
private void OnFineshHandler()
{
new Action(() =>
{
for (int i = 0; i < dlInfo.TaskInfoList.Count; i++)
{
var tempFile = dlInfo.TaskInfoList[i].filePath;
File.Delete(tempFile);
}
this.OnFinsh?.Invoke();
}).BeginInvoke(null, null);
}
private void OnDownloadHandler()
{
new Action(() =>
{
long current = GetDownloadLength();
this.OnDownload?.Invoke(current, dlInfo.count);
}).BeginInvoke(null, null);
} public long GetDownloadLength()
{
if (dlInfo.IsSupportMultiThreading) return dls.Sum(dl => dl.GetDownloadedCount());
else return this.fromIndex;
}
}

以上就是这个下载的核心,使用方式也比较简单,下面是自己在winform上的简单实现效果

代码:

    public partial class DownloadForm : Form
{
private DownloadManager downloadManager; public DownloadForm()
{
InitializeComponent();
}
private void ShowLog(string log)
{
this.rtbLog.Invoke(new Action(() =>
{
this.rtbLog.Text = string.Format("{0}\r\n{1}", log,this.rtbLog.Text);
}));
}
private void btnCreateTask_Click(object sender, EventArgs e)
{ var downloadInfo = new DownloadInfo();
downloadInfo.saveDir = tbDir.Text;
downloadInfo.downloadUrlList = new List<string> {
tbUrl.Text
};
downloadInfo.taskCount = 1;
downloadManager = new DownloadManager(downloadInfo);
downloadManager.OnDownload += DownloadManager_OnDownload;
downloadManager.OnStart += DownloadManager_OnStart;
downloadManager.OnStop += DownloadManager_OnStop;
downloadManager.OnFinsh += DownloadManager_OnFinsh; ShowLog("新建任务");
} private void DownloadManager_OnStop()
{
ShowLog("暂停下载");
} private void DownloadManager_OnFinsh()
{
ShowLog("完成下载");
} private void DownloadManager_OnStart()
{
ShowLog("开始下载");
} private void DownloadManager_OnDownload(long arg1, long arg2)
{ this.lbProcess.Invoke(new Action(() =>
{ this.pgbProcess.Value = (int)(arg1 * 100.00 / arg2);
this.lbProcess.Text = string.Format("{0}/{1}", arg1, arg2);
}));
} private void btnStartDownload_Click(object sender, EventArgs e)
{
if (downloadManager == null) btnCreateTask_Click(null, null);
downloadManager.Start();
} private void btnStop_Click(object sender, EventArgs e)
{
downloadManager.Stop();
} private void btnReStart_Click(object sender, EventArgs e)
{
if (downloadManager == null) btnCreateTask_Click(null, null);
downloadManager.ReStart();
}
}

补上 HttpHelper 类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks; namespace Downloader
{
public class HttpHelper
{
public static void init_Request(ref System.Net.HttpWebRequest request)
{
request.Accept = "text/json,*/*;q=0.5";
request.Headers.Add("Accept-Charset", "utf-8;q=0.7,*;q=0.7");
request.Headers.Add("Accept-Encoding", "gzip, deflate, x-gzip, identity; q=0.9");
request.AutomaticDecompression = System.Net.DecompressionMethods.GZip;
request.Timeout = 8000;
} private static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
return true; //总是接受
} public static System.Net.HttpWebRequest GetHttpWebRequest(string url)
{
HttpWebRequest request = null;
if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase))
{
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult);
request = WebRequest.Create(url) as HttpWebRequest;
request.ProtocolVersion = HttpVersion.Version10;
}
else
{
request = WebRequest.Create(url) as HttpWebRequest;
}
return request;
}
public static WebResponse Download(string downloadUrl, long from, long to, string method)
{
var request = HttpHelper.GetHttpWebRequest(downloadUrl);
HttpHelper.init_Request(ref request);
request.Accept = "text/json,*/*;q=0.5";
request.AddRange(from, to);
request.Headers.Add("Accept-Charset", "utf-8;q=0.7,*;q=0.7");
request.Headers.Add("Accept-Encoding", "gzip, deflate, x-gzip, identity; q=0.9");
request.AutomaticDecompression = System.Net.DecompressionMethods.GZip;
request.Timeout = 120000;
request.Method = method;
request.KeepAlive = false;
request.ContentType = "application/json; charset=utf-8";
return request.GetResponse();
}
public static string Get(string url, IDictionary<string, string> param)
{
var paramBuilder = new List<string>();
foreach (var item in param)
{
paramBuilder.Add(string.Format("{0}={1}", item.Key, item.Value));
}
url = string.Format("{0}?{1}", url.TrimEnd('?'), string.Join(",", paramBuilder.ToArray()));
return Get(url);
}
public static string Get(string url)
{
try
{
var request = GetHttpWebRequest(url);
if (request != null)
{
string retval = null;
init_Request(ref request);
using (var Response = request.GetResponse())
{
using (var reader = new System.IO.StreamReader(Response.GetResponseStream(), System.Text.Encoding.UTF8))
{
retval = reader.ReadToEnd();
}
}
return retval;
}
}
catch
{ }
return null;
}
public static string Post(string url, string data)
{
try
{
var request = GetHttpWebRequest(url);
if (request != null)
{
string retval = null;
init_Request(ref request);
request.Method = "POST";
request.ServicePoint.Expect100Continue = false;
request.ContentType = "application/json; charset=utf-8";
request.Timeout = 800;
var bytes = System.Text.UTF8Encoding.UTF8.GetBytes(data);
request.ContentLength = bytes.Length;
using (var stream = request.GetRequestStream())
{
stream.Write(bytes, 0, bytes.Length);
}
using (var response = request.GetResponse())
{
using (var reader = new System.IO.StreamReader(response.GetResponseStream()))
{
retval = reader.ReadToEnd();
}
}
return retval;
}
}
catch
{ }
return null;
} }
}

界面效果

好了,基本就是这样,有不完善之处,还请发谅解并指出。项目源码就不发了,文章已经包含了这个客户端实现的所有代码。

.net c# 文件分片/断点续传之下载--客户端的更多相关文章

  1. 用java实现文件的断点续传并发下载

    需求: 支持文件批量下载.现在有很多小图片需要批量下载,不希望在服务器打包下载. 支持大文件断点下载.比如下载10G的文件. PC端全平台支持.Windows,macOS,Linux 全浏览器支持.i ...

  2. java实现文件的断点续传的下载

    java的断点续传是基于之前java文件下载基础上的功能拓展 首先设置一个以线程ID为名的下载进度文件, 每一次下载的进度会保存在这个文件中,下一次下载的时候,会根据进度文件里面的内容来判断下载的进度 ...

  3. http文件的断点续传和下载

    http://www.tuicool.com/articles/ZbyymqJ Content-Disposition:inline; filename= "c501b_01_h264_sd ...

  4. golang文件下载断点续传(下载客户端)

    客户端: //const ( // UA = "Golang Downloader from Kejibo.com" //) func DownloadController(ctx ...

  5. php+html5实现无刷新上传,大文件分片上传,断点续传

    核心原理: 该项目核心就是文件分块上传.前后端要高度配合,需要双方约定好一些数据,才能完成大文件分块,我们在项目中要重点解决的以下问题. * 如何分片: * 如何合成一个文件: * 中断了从哪个分片开 ...

  6. 用FileZilla服务器端和客户端实现本机与虚拟机之间文件上传和下载

    1. FileZilla简介 2.准备工作3.安装 FileZilla Server和配置3.1.问题及解决方法3.2.添加目录3.3.测试FIP4.安装FileZilla Client5.连接服务器 ...

  7. net core WebApi——文件分片下载

    目录 前言 开始 测试 小结 @ 前言 上一篇net core WebApi--文件分片上传与跨域请求处理介绍完文件的上传操作,本来是打算紧接着写文件下载,中间让形形色色的事给耽误的,今天还是抽个空整 ...

  8. b/s利用webuploader实现超大文件分片上传、断点续传

    本人在2010年时使用swfupload为核心进行文件的批量上传的解决方案.见文章:WEB版一次选择多个文件进行批量上传(swfupload)的解决方案. 本人在2013年时使用plupload为核心 ...

  9. 前端利用webuploader实现超大文件分片上传、断点续传

    本人在2010年时使用swfupload为核心进行文件的批量上传的解决方案.见文章:WEB版一次选择多个文件进行批量上传(swfupload)的解决方案. 本人在2013年时使用plupload为核心 ...

  10. 使用webuploader组件实现大文件分片上传,断点续传

    本人在2010年时使用swfupload为核心进行文件的批量上传的解决方案.见文章:WEB版一次选择多个文件进行批量上传(swfupload)的解决方案. 本人在2013年时使用plupload为核心 ...

随机推荐

  1. CentOS 安装openssh-6.XX

    安装openssh-6.0p1 1.安装依赖包 有遇到 报ZLIB有问题的,要安装以下包 rpm -ivh zlib-devel-1.2.3-3.* rpm -ivh libsepol-devel-1 ...

  2. 一个.NET开源的功能丰富、灵活易用的 Windows 窗口增强神器

    前言 通常情况下 Windows 中的软件窗口界面一般只包含还原.移动.大小.最大化.最小化.关闭等几个基本的操作: 今天大姚给大家推荐一个.NET开源.免费(MIT License).功能丰富.灵活 ...

  3. Hologres如何支持超高基数UV计算(基于roaringbitmap实现)

    简介: 本文将会介绍Hologres基于roaringbitmap实现超高基数的UV计算 RoaringBitmap是一种压缩位图索引,RoaringBitmap自身的数据压缩和去重特性十分适合对于大 ...

  4. [Go] httprouter 自动 OPTIONS 响应 和 CORS

    httprouter 是 Gin framework 使用的路由组件. 要对 OPTIONS 请求自动响应,比如支持 CORS 请求或者设置请求头,可用 Router.GlobalOPTIONS. r ...

  5. 在 UOS 统信运行 dotnet 程序提示没有通过系统安全验证无法运行

    本文记录 dotnet 应用程序在 UOS 统信系统上运行时,提示 没有通过系统安全验证,无法运行 的问题 这个问题是因为没有开启 UOS 统信的开发者模式,直接将自己构建完成的包放上去跑导致的问题 ...

  6. 读书笔记 dotnet 的字符串在内存是如何存放

    本文是读伟民哥翻译的 .NET内存管理宝典 这本书的笔记,我认为读书的过程也需要实践,这样对一知半解的知识也有较为清晰的了解.在阅读到 string 在内存的布局时,我看到 RuntimeHelper ...

  7. 2019-8-31-C#-自动翻页-PPT-测试脚本

    title author date CreateTime categories C# 自动翻页 PPT 测试脚本 lindexi 2019-08-31 16:55:58 +0800 2019-08-1 ...

  8. k8s自动扩缩容方案-HPA-VPA-KPA(18)

    一.自动(弹性)扩缩容背景分析 背景: 弹性伸缩是根据用户的业务需求和策略,自动"调整"其"弹性资源"的管理服务.通过弹 性伸缩功能,用户可设置定时.周期或监控 ...

  9. SpringBoot注入时设置《多例》

    SpringBoot设置多例 1.准备数据 测试接口 package com.cc.jschdemo.springmultiton; /** * <p>spring多例测试</p&g ...

  10. Util 应用框架 UI 全新升级

    Util UI 已经开发多年, 并在多家公司的项目使用. 不过一直以来, Util UI 存在一些缺陷, 始终未能解决. 最近几个月, Util 团队下定决心, 终于彻底解决了所有已知缺陷. Util ...