有一个简单的下载代码:
internal static async Task<ImageFile> DownloadFile(Uri uri)
{
byte[] result;
WebResponse response;
var file = new ImageFile();
var request = WebRequest.Create(uri);
try
{
response = await request.GetResponseAsync();
using (var ms = new MemoryStream())
{
response.GetResponseStream().CopyTo(ms);
result = ms.ToArray();
}
}
catch (System.Exception ex) { }
if (response.ContentLength == result.LongLength)
file.Body = result;
return file;
}
我想添加一个下载速度指示器。谷歌建议你可以关注阅读流的速度:
result = await CopyTo(response.GetResponseStream(), response.ContentLength, progressChanged);
private static async Task<byte[]> CopyTo(Stream from, long totalBytes, Action<DownloadProgress> loadedEvent)
{
var sw = new Stopwatch();
sw.Start();
var data = new byte[totalBytes];
byte[] buffer = new byte[81920];
int currentIndex = 0;
while (true)
{
int num = await from.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
if (num != 0)
{
Array.Copy(buffer, 0, data, currentIndex, num);
currentIndex += num;
loadedEvent?.Invoke(new DownloadProgress(currentIndex, totalBytes, sw.ElapsedMilliseconds));
}
else
break;
}
sw.Stop();
return data;
}
为了存储信息(并向上滚动),我开始了一个简单的结构:
public struct DownloadProgress
{
public readonly long BytesReceived;
public readonly long TotalBytesToReceive;
public readonly long TimeMs;
public double GetSpeed()
{
var seconds = TimeMs / 1000.0;
if (seconds > 0)
return BytesReceived / seconds;
return 0;
}
public DownloadProgress(long received, long total, long time)
{
this.BytesReceived = received;
this.TotalBytesToReceive = total;
this.TimeMs = time;
}
}
总的来说,如果你在一个流中下载,那么GetSpeed在任何时候(除了某处的第一秒)它都会显示真实速度。
下载总图的类最终存储:
this.Speed = 0;
var file = await ImageFile.DownloadFile(this.ImageLink, dp => this.Speed = dp.GetSpeed());
this.Speed = 0;
然后我认为剩下的是最简单的 - 我只是在顶层添加了所有速度,仅此而已:
return this.ActivePages != null && this.ActivePages.Any() ?
this.ActivePages.Sum(p => p.Speed) : 0;
事实上,结果是非常不愉快的行为:
大多数时候速度实际上是正确显示的。
速度经常跳跃,点差有时超过通道宽度。对于 650 kb/s 的信道,数字从 300 kb/s 跃升至 5-8 Mb/s。
如果在一次下载结束和下一次下载开始时速度很可能较低,那么从技术上讲,超过通道宽度显然是不可能的。也许我错过了某处的四舍五入,或者Stopwatch对于这样的计算来说不够准确?
UPD:第一个怀疑是有道理的——最初的速度跳跃破坏了整体统计数据。如果速度计算方法是这样进行的:
public double GetSpeed()
{
var seconds = TimeMs / 1000.0;
if (seconds > 0.1)
return BytesReceived / seconds;
return 0;
}
那个速度一般就够用了,偶尔也会有超过通道宽度的跳跃,但不会超过5%。你可以增加被忽略的时间,这样就完全没有跳跃了。
总分有问题。如果对于一个流,该数字是可靠的,那么对于多线程下载,该数字通常是谎言,指标的平均值结果低于真实值(总时间为总容量)。
UPD2:最小化所有计算,删除结构,将逻辑移至静态类:
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.Value.Any())
{
long added;
if (receivedStorage.Value.TryDequeue(out added))
{
now += added;
}
}
lastSpeeds.Value.Enqueue(now);
totalSpeed = lastSpeeds.Value.Average();
}, null, 0, TimerInterval);
private static Lazy<LimitedConcurrentQueue<double>> lastSpeeds = new Lazy<LimitedConcurrentQueue<double>>(() => new LimitedConcurrentQueue<double>(Seconds));
private static Lazy<ConcurrentQueue<long>> receivedStorage = new Lazy<ConcurrentQueue<long>>();
public static void Clear()
{
while (receivedStorage.Value.Count > 0)
{
long dd;
receivedStorage.Value.TryDequeue(out dd);
}
while (lastSpeeds.Value.Count > 0)
{
double dd;
lastSpeeds.Value.TryDequeue(out dd);
}
}
public static void AddInfo(long received)
{
receivedStorage.Value.Enqueue(received);
}
private class LimitedConcurrentQueue<T> : ConcurrentQueue<T>
{
public uint Limit { get; }
public new void Enqueue(T item)
{
while (Count >= Limit)
{
T deleted;
TryDequeue(out deleted);
}
base.Enqueue(item);
}
public LimitedConcurrentQueue(uint limit)
{
Limit = limit;
}
}
}
结果,在下载时,报告下一时刻下载了多少字节就足够了:
NetworkSpeed.AddInfo(num);
就是这样,指示器NetworkSpeed.TotalSpeed将显示过去 3 秒内的平均速度。平均指标总体上或多或少变得稳定,尽管它略微高估了我数据中的指标。好吧,很明显,如果线程池过载,那么计时器将无法及时计时,速度将开始“跳跃”。
我正在发布对我有用的解决方案。最终数字的准确率为95-99%。
如何使用它 - 在顶层,我们
Clear()在开始或完成下载时调用它,以便结果独立于其他下载。在实际下载发生的地方,我们调用方法AddInfo,指示在下一个下载周期我们收到了多少字节。可以CopyTo从标题或DownloadProgressChangedy使用WebClient。最主要的是准确传达先前指标与当前指标之间的差异。测量的准确性由计时器 (
System.Threading.Timer) 提供,因此,为了使读数的准确性可靠,线程池必须可以自由调用callback。嗯,当然,所有测量的结果都在
TotalSpeed. 如果需要,您可以添加有关其更改的事件,以便及时显示在 UI 中。它的更新频率特别与定时器同步——否则数字变化太频繁,用户不明白速度是多少。