MultiThreadDownloader.cs 12 KB

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