Skip to content

Commit 6191b33

Browse files
authored
Merge pull request #62 from dotnet-campus/t/lindexi/SegmentFileDownloaderByHttpClient
日常维护代码翻新 优化断点下载
2 parents 7a5b887 + 9057fe7 commit 6191b33

13 files changed

Lines changed: 901 additions & 187 deletions

src/FileDownloader.Tests/BreakpointResumptionTransmissionManagerTest.cs

Lines changed: 296 additions & 32 deletions
Large diffs are not rendered by default.

src/FileDownloader.Tests/BreakpointResumptionTransmissionRecordFileFormatterTest.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class BreakpointResumptionTransmissionRecordFileFormatterTest
1515
[ContractTestCase]
1616
public void Format()
1717
{
18-
"写入断点续传信息之后,可以读取数据".Test(() =>
18+
"写入断点续传信息之后,可以读取数据".Test(async () =>
1919
{
2020
var formatter = new BreakpointResumptionTransmissionRecordFileFormatter();
2121

@@ -25,21 +25,22 @@ public void Format()
2525
var downloadLength = 1024;
2626
var downloadedInfo = new List<DataRange>()
2727
{
28-
new DataRange(0, 10),
29-
new DataRange(10,20),
30-
new DataRange(100,2)
28+
new DataRange(0, 10,0),
29+
new DataRange(10,20,0),
30+
new DataRange(100,2,0)
3131
};
3232

3333
var info = new BreakpointResumptionTransmissionInfo(downloadLength, downloadedInfo);
3434
formatter.Write(writer, info);
3535

3636
memoryStream.Seek(0, SeekOrigin.Begin);
3737

38-
var result = formatter.Read(memoryStream);
38+
var result = await formatter.ReadAsync(memoryStream);
3939

4040
Assert.IsNotNull(result);
4141

4242
Assert.AreEqual(downloadLength, result.DownloadLength);
43+
Assert.IsNotNull(result.DownloadedInfo);
4344
Assert.AreEqual(downloadedInfo.Count, result.DownloadedInfo.Count);
4445

4546
for (int i = 0; i < downloadedInfo.Count; i++)

src/FileDownloader.Tests/DataRangeTest.cs

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,39 @@ namespace FileDownloader.Tests
1010
[TestClass]
1111
public class DataRangeTest
1212
{
13-
[ContractTestCase]
14-
public void TryMerge()
15-
{
16-
"传入两个不相邻的 DataRange 对象,返回不可合并".Test(() =>
17-
{
18-
var a = new DataRange(0, 1);
19-
var b = new DataRange(3, 2);
20-
var result = DataRange.TryMerge(a, b, out var data);
21-
Assert.AreEqual(false, result);
22-
});
13+
//[ContractTestCase]
14+
//public void TryMerge()
15+
//{
16+
// "传入两个不相邻的 DataRange 对象,返回不可合并".Test(() =>
17+
// {
18+
// var a = new DataRange(0, 1);
19+
// var b = new DataRange(3, 2);
20+
// var result = DataRange.TryMerge(a, b, out var data);
21+
// Assert.AreEqual(false, result);
22+
// });
2323

24-
"给定两个相邻的 DataRange 对象,传入顺序是先将传入起点较大的,再传入起点较小的,可以进行合并".Test(() =>
25-
{
26-
var a = new DataRange(0, 3);
27-
var b = new DataRange(3, 2);
24+
// "给定两个相邻的 DataRange 对象,传入顺序是先将传入起点较大的,再传入起点较小的,可以进行合并".Test(() =>
25+
// {
26+
// var a = new DataRange(0, 3);
27+
// var b = new DataRange(3, 2);
2828

29-
// 传入顺序是先将传入起点较大的,再传入起点较小的
30-
var result = DataRange.TryMerge(b, a, out var data);
31-
Assert.IsTrue(result);
32-
Assert.AreEqual(a.StartPoint, data.StartPoint);
33-
Assert.AreEqual(a.Length + b.Length, data.Length);
34-
});
29+
// // 传入顺序是先将传入起点较大的,再传入起点较小的
30+
// var result = DataRange.TryMerge(b, a, out var data);
31+
// Assert.IsTrue(result);
32+
// Assert.AreEqual(a.StartPoint, data.StartPoint);
33+
// Assert.AreEqual(a.Length + b.Length, data.Length);
34+
// });
3535

36-
"给定两个相邻的 DataRange 对象,可以进行合并".Test(() =>
37-
{
38-
var a = new DataRange(0, 3);
39-
var b = new DataRange(3, 2);
36+
// "给定两个相邻的 DataRange 对象,可以进行合并".Test(() =>
37+
// {
38+
// var a = new DataRange(0, 3);
39+
// var b = new DataRange(3, 2);
4040

41-
var result = DataRange.TryMerge(a, b, out var data);
42-
Assert.IsTrue(result);
43-
Assert.AreEqual(a.StartPoint, data.StartPoint);
44-
Assert.AreEqual(a.Length + b.Length, data.Length);
45-
});
46-
}
41+
// var result = DataRange.TryMerge(a, b, out var data);
42+
// Assert.IsTrue(result);
43+
// Assert.AreEqual(a.StartPoint, data.StartPoint);
44+
// Assert.AreEqual(a.Length + b.Length, data.Length);
45+
// });
46+
//}
4747
}
4848
}

src/FileDownloader.Tests/SegmentFileDownloaderTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public interface IMockSegmentFileDownloader
150150
}
151151
}
152152

153+
#pragma warning disable SYSLIB0014 // warning SYSLIB0014: “WebRequest.WebRequest()”已过时:“WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete. Use HttpClient instead.”
154+
// 这里就是专门测试旧代码的,忽略即可
153155
class FakeHttpWebRequest : WebRequest
154156
{
155157
public FakeHttpWebRequest(FakeWebResponse fakeWebResponse)
@@ -192,4 +194,6 @@ public override Stream GetResponseStream()
192194
return Stream;
193195
}
194196
}
197+
#pragma warning restore SYSLIB0014
198+
195199
}

src/dotnetCampus.FileDownloader.Tool/Program.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.IO;
3+
using System.Net.Http;
34
using System.Runtime.InteropServices.ComTypes;
45
using System.Threading.Tasks;
56
using CommandLine;
@@ -21,19 +22,29 @@ static async Task Main(string[] args)
2122

2223
// https://www.speedtest.cn/
2324
var url =
24-
"https://speedtest1.gd.chinamobile.com.prod.hosts.ooklaserver.net:8080/download?size=25000000&r=0.2978374611691549";
25+
"https://node-103-27-27-20.speedtest.cn:51090/download?size=25000000&r=0.625866791098137";
2526
url =
26-
"https://download.jetbrains.8686c.com/resharper/ReSharperUltimate.2020.1.3/JetBrains.ReSharperUltimate.2020.1.3.exe";
27+
"https://download.jetbrains.com/resharper/dotUltimate.2025.1/JetBrains.dotUltimate.2025.1.exe";
2728
//var md5 = "7d6bbeb6617a7c0b7e615098fca1b167";// resharper
2829

29-
url = "http://localhost:5000";
30+
//url = "http://localhost:5000";
3031

31-
var file = new FileInfo(@"File.txt");
32+
// 这里的 gitdl.cn 是 iFileProxy 离线下载工具的地址,这是一个非常好的工具。开源地址: https://git.linxi.info/xianglin_admin/iFileProxy
33+
url = "https://gitdl.cn/https://github.com/srwi/EverythingToolbar/releases/download/1.5.2/EverythingToolbar-1.5.2.msi";
34+
35+
//url =
36+
// "https://down.pc.yyb.qq.com/pcyyb/packing/14e1e37f997f49a58d560ab97fa335aa/pcyyb_2702800040_installer.exe";
37+
//url = "https://pm.myapp.com/invc/xfspeed/qqpcmgr/download/QQPCDownload320001.exe";
38+
//// 这个地址带了 Content-Disposition 头,文件名是从这个头中获取的
39+
url =
40+
"https://sw.pcmgr.qq.com/2f472366ca30d8ac1ad4acb64c77d2ad/680c8f51/spcmgr/download/BaiduNetdisk_txgj1_7.50.0.132.exe";
41+
//url = "https://pc-package.wpscdn.cn/wps/download/W.P.S.60.1955.exe";
42+
43+
var downloadFolder = new DirectoryInfo(@"DownloadFolder");
3244

3345
var progress = new Progress<DownloadProgress>();
3446

35-
var segmentFileDownloader = new SegmentFileDownloader(url, file, logger, progress);
36-
await segmentFileDownloader.DownloadFileAsync();
47+
await FileDownloaderHelper.DownloadFileToFolderAsync(url, downloadFolder, progress:progress);
3748
#endif
3849
await Task.Delay(100);
3950
});
@@ -76,7 +87,7 @@ private static async Task DownloadFileAsync(DownloadOption option)
7687

7788
var file = new FileInfo(output);
7889
var url = option.Url;
79-
var segmentFileDownloader = new SegmentFileDownloader(url, file, logger, progress);
90+
using var segmentFileDownloader = new SegmentFileDownloaderByHttpClient(url, file, httpClient:null, logger, progress);
8091

8192
await segmentFileDownloader.DownloadFileAsync();
8293

src/dotnetCampus.FileDownloader.WPF/Business/DownloadFileListManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public class DownloadFileListManager
1717
/// 读取本地存储的下载列表
1818
/// </summary>
1919
/// <returns></returns>
20-
public async Task<List<DownloadFileInfo>> ReadDownloadedFileListAsync()
20+
public async Task<List<DownloadFileInfo>?> ReadDownloadedFileListAsync()
2121
{
2222
var file = GetStorageFilePath();
2323

src/dotnetCampus.FileDownloader/DownloadByHttpClient/SegmentFileDownloaderByHttpClient.cs

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
using Microsoft.Extensions.Logging;
1818

19+
// ReSharper disable once CheckNamespace
1920
namespace dotnetCampus.FileDownloader;
2021

2122
/// <summary>
@@ -61,7 +62,7 @@ public SegmentFileDownloaderByHttpClient(string url, FileInfo file,
6162
_shouldDisposeHttpClient = httpClient is null;
6263
HttpClient = httpClient ?? CreateDefaultHttpClient();
6364

64-
DownloadDataList = Channel.CreateUnbounded<DownloadData>(new UnboundedChannelOptions()
65+
DownloadDataChannel = Channel.CreateUnbounded<DownloadData>(new UnboundedChannelOptions()
6566
{
6667
SingleReader = false,
6768
AllowSynchronousContinuations = true,
@@ -135,6 +136,12 @@ private static SocketsHttpHandler CreateDefaultSocketsHttpHandler()
135136
//{
136137
// return ValueTask.FromResult(context.PlaintextStream);
137138
//}
139+
140+
// 忽略证书错误
141+
//SslOptions =
142+
//{
143+
// RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true
144+
//}
138145
};
139146

140147
return socketsHttpHandler;
@@ -163,12 +170,12 @@ private static SocketsHttpHandler CreateDefaultSocketsHttpHandler()
163170
/// </summary>
164171
public string Url { get; }
165172

166-
private Channel<DownloadData> DownloadDataList { get; }
173+
private Channel<DownloadData> DownloadDataChannel { get; }
167174

168175
/// <summary>
169-
/// 被加入到 <see cref="DownloadDataList"/> 的下载数量
176+
/// 被加入到 <see cref="DownloadDataChannel"/> 的下载数量
170177
/// </summary>
171-
private int _workTaskCount;
178+
private int _workingTaskCount;
172179

173180
/// <summary>
174181
/// 下载的文件
@@ -233,7 +240,7 @@ private async void ControlSwitch()
233240
{
234241
LogDebugInternal("Start ControlSwitch");
235242
var (segment, runCount, maxReportTime) = SegmentManager.GetMaxWaitReportTimeDownloadSegmentStatus();
236-
var waitCount = _workTaskCount;
243+
var waitCount = _workingTaskCount;
237244

238245
LogDebugInternal("ControlSwitch 当前等待数量:{0},待命最大响应时间:{1},运行数量:{2},运行线程{3}", waitCount, maxReportTime, runCount, _threadCount);
239246

@@ -320,10 +327,13 @@ public async ValueTask DownloadFileAsync()
320327
return;
321328
}
322329

323-
FileStream = File.Create();
324-
FileStream.SetLength(contentLength);
330+
FileStream = File.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read/*允许边下边播*/);
331+
if (FileStream.Length == 0)
332+
{
333+
// 如果是刚刚创建的,则预先分配空间。实际测试下载器预先分配空间能够获取更好的机械硬盘性能
334+
FileStream.SetLength(contentLength);
335+
}
325336
FileWriter = new RandomFileWriterWithOrderFirst(FileStream);
326-
FileWriter.StepWriteFinished += (sender, args) => SharedArrayPool.Return(args.Data);
327337

328338
if (BreakpointResumptionTransmissionRecordFile is null)
329339
{
@@ -333,11 +343,13 @@ public async ValueTask DownloadFileAsync()
333343
else
334344
{
335345
// 有断点续传
336-
var manager = new BreakpointResumptionTransmissionManager(BreakpointResumptionTransmissionRecordFile, FileWriter, contentLength);
346+
var manager = new BreakpointResumptionTransmissionManager(BreakpointResumptionTransmissionRecordFile, FileWriter, SharedArrayPool, contentLength, BufferLength);
337347
// 有断点续传情况下,先读取断点续传文件,通过此文件获取到需要下载的内容
338-
SegmentManager = manager.CreateSegmentManager();
348+
SegmentManager = await manager.CreateSegmentManagerAsync(FileStream);
339349
BreakpointResumptionTransmissionManager = manager;
340350
}
351+
// 由于 BreakpointResumptionTransmissionManager 也在监控 StepWriteFinished 事件,如果这里的事件加等更快执行,则会导致数据已经还给了池,被其他地方使用,在 BreakpointResumptionTransmissionManager 存在线程安全
352+
FileWriter.StepWriteFinished += (sender, args) => SharedArrayPool.Return(args.Data);
341353

342354
_progress.Report(new DownloadProgress($"file length = {contentLength}", SegmentManager));
343355

@@ -559,15 +571,20 @@ private async ValueTask DownloadTask()
559571
DownloadData data;
560572
try
561573
{
562-
var canRead = await DownloadDataList.Reader.WaitToReadAsync();
574+
var canRead = await DownloadDataChannel.Reader.WaitToReadAsync();
563575
if (!canRead)
564576
{
565577
// 不能读取了,那就返回吧
566578
return;
567579
}
568580

569-
data = await DownloadDataList.Reader.ReadAsync().ConfigureAwait(false);
570-
Interlocked.Decrement(ref _workTaskCount);
581+
if (!DownloadDataChannel.Reader.TryRead(out data))
582+
{
583+
// 居然读取不到数据,那就再次进入循环吧
584+
continue;
585+
}
586+
587+
Interlocked.Decrement(ref _workingTaskCount);
571588
}
572589
catch (ChannelClosedException)
573590
{
@@ -712,8 +729,8 @@ private void LogDownloadSegment(DownloadSegment downloadSegment)
712729
private async void Download(HttpResponseMessage? httpResponseMessage, DownloadSegment downloadSegment)
713730
{
714731
LogDebugInternal("[Download] Enqueue Download. {0}", downloadSegment);
715-
await DownloadDataList.Writer.WriteAsync(new DownloadData(httpResponseMessage, downloadSegment)).ConfigureAwait(false);
716-
Interlocked.Increment(ref _workTaskCount);
732+
await DownloadDataChannel.Writer.WriteAsync(new DownloadData(httpResponseMessage, downloadSegment)).ConfigureAwait(false);
733+
Interlocked.Increment(ref _workingTaskCount);
717734
}
718735

719736
private void Download(DownloadSegment? downloadSegment)
@@ -748,7 +765,7 @@ private async ValueTask FinishDownload()
748765
await FileWriter.DisposeAsync().ConfigureAwait(false);
749766
await FileStream.DisposeAsync().ConfigureAwait(false);
750767

751-
DownloadDataList.Writer.Complete();
768+
DownloadDataChannel.Writer.Complete();
752769

753770
BreakpointResumptionTransmissionManager?.Dispose();
754771
// 默认下载完成删除断点续传文件

src/dotnetCampus.FileDownloader/DownloadSegment.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,19 @@ public DownloadSegment(long startPoint, long requirementDownloadPoint)
4848
/// 当前的信息,仅用于调试
4949
/// </summary>
5050
public string? Message { get; set; }
51+
52+
/// <summary>
53+
/// 最后的下载状态更新时间
54+
/// </summary>
55+
/// todo 改名 LastDownloadTime 或 LastUpdateTime
5156
public DateTime LastDownTime { get; set; } = DateTime.Now;
57+
58+
/// <summary>
59+
/// 下载状态
60+
/// </summary>
61+
/// todo 改名 DownloadingState
5262
public DownloadingState LoadingState { get; set; } = DownloadingState.Pause;
63+
5364
/// <summary>
5465
/// 需要下载到的点
5566
/// </summary>
@@ -98,13 +109,24 @@ internal set
98109
/// </summary>
99110
public SegmentManager? SegmentManager { set; get; }
100111
}
112+
101113
/// <summary>
102114
/// 下载状态
103115
/// </summary>
104116
public enum DownloadingState
105117
{
118+
/// <summary>
119+
/// 运行中
120+
/// </summary>
121+
/// todo 改名 Running
106122
Runing = 1,
123+
/// <summary>
124+
/// 暂停
125+
/// </summary>
107126
Pause = 0,
127+
/// <summary>
128+
/// 完成
129+
/// </summary>
108130
Finished = -1
109131
}
110132
}

0 commit comments

Comments
 (0)