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

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. Luogu P3007 奶牛议会

    观前须知 本题解使用 CC BY-NC-SA 4.0 许可. 同步发布于 Luogu 题解区. 更好的观看体验 请点这里. 笔者的博客主页 正文 Luogu P3007 [USACO11JAN] Th ...

  2. centos docker换源 centos7 docker-ce

    centos docker换源 centos7 docker-ce 转载 mob6454cc71b244 2023-07-04 13:14:30 文章标签 centos docker换源 docker ...

  3. nginx 学习的前提

    前言 在nginx 中,需要学会的是如何安装.基础的命令.看懂配置那么这时候才是一切的刚刚开始. 正文 安装可以去看菜鸟驿站的: https://www.runoob.com/linux/nginx- ...

  4. Android开发 Error:The number of method references in a .dex file cannot exceed 64K.Android开发 Error:The number of method references in a .dex file cannot exceed 64K

    前言 错误起因: 在Android系统中,一个App的所有代码都在一个Dex文件里面. Dex是一个类似Jar的存储了多有Java编译字节码的归档文件. 因为Android系统使用Dalvik虚拟机, ...

  5. Pytorch-tensor维度的扩展,挤压,扩张

    数据本身不发生改变,数据的访问方式发生了改变 1.维度的扩展 函数:unsqueeze() # a是一个4维的 a = torch.randn(4, 3, 28, 28) print('a.shape ...

  6. kolla-ansible部署OpenStack Train版技术方案

    简单架构示意 项目目标 1. 实现容器化部署docker+ Ansible+openstack-tarin 2. 使用keeplived监控nova服务实现在单台服务器宕机的情况下能迅速切断连接减轻平 ...

  7. 【Oracle】Oracle数据库,第N大数据取值

    Oracle数据库,第N大数据取值 没想到力扣还有数据库的练习,正好本菜鸡跑过来练手 要显示第二大的数据可以使用order by进行排序,然后用limit对显示的数据进行限制,limit1,1,以此来 ...

  8. CF1832B Maximum Sum 题解

    [题目描述] 给定一个长度为 \(n\) 的数列,其中每个元素互不相同,进行 \(k\) 次操作,每次可以选择删除序列中最小的两个数或最大的一个数.求操作后剩余数的和的最大值. [思路] 我们构造一组 ...

  9. Morphling:云原生部署 AI , 如何把降本做到极致?

    ​简介: Morphling 本意是游戏 Dota 中的英雄"水人",他可以根据环境要求,通过灵活改变自身形态,优化战斗表现.我们希望通过 Morphling 项目,实现针对机器学 ...

  10. [PHP] 自定义 laravel/passport 的误区讲解

    Passport 的 Client 模型对应用户是默认的 User 模型.使用的 guards 是 api. 如果你发现自定义 passport 时总是调试不成功,那么很有可能是以下原因. /** * ...