断点续传客户端实现主要参考了以下文章:
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; } } }
界面效果
好了,基本就是这样,有不完善之处,还请发谅解并指出。项目源码就不发了,文章已经包含了这个客户端实现的所有代码。
本博客Android APP 下载 |
支持我们就给我们点打赏 |
支付宝打赏 支付宝扫一扫二维码 |
微信打赏 微信扫一扫二维码 |
如果想下次快速找到我,记得点下面的关注哦!