MultiThreadDownloader.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. namespace Masuit.Tools.Net
  10. {
  11. /// <summary>
  12. /// 文件合并改变事件
  13. /// </summary>
  14. /// <param name="sender"></param>
  15. /// <param name="e"></param>
  16. public delegate void FileMergeProgressChangedEventHandler(object sender, int e);
  17. /// <summary>
  18. /// 多线程下载器
  19. /// </summary>
  20. public class MultiThreadDownloader
  21. {
  22. #region 属性
  23. private string _url;
  24. private bool _rangeAllowed;
  25. private readonly HttpWebRequest _request;
  26. private Action<HttpWebRequest> _requestConfigure = req => req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36";
  27. #endregion
  28. #region 公共属性
  29. /// <summary>
  30. /// RangeAllowed
  31. /// </summary>
  32. public bool RangeAllowed
  33. {
  34. get => _rangeAllowed;
  35. set => _rangeAllowed = value;
  36. }
  37. /// <summary>
  38. /// 临时文件夹
  39. /// </summary>
  40. public string TempFileDirectory { get; set; }
  41. /// <summary>
  42. /// url地址
  43. /// </summary>
  44. public string Url
  45. {
  46. get => _url;
  47. set => _url = value;
  48. }
  49. /// <summary>
  50. /// 第几部分
  51. /// </summary>
  52. public int NumberOfParts { get; set; }
  53. /// <summary>
  54. /// 已接收字节数
  55. /// </summary>
  56. public long TotalBytesReceived
  57. {
  58. get
  59. {
  60. try
  61. {
  62. return PartialDownloaderList.Where(t => t != null).Sum(t => t.TotalBytesRead);
  63. }
  64. catch (Exception e)
  65. {
  66. return 0;
  67. }
  68. }
  69. }
  70. /// <summary>
  71. /// 总进度
  72. /// </summary>
  73. public float TotalProgress { get; private set; }
  74. /// <summary>
  75. /// 文件大小
  76. /// </summary>
  77. public long Size { get; private set; }
  78. /// <summary>
  79. /// 下载速度
  80. /// </summary>
  81. public float TotalSpeedInBytes => PartialDownloaderList.Sum(t => t.SpeedInBytes);
  82. /// <summary>
  83. /// 下载块
  84. /// </summary>
  85. public List<PartialDownloader> PartialDownloaderList { get; }
  86. /// <summary>
  87. /// 文件路径
  88. /// </summary>
  89. public string FilePath { get; set; }
  90. #endregion
  91. #region 变量
  92. /// <summary>
  93. /// 总下载进度更新事件
  94. /// </summary>
  95. public event EventHandler TotalProgressChanged;
  96. /// <summary>
  97. /// 文件合并事件
  98. /// </summary>
  99. public event FileMergeProgressChangedEventHandler FileMergeProgressChanged;
  100. private readonly AsyncOperation _aop;
  101. #endregion
  102. #region 下载管理器
  103. /// <summary>
  104. /// 多线程下载管理器
  105. /// </summary>
  106. /// <param name="sourceUrl"></param>
  107. /// <param name="tempDir"></param>
  108. /// <param name="savePath"></param>
  109. /// <param name="numOfParts"></param>
  110. public MultiThreadDownloader(string sourceUrl, string tempDir, string savePath, int numOfParts)
  111. {
  112. _url = sourceUrl;
  113. NumberOfParts = numOfParts;
  114. TempFileDirectory = tempDir;
  115. PartialDownloaderList = new List<PartialDownloader>();
  116. _aop = AsyncOperationManager.CreateOperation(null);
  117. FilePath = savePath;
  118. _request = WebRequest.Create(sourceUrl) as HttpWebRequest;
  119. }
  120. /// <summary>
  121. /// 多线程下载管理器
  122. /// </summary>
  123. /// <param name="sourceUrl"></param>
  124. /// <param name="savePath"></param>
  125. /// <param name="numOfParts"></param>
  126. public MultiThreadDownloader(string sourceUrl, string savePath, int numOfParts) : this(sourceUrl, null, savePath, numOfParts)
  127. {
  128. TempFileDirectory = Environment.GetEnvironmentVariable("temp");
  129. }
  130. /// <summary>
  131. /// 多线程下载管理器
  132. /// </summary>
  133. /// <param name="sourceUrl"></param>
  134. /// <param name="numOfParts"></param>
  135. public MultiThreadDownloader(string sourceUrl, int numOfParts) : this(sourceUrl, null, numOfParts)
  136. {
  137. }
  138. #endregion
  139. #region 事件
  140. private void temp_DownloadPartCompleted(object sender, EventArgs e)
  141. {
  142. WaitOrResumeAll(PartialDownloaderList, true);
  143. if (TotalBytesReceived == Size)
  144. {
  145. UpdateProgress();
  146. MergeParts();
  147. return;
  148. }
  149. OrderByRemaining(PartialDownloaderList);
  150. int rem = PartialDownloaderList[0].RemainingBytes;
  151. if (rem < 50 * 1024)
  152. {
  153. WaitOrResumeAll(PartialDownloaderList, false);
  154. return;
  155. }
  156. int from = PartialDownloaderList[0].CurrentPosition + rem / 2;
  157. int to = PartialDownloaderList[0].To;
  158. if (from > to)
  159. {
  160. WaitOrResumeAll(PartialDownloaderList, false);
  161. return;
  162. }
  163. PartialDownloaderList[0].To = from - 1;
  164. WaitOrResumeAll(PartialDownloaderList, false);
  165. var temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, true);
  166. temp.DownloadPartCompleted += temp_DownloadPartCompleted;
  167. temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
  168. PartialDownloaderList.Add(temp);
  169. temp.Start(_requestConfigure);
  170. }
  171. void temp_DownloadPartProgressChanged(object sender, EventArgs e)
  172. {
  173. UpdateProgress();
  174. }
  175. void UpdateProgress()
  176. {
  177. int pr = (int)(TotalBytesReceived * 1d / Size * 100);
  178. if (TotalProgress != pr)
  179. {
  180. TotalProgress = pr;
  181. if (TotalProgressChanged != null)
  182. {
  183. _aop.Post(state => TotalProgressChanged(this, EventArgs.Empty), null);
  184. }
  185. }
  186. }
  187. #endregion
  188. #region 方法
  189. void CreateFirstPartitions()
  190. {
  191. Size = GetContentLength(ref _rangeAllowed, ref _url);
  192. int maximumPart = (int)(Size / (25 * 1024));
  193. maximumPart = maximumPart == 0 ? 1 : maximumPart;
  194. if (!_rangeAllowed)
  195. {
  196. NumberOfParts = 1;
  197. }
  198. else if (NumberOfParts > maximumPart)
  199. {
  200. NumberOfParts = maximumPart;
  201. }
  202. for (int i = 0; i < NumberOfParts; i++)
  203. {
  204. var temp = CreateNewPd(i, NumberOfParts, Size);
  205. temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
  206. temp.DownloadPartCompleted += temp_DownloadPartCompleted;
  207. PartialDownloaderList.Add(temp);
  208. temp.Start(_requestConfigure);
  209. }
  210. }
  211. void MergeParts()
  212. {
  213. var mergeOrderedList = SortPDsByFrom(PartialDownloaderList);
  214. var dir = new FileInfo(FilePath).DirectoryName;
  215. if (!Directory.Exists(dir))
  216. {
  217. Directory.CreateDirectory(dir);
  218. }
  219. using var fs = new FileStream(FilePath, FileMode.Create, FileAccess.ReadWrite);
  220. long totalBytesWritten = 0;
  221. int mergeProgress = 0;
  222. foreach (var item in mergeOrderedList)
  223. {
  224. using var pds = new FileStream(item.FullPath, FileMode.Open, FileAccess.Read);
  225. byte[] buffer = new byte[4096];
  226. int read;
  227. while ((read = pds.Read(buffer, 0, buffer.Length)) > 0)
  228. {
  229. fs.Write(buffer, 0, read);
  230. totalBytesWritten += read;
  231. int temp = (int)(totalBytesWritten * 1d / Size * 100);
  232. if (temp != mergeProgress && FileMergeProgressChanged != null)
  233. {
  234. mergeProgress = temp;
  235. _aop.Post(state => FileMergeProgressChanged(this, temp), null);
  236. }
  237. }
  238. try
  239. {
  240. File.Delete(item.FullPath);
  241. }
  242. catch
  243. {
  244. // ignored
  245. }
  246. }
  247. }
  248. PartialDownloader CreateNewPd(int order, int parts, long contentLength)
  249. {
  250. int division = (int)contentLength / parts;
  251. int remaining = (int)contentLength % parts;
  252. int start = division * order;
  253. int end = start + division - 1;
  254. end += (order == parts - 1) ? remaining : 0;
  255. return new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), start, end, true);
  256. }
  257. /// <summary>
  258. /// 暂停或继续
  259. /// </summary>
  260. /// <param name="list"></param>
  261. /// <param name="wait"></param>
  262. public static void WaitOrResumeAll(List<PartialDownloader> list, bool wait)
  263. {
  264. foreach (var item in list)
  265. {
  266. if (wait)
  267. {
  268. item.Wait();
  269. }
  270. else
  271. {
  272. item.ResumeAfterWait();
  273. }
  274. }
  275. }
  276. /// <summary>
  277. /// 冒泡排序
  278. /// </summary>
  279. /// <param name="list"></param>
  280. private static void BubbleSort(List<PartialDownloader> list)
  281. {
  282. bool switched = true;
  283. while (switched)
  284. {
  285. switched = false;
  286. for (int i = 0; i < list.Count - 1; i++)
  287. {
  288. if (list[i].RemainingBytes < list[i + 1].RemainingBytes)
  289. {
  290. PartialDownloader temp = list[i];
  291. list[i] = list[i + 1];
  292. list[i + 1] = temp;
  293. switched = true;
  294. }
  295. }
  296. }
  297. }
  298. /// <summary>
  299. /// Sorts the downloader by From property to merge the parts
  300. /// </summary>
  301. /// <param name="list"></param>
  302. /// <returns></returns>
  303. public static List<PartialDownloader> SortPDsByFrom(List<PartialDownloader> list)
  304. {
  305. return list.OrderBy(x => x.From).ToList();
  306. }
  307. /// <summary>
  308. /// 按剩余时间排序
  309. /// </summary>
  310. /// <param name="list"></param>
  311. public static void OrderByRemaining(List<PartialDownloader> list)
  312. {
  313. BubbleSort(list);
  314. }
  315. /// <summary>
  316. /// 配置请求头
  317. /// </summary>
  318. /// <param name="config"></param>
  319. public void Configure(Action<HttpWebRequest> config)
  320. {
  321. _requestConfigure = config;
  322. }
  323. /// <summary>
  324. /// 获取内容长度
  325. /// </summary>
  326. /// <param name="rangeAllowed"></param>
  327. /// <param name="redirectedUrl"></param>
  328. /// <returns></returns>
  329. public long GetContentLength(ref bool rangeAllowed, ref string redirectedUrl)
  330. {
  331. _request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36";
  332. _request.ServicePoint.ConnectionLimit = 4;
  333. _requestConfigure(_request);
  334. using var resp = _request.GetResponse() as HttpWebResponse;
  335. redirectedUrl = resp.ResponseUri.OriginalString;
  336. var ctl = resp.ContentLength;
  337. rangeAllowed = resp.Headers.AllKeys.Select((v, i) => new
  338. {
  339. HeaderName = v,
  340. HeaderValue = resp.Headers[i]
  341. }).Any(k => k.HeaderName.ToLower().Contains("range") && k.HeaderValue.ToLower().Contains("byte"));
  342. _request.Abort();
  343. return ctl;
  344. }
  345. #endregion
  346. #region 公共方法
  347. /// <summary>
  348. /// 暂停下载
  349. /// </summary>
  350. public void Pause()
  351. {
  352. foreach (var t in PartialDownloaderList.Where(t => !t.Completed))
  353. {
  354. t.Stop();
  355. }
  356. Thread.Sleep(200);
  357. }
  358. /// <summary>
  359. /// 开始下载
  360. /// </summary>
  361. public void Start()
  362. {
  363. Task th = new Task(CreateFirstPartitions);
  364. th.Start();
  365. }
  366. /// <summary>
  367. /// 唤醒下载
  368. /// </summary>
  369. public void Resume()
  370. {
  371. int count = PartialDownloaderList.Count;
  372. for (int i = 0; i < count; i++)
  373. {
  374. if (PartialDownloaderList[i].Stopped)
  375. {
  376. int from = PartialDownloaderList[i].CurrentPosition + 1;
  377. int to = PartialDownloaderList[i].To;
  378. if (from > to)
  379. {
  380. continue;
  381. }
  382. var temp = new PartialDownloader(_url, TempFileDirectory, Guid.NewGuid().ToString(), from, to, _rangeAllowed);
  383. temp.DownloadPartProgressChanged += temp_DownloadPartProgressChanged;
  384. temp.DownloadPartCompleted += temp_DownloadPartCompleted;
  385. PartialDownloaderList.Add(temp);
  386. PartialDownloaderList[i].To = PartialDownloaderList[i].CurrentPosition;
  387. temp.Start(_requestConfigure);
  388. }
  389. }
  390. }
  391. #endregion
  392. }
  393. }