/// <summary>
/// 部分下载器
/// </summary>
public class PartialDownloader
{
#region Variables
/// <summary>
/// 这部分完成事件
/// </summary>
public event EventHandler DownloadPartCompleted;
/// <summary>
/// 部分下载进度改变事件
/// </summary>
public event EventHandler DownloadPartProgressChanged;
/// <summary>
/// 部分下载停止事件
/// </summary>
public event EventHandler DownloadPartStopped;
HttpWebRequest _req;
HttpWebResponse _resp;
Stream _tempStream;
FileStream _file;
private readonly AsyncOperation _aop = AsyncOperationManager.CreateOperation(null);
private readonly Stopwatch _stp;
readonly int[] _lastSpeeds;
int _counter;
bool _stop, _wait;
#endregion
#region PartialDownloader
/// <summary>
/// 部分块下载
/// </summary>
/// <param name="url"></param>
/// <param name="dir"></param>
/// <param name="fileGuid"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="rangeAllowed"></param>
public PartialDownloader(string url, string dir, string fileGuid, int from, int to, bool rangeAllowed)
{
_from = from;
_to = to;
_url = url;
_rangeAllowed = rangeAllowed;
_fileGuid = fileGuid;
_directory = dir;
_lastSpeeds = new int[10];
_stp = new Stopwatch();
}
#endregion
void DownloadProcedure()
{
_file = new FileStream(FullPath, FileMode.Create, FileAccess.ReadWrite);
#region Request-Response
_req = WebRequest.Create(_url) as HttpWebRequest;
if (_req != null)
{
_req.UserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)";
_req.AllowAutoRedirect = true;
_req.MaximumAutomaticRedirections = 5;
_req.ServicePoint.ConnectionLimit += 1;
_req.ServicePoint.Expect100Continue = true;
_req.ProtocolVersion = HttpVersion.Version10;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Ssl3;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.SystemDefault;
ServicePointManager.Expect100Continue = true;
if (_rangeAllowed)
_req.AddRange(_from, _to);
_resp = _req.GetResponse() as HttpWebResponse;
#endregion
#region Some Stuff
if (_resp != null)
{
_contentLength = _resp.ContentLength;
if (_contentLength <= 0 || (_rangeAllowed && _contentLength != _to - _from + 1))
throw new Exception("Invalid response content");
_tempStream = _resp.GetResponseStream();
int bytesRead;
byte[] buffer = new byte[4096];
_stp.Start();
#endregion
#region Procedure Loop
while (_tempStream != null && (bytesRead = _tempStream.Read(buffer, 0, buffer.Length)) > 0)
{
while (_wait)
{
}
if (_totalBytesRead + bytesRead > _contentLength)
bytesRead = (int)(_contentLength - _totalBytesRead);
_file.Write(buffer, 0, bytesRead);
_totalBytesRead += bytesRead;
_lastSpeeds[_counter] = (int)(_totalBytesRead / Math.Ceiling(_stp.Elapsed.TotalSeconds));
_counter = (_counter >= 9) ? 0 : _counter + 1;
int tempProgress = (int)(_totalBytesRead * 100 / _contentLength);
if (_progress != tempProgress)
{
_progress = tempProgress;
_aop.Post(state =>
{
DownloadPartProgressChanged?.Invoke(this, EventArgs.Empty);
}, null);
}
if (_stop || (_rangeAllowed && _totalBytesRead == _contentLength))
{
break;
}
}
#endregion
#region Close Resources
_file.Close();
_resp.Close();
}
_tempStream?.Close();
_req.Abort();
}
_stp.Stop();
#endregion
#region Fire Events
if (!_stop && DownloadPartCompleted != null)
_aop.Post(state =>
{
_completed = true;
DownloadPartCompleted(this, EventArgs.Empty);
}, null);
if (_stop && DownloadPartStopped != null)
_aop.Post(state => DownloadPartStopped(this, EventArgs.Empty), null);
#endregion
}
#region Public Methods
/// <summary>
/// 启动下载
/// </summary>
public void Start()
{
_stop = false;
Thread procThread = new Thread(DownloadProcedure);
procThread.Start();
}
/// <summary>
/// 下载停止
/// </summary>
public void Stop()
{
_stop = true;
}
/// <summary>
/// 暂停等待下载
/// </summary>
public void Wait()
{
_wait = true;
}
/// <summary>
/// 稍后唤醒
/// </summary>
public void ResumeAfterWait()
{
_wait = false;
}
#endregion
#region Property Variables
private readonly int _from;
private int _to;
private readonly string _url;
private readonly bool _rangeAllowed;
private long _contentLength;
private int _totalBytesRead;
private readonly string _fileGuid;
private readonly string _directory;
private int _progress;
private bool _completed;
#endregion
#region Properties
/// <summary>
/// 下载已停止
/// </summary>
public bool Stopped => _stop;
/// <summary>
/// 下载已完成
/// </summary>
public bool Completed => _completed;
/// <summary>
/// 下载进度
/// </summary>
public int Progress => _progress;
/// <summary>
/// 下载目录
/// </summary>
public string Directory => _directory;
/// <summary>
/// 文件名
/// </summary>
public string FileName => _fileGuid;
/// <summary>
/// 已读字节数
/// </summary>
public long TotalBytesRead => _totalBytesRead;
/// <summary>
/// 内容长度
/// </summary>
public long ContentLength => _contentLength;
/// <summary>
/// RangeAllowed
/// </summary>
public bool RangeAllowed => _rangeAllowed;
/// <summary>
/// url
/// </summary>
public string Url => _url;
/// <summary>
/// to
/// </summary>
public int To
{
get => _to;
set
{
_to = value;
_contentLength = _to - _from + 1;
}
}
/// <summary>
/// from
/// </summary>
public int From => _from;
/// <summary>
/// 当前位置
/// </summary>
public int CurrentPosition => _from + _totalBytesRead - 1;
/// <summary>
/// 剩余字节数
/// </summary>
public int RemainingBytes => (int)(_contentLength - _totalBytesRead);
/// <summary>
/// 完整路径
/// </summary>
public string FullPath => Path.Combine(_directory, _fileGuid);
/// <summary>
/// 下载速度
/// </summary>
public int SpeedInBytes
{
get
{
if (_completed)
return 0;
int totalSpeeds = _lastSpeeds.Sum();
return totalSpeeds / 10;
}
}
#endregion
}
/// <summary>
/// 文件合并改变事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public delegate void FileMergeProgressChangedEventHandler(object sender, int e);
/// <summary>
/// 多线程下载器
/// </summary>
public class MultiThreadDownloader
{
#region 属性
private string _url;
private bool _rangeAllowed;
#endregion
#region 公共属性
/// <summary>
/// RangeAllowed
/// </summary>
public bool RangeAllowed
{
get => _rangeAllowed;
set => _rangeAllowed = value;
}
/// <summary>
/// 临时文件夹
/// </summary>
public string TempFileDirectory { get; set; }
/// <summary>
/// url地址
/// </summary>
public string Url
{
get => _url;
set => _url = value;
}
/// <summary>
/// 第几部分
/// </summary>
public int NumberOfParts { get; set; }
/// <summary>
/// 已接收字节数
/// </summary>
public long TotalBytesReceived => PartialDownloaderList.Where(t => t != null).Sum(t => t.TotalBytesRead);
/// <summary>
/// 总进度
/// </summary>
public float TotalProgress { get; private set; }
/// <summary>
/// 文件大小
/// </summary>
public long Size { get; private set; }
/// <summary>
/// 下载速度
/// </summary>
public float TotalSpeedInBytes => PartialDownloaderList.Sum(t => t.SpeedInBytes);
/// <summary>
/// 下载块
/// </summary>
public List<PartialDownloader> PartialDownloaderList { get; }
/// <summary>
/// 文件路径
/// </summary>
public string FilePath { get; set; }
#endregion
#region 变量
/// <summary>
/// 总下载进度更新事件
/// </summary>
public event EventHandler TotalProgressChanged;
/// 文件合并事件
/// </summary>
public event FileMergeProgressChangedEventHandler FileMergeProgressChanged;
private readonly AsyncOperation _aop;
#endregion
#region 下载管理器
/// <summary>
/// 多线程下载管理器
/// </summary>
/// <param name="sourceUrl"></param>
/// <param name="tempDir"></param>
/// <param name="savePath"></param>
/// <param name="numOfParts"></param>
public MultiThreadDownloader(string sourceUrl, string tempDir, string savePath, int numOfParts)
{
_url = sourceUrl;
NumberOfParts = numOfParts;
TempFileDirectory = tempDir;
PartialDownloaderList = new List<PartialDownloader>();
_aop = AsyncOperationManager.CreateOperation(null);
FilePath = savePath;
}
/// <summary>
/// 多线程下载管理器
/// </summary>
/// <param name="sourceUrl"></param>
/// <param name="savePath"></param>
/// <param name="numOfParts"></param>
public MultiThreadDownloader(string sourceUrl, string savePath, int numOfParts) : this(sourceUrl, null, savePath, numOfParts)
{
TempFileDirectory = Environment.GetEnvironmentVariable("temp");
}
/// <summary>
/// 多线程下载管理器
/// </summary>
/// <param name="sourceUrl"></param>
/// <param name="numOfParts"></param>
public MultiThreadDownloader(string sourceUrl, int numOfParts) : this(sourceUrl, null, numOfParts)
{
}
#endregion
#region 事件
private void temp_DownloadPartCompleted(object sender, EventArgs e)
{
WaitOrResumeAll(PartialDownloaderList, true);
if (TotalBytesReceived == Size)
{
UpdateProgress();
MergeParts();
return;
}
OrderByRemaining(PartialDownloaderList);
int rem = PartialDownloaderList[0].RemainingBytes;
if (rem < 50 * 1024)
{
WaitOrResumeAll(PartialDownloaderList, false);
return;
}
int from = PartialDownloaderList[0].CurrentPosition + rem / 2;
int to = PartialDownloaderList[0].To;
if (from > to)
{
WaitOrResumeAll(PartialDownloaderList, false);
return;
}
PartialDownloaderList[0].To = from - 1;
WaitOrResumeAll(PartialDownloaderList, false);
PartialDownloader temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, true);
temp.DownloadPartCompleted += temp_DownloadPartCompleted;
temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
PartialDownloaderList.Add(temp);
temp.Start();
}
void temp_DownloadPartProgressChanged(object sender, EventArgs e)
{
UpdateProgress();
}
void UpdateProgress()
{
int pr = (int)(TotalBytesReceived * 1d / Size * 100);
if (TotalProgress != pr)
{
TotalProgress = pr;
if (TotalProgressChanged != null)
{
_aop.Post(state => TotalProgressChanged(this, EventArgs.Empty), null);
}
}
}
#endregion
#region 方法
void CreateFirstPartitions()
{
Size = GetContentLength(_url, ref _rangeAllowed, ref _url);
int maximumPart = (int)(Size / (25 * 1024));
maximumPart = maximumPart == 0 ? 1 : maximumPart;
if (!_rangeAllowed)
NumberOfParts = 1;
else if (NumberOfParts > maximumPart)
NumberOfParts = maximumPart;
for (int i = 0; i < NumberOfParts; i++)
{
PartialDownloader temp = CreateNewPd(i, NumberOfParts, Size);
temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
temp.DownloadPartCompleted += temp_DownloadPartCompleted;
PartialDownloaderList.Add(temp);
temp.Start();
}
}
void MergeParts()
{
List<PartialDownloader> mergeOrderedList = SortPDsByFrom(PartialDownloaderList);
using (var fs = new FileStream(FilePath, FileMode.Create, FileAccess.ReadWrite))
{
long totalBytesWritten = 0;
int mergeProgress = 0;
foreach (var item in mergeOrderedList)
{
using (FileStream pds = new FileStream(item.FullPath, FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[4096];
int read;
while ((read = pds.Read(buffer, 0, buffer.Length)) > 0)
{
fs.Write(buffer, 0, read);
totalBytesWritten += read;
int temp = (int)(totalBytesWritten * 1d / Size * 100);
if (temp != mergeProgress && FileMergeProgressChanged != null)
{
mergeProgress = temp;
_aop.Post(state => FileMergeProgressChanged(this, temp), null);
}
}
}
File.Delete(item.FullPath);
}
}
}
PartialDownloader CreateNewPd(int order, int parts, long contentLength)
{
int division = (int)contentLength / parts;
int remaining = (int)contentLength % parts;
int start = division * order;
int end = start + division - 1;
end += (order == parts - 1) ? remaining : 0;
return new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), start, end, true);
}
/// <summary>
/// 暂停或继续
/// </summary>
/// <param name="list"></param>
/// <param name="wait"></param>
public static void WaitOrResumeAll(List<PartialDownloader> list, bool wait)
{
foreach (PartialDownloader item in list)
{
if (wait)
item.Wait();
else
item.ResumeAfterWait();
}
}
/// <summary>
/// 冒泡排序
/// </summary>
/// <param name="list"></param>
private static void BubbleSort(List<PartialDownloader> list)
{
bool switched = true;
while (switched)
{
switched = false;
for (int i = 0; i < list.Count - 1; i++)
{
if (list[i].RemainingBytes < list[i + 1].RemainingBytes)
{
PartialDownloader temp = list[i];
list[i] = list[i + 1];
list[i + 1] = temp;
switched = true;
}
}
}
}
/// <summary>
/// Sorts the downloader by From property to merge the parts
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
public static List<PartialDownloader> SortPDsByFrom(List<PartialDownloader> list)
{
return list.OrderBy(x => x.From).ToList();
}
/// <summary>
/// 按剩余时间排序
/// </summary>
/// <param name="list"></param>
public static void OrderByRemaining(List<PartialDownloader> list)
{
BubbleSort(list);
}
/// <summary>
/// 获取内容长度
/// </summary>
/// <param name="url"></param>
/// <param name="rangeAllowed"></param>
/// <param name="redirectedUrl"></param>
/// <returns></returns>
public static long GetContentLength(string url, ref bool rangeAllowed, ref string redirectedUrl)
{
HttpWebRequest req = WebRequest.Create(url) as HttpWebRequest;
req.UserAgent = "Mozilla/4.0 (compatible; MSIE 11.0; Windows NT 6.2; .NET CLR 1.0.3705;)";
req.ServicePoint.ConnectionLimit = 4;
long ctl;
using (HttpWebResponse resp = req.GetResponse() as HttpWebResponse)
{
redirectedUrl = resp.ResponseUri.OriginalString;
ctl = resp.ContentLength;
rangeAllowed = resp.Headers.AllKeys.Select((v, i) => new
{
HeaderName = v,
HeaderValue = resp.Headers[i]
}).Any(k => k.HeaderName.ToLower().Contains("range") && k.HeaderValue.ToLower().Contains("byte"));
resp.Close();
}
req.Abort();
return ctl;
}
#endregion
#region 公共方法
/// <summary>
/// 暂停下载
/// </summary>
public void Pause()
{
foreach (var t in PartialDownloaderList)
{
if (!t.Completed)
t.Stop();
}
Thread.Sleep(200);
}
/// <summary>
/// 开始下载
/// </summary>
public void Start()
{
Task th = new Task(CreateFirstPartitions);
th.Start();
}
/// <summary>
/// 唤醒下载
/// </summary>
public void Resume()
{
int count = PartialDownloaderList.Count;
for (int i = 0; i < count; i++)
{
if (PartialDownloaderList[i].Stopped)
{
int from = PartialDownloaderList[i].CurrentPosition + 1;
int to = PartialDownloaderList[i].To;
if (from > to) continue;
PartialDownloader temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, _rangeAllowed);
temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
temp.DownloadPartCompleted += temp_DownloadPartCompleted;
PartialDownloaderList.Add(temp);
PartialDownloaderList[i].To = PartialDownloaderList[i].CurrentPosition;
temp.Start();
}
}
}
#endregion
}