PageSourceSearch

https://shenao1100.github.io/assets/2024-11-14-csharp-multi-thread-download-file-DvoQKdzT.js

js shenao1100.github.io collected 2026-10-03 09:56:33 UTC 29,510 bytes, 929 lines download raw bytes

1const n=`---
2title: C#多线程下载文件
3description: C#多线程并发下载文件
4author: ShenNya
5date: 2024-12-08 02:06:00 +0800
6categories: [CSharp]
7tags: [CSharp, 多线程, Thread, 并发, 下载]
8#pin: true
9math: true
10mermaid: true
11---
12
13# C#多线程下载文件
14
15## 前言
16
17> 这段就算是简单多线程的实战了
18> 
19> 如果需要多线程入门,点[这里](/posts/csharp-multi-thread/)
20
21日常中经常会遇到各种下载慢的情况,很多软件可以解决这个问题,如IDM、FDM、aria2c,甚至是某MC启动器PCL
22
23本质上他们都是多线程下载,其主要思想是将文件**切片**
24
25假设我们有大小为255byte的文件,一个人下就是从0下载直到255结束
26
27这个时候,如果将文件分为多片,建立多个线程同时下载,就相当于有多个人在帮你下载同一个文件了,也就实现了加速(BT种子也是这样,也许以后数学好了我也会发一篇这个的笔记)
28
29
30
31## 代码及用例
32这里先贴代码:
33
34从以下URL下载文件:
35\`\`\`
36http://192.168.10.199/p/Local%20500G%20%E5%91%9C%E5%91%9C/SurfacePro3_Win10_18362_1902002_0.msi?sign=hwUl4yfX8aRrmAb-U0oqcyELDWUY_XxPzlFHXQuVjUk=:0
37\`\`\`
38使用了浏览器的UA:
39\`\`\`
40Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0
41\`\`\`
42保存到\`a.msi\`
43
44\`\`\`csharp
45using System;
46using System.IO;
47using System.Net.Http;
48using System.Threading.Tasks;
49using System.Threading;
50
51public class Range
52{
53    public long from { get; set; }
54    public long to { get; set; }
55}
56
57public class Downloader
58{
59    private readonly object _lock = new object();
60    private FileStream FileStream {  get; set; }
61    private int TotalThread {  get; set; }
62    private int CurrentThread { get; set; }
63    private string Url { get; set; }
64    private string UserAgent { get; set; }
65
66    private long downloadSize = 0;
67    private int downloadProgress = 0;
68    private long downloaded = 0;
69    private long totalSize = 0;
70    public Downloader(string url, string user_agent, string file_path, int thread)
71    {
72        TotalThread = thread;
73        CurrentThread = 0;
74        Url = url;
75        UserAgent = user_agent;
76        FileStream = new FileStream(file_path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
77    }
78    public async Task<long> GetInfo()
79    {
80        using (HttpClient client = new HttpClient()){
81            client.DefaultRequestHeaders.Add("User-Agent", UserAgent);
82            using (HttpResponseMessage response = await client.GetAsync(Url))
83            {
84                response.EnsureSuccessStatusCode();
85
86                totalSize = response.Content.Headers.ContentLength ?? 0;
87                FileStream.SetLength(totalSize);
88                Console.WriteLine(totalSize.ToString());
89                return totalSize;
90            }
91        }
92        return 0;
93    }
94    public List<Range> CalcRange()
95    {
96        var range = new List<Range>();
97        long start = 0;
98        if (totalSize > 0)
99        {
100            long average_size = totalSize / TotalThread;
101            for (int i = 0; i < TotalThread; i++)
102            {
103                if (i == TotalThread - 1)
104                {
105                    range.Add(new Range
106                    {
107                        from = start,
108                        to = totalSize
109                    });
110                }
111                else
112                {
113                    range.Add(new Range
114                    {
115                        from = start,
116                        to = start + average_size
117                    });
118                    start += average_size;
119                }
120            }
121        }
122        return range;
123    }
124    public async Task DownloadThread(Range range)
125    {
126
127        long pointer = range.from;
128        using (HttpClient client = new HttpClient())
129        {
130            client.DefaultRequestHeaders.Add("Range", $"bytes={range.from}-{range.to}");
131            client.DefaultRequestHeaders.Add("User-Agent", UserAgent);
132            using (HttpResponseMessage response = await client.GetAsync(Url))
133            {
134                response.EnsureSuccessStatusCode();
135
136                long? totalSize = response.Content.Headers.ContentLength;
137                using (Stream contentStream = await response.Content.ReadAsStreamAsync())
138                {
139                    byte[] buffer = new byte[8192];
140                    int bytesRead;
141                    while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
142                    {
143                        //await write 
144                        downloaded += bytesRead;
145                        lock (_lock)
146                        {
147                            Console.WriteLine("WriteTo: " + pointer.ToString() + " By: " + range.from.ToString());
148
149                            FileStream.Seek(pointer, SeekOrigin.Begin);
150                            FileStream.Write(buffer, 0, bytesRead);
151                        }
152                        pointer += bytesRead;
153                        
154                    }
155                }
156            }
157        }
158    }
159    public void releaseFile()
160    {
161        Console.WriteLine("File has been relese");
162        lock (_lock)
163        {
164            FileStream.Close();
165        }
166    }
167    public async void Run()
168    {
169        long size = await GetInfo();
170        Console.WriteLine("Getinfo complete, size: " + totalSize.ToString());
171        //CountdownEvent countDown = new CountdownEvent(TotalThread);
172        List<Task> tasks = new List<Task>();
173        foreach (Range range in CalcRange())
174        {
175            Console.WriteLine("Thread start: " + range.from.ToString());
176            tasks.Add(DownloadThread(range));
177        }
178        await Task.WhenAll(tasks);
179        //countDown.Wait();
180        releaseFile();
181    }
182}
183
184class Program
185{
186    static void Main(string[] args)
187    {
188        Downloader download = new Downloader("http://192.168.10.199/p/Local%20500G
188%20%E5%91%9C%E5%91%9C/SurfacePro3_Win10_18362_1902002_0.msi?sign=hwUl4yfX8aRrmAb-U0oqcyELDWUY_XxPzlFHXQuVjUk=:0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0", "a.msi", 32);
189        download.Run();
190        Console.ReadLine();
191    }
192}
193\`\`\`
194## 明确需求
195
196刚刚说到多线程下载文件的核心是切片,那么就引申出了几个问题:
197
198- 如何提前预知文件的大小
199- 如何计算每个分片的大小并让服务器返回对应分片
200- 如何将这些分片整合
201
202
203## 预知大小
204
205在HTTP传输中,正文会包含一个拥有许多信息的\`头(Header)\`,头里面一般会有一个叫\`Content-Length\`的值,这就是body的大小,也就是我们文件的大小
206
207打开你的浏览器,按下\`F12\`打开开发者工具,选择网络,刷新一下网页,就可以看到\`Response Headers\`中包含有\`Content-Length\`
208
209![alt text](../imgs/2024-11-14-csharp-multi-thread-download-file/image-1.png)
210
211这个数值就是这个页面的大小
212
213可以使用以下代码获取:
214\`\`\`csharp
215long totalSize;
216using (HttpClient client = new HttpClient()){
217    using (HttpResponseMessage response = await client.GetAsync(Url))
218    {
219        response.EnsureSuccessStatusCode();
220        totalSize = response.Content.Headers.ContentLength ?? 0;
221        FileStream.SetLength(totalSize);
222        Console.WriteLine("文件大小为: " + totalSize.ToString());
223        return totalSize;
224    }
225}
226return 0;
227\`\`\`
228> 由于文件一般很大,文件大小最好用\`long\`存储
229>
230> 如果无法获取,有可能是这个页面不支持\`Content-Length\`或是未经授权,可以尝试\`client.DefaultRequestHeaders.Add\`语句添加\`User-Agent\`和\`Host\`
231{: .prompt-info }
232
233## 计算切片大小
234
235这里我们简化以下,假设有255长度的文件,为了方便表示我们把他切成不等的两片:
236
237![alt text](../imgs/2024-11-14-csharp-multi-thread-download-file/image.png)
238
239可以看到第一段的范围为\`0-63\`
240
241第二段范围为\`64-255\`
242
243将这两段合并即为整个文件
244
245这里的规则是:第一段以\`0\`为起点,\`n\`为终点,后一段以\`n+1\`为起点\`m\`为终点,以此类推,直到终点为文件总长度
246
247---
248
249既然要使用多线程,那么就要定义线程数,每个线程下载对应的部分,这里使用均分的方法:
250
251
252\`\`\`csharp
253
254public class Range
255{
256    public long from { get; set; }
257    public long to { get; set; }
258}
259
260private long totalSize = 0;           // 在这里定义文件大小
261private int TotalThread {  get; set; }// 在这里定义线程数
262
263public List<Range> CalcRange()
264{
265  var range = new List<Range>();
266  long start = 0;
267  if (totalSize > 0)
268  {
269      long average_size = totalSize / TotalThread;
270      for (int i = 0; i < TotalThread; i++)
271      {
272          if (i == TotalThread - 1) // 最后一段要把剩余所有没下的都要下上
273          {
274              range.Add(new Range
275              {
276                  from = start,
277                  to = totalSize
278              });
279          }
280          else                    // 之前的直接均分
281          {
282              range.Add(new Range
283              {
284                  from = start,
285                  to = start + average_size
286              });
287              start += average_size;
288          }
289      }
290  }
291  return range;
292}
293\`\`\`
294
295## 让服务器返回对应的文件块
296
297和\`Response Headers\`中含有文件大小的\`Content-Length\`一样,你也可以在\`Request Headers\`中定义想要文件的哪一部分
298
299具体操作为,在\`Request Headers\`中定义:
300\`\`\`
301Range: bytes=0-63
302\`\`\`
303使用C#:
304\`\`\`csharp
305Range range = new Range{from=0, to=63};
306using (HttpClient client = new HttpClient())
307{
308  client.DefaultRequestHeaders.Add("Range", $"bytes={range.from}-{range.to}");
309  using (HttpResponseMessage response = await client.GetAsync(Url))
310  {
311      response.EnsureSuccessStatusCode();
312      ...
313  }
314}
315\`\`\`
316
317这里我们使用**流式传输 (Stream)**的方式下载文件块
318
319如果不使用流式传输,那么整个请求会进入*假死*状态,直到下载完毕整个文件块再执行后面的代码
320
321而使用了流式传输,我们就可以接收一点文件(如8192字节的文件)后去完成些别的任务(如计算下载进度、下载速率等),之后重复这一步骤直到文件块接收完毕
322
323代码如下:
324\`\`\`csharp
325
326
327public async Task DownloadThread()
328{
329  Range range = new Range{from=0, to=63}; // 使用之前写的Range
330  string UserAgent = "....."  //这里为了通过验证填入UA
331  long pointer = range.from;              
332  using (HttpClient client = new HttpClient())
333  {
334      client.DefaultRequestHeaders.Add("Range", $"bytes={range.from}-{range.to}");//在Request Headers中定义下载范围
335      client.DefaultRequestHeaders.Add("User-Agent", UserAgent);          //设置UA
336      using (HttpResponseMessage response = await client.GetAsync(Url))
337      {
338          response.EnsureSuccessStatusCode(); //确保状态码无误
339          long? totalSize = response.Content.Headers.ContentLength;             //获取这部分文件块的大小
340          using (Stream contentStream = await response.Content.ReadAsStreamAsync())   //使用流式传输
341          {
342              byte[] buffer = new byte[8192];   // 新建buffer,用来存储流式传输每次下载的部分文件
343              int bytesRead;
344              while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) //获取最多8192字节的内容,获取到的内容长度存入bytesRead
345              {
346                  downloaded += bytesRead;    //已经下载完毕的大小,用来计算下载进度
347                  lock (_lock)      //使用线程锁,之后会提到
348                  {
349                      Console.WriteLine("WriteTo: " + pointer.ToString() + " By: " + range.from.ToString());
350                      FileStream.Seek(pointer, SeekOrigin.Begin);   // 将文件指针移动到需要写入数据的地方
351                      FileStream.Write(buffer, 0, bytesRead);       // 将获取到的那点数据写入
352                  }
353                  pointer += bytesRead;
354                  
355              }
356          }
357      }
358  }
359}
360\`\`\`
361
362## 多线程管理
363
364由于这是个典型的**I/O密集型操作**,我们使用并发的方式进行处理
365
366这里的想法是:
3671. 向服务器获取文件大小
3682. 创建一个文件实例,让所有线程都使用这一文件实例进行文件写入操作
3693. 计算每个线程下载的文件块大小
3704. 建立多个线程同时下载文件,使用线程池自动管理
3715. 等待所有文件块下载完毕,此时所有下载线程关闭,释放文件块
372
373这里使用\`Task\`来对线程池进行自动管理
374
375代码如下:
376\`\`\`csharp
377public async void Run()
378{
379    long size = await GetInfo();      // 获取文件大小,获取完毕进入下一步
380    Console.WriteLine("Getinfo complete, size: " + totalSize.ToString());
381
382    // 你也可以使用倒计时器countDown对线程是否全部关闭做出判断,但是Task更加简单好用
383    //CountdownEvent countDown = new CountdownEvent(TotalThread);   
384
385    List<Task> tasks = new List<Task>();      // 这个列表用来存储、等待 所有的下载线程
386    foreach (Range range in CalcRange())      // 计算下载大小并拉起线程进行下载
387    {
388        Console.WriteLine("Thread start: " + range.from.ToString());
389        tasks.Add(DownloadThread(range));     // 将线程加入列表
390    }
391    await Task.WhenAll(tasks);                // 等待所有线程下载完毕后退出
392    //countDown.Wait();
393    releaseFile();                            // 释放文件资源
394}
395\`\`\`
396
397## 改进
398
399这里只是一个简单的示例,你还可以改进这段代码,增加速度、进度的计算,下载时的程序回调
400
401使用**线程阻塞**实现暂停,**取消线程**实现取消下载文件等等
402
403# 改进过的多线程下载
404
405下文是实战中编写的,拥有*暂停、取消*的多线程下载
406
407针对一些特殊的下载场景(服务器限制每次分片下载大小、连接线程数、限速等)做了优化
408
409## 问题和特殊要求
410
4111. 服务器限制了连接数,每个连接的下载大小
4122. 需要向UI线程回调
4133. 需要兼容其他的下载器
4144. 服务器会返回多个相同文件的下载地址,可以使用多个线程分别下载多个地址的文件
415
416
417## 使用Interface兼容其他的下载器
418
419> 如果您熟悉\`Interface\`的使用,请不要在此处浪费时间
420
421一个软件可能会有多个下载器,比如需要同时兼容单线程下载、多线程下载、外部下载器(aria2c, motrix)的调用
422
423可以预见的是,这些下载方式具有的功能是高度重复的,都可以简化为以下功能:
424
425- 添加下载任务
426- 暂停任务
427- 继续任务
428- 删除任务
429- 获取下载进度
430
431对此,我们可以使用\`Interface\`来简化我们的代码:
432
433声明一个Interface, 内含所有下载方式都共有的函数
434> 在声明接口时,最好使用\`I + 类名\`来声明接口名
435
436\`\`\`csharp
437public class Range
438{
439    public long from { get; set; }
440    public long to { get; set; }
441}
442public class DownloadStatus
443{
444    public float downloadProgress;
445    public long downloaded;
446    public long totalSize;
447    public int linkCount;
448    public int downloadingThread;
449    public long downloadBlockSize;
450
451    public bool isPaused;
452    public bool isDownloading;
453    public bool isComplete;
454}
455public interface IDownload
456{
457    void SetCallBack(Action callBack);    // UI线程的程序回调
458    void AddUrl(string url);              // 有多个Url的话可以继续添加
459    DownloadStatus Status();              // 用于获取下载进度等
460    void Pause();                         // 暂停
461    void Continue();                      // ç»§ç»­
462    void Cancel();                        //取消
463    Task DownloadThread(Range range, string url);   // 下载函数
464    void releaseFile();                   // 释放被占用的文件
465    void Run();                           // 开始下载
466}
467\`\`\`
468
469之后创建一个类去使用这个接口:
470
471这里先贴代码,解释在后面
472
473\`\`\`csharp
474public class Downloader: IDownload        // 声明使用IDownload接口
475{
476    // 锁,用于保证不同线程无法同时访问IO资源
477    private readonly object _lock = new object();
478    // 阻塞,用于暂停/继续下载,默认继续下载
479    private readonly ManualResetEvent resetEvent = new ManualResetEvent(true);
480    // 取消信号,判断用户是否取消任务
481    private CancellationTokenSource cts = new CancellationTokenSource();
482    private FileStream FileStream { get; set; }
483    private int TotalThread { get; set; }
484    private int CurrentThread { get; set; }
485    private string Url { get; set; }
486    private string UserAgent { get; set; }
487    private string filePath { get; set; }
488
489    public Action? CallBack { get; set; }
490
491    private Dictionary<int, string> linkList = new Dictionary<int, string>();
492
493    public float downloadProgress = 0;
494    public long downloaded = 0;
495    public long totalSize = 0;
496    public int linkCount = 0;
497    public int downloadingThread = 0;
498    public long downloadBlockSize = 500087;
499
500    public bool isPaused = false;
501    public bool isDownloading = true;
502    public bool isComplete = false;
503
504
505    public Downloader(string url, string user_agent, string file_path, long total_size, int thread)
506    {
507        TotalThread = thread;
508        CurrentThread = 0;
509        Url = url;
510        linkList.Add(linkCount, url);
511        linkCount++;
512
513        UserAgent = user_agent;
514        totalSize = total_size;
515        filePath = file_path;
516
517        FileStream = new FileStream(file_path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
518        FileStream.SetLength(total_size);
519    }
520    public void AddUrl(string url)
521    {
522        linkList.Add(linkCount, url);
523        linkCount++;
524
525    }
526    public void SetCallBack(Action callBack)
527    {
528        CallBack = callBack;
529    }
530    private void CalcProgress()
531    {
532        if (downloaded > 0)
533        {
534            downloadProgress = (float)downloaded / totalSize;
535        }
536        if (downloadingThread == 0)
537        {
538            isDownloading = false;
539        }
540        else
541        {
542            isDownloading = true;
543        }
544        if (downloaded == totalSize)
545        {
546            isDownloading = false;
547            isComplete = true;
548        }
549        
550        Console.WriteLine(downloadProgress.ToString() + "\\r");
551    }
552
553    // 因为服务器限制每次下载的大小,所以这里做了修改
554    // 每个线程只下载 downloadBlockSize 大小的文件后就退出
555    private List<Range> CalcRange()
556    {
557        List<Range> ranges = new List<Range>();
558        long calced_size = 0;
559        while (true)
560        {
561            if (calced_size + downloadBlockSize > totalSize)
562            {
563                ranges.Add(new Range
564                {
565                    from = calced_size,
566                    to = totalSize
567                });
568                break;
569
570            }
571            else
572            {
573                ranges.Add(new Range
574                {
575                    from = calced_size,
576                    to = calced_size + downloadBlockSize,
577                });
578            }
579            calced_size += downloadBlockSize + 1;
580
581        }
582        return ranges;
583    }
584    /// <summary>
585    /// block thread to pause
586    /// </summary>
587    public void Pause()
588    {
589        resetEvent.Reset();
590        isPaused = true;
591        CalcProgress();
592        CallBack?.Invoke();
593
594    }
595
596    /// <summary>
597    /// release resetEvent to cntinue
598    /// </summary>
599    public void Continue()
600    {
601        resetEvent.Set();
602        isPaused = false;
603        CalcProgress();
604        CallBack?.Invoke();
605
606    }
607    public void Cancel()
608    {
609        resetEvent.Set();
610        isPaused = false;
611        cts.Cancel();
612        isComplete = true;
613        CalcProgress();
614        CallBack?.Invoke();
615
616    }
617    public DownloadStatus Status()
618    {
619        return new DownloadStatus
620        {
621            downloadProgress = downloadProgress,
622            downloaded = downloaded,
623            totalSize = totalSize,
624            linkCount = linkCount,
625            downloadingThread = downloadingThread,
626            downloadBlockSize = downloadBlockSize,
627
628            isPaused = isPaused,
629            isDownloading = isDownloading,
630            isComplete = isComplete,
631        };
632    }
633    public async Task DownloadThread(Range range, string url)
634    {
635
636        long pointer = range.from;
637        try
638        {
639            using (HttpClient client = new HttpClient())
640            {
641                client.DefaultRequestHeaders.Add("Range", $"bytes={range.from}-{range.to}");
642                client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", UserAgent);
643
644                client.Timeout = TimeSpan.FromSeconds(10);
645                using (HttpResponseMessage response = await client.GetAsync(url))
646                {
647                    response.EnsureSuccessStatusCode();
648                    resetEvent.WaitOne();
649                    if (cts.IsCancellationRequested)
650                    {
651                        releaseFile();
652                        // exit thread
653                        return;
654                    }
655                    long? totalSize = response.Content.Headers.ContentLength;
656                    using (Stream contentStream = await response.Content.ReadAsStreamAsync())
657                    {
658                        byte[] buffer = new byte[1000];
659                        int bytesRead;
660                        while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
661                        {
662                            resetEvent.WaitOne();
663                            if (cts.IsCancellationRequested)
664                            {
665                                releaseFile();
666                                // exit thread
667                                return;
668                            }
669                            //await write 
670                            downloaded += bytesRead;
671                            lock (_lock)
672                            {
673                                Console.WriteLine("WriteTo: " + pointer.ToString() + " By: " + range.from.ToString());
674                                CalcProgress();
675                                Dispatcher.UIThread.InvokeAsync(() =>
676                                {
677                                    CallBack?.Invoke(); // 在 UI 线程中调用
678                                });
679                                FileStream.Seek(pointer, SeekOrigin.Begin);
680                                FileStream.Write(buffer, 0, bytesRead);
681                            }
682                            pointer += bytesRead;
683
684                        }
685                    }
686                }
687            }
688        }
689        catch (Exception ex)
690        {
691            await DownloadThread(range, url);
692        }
693
694    }
695    private bool isFileReleased() {
696        try
697        {
698            return FileStream.SafeFileHandle.IsClosed;
699        }
700        catch (ObjectDisposedException)
701        {
702            return true;
703        }
704    }
705    public void releaseFile()
706    {
707        lock (_lock)
708        {
709            if (!isFileReleased())
710            {
711                FileStream.Close();
712
713            }
714        }
715        if (cts.IsCancellationRequested)
716        {
717            if (File.Exists(filePath))
718            {
719                File.Delete(filePath);
720            }
721        }
722        isDownloading = false;
723    }
724    public async void Run()
725    {
726        List<Range> rangeLsit = CalcRange();
727        SemaphoreSlim semaphore = new SemaphoreSlim(TotalThread);
728        Console.WriteLine(rangeLsit.Count.ToString());
729        Task[] tasks = new Task[rangeLsit.Count];
730        for (int i = 0; i < rangeLsit.Count; i++)
731        {
732            if (cts.IsCancellationRequested)
733            {
734                break;
735            }
736            await semaphore.WaitAsync();
737            int threadId = i;
738            int arrangeLinkId = threadId % linkList.Count;
739            tasks[threadId] = Task.Run(async () =>
740            {
741                try
742                {
743                    downloadingThread++;
744                    await DownloadThread(rangeLsit[threadId], linkList[arrangeLinkId]);
745                    downloadingThread--;
746                }
747                finally
748                {
749                    semaphore.Release();
750                }
751            });
752        }
753        if (!tasks.Contains(null))
754        {
755            await Task.WhenAll(tasks);
756
757        }
758        releaseFile();
759        CalcProgress();
760        CallBack?.Invoke(); 
761
762    }
763
764}
765\`\`\`
766
767## 暂停/继续的实现
768
769这里使用线程阻塞\`ManualResetEvent\`来实现,用法和\`线程锁\`很像,你可以在[这里](/posts/csharp-multi-thread/#线程锁和线程阻塞的区别)找到他们的区别
770
771声明一个\`ManualResetEvent\`
772\`\`\`csharp
773private readonly ManualResetEvent resetEvent = new ManualResetEvent(true);
774\`\`\`
775
776我们此时传入了\`true\`,所以默认不阻塞
777
778通过下面的函数可以切换是否阻塞:
779\`\`\`csharp
780resetEvent.Reset();   // 阻塞线程
781resetEvent.Set();     // 取消阻塞
782\`\`\`
783
784下面的函数可以在阻塞时暂停线程的运行,取消阻塞时放行:
785
786\`\`\`csharp
787resetEvent.WaitOne();
788\`\`\`
789
790所以我在流式传输的等待服务器相应和下载部分添加了WaitOne方法:
791
792\`\`\`csharp
793Some Code...
794using (HttpClient client = new HttpClient())
795{
796    client.DefaultRequestHeaders.Add("Range", $"bytes={range.from}-{range.to}");
797    client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", UserAgent);
798
799    client.Timeout = TimeSpan.FromSeconds(10);
800    using (HttpResponseMessage response = await client.GetAsync(url))
801    {
802        response.EnsureSuccessStatusCode();
803        resetEvent.WaitOne();           // 如果线程被阻塞,线程会在这里暂停运行
804        long? totalSize = response.Content.Headers.ContentLength;
805        using (Stream contentStream = await response.Content.ReadAsStreamAsync())
806        {
807            byte[] buffer = new byte[1000];
808            int bytesRead;
809            while ((bytesRead = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
810            {
811                resetEvent.WaitOne();   // 如果线程被阻塞,线程会在这里暂停运行
812Some Code...
813\`\`\`
814
815并编写\`Pause\` \`Continue\`方法来控制是否阻塞:
816\`\`\`csharp
817/// <summary>
818/// block thread to pause
819/// </summary>
820public void Pause()
821{
822    resetEvent.Reset();   // 阻塞线程来达到暂停效果
823    isPaused = true;
824    CalcProgress();
825    CallBack?.Invoke();
826
827}
828
829/// <summary>
830/// release resetEvent to cntinue
831/// </summary>
832public void Continue()
833{
834    resetEvent.Set();   // 取消阻塞继续工作
835    isPaused = false;
836    CalcProgress();
837    CallBack?.Invoke();
838
839}
840\`\`\`
841
842## 取消下载的实现
843
844C#中存在取消信号\`CancellationTokenSource\`可以用于此处
845
846> 这部分应该会在多线程进阶里,但是我太懒了没去写 + 在准备CET4,So SOON[TM]
847
848声明取消信号
849\`\`\`csharp
850private CancellationTokenSource cts = new CancellationTokenSource();
851\`\`\`
852
853他拥有一个布尔类型\`IsCancellationRequested\`可用于检测是否触发取消:
854
855\`\`\`csharp
856cts.IsCancellationRequested
857\`\`\`
858
859所以我们在连接服务器和下载时检测是否取消即可:
860
861\`\`\`csharp
862if (cts.IsCancellationRequested)
863{
864    releaseFile();    // 别忘了释放被占用的文件
865    // 直接return退出线程即可
866    return;           
867}
868\`\`\`
869
870## UI回调的实现
871
872这里实现的方式各有千秋,我的实现方式是再写入文件后通过一个CallBack函数实现实时的数据回调
873
874\`\`\`csharp
875public Action? CallBack { get; set; }
876\`\`\`
877
878使用此方法设置回调函数:
879\`\`\`csharp
880public void SetCallBack(Action callBack)
881{
882    CallBack = callBack;
883}
884\`\`\`
885
886使用调度器\`Dispatcher\`可以将我们的代码调度到UI线程执行:
887
888> 是的,又是一个应该在多线程进阶里出现的东西,挖的坑以后再填
889
890于是在写入文件的代码之后加上:
891
892\`\`\`csharp
893Dispatcher.UIThread.InvokeAsync(() =>
894{
895    CallBack?.Invoke(); // 在 UI 线程中调用
896});
897\`\`\`
898
899现在每次下载完文件块就会调用CallBack函数了,在CallBack函数中添加状态显示的逻辑代码即可
900
901## 优化
902
903这部分代码仍有以下问题
904### 由于网络环境很差,文件释放不及时
905
906这部分原因其实不是代码导致的,而是很多下载服务器会**限速**
907
908限速的原理的简化版本是:
909
910下载的第一秒传输数据,后9秒不传输数据,如此循环
911
912经过速度算法的掩盖(可以使\`显示的下载速度\`更加平滑,明明已经是\`0B/s\`但是显示\`100KB/s\`)普通人很难察觉到
913
914一个很简单的检验方法:打开任务管理器,看看网卡占用的图表,如果看着像一座一座山,就是被限速了
915
916解决方法:
917
918- 添加更多的取消信号监测点
919- 取消任务后直接释放文件,并在写入文件前添加\`FileStream\`是否可用的检测
920
921### 如果网络出错,下载会疯狂重试进入死循环
922
923解决方法:
924- 添加最大重试数
925
926## 结尾
927
928我好累...
929`;export{n as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.