.net c# 文件分片/断点续传--客户端

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

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;
        }
        
    }
}

界面效果

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

猜你喜欢

转载自blog.csdn.net/mengtoumingren/article/details/81157281