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