MultiThreadDownloader.cs 12 KB

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