C#开源强大的HttpWebRequest断点续传库再也不怕下载失败啦

.Net/.Net core的HttpWebRequest其实十分强大的,用得好,能够发挥出很稳定的下载效果。只是网上很多的资料比较杂,经过研究和整合,并且应用到我的项目中,大约1万用户的使用。证明我这个下载库是十分稳定的。现在开源出来。

其实里面一些思路不仅仅是.net,java、python等等都需要关注的点。
就是服务器请求头的返回值,filesize>0, acceptRange=bytes证明支持断点续传,以及设置cookies,和禁用https的保护来保证某些下载的稳定等。

主要就是2个类。拷贝到自己的代码中。

第一个辅助类:主要用途是干2个事情,一个是给Request对象设置超时和userAgent,或者cookies满足某些下载的请求的特殊设置。第二个事情是针对链接link,GetRemoteLinkDownloadInfo()获取下载信息,用于下一步真实下载的信息。

using System;
using System.Diagnostics;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;

namespace HttpDown
{
    public class RemoteLinkDownloadInfo
    {
        public string timestampstr;
        public long timestamp;
        public long filesize;
        public string eTag;
        public string AcceptRanges;

        public override string ToString()
        {
            return "size=" + filesize + ",ts=" + timestampstr;
        }
    }

    public class RemoteLinkDownloadWithLinkInfo : RemoteLinkDownloadInfo
    {
        public string link;
        public bool isUsed;
        public int isInit;
        public bool supportResumeDownload;

        public RemoteLinkDownloadWithLinkInfo()
        {
            isInit = -1;
        }

        public RemoteLinkDownloadWithLinkInfo(RemoteLinkDownloadInfo info, string downloadlInk)
        {
            if (info != null)
            {
                timestampstr = info.timestampstr;
                timestamp = info.timestamp;
                filesize = info.filesize;
                eTag = info.eTag;
                AcceptRanges = info.AcceptRanges;
                supportResumeDownload = (info.AcceptRanges == null || filesize <= 0) ? false
                    : info.AcceptRanges.ToLower().Contains("byte");
            }

            link = downloadlInk;
            isUsed = false;
            isInit = 1;
        }

        public override string ToString()
        {
            return string.Format("size={0}, ts={1}, link={2}", filesize, timestampstr, link);
        }

        public string GetPrintTxt()
        {
            return string.Format("{0}, {1}, size({2}), suppoortResume({3})", link, eTag, filesize, supportResumeDownload);
        }
    }

    public class HttpUtilWrapper
    {
        private static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
        {
            // 总是接受    
            return true;
        }

        private static bool mInitXXXBe4XXed = false;
        public static void InitServerCerValidationCbBe4CreateRequest()
        {
            if (mInitXXXBe4XXed) return;
            if (mInitXXXBe4XXed) return;
            mInitXXXBe4XXed = true;
            //处理HttpWebRequest访问https有安全证书的问题( 请求被中止: 未能创建 SSL/TLS 安全通道。)
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult);
            ServicePointManager.ServerCertificateValidationCallback += (s, cert, chain, sslPolicyErrors) => true; //这行与上述那行等价,设置一个回调用于ssl验证
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
            //.net3.5 SecurityProtocolType.Tls;
        }

        public static void SetCookieUnSafe(HttpWebRequest req)
        {
            //创建证书文件,此证书可能是私有网站证书的添加方式,这种共有网站使用的是系统标准的证书,不需要指定也可以访问
            //对于使用私人证书的情景,可以使用下边的方式,指定对应的证书地址
            //有人说需要使用X509Certificate2类代替X509Certificate,但是并未验证,而且需要考虑程序用户权限问题
            //X509Certificate2 objx509 = new X509Certificate2("F://1234.cer");
            //request.ClientCertificates.Add(objx509);

            //cookie部分,如果cookie中需要校验用户密码,可以按照下边的方式进行创建,访问curse的网页,只要填空就可以,验证是没问题的
            CookieContainer objcok = new CookieContainer();
            //objcok.Add(new Uri("http://testurl"), new Cookie("键", "值"));
            //objcok.Add(new Uri("http://testurl"), new Cookie("键", "值"));
            //objcok.Add(new Uri("http://testurl"), new Cookie("sidi_sessionid", "360A748941D055BEE8C960168C3D4233"));

            req.CookieContainer = objcok;
        }

        public static RemoteLinkDownloadInfo GetRemoteLinkDownloadInfo(string downloadUrl, int retryCount = 0)
        {
            InitServerCerValidationCbBe4CreateRequest();
            try
            {
                // 与指定URL创建HTTP请求       
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(downloadUrl);
                request.Timeout = 4 * 1000;
                request.UserAgent = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36";
                SetCookieUnSafe(request);
                // 获取对应HTTP请求的响应      
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                string[] etag = null;// response.Headers.GetValues("ETag");
                string[] AcceptRangess = null;//
                int i = 0;
                foreach (var k in response.Headers.AllKeys)
                {
                    if (k == "ETag")
                    {
                        etag = response.Headers.GetValues(k);
                        i++;
                    }
                    else if (k == "Accept-Ranges")
                    {
                        AcceptRangess = response.Headers.GetValues(k);
                        i++;
                    }
                    if (i == 2)
                    {
                        break;
                    }
                }

                DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
                TimeSpan toNow = response.LastModified.Subtract(dtStart);
                long timeStamp = toNow.Ticks;
                timeStamp = long.Parse(timeStamp.ToString().Substring(0, timeStamp.ToString().Length - 4)) / 1000;
                var r = new RemoteLinkDownloadInfo
                {
                    AcceptRanges = AcceptRangess?[0],
                    filesize = response.ContentLength,
                    timestamp = timeStamp,
                    eTag = etag?[0].Replace("\"", ""),
                    timestampstr = response.LastModified.ToString()
                };

                request.Abort();
                response.Close();
                return r;
            }
            catch (Exception e)
            {
                if (retryCount > 2)
                {
                    Thread.Sleep(8 * 1000);
                }
                else
                {
                    Thread.Sleep(4 * 1000);
                }

                if (retryCount <= 4)
                {
                    return GetRemoteLinkDownloadInfo(downloadUrl, ++retryCount);
                }
                else
                {
                    Debug.WriteLine(downloadUrl + "Get RemoteLink DownloadInfo failed " + e.ToString());
                }
            }
            return null;
        }
    }
}

第二个类,下载库:

using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;

namespace HttpDown
{
    public class HttpStableDownloader
    {
        /// <summary>
        /// 中文:添加Resume模式的回调,根据ResumeDownlaodInfo.supportResumeDownload = true才添加这个。
        /// En:Add your Resume Mod callback, according to ResumeDownlaodInfo.supportResumeDownload = true
        /// </summary>
        public ResumeDownloadProgressDelegate ResumeProgressDelegate { set; get; } = null;

        /// <summary>
        /// 中文:添加NotResume Size回调,根据是否支持Resume才添加这个。ResumeDownlaodInfo.supportResumeDownload = false
        /// En:Add your not Resume Mod callback, according to ResumeDownlaodInfo.supportResumeDownload = false
        /// </summary>
        public NotResumeDownloadSizeChangeDelegate NotResumeSizeChangeDelegate { set; get; } = null;

        /// <summary>
        /// 中文:添加最近7.5s内的速度回调
        /// En:Add recently 7.5s speed callback
        /// </summary>
        public SpeedMonitorDelegate CurrentSpeedDelegate { set; get; } = null;

        /// <summary>
        /// 整体下载速度监控
        /// En: full download progress speed monitor.
        /// </summary>
        public SpeedFullDelegate FullSpeedDelegate { set; get; } = null;

        private const long DEFAULT_MINI_DOWN_SPEED = 7000L;// byte/s
        private const long DEFAULT_CURRENT_SPEED_RETRY = 2500;

        /// <summary>
        /// 是否在速度过低进行取消下载
        /// En:If speed is too slow cancel this download
        /// </summary>
        public bool ConsideSpeed { set; get; } = true;
        /// <summary>
        /// 取消下载的临界值
        /// En: The mini download Speed to cancel download
        /// </summary>
        public long MiniDownloadSpeed { set; get; } = DEFAULT_MINI_DOWN_SPEED;
        public long MiniDownloadSpeedTimeout { set; get; } = 12000;

        /// <summary>
        /// 最多重试次数。因为会断掉。
        /// Because of failed, max try num.
        /// </summary>
        public int MaxTryCount { set; get; } = -1;

        #region CODE
        public static string ParseDownCode(int code) {
            switch (code) {
                case DOWN_CODE_ERROR_NO_URL:
                    return @"DOWN_CODE_ERROR_NO_URL";
                case DOWN_CODE_ERROR_REMOTE_FILESIZE:
                    return @"DOWN_CODE_ERROR_REMOTE_FILESIZE";
                case DOWN_CODE_ERROR_TRY_NUM_MAX:
                    return "DOWN_CODE_ERROR_TRY_NUM_MAX";
                case DOWN_CODE_CONTINUE_RETRY:
                    return @"DOWN_CODE_CONTINUE_RETRY";
                case DOWN_CODE_ERROR_MANUAL_CANCELED:
                    return @"DOWN_CODE_ERROR_MANUAL_CANCELED";
                case DOWN_CODE_ERROR_SPEED_TOO_LOW:
                    return @"DOWN_CODE_ERROR_SPEED_TOO_LOW";
                case DOWN_CODE_SUCCESS:
                    return @"DOWN_CODE_SUCCESS";
                case DOWN_CODE_NORMAL_FULL:
                    return @"DOWN_CODE_NORMAL_FULL";
                case DOWN_CODE_PROCESSING:
                    return @"DOWN_CODE_PROCESSING";
            }
            return "" + code;
        }

        /// <summary>
        /// 下载地址错误
        /// </summary>
        public const int DOWN_CODE_ERROR_NO_URL = -1;
        /// <summary>
        /// 远程下载大小信息错误
        /// </summary>
        public const int DOWN_CODE_ERROR_REMOTE_FILESIZE = -2;
        /// <summary>
        /// 下载失败1,重试次数太多仍然没有成功
        /// </summary>
        public const int DOWN_CODE_ERROR_TRY_NUM_MAX = -3;

        /// <summary>
        /// 下载失败2,重试。一般作为内部使用。
        /// </summary>
        public const int DOWN_CODE_CONTINUE_RETRY = -4;

        public const int DOWN_CODE_ERROR_MANUAL_CANCELED = -5;

        public const int DOWN_CODE_ERROR_SPEED_TOO_LOW = -6;
        /// <summary>
        /// 下载完成
        /// </summary>
        public const int DOWN_CODE_SUCCESS = 2;
        /// <summary>
        /// 文件大小已经足够,不用下载
        /// </summary>
        public const int DOWN_CODE_NORMAL_FULL = 1;

        /// <summary>
        /// 正在下载中。。。
        /// </summary>
        public const int DOWN_CODE_PROCESSING = 0;
        #endregion
        /// <summary>
        /// 0表示正在工作;-1表示停止下来并下载失败;1表示下载停止并下载成功
        /// </summary>
        public static int IsResumeCodeMeansEnd(int code)
        {
            switch (code)
            {
                case DOWN_CODE_ERROR_NO_URL:
                case DOWN_CODE_ERROR_REMOTE_FILESIZE:
                case DOWN_CODE_ERROR_TRY_NUM_MAX:
                case DOWN_CODE_ERROR_SPEED_TOO_LOW:
                    return -1;
                case DOWN_CODE_CONTINUE_RETRY:
                case DOWN_CODE_PROCESSING:
                    return 0;
                case DOWN_CODE_SUCCESS:
                case DOWN_CODE_NORMAL_FULL:
                    return 1;
            }
            return 0;
        }

        const int BYTE_BUFF_SIZE = 2048;

        private int CalMaxTryNum(long totalsize, bool supportResm)
        {
            if (!supportResm)
            {
                return 5;
            }

            if (MaxTryCount != -1)
            {
                return MaxTryCount;
            }

            if (totalsize >= 20 * 1024 * 1024)
            {
                return 150;
            }

            if (totalsize >= 5 * 1024 * 1024)
            {
                return 100;
            }

            if (totalsize >= 3 * 1024 * 1024)
            {
                return 80;
            }

            if (totalsize >= 1024 * 1024)
            {
                return 70;
            }

            if (totalsize >= 300 * 1024)
            {
                return 50;
            }

            if (totalsize >= 100 * 1024)
            {
                return 40;
            }

            return 30;
        }

        private int CalTimeOut(int currentTryNum, bool supportResume)
        {
            if (!supportResume)
            {
                return currentTryNum > 3 ? 2800 : 2200;
            }
            //实测晚高峰github,20次即2200延迟,能够稍微动一下。

            if (currentTryNum < 8)
            {
                return 2000;
            }

            if (currentTryNum < 15)
            {
                return 2500;
            }

            return 2800;
        }

        private void MarkupSleepWhenRetry(int currentTryNum, bool supportResume)
        {
            if (!supportResume)
            {
                Thread.Sleep(5000);
                return;
            }
            if (currentTryNum < 20)
            {
                if (currentTryNum % 2 == 1)
                {
                    Thread.Sleep(3000);
                }
            }
            else
            {
                Thread.Sleep(3000);
            }
        }

        private const string TAG = " HttpDownloader: ";
#if DEBUG
        private const bool DEBUG = true;
#else
        private const bool DEBUG = false;
#endif

        private readonly string _url, _savepath, _etag;
        private readonly long _filesize;
        //private DownloadSpeedHelper _speedHelper;

        public HttpStableDownloader(string url, string savepath, string etag, long filesize)
        {
            _url = url;
            _savepath = savepath;
            _etag = etag;
            _filesize = filesize;
        }

        public delegate void ResumeDownloadProgressDelegate(int progress, int code);
        public delegate void NotResumeDownloadSizeChangeDelegate(long receivedKB, int code);
        public delegate void SpeedMonitorDelegate(float bytesPerSecond);
        public delegate void SpeedFullDelegate(long filesize, long totalDownloadAddSize, long totalTime);

        private static string GenerateLines(int num)
        {
            string w = " |";
            for (int i = 0; i < num - 1; i++)
            {
                w += "|";
            }
            return w;
        }

        public void NotResumeDownloadAsync()
        {
            Task t = new Task(() => {
                NotResumeDownload();
            });

            t.Start();
        }

        public void ResumeDownloadAsync()
        {
            Task t = new Task(() => {
                ResumeDownload();
            });

            t.Start();
        }

        public int NotResumeDownload()
        {
            if (_url == null || _url.Length <= 2)
            {
                NotResumeSizeChangeDelegate?.Invoke(-1, DOWN_CODE_ERROR_NO_URL);
                return DOWN_CODE_ERROR_NO_URL;
            }
            int r;
            string MagicCode = Guid.NewGuid().ToString().Substring(0, 6);
            int curTryNum = 1;
            do
            {
                r = DownloadInThreadInner(false, MagicCode, curTryNum);
                curTryNum++;
            } while (r == DOWN_CODE_CONTINUE_RETRY);
            return r;
        }

        public int ResumeDownload()
        {
            if (_url == null || _url.Length <= 2)
            {
                ResumeProgressDelegate?.Invoke(-1, DOWN_CODE_ERROR_NO_URL);
                return DOWN_CODE_ERROR_NO_URL;
            }

            if (_filesize <= 0)
            {
                ResumeProgressDelegate?.Invoke(-1, DOWN_CODE_ERROR_REMOTE_FILESIZE);
                return DOWN_CODE_ERROR_REMOTE_FILESIZE;
            }

            int r;
            string MagicCode = Guid.NewGuid().ToString().Substring(0, 6);
            int curTryNum = 1;
            do
            {
                r = DownloadInThreadInner(true, MagicCode, curTryNum);
                curTryNum++;
            } while (r == DOWN_CODE_CONTINUE_RETRY);
            return r;
        }

        private long _initTime, _totalAddFileSize = 0;

        /// <summary>
        /// 下载文件(同步)  支持断点续传
        /// 必须在外部判断这个网址支持accept range。并传入size
        /// </summary>
        /// <param name="url">文件url</param>
        /// <param name="savepath">本地保存路径</param>
        /// <param name="filesize">下载文件大小</param>
        /// <param name="progress">下载进度(百分比)</param>
        private int DownloadInThreadInner(bool supportResume, string magicCod, int tryNum)
        {
            string debugLines = null;
            string logtag = null;
            string logtagAndLine = null;
            if (tryNum == 1) //全局速度监控
            {
                _initTime = GetTimestampM();
            }

            if (!supportResume)
            {
                MiniDownloadSpeed = DEFAULT_MINI_DOWN_SPEED;
            }

            if (DEBUG)
            {
                debugLines = GenerateLines(tryNum);
                logtag = magicCod + "[" + supportResume + "]-" + TAG;
                logtagAndLine = logtag + debugLines;

                if (tryNum == 1)
                {
                    Debug.WriteLine(logtag + "「resume    url " + _url);
                    Debug.WriteLine(logtagAndLine + "savePath " + _savepath);
                    Debug.WriteLine(logtagAndLine + "filesize " + _filesize);
                    Debug.WriteLine(logtagAndLine + "maxtryNum " + CalMaxTryNum(_filesize, supportResume));
                }
                else
                {
                    Debug.WriteLine(logtag + "notResume url " + _url);
                    Debug.WriteLine(logtagAndLine + "savePath " + _savepath);
                    Debug.WriteLine(logtagAndLine + "filesize " + _filesize);
                }
            }

            HttpUtilWrapper.InitServerCerValidationCbBe4CreateRequest();
            //打开上次下载的文件
            long lStartPos = 0;
            FileStream fs = null;
            bool newfile = true;

            if (File.Exists(_savepath))
            {
                //第一次进来就有size,说明是之前的了。要进行判断,而直接来的是循环,则不用
                if ((!supportResume) || (tryNum == 1 && !Data_CanItResume(_savepath, _filesize, _etag, _url)))
                {
                    File.Delete(_savepath);
                }
                else
                {
                    newfile = false;
                    fs = File.OpenWrite(_savepath);
                    lStartPos = fs.Length;
                    if (DEBUG) Debug.WriteLine(logtagAndLine + "已经存在 " + lStartPos);
                    fs.Seek(lStartPos, SeekOrigin.Current);//移动文件流中的当前指针
                }
            }


            if (tryNum == 1 && supportResume)
            { //else表示文件不存在;刚刚开始下载。我们就存储信息
                Data_SaveResumeInfo(_savepath, _filesize, _etag, _url);
            }

            if (newfile)
            {
                string direName = Path.GetDirectoryName(_savepath);
                if (!Directory.Exists(direName))//如果不存在保存文件夹路径,新建文件夹
                {
                    Directory.CreateDirectory(direName);
                }
                fs = new FileStream(_savepath, FileMode.Create);
                lStartPos = 0;
                if (DEBUG) Debug.WriteLine(logtagAndLine + "新创建");
            }

            if (_filesize > 0 && supportResume)
            {
                ResumeProgressDelegate?.Invoke((int)(lStartPos * 100 / _filesize), DOWN_CODE_PROCESSING);
            }

            HttpWebRequest request = null;
            long totalSize = 0;
            long curSize = 0;
            try
            {
                if (_filesize == lStartPos && supportResume)
                {
                    //下载完成
                    if (DEBUG) Debug.WriteLine(logtagAndLine + "不用下载咯!");
                    fs.Close();
                    ResumeProgressDelegate?.Invoke(100, DOWN_CODE_NORMAL_FULL);
                    Data_DeleteResumeInfo(_etag, _savepath);
                    FullSpeedDelegate?.Invoke(_filesize, 0, GetTimestampM() - _initTime);
                    return DOWN_CODE_NORMAL_FULL;
                }

                MarkupSleepWhenRetry(tryNum, supportResume); //**补偿休眠。防止过于频繁请求。被拒绝。

                request = (HttpWebRequest)WebRequest.Create(_url);
                var to = CalTimeOut(tryNum, supportResume); //**动态的超时时间;越重试次数越多越延迟高
                request.Timeout = to;
                request.ReadWriteTimeout = to;
                request.UserAgent = @"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36";

                if (!supportResume) HttpUtilWrapper.SetCookieUnSafe(request);

                if (lStartPos > 0)
                {
                    request.AddRange((int)lStartPos);//设置Range值,断点续传
                }

                //向服务器请求,获得服务器回应数据流
                WebResponse respone = request.GetResponse();
                totalSize = respone.ContentLength + (supportResume ? lStartPos : 0);
                if (totalSize == 0 && supportResume)
                {
                    Debug.WriteLine(logtag + " 错误! totalsize为0");
                    ResumeProgressDelegate?.Invoke(-1, DOWN_CODE_ERROR_REMOTE_FILESIZE);
                    FullSpeedDelegate?.Invoke(_filesize, 0, GetTimestampM() - _initTime);
                    return DOWN_CODE_ERROR_REMOTE_FILESIZE;
                }
                long eachPerSize = totalSize / 240;
                if (eachPerSize <= 0)
                {
                    eachPerSize = 1024;
                    if (totalSize == -1)
                    {
                        totalSize = _filesize;
                    }
                }

                long eachPerSizeMach = 0;

                if (DEBUG && supportResume) Debug.WriteLine(logtagAndLine + "totalSize:" + totalSize + " content length: " + (respone.ContentLength));
                curSize = lStartPos;
                if (DEBUG && supportResume) Debug.WriteLine(logtagAndLine + "curSize:" + curSize + " alreadyDown percent:" + (int)(curSize * 100 / totalSize));
                //progress = (int)(curSize / totalSize * 100);
                if (supportResume) ResumeProgressDelegate?.Invoke((int)(curSize * 100 / totalSize), DOWN_CODE_PROCESSING);
                else NotResumeSizeChangeDelegate?.Invoke(0, DOWN_CODE_PROCESSING);
                Stream ns = respone.GetResponseStream();

                byte[] nbytes = new byte[BYTE_BUFF_SIZE];
                int nReadSize = 0;

                int inWhileCount = 0;
                long bytesReceived = 0; //总共接受的字节

                long lastTimeMs = GetTimestampM();
                long firstTimeMs = lastTimeMs;
                long curTimeMs, deltaTs;
                long AddedFileSize = 0L;

                if (!supportResume)
                {
                    _totalAddFileSize = 0;
                }

                long lastCurSize = lStartPos;

                while (true)
                {
                    nReadSize = ns.Read(nbytes, 0, BYTE_BUFF_SIZE);
                    if (nReadSize <= 0)
                    {
                        break;
                    }

                    curSize += nReadSize;
                    eachPerSizeMach += nReadSize;
                    bytesReceived += nReadSize;
                    fs.Write(nbytes, 0, nReadSize);

                    AddedFileSize += nReadSize;
                    _totalAddFileSize += nReadSize;

                    curTimeMs = GetTimestampM();
                    deltaTs = curTimeMs - firstTimeMs;
                    //Debug.WriteLine("deltats " + deltaTs + " AddedFileSize " + AddedFileSize + " speed= " + speed);
                    if (deltaTs > MiniDownloadSpeedTimeout)
                    {//计算速度
                        var sp = (AddedFileSize * 1000 / deltaTs);
                        if (AddedFileSize < 300 ||
                            (MiniDownloadSpeed > 0L && sp < MiniDownloadSpeed))
                        {
                            throw new Exception("SpeedError"); //速度不够停止。
                        }
                    }

                    if (eachPerSizeMach > eachPerSize)
                    {
                        eachPerSizeMach = 0;
                        if (inWhileCount++ % 2 == 0)
                        {
                            if (supportResume) ResumeProgressDelegate?.Invoke((int)(curSize * 100 / totalSize), DOWN_CODE_PROCESSING);
                            if (!supportResume) NotResumeSizeChangeDelegate?.Invoke(curSize / 1024, DOWN_CODE_PROCESSING);
                        }
                    }

                    if (curTimeMs > lastTimeMs + 7500)
                    {
                        //增加当前速度监控
                        var currentSpeed = (curSize - lastCurSize) * 1000 / (curTimeMs - lastTimeMs);
                        CurrentSpeedDelegate?.Invoke(currentSpeed / 1024);

                        lastCurSize = curSize;
                        lastTimeMs = curTimeMs;
                        Debug.WriteLine(logtagAndLine + "curSpeedKB(" + (currentSpeed / 1024) + ")cur/total/filesize(" + curSize + "/" + totalSize + "/" + _filesize + ")");

                        if (deltaTs > MiniDownloadSpeedTimeout &&
                            (AddedFileSize < 300 || currentSpeed < DEFAULT_CURRENT_SPEED_RETRY))
                        {//计算速度
                            if (ConsideSpeed)
                            {
                                Debug.WriteLine("》》SpeedRetry重试!");
                                throw new Exception("SpeedRetry"); //速度不够重试。
                            }
                        }
                    }
                }
                fs.Flush();
                ns.Close();
                fs.Close();
                if (curSize != totalSize && supportResume)//文件长度不等于下载长度,下载出错
                {
                    throw new Exception();
                }

                curSize = -1; //完成了!标记为-1
                if (request != null)
                {
                    request.Abort();
                }
                if (supportResume) ResumeProgressDelegate?.Invoke(100, DOWN_CODE_SUCCESS);
                if (!supportResume) NotResumeSizeChangeDelegate?.Invoke(curSize / 1024, DOWN_CODE_SUCCESS);
                if (DEBUG) Debug.WriteLine(logtag + " 」下载正确完成!!!");
                if (supportResume) Data_DeleteResumeInfo(_etag, _savepath);

                FullSpeedDelegate?.Invoke(_filesize, _totalAddFileSize, GetTimestampM() - _initTime);
                return DOWN_CODE_SUCCESS;
            }
            catch (Exception e)
            {
                bool speedError = e.Message == "SpeedError";
                bool speedRetry = e.Message == "SpeedRetry";

                if (DEBUG) Debug.WriteLine(logtagAndLine + "下载失败!!!!" + e);
                if (request != null)
                {
                    request.Abort();
                }

                fs.Close();

                if (speedError || tryNum > CalMaxTryNum(_filesize, supportResume))
                {
                    if (DEBUG && speedError) Debug.WriteLine(logtag + " 」重试次数已经超标咯!");
                    var code = speedError ? DOWN_CODE_ERROR_SPEED_TOO_LOW : DOWN_CODE_ERROR_TRY_NUM_MAX;
                    if (speedRetry)
                    {
                        code = DOWN_CODE_CONTINUE_RETRY;
                    }
                    if (totalSize > 0 && supportResume) ResumeProgressDelegate?.Invoke((int)(curSize * 100 / totalSize), code);
                    if (!supportResume) NotResumeSizeChangeDelegate?.Invoke(-1, code);

                    FullSpeedDelegate?.Invoke(_filesize, _totalAddFileSize, GetTimestampM() - _initTime);
                    return code;
                }
                else
                {
                    tryNum++;
                    if (DEBUG) Debug.WriteLine(logtagAndLine + "重试次数( " + tryNum);
                }

                if (totalSize > 0 && supportResume) ResumeProgressDelegate?.Invoke((int)(curSize * 100 / totalSize), DOWN_CODE_CONTINUE_RETRY);
                if (!supportResume) NotResumeSizeChangeDelegate?.Invoke(-1, DOWN_CODE_CONTINUE_RETRY);
                return DOWN_CODE_CONTINUE_RETRY;
            }
        }

        private string FormatSize(long bytes)
        {
            if (bytes >= 1024)
            {
                return (bytes / 1024 + "KB");
            }
            else
            {
                return "" + bytes;
            }
        }

        /// <summary>
        /// startpos指的是之前没有下载完cache掉的文件大小
        /// </summary>
        /// <param name="filepath"></param>
        /// <param name="filesize"></param>
        /// <param name="etag"></param>
        private static void Data_SaveResumeInfo(string lastFilePath, long lastFileSize, string etag, string link)
        {
            if (etag == null || etag.Length == 0)
            {
                return;
            }
            var dir = GetSaveResumeDownInfoDir;
            var file = Path.Combine(dir, "inf_" + Path.GetFileNameWithoutExtension(lastFilePath));
            File.WriteAllText(file, lastFilePath + "\n" + lastFileSize + "\n" + link + "\n" + etag);
            Debug.WriteLine(TAG + "DATA: Save ResumeData." + lastFilePath);
        }

        private static void Data_DeleteResumeInfo(string etag, string savepath)
        {
            if (etag == null || etag.Length == 0)
            {
                return;
            }
            var dir = GetSaveResumeDownInfoDir;
            var file = Path.Combine(dir, "inf_" + Path.GetFileNameWithoutExtension(savepath));
            if (File.Exists(file)) File.Delete(file);
            Debug.WriteLine(TAG + "DATA: Delete ResumeData." + savepath);
        }

        private static bool Data_CanItResume(string newFilePath, long newTotalFileSize, string newEtg, string newLink)
        {
            if (newEtg == null || newEtg.Length == 0)
            {
                return false;
            }
            var dir = GetSaveResumeDownInfoDir;
            var file = Path.Combine(dir, "inf_" + Path.GetFileNameWithoutExtension(newFilePath));
            if (!File.Exists(file))
            {
                Debug.WriteLine(TAG + "DATA: can it resume false 1! " + newFilePath);
                return false;
            }

            var lines = File.ReadAllLines(file);
            if (lines != null && lines.Length >= 4)
            {
                string oldFilePath = lines[0];
                long.TryParse(lines[1], out long oldfilesize);
                string oldLink = lines[2];
                string oldEtag = lines[3];
                if (oldFilePath == newFilePath
                    && oldfilesize == newTotalFileSize
                    && newLink == oldLink
                    && oldEtag == newEtg)
                {
                    Debug.WriteLine(TAG + "DATA: can it resume true! " + newFilePath);
                    return true;
                }
            }
            Debug.WriteLine(TAG + "DATA: can it resume false 2! " + newFilePath);
            return false;
        }

        static long GetTimestampM()
        {
            TimeSpan ts = DateTime.Now - new DateTime(1970, 1, 1);
            return (long)ts.TotalMilliseconds;
        }

        //TODO 改成你自己愿意保存的位置。或者你自行改成数据库等存储操作,甚至是内存变量数组list保存也行。
        static string GetSaveResumeDownInfoDir => @"/Users/Allan/Downloads/temp/info";// Utils.GetMyDocumentPath("resumedown", "info");
    }
}

下载代码:

        private static void Test(string link1, string savePathFile) {
            Task t = new Task(() => {
                var info1 = HttpUtilWrapper.GetRemoteLinkDownloadInfo(link1);
                var downInfo1 = new RemoteLinkDownloadWithLinkInfo(info1, link1);
                var downloader = new HttpStableDownloader(link1, savePathFile, downInfo1.eTag, downInfo1.filesize);
                if (downInfo1.supportResumeDownload)
                {
                    downloader.ResumeProgressDelegate = (progress, errorCode) =>
                    {
                        Console.WriteLine("progress " + progress + " code " + errorCode);
                    };
                    downloader.ResumeDownload();//ResumeDownloadAsync();
                }
                else
                {
                    downloader.NotResumeSizeChangeDelegate = (receivedKB, errorCode) =>
                    {
                        Console.WriteLine("receivedKB " + receivedKB + " code " + errorCode);
                    };
                    downloader.NotResumeDownload();//NotResumeDownloadAsync();
                }
            });
            t.Start();
        }

        static void Main(string[] args)
        {
            //Support Resume Download link      //Test(@"http://opensource.spotify.com/cefbuilds/cef_binary_81.2.24%2Bgc0b313d%2Bchromium-81.0.4044.113_windows32.tar.bz2", @"/Users/Allan/Downloads/temp/info/cef.tar.bz2");
            //Not Support Resume Download link
            Test(@"https://github.com/picoe/Eto/archive/develop.zip", @"/Users/Allan/Downloads/temp/info/githubproj.zip");
            Console.ReadLine();
        }

由于HttpStableDownloader代码中做断点续传为了防止关闭了程序,再接着下的情况,采取了本地文件保存断点信息。所以一定要把TODO的地方自行修改。或者你可以把Data_开头的函数进行修改为内存List、Map存储也可以,或者修改为数据库存储也可以,自行抉择。目前只需要修改为正确的存储目录即可。因为我在mac上调试,故而放的是linux目录。你自己修改。

一般情况,你只需要添加关于下载进度的delegate,根据获取到的RemoteInfo,是否支持断点supportResumeDownload, 来判断添加ResumeProgressDelegate/NotResumeProgressDelegate,然后开始下载ResumeDownload或者NotResumeDownload。

其他的一些设置参数,比如是否考虑速度进行取消,速度最小限制值等等。可以自行看看。一般不用管,我根据不同的网络情况进行了大致的排查设置的,一般来讲比较合理。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值