Zählen der Download-Geschwindigkeit in Ihrer Anwendung

Hintergrund


Ich habe ein kleines und gemütliches Haustierprojekt, mit dem Sie Dateien aus dem Internet herunterladen können. Die Dateien sind zusammen gruppiert und dem Benutzer wird nicht jede Datei angezeigt, sondern eine Gruppierung. Der gesamte Download-Prozess (und die Anzeige dieses Prozesses) war stark von den Daten abhängig. Die Daten wurden im laufenden Betrieb erhalten, d.h. Der Benutzer beginnt mit dem Download und es gibt keine Informationen darüber, wie viel Sie in der Realität herunterladen müssen.


Die naive Implementierung von mindestens einer Art von Information wird vereinfacht - der Download-Fortschritt wird als Verhältnis der Anzahl der heruntergeladenen zur Gesamtzahl angezeigt. Es gibt nicht viele Informationen für den Benutzer - nur einen kriechenden Streifen, aber dies ist besser als nichts und es ist merklich besser als der derzeit beliebte Lademechanismus, ohne den Fortschritt anzuzeigen.


Und hier erscheint ein Benutzer mit einem logischen Problem - in einer großen Gruppe ist nicht klar, warum der Fortschritt kaum schleicht - müssen Sie viele Dateien oder eine niedrige Geschwindigkeit herunterladen? Wie oben erwähnt - die Anzahl der Dateien ist nicht im Voraus bekannt. Deshalb habe ich beschlossen, einen Geschwindigkeitszähler hinzuzufügen.


Analyse


Es ist empfehlenswert, diejenigen zu sehen, die bereits ein ähnliches Problem gelöst haben, um das Rad nicht neu zu erfinden. Unterschiedliche Software schließt diese unterschiedlichen Aufgaben, aber die Anzeige sieht ziemlich gleich aus:


uTorrentDownloadmaster
uTorrentDownloadmaster

Der entscheidende Punkt, den ich für mich selbst identifiziert habe, ist, dass zum aktuellen Zeitpunkt die erste Anzeige der Geschwindigkeit erforderlich ist. Nicht welche Geschwindigkeit durchschnittlich war, nicht welche Geschwindigkeit insgesamt von Anfang an durchschnittlich war, nämlich was diese Zahl im aktuellen Moment ist. In der Tat ist dies wichtig, wenn ich zum Code komme - ich werde es separat erklären.


Wir brauchen also eine einfache Ziffer wie 10 MB/s oder so ähnlich. Wie berechnen wir das?


Theorie und Praxis


Die vorhandene Download-Implementierung verwendete HttpWebRequest und ich entschied mich, den Download selbst nicht zu wiederholen - berühren Sie nicht den Arbeitsmechanismus.


Also die erste Implementierung ohne Berechnung:


  var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); using (var ms = new MemoryStream()) { await response.GetResponseStream().CopyToAsync(ms); return ms.ToArray(); } 

Auf der Ebene einer solchen API können Sie nur auf einen vollständigen Download einer Datei reagieren. Für kleine Gruppen (oder sogar für eine einzelne Datei) können Sie die Geschwindigkeit nicht berechnen. Wir folgen dem CopyToAsync-Quellcode und kopieren die einfache Logik von dort:


  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); } 

Jetzt können wir auf jeden Puffer reagieren, der uns über das Netzwerk zur Verfügung gestellt wird.


Also, zuerst, was wir anstelle des Boxed CopyToAsync tun:


  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(); } } 

Das einzige, was wirklich hinzugefügt wurde, ist NetworkSpeed.AddInfo . Und das einzige, was wir übertragen, ist die Anzahl der heruntergeladenen Bytes.


Der Code selbst zum Herunterladen sieht folgendermaßen aus:


  var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); var array = await response.GetResponseStream().GetBytesAsync(); 

Option für 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); 

Option für HttpClient
  var httpClient = new HttpClient(); var content = await httpClient.GetStreamAsync(uri); var array = await content.GetBytesAsync(); 

Nun, das halbe Problem ist gelöst - wir wissen, wie viel wir heruntergeladen haben. Wir wenden uns der Geschwindigkeit zu.


Laut Wikipedia :


Datenübertragungsrate - Die pro Zeiteinheit übertragene Datenmenge.

Erster naiver Ansatz


Wir haben ein Volumen. Die Zeit kann buchstäblich vom Start genommen werden und den Unterschied mit DateTime.Now . Nehmen und teilen?
Für Konsolendienstprogramme wie Curl ist dies möglich und sinnvoll.
Wenn Ihre Anwendung jedoch etwas komplizierter ist, wird die Pause-Taste Ihr Leben buchstäblich erheblich verkomplizieren.


Ein wenig über die Pause
Vielleicht bin ich sehr naiv, oder vielleicht ist die Frage wirklich nicht so einfach - aber die Pause lässt mich ständig nachdenken. Eine Pause beim Herunterladen kann sich auf mindestens drei Arten verhalten:


  • Datei-Upload unterbrechen, danach erneut starten
  • Laden Sie die Datei einfach nicht weiter herunter und hoffen Sie, dass der Server danach fortgesetzt wird
  • Laden Sie bereits gestartete Dateien herunter, laden Sie keine neuen herunter, laden Sie danach neue herunter

Da die ersten beiden zum Verlust bereits heruntergeladener Informationen führen, verwende ich die dritte.
Etwas höher bemerkte ich, dass Geschwindigkeit genau zu einem bestimmten Zeitpunkt benötigt wird. Eine Pause erschwert diese Angelegenheit:


  • Sie können die Durchschnittsgeschwindigkeit nicht richtig berechnen, indem Sie nur die Lautstärke für eine Weile messen
  • Eine Pause kann externe Gründe haben, die die Geschwindigkeit und den Kanal ändern (erneutes Verbinden mit dem Netzwerk des Anbieters, Umschalten auf VPN, Beenden von uTorrent, das den gesamten Kanal beansprucht), was zu einer Änderung der tatsächlichen Geschwindigkeit führt
    Tatsächlich unterteilt eine Pause alle Indikatoren in vorher und nachher. Dies wirkt sich nicht besonders auf den folgenden Code aus, nur eine Minute lustiger Informationen zum Nachdenken.

Zweiter naiver Ansatz


Fügen Sie einen Timer hinzu. Der Timer erfasst in jedem Zeitraum die neuesten Informationen zum heruntergeladenen Volume und berechnet die Geschwindigkeitsanzeige neu. Wenn Sie den Timer pro Sekunde einstellen, entsprechen alle für diese Sekunde empfangenen Informationen über das heruntergeladene Volume der Geschwindigkeit für diese Sekunde:


Die gesamte Implementierung der NetworkSpeed-Klasse
  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); } } 

Im Vergleich zur ersten Option reagiert eine solche Implementierung auf eine Pause - die Geschwindigkeit sinkt in der nächsten Sekunde auf 0, nachdem die externen Daten eingetroffen sind.
Es gibt aber auch Nachteile. Wir arbeiten mit einem Puffer von 80 KB, was bedeutet, dass der in einer Sekunde gestartete Download erst in der nächsten angezeigt wird. Und bei einem großen Strom paralleler Downloads zeigen solche Messfehler alles an - ich hatte einen Spread von bis zu 30% der reellen Zahlen. Ich hätte es vielleicht nicht bemerkt, aber mehr als 100 Mbit sahen zu verdächtig aus .


Dritter Ansatz


Die zweite Option ist bereits nah genug an der Wahrheit, und sein Fehler wurde zu Beginn des Downloads und nicht während des gesamten Lebenszyklus häufiger beobachtet.
Eine einfache Lösung besteht daher darin, nicht die Zahl pro Sekunde, sondern den Durchschnitt der letzten drei Sekunden als Indikator zu verwenden. Drei hier ist eine magische Konstante, die mit dem Auge übereinstimmt. Einerseits wollte ich eine angenehme Darstellung des Wachstums und des Rückgangs der Geschwindigkeit, andererseits - damit die Geschwindigkeit näher an der Wahrheit lag.


Die Implementierung ist etwas kompliziert, aber im Allgemeinen nichts dergleichen:


Die gesamte Implementierung der NetworkSpeed-Klasse
  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); } } 

Ein paar Punkte:


  • Zum Zeitpunkt der Implementierung habe ich die fertige Warteschlange mit einer Begrenzung der Anzahl der Elemente nicht gefunden und im Internet verwendet. Im obigen Code ist sie LimitedConcurrentQueue .
  • Anstatt INotifyPropertyChanged aus irgendeinem Grund zu implementieren, ist die Verwendung praktisch dieselbe. Ich erinnere mich nicht an die Gründe. Die Logik ist einfach - der Indikator ändert sich, Benutzer müssen darüber informiert werden. Die Implementierung kann eine beliebige sein, auch IObservable , für die sie bequemer ist.

Und ein bisschen Lesbarkeit


Die API gibt die Geschwindigkeit in Bytes an. Für die Lesbarkeit ist eine einfache (im Internet verwendete) nützlich


Konverter
  public static string HumanizeByteSize(this long byteCount) { string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; //Longs run out around EB if (byteCount == 0) return "0" + suf[0]; long bytes = Math.Abs(byteCount); int place = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); double num = Math.Round(bytes / Math.Pow(1024, place), 1); return Math.Sign(byteCount) * num + suf[place]; } public static string HumanizeByteSize(this double byteCount) { if (double.IsNaN(byteCount) || double.IsInfinity(byteCount) || byteCount == 0) return string.Empty; return HumanizeByteSize((long)byteCount); } 

Ich möchte Sie daran erinnern, dass die Geschwindigkeit in Bytes, d. H. pro 100mbit Kanal sollte nicht mehr als 12,5MB ausgeben.


Wie es letztendlich aussieht:


Laden Sie das Ubuntu-Bild herunter
Aktuelle Geschwindigkeit 904,5 KB / s
Aktuelle Geschwindigkeit 1,8 MB / s
Aktuelle Geschwindigkeit 2,9 MB / s
Aktuelle Geschwindigkeit 3,2 MB / s
Aktuelle Geschwindigkeit 2,9 MB / s
Aktuelle Geschwindigkeit 2,8 MB / s
Aktuelle Geschwindigkeit 3MB / s
Aktuelle Geschwindigkeit 3,1 MB / s
Aktuelle Geschwindigkeit 3,2 MB / s
Aktuelle Geschwindigkeit 3,3 MB / s
Aktuelle Geschwindigkeit 3,5 MB / s
Aktuelle Geschwindigkeit 3,6 MB / s
Aktuelle Geschwindigkeit 3,6 MB / s
Aktuelle Geschwindigkeit 3,6 MB / s
...

Nun, mehrere Bilder gleichzeitig
Aktuelle Geschwindigkeit 1,2 MB / s
Aktuelle Geschwindigkeit 3,8 MB / s
Aktuelle Geschwindigkeit 7,3 MB / s
Aktuelle Geschwindigkeit 10MB / s
Aktuelle Geschwindigkeit 10,3 MB / s
Aktuelle Geschwindigkeit 10MB / s
Aktuelle Geschwindigkeit 9,7 MB / s
Aktuelle Geschwindigkeit 9,8 MB / s
Aktuelle Geschwindigkeit 10,1 MB / s
Aktuelle Geschwindigkeit 9,8 MB / s
Aktuelle Geschwindigkeit 9,1 MB / s
Aktuelle Geschwindigkeit 8,6 MB / s
Aktuelle Geschwindigkeit 8,4 MB / s
...

Fazit


Es war interessant, sich mit einer scheinbar banalen Aufgabe zu befassen, Geschwindigkeit zu zählen. Und obwohl der Code funktioniert und einige Zahlen herausgibt, möchte ich Kritikern zuhören - was ich vermisst habe, wie könnte ich es besser machen, vielleicht gibt es einige vorgefertigte Lösungen.


Ich möchte mich bei Stack Overflow auf Russisch und speziell bei VladD-exrabbit bedanken - obwohl eine gute Frage die Hälfte der Antwort enthält, bringen Sie alle Hinweise und jede Hilfe immer weiter.


Ich möchte Sie daran erinnern, dass dies ein Haustierprojekt ist - deshalb ist die Klasse statisch und überhaupt eine, daher ist die Genauigkeit nicht wirklich. Ich sehe viele kleine Dinge, die besser gemacht werden könnten, aber ... es gibt immer etwas anderes zu tun, also denke ich, dass dies die Geschwindigkeit ist und ich denke, dass dies keine schlechte Option ist.

Source: https://habr.com/ru/post/de465669/


All Articles