背景知识
我有一个小型且舒适的宠物项目,可让您从Internet下载文件。 这些文件被分组在一起,并且没有为用户显示每个文件,而是进行了一些分组。 整个下载过程(以及该过程的显示)高度依赖于数据。 数据是即时获得的,即 用户开始下载,实际上没有信息您需要下载多少。
至少某种形式的通知的简单实现很简单-下载进度显示为下载数量与总数之比。 对于用户而言,没有太多信息-只是一条爬行带,但这总比没有好,并且比没有提示进度的当前流行的加载机制要好得多。
然后,用户出现一个逻辑问题-一大群人不清楚为什么进展几乎没有进展-我需要下载很多文件还是速度慢? 正如我上面提到的那样,文件数量是未知的。 因此,我决定添加一个速度计数器。
分析方法
最好的做法是让那些已经解决了类似问题的人避免重新发明轮子。 不同的软件会关闭这些不同的任务,但显示效果几乎相同:
我为自己确定的关键点是当前需要首次显示速度。 不是开始时的平均速度,不是开始时的整体平均速度,即当前的数字。 实际上,当我接触到代码时,这很重要-我将单独解释它。
因此,我们需要一个简单的数字,例如10 MB/s
或类似的数字。 我们如何计算呢?
理论与实践
现有的下载实现使用HttpWebRequest
,我决定不重做下载本身-不涉及工作机制。
因此,初始实现无需任何计算:
var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); using (var ms = new MemoryStream()) { await response.GetResponseStream().CopyToAsync(ms); return ms.ToArray(); }
在此类API的级别上,您只能响应文件的完整下载,对于小组(甚至是单个文件),您实际上无法计算速度。 我们遵循CopyToAsync源代码 ,从此处复制粘贴简单的逻辑:
byte[] buffer = new byte[bufferSize]; int bytesRead; while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) { await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); }
现在,我们可以响应网络上提供给我们的每个缓冲区。
因此,首先,我们要做的是代替装箱的CopyToAsync:
public static async Task<byte[]> GetBytesAsync(this Stream from) { using (var memory = new MemoryStream()) { byte[] buffer = new byte[81920]; int bytesRead; while ((bytesRead = await from.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0) { await memory.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false); NetworkSpeed.AddInfo(bytesRead); } return memory.ToArray(); } }
真正添加的唯一东西是NetworkSpeed.AddInfo
。 我们传输的唯一内容是下载的字节数。
用于下载的代码本身如下所示:
var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); var array = await response.GetResponseStream().GetBytesAsync();
WebClient的选项 var client = new WebClient(); var lastRecorded = 0L; client.DownloadProgressChanged += (sender, eventArgs) => { NetworkSpeed.AddInfo(eventArgs.BytesReceived - lastRecorded); lastRecorded = eventArgs.BytesReceived; }; var array = await client.DownloadDataTaskAsync(uri);
HttpClient的选项 var httpClient = new HttpClient(); var content = await httpClient.GetStreamAsync(uri); var array = await content.GetBytesAsync();
好了,问题解决了一半-我们知道下载了多少 。 我们转向速度。
根据维基百科 :
数据传输速率-每单位时间传输的数据量。
第一种幼稚的方法
我们有数量。 从字面上看,时间可以从启动开始获取,并通过DateTime.Now
获得差异。 共享?
对于curl之类的控制台实用程序,这是可行的并且有意义。
但是,如果您的应用程序稍微复杂一点,那么暂停按钮实际上会使您的生活复杂化。
关于暂停的一点
也许我很天真,或者问题真的不是那么简单-但是停顿让我不断思考。 下载时暂停至少可以通过三种方式进行:
- 中断文件上传,之后重新开始
- 只是不要进一步下载文件,希望服务器在以后继续
- 下载已经开始的文件,不要下载新文件,之后再下载新文件
由于前两个导致已经下载的信息丢失,因此我使用第三个。
稍微高一点,我注意到精确地需要一个时间点。 因此,暂停会使此问题复杂化:
- 您无法正确计算平均速度,只是将音量调了一段时间
- 暂停可能有外部原因,这会改变速度和通道(重新连接到提供商的网络,切换到VPN,终止占用整个通道的uTorrent),这将导致实际速度的改变
实际上,暂停会将所有指标分为之前和之后。 这不会对下面的代码产生特别的影响,只需考虑一分钟的有趣信息即可。
第二种天真的方法
添加一个计时器。 每个时间段的计时器将获取有关下载音量的所有最新信息,并重新计算速度指示器。 并且,如果您将计时器设置为每秒,那么此刻接收到的有关下载音量的所有信息将等于此刻的速度:
整个NetworkSpeed类的实现 public class NetworkSpeed { public static double TotalSpeed { get { return totalSpeed; } } private static double totalSpeed = 0; private const uint TimerInterval = 1000; private static Timer speedTimer = new Timer(state => { var now = 0L; while (ReceivedStorage.TryDequeue(out var added)) now += added; totalSpeed = now; }, null, 0, TimerInterval); private static readonly ConcurrentQueue<long> ReceivedStorage = new ConcurrentQueue<long>(); public static void Clear() { while (ReceivedStorage.TryDequeue(out _)) { } totalSpeed = 0; } public static void AddInfo(long received) { ReceivedStorage.Enqueue(received); } }
与第一个选项相比,这样的实现开始对暂停做出响应-在外部数据到达后的第二秒,速度降为0。
但是,也有缺点。 我们正在使用80kb的缓冲区,这意味着在一秒钟内开始的下载将仅在下一秒内显示。 由于大量并行下载,此类测量错误将显示任何内容-我的实数传播高达30%。 我可能没有注意到,但是超过100 Mbit似乎太可疑了 。
第三种方法
第二个选项已经足够接近事实了,再加上在下载开始时就发现了他的错误,而不是整个生命周期。
因此,一个简单的解决方案不是以每秒的数字为指标,而是以最近三秒的平均值作为指标。 这里的三个是眼睛匹配的魔术常数。 一方面,我希望显示速度的增长和下降,另一方面,让速度更接近真实。
实现有点复杂,但是总的来说,没有什么像下面这样:
整个NetworkSpeed类的实现 public class NetworkSpeed { public static double TotalSpeed { get { return totalSpeed; } } private static double totalSpeed = 0; private const uint Seconds = 3; private const uint TimerInterval = 1000; private static Timer speedTimer = new Timer(state => { var now = 0L; while (ReceivedStorage.TryDequeue(out var added)) now += added; LastSpeeds.Enqueue(now); totalSpeed = LastSpeeds.Average(); OnUpdated(totalSpeed); }, null, 0, TimerInterval); private static readonly LimitedConcurrentQueue<double> LastSpeeds = new LimitedConcurrentQueue<double>(Seconds); private static readonly ConcurrentQueue<long> ReceivedStorage = new ConcurrentQueue<long>(); public static void Clear() { while (ReceivedStorage.TryDequeue(out _)) { } while (LastSpeeds.TryDequeue(out _)) { } totalSpeed = 0; } public static void AddInfo(long received) { ReceivedStorage.Enqueue(received); } public static event Action<double> Updated; private class LimitedConcurrentQueue<T> : ConcurrentQueue<T> { public uint Limit { get; } public new void Enqueue(T item) { while (Count >= Limit) TryDequeue(out _); base.Enqueue(item); } public LimitedConcurrentQueue(uint limit) { Limit = limit; } } private static void OnUpdated(double obj) { Updated?.Invoke(obj); } }
要点:
- 在实施时,我没有找到限制元素数量的完整队列,而是将其放在Internet上,在上面的代码中是
LimitedConcurrentQueue
。 - 出于某种原因而不是实现
INotifyPropertyChanged
而不是Action
,其用法实际上是相同的,我不记得原因了。 逻辑很简单-指示器在变化,需要通知用户。 该实现可以是任何更方便的实现,甚至是IObservable
。
和一点可读性
API提供了以字节为单位的速度,对于可读性而言,一个简单的速度(从Internet上获取)非常有用
转换器 public static string HumanizeByteSize(this long byteCount) { string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
让我提醒您,速度以字节为单位,即 每100mbit频道应发布的空间不超过12.5MB。
最终效果如何:
下载ubuntu图片当前速度904.5KB / s
当前速度1.8MB / s
当前速度2.9MB / s
当前速度3.2MB / s
当前速度2.9MB / s
当前速度2.8MB / s
当前速度3MB / s
当前速度3.1MB / s
当前速度3.2MB / s
当前速度3.3MB / s
当前速度3,5MB / s
当前速度3.6MB / s
当前速度3.6MB / s
当前速度3.6MB / s
...
好吧,一次多张图片当前速度1,2MB / s
当前速度3.8MB / s
当前速度7.3MB / s
当前速度10MB / s
当前速度10.3MB / s
当前速度10MB / s
当前速度9.7MB / s
当前速度9.8MB / s
当前速度10.1MB / s
当前速度9.8MB / s
当前速度9.1MB / s
当前速度8.6MB / s
当前速度8.4MB / s
...
结论
处理看似平凡的速度计算任务很有趣。 即使代码有效并且给出了一些数字,我还是想听听批评者的意见-我错过了什么,我如何做得更好,也许有一些现成的解决方案。
我要说的是感谢俄语中的Stack Overflow ,尤其是VladD-exrabbit-尽管一个好问题只能解决一半,但任何提示和任何帮助都会使您前进。
我想提醒您,这是一个宠物项目-这就是为什么该类是静态的并且是一个类的原因,因此准确性不是真的。 我看到许多可以做得更好的小事情,但是...总是有其他事情要做,所以现在我认为这就是速度,我认为这不是一个坏选择。