.net7 mvc jquery bootstrap json 学习中 第一次学PHP,正在研究中。自学进行时... ...
我的博客 https://enhweb.github.io/
不错的皮肤:darkgreentrip,iMetro_HD
断点续传客户端实现主要参考了以下文章:
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();
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;
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)
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);
//不支持断点续传则一开始就直接读完整流
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)
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;
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)
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)
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;
好了,基本就是这样,有不完善之处,还请发谅解并指出。项目源码就不发了,文章已经包含了这个客户端实现的所有代码。
转自:http://Www.CnBlogs.Com/WebEnh/
本博客Android APP 下载
支持我们就给我们点打赏