Contexte
J'ai un petit et confortable projet pour animaux de compagnie, qui vous permet de télécharger des fichiers depuis Internet. Les fichiers sont regroupés et l'utilisateur ne voit pas chaque fichier, mais un certain regroupement. Et l'ensemble du processus de téléchargement (et l'affichage de ce processus) dépendait fortement des données. Les données ont été obtenues à la volée, c'est-à-dire l'utilisateur commence à télécharger et il n'y a aucune information sur le montant à télécharger en réalité.
La mise en œuvre naïve d'au moins une sorte d'information est simplifiée - la progression du téléchargement est affichée sous la forme du rapport entre le nombre de téléchargements et le nombre total. Il n'y a pas beaucoup d'informations pour l'utilisateur - juste une bande rampante, mais c'est mieux que rien, et c'est nettement mieux que le mécanisme de chargement actuellement populaire sans indiquer de progrès.
Et puis un utilisateur apparaît avec un problème logique - dans un grand groupe, il n'est pas clair pourquoi les progrès progressent à peine - dois-je télécharger beaucoup de fichiers ou à faible vitesse? Comme je l'ai mentionné ci-dessus - le nombre de fichiers n'est pas connu à l'avance. J'ai donc décidé d'ajouter un compteur de vitesse.
Analyse
C’est une bonne pratique de voir ceux qui ont déjà résolu un problème similaire afin de ne pas réinventer la roue. Un logiciel différent ferme ces différentes tâches, mais l'affichage est à peu près le même:
Le point clé que je me suis identifié est que le premier affichage de la vitesse est nécessaire à l'heure actuelle. Pas quelle vitesse était moyenne, pas quelle vitesse dans son ensemble était moyenne depuis le début, à savoir ce que ce chiffre est au moment actuel. En fait, c'est important lorsque j'arrive au code - je l'expliquerai séparément.
Donc, nous avons besoin d'un simple chiffre comme 10 MB/s
ou quelque chose comme ça. Comment le calculons-nous?
Théorie et pratique
L'implémentation de téléchargement existante a utilisé HttpWebRequest
et j'ai décidé de ne pas refaire le téléchargement lui-même - ne touchez pas au mécanisme de travail.
Donc, l'implémentation initiale sans aucun calcul:
var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); using (var ms = new MemoryStream()) { await response.GetResponseStream().CopyToAsync(ms); return ms.ToArray(); }
Au niveau d'une telle API, vous ne pouvez répondre qu'à un téléchargement de fichier complet, pour de petits groupes (ou même pour un seul fichier), la vitesse ne peut en fait pas être calculée. Nous suivons le code source CopyToAsync , copier-coller la logique simple à partir de là:
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); }
Nous pouvons maintenant répondre à chaque tampon qui nous est donné sur le réseau.
Donc, premièrement, ce que nous faisons au lieu du CopyToAsync encadré:
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(); } }
La seule chose vraiment ajoutée est NetworkSpeed.AddInfo
. Et la seule chose que nous transmettons est le nombre d'octets téléchargés.
Le code lui-même pour le téléchargement ressemble à ceci:
var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); var array = await response.GetResponseStream().GetBytesAsync();
Option pour 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 pour HttpClient var httpClient = new HttpClient(); var content = await httpClient.GetStreamAsync(uri); var array = await content.GetBytesAsync();
Eh bien, la moitié du problème est résolu - nous savons combien nous avons téléchargé. Nous nous tournons vers la vitesse.
Selon wikipedia :
Taux de transfert de données - la quantité de données transmises par unité de temps.
Première approche naïve
Nous avons un volume. Le temps peut être pris littéralement au démarrage et faire la différence avec DateTime.Now
. Prendre et partager?
Pour les utilitaires de console comme curl, cela est possible et logique.
Mais si votre application est un peu plus compliquée, le bouton de pause compliquera littéralement votre vie.
Un peu sur la pause
Peut-être que je suis très naïf, ou peut-être que la question n'est vraiment pas si simple - mais la pause me fait réfléchir constamment. Une pause pendant le téléchargement peut se comporter d'au moins trois façons:
- interrompre le téléchargement du fichier, recommencer après
- ne téléchargez simplement pas le fichier plus loin, espérons que le serveur continuera après
- téléchargez les fichiers déjà démarrés, n'en téléchargez pas de nouveaux, téléchargez-en de nouveaux après
Étant donné que les deux premiers entraînent la perte d'informations déjà téléchargées, j'utilise le troisième.
Un peu plus haut, j'ai remarqué que la vitesse est nécessaire précisément à un moment donné. Donc, une pause complique cette affaire:
- vous ne pouvez pas calculer correctement la vitesse moyenne en prenant simplement le volume pendant un certain temps
- Une pause peut avoir des raisons externes qui changeront la vitesse et le canal (se reconnecter au réseau du fournisseur, passer au VPN, mettre fin à uTorrent qui prend tout le canal), ce qui entraînera un changement de la vitesse réelle
En fait, une pause divise tous les indicateurs en avant et après. Cela n'affecte pas particulièrement le code ci-dessous, juste une minute d'informations amusantes à penser.
Seconde approche naïve
Ajoutez une minuterie. La minuterie à chaque période prendra toutes les dernières informations sur le volume téléchargé et recalculera l'indicateur de vitesse. Et si vous définissez le minuteur par seconde, toutes les informations reçues pour cette seconde sur le volume téléchargé seront égales à la vitesse pour cette seconde:
L'implémentation complète de la classe 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); } }
Par rapport à la première option, une telle implémentation commence à répondre à une pause - la vitesse tombe à 0 dans la seconde suivante après l'arrivée des données à l'extérieur.
Mais il y a aussi des inconvénients. Nous travaillons avec une mémoire tampon de 80 Ko, ce qui signifie que le téléchargement démarré en une seconde ne sera affiché que la prochaine. Et avec un grand flux de téléchargements parallèles, de telles erreurs de mesure afficheront n'importe quoi - j'avais un écart allant jusqu'à 30% des nombres réels. Je ne l'avais peut-être pas remarqué, mais dépasser 100 Mbit semblait trop suspect .
Troisième approche
La deuxième option est déjà assez proche de la vérité, et son erreur a été davantage observée au début du téléchargement, et non tout au long du cycle de vie.
Par conséquent, une solution simple consiste à prendre comme indicateur non pas le chiffre par seconde, mais la moyenne des trois dernières secondes. Trois ici est une constante magique assortie à l'œil. D'une part, je voulais un affichage agréable de la croissance et du déclin de la vitesse, d'autre part - pour que la vitesse soit plus proche de la vérité.
L'implémentation est un peu compliquée, mais en général, rien de tel:
L'implémentation complète de la classe 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); } }
Quelques points:
- au moment de la mise en œuvre, je n'ai pas trouvé la file d'attente terminée avec une limite sur le nombre d'éléments et l'ai prise sur Internet, dans le code ci-dessus c'est
LimitedConcurrentQueue
. - au lieu d'implémenter
INotifyPropertyChanged
pour une raison quelconque, Action
, l'utilisation est pratiquement la même, je ne me souviens pas des raisons. La logique est simple - l'indicateur change, les utilisateurs doivent en être informés. L'implémentation peut être n'importe laquelle, même IObservable
, à qui elle est plus pratique.
Et un peu de lisibilité
L'API donne la vitesse en octets, pour plus de lisibilité une simple (prise sur Internet) est utile
convertisseur public static string HumanizeByteSize(this long byteCount) { string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
Permettez-moi de vous rappeler que la vitesse en octets, c'est-à-dire par canal de 100 Mbits ne devrait pas émettre plus de 12,5 Mo.
À quoi cela ressemble finalement:
Téléchargez l'image ubuntuVitesse actuelle 904,5 Ko / s
Vitesse actuelle 1,8 Mo / s
Vitesse actuelle 2,9 Mo / s
Vitesse actuelle 3,2 Mo / s
Vitesse actuelle 2,9 Mo / s
Vitesse actuelle 2,8 Mo / s
Vitesse actuelle 3MB / s
Vitesse actuelle 3,1 Mo / s
Vitesse actuelle 3,2 Mo / s
Vitesse actuelle 3,3 Mo / s
Vitesse actuelle 3,5 Mo / s
Vitesse actuelle 3,6 Mo / s
Vitesse actuelle 3,6 Mo / s
Vitesse actuelle 3,6 Mo / s
...
Eh bien, plusieurs images à la foisVitesse actuelle 1,2 Mo / s
Vitesse actuelle 3,8 Mo / s
Vitesse actuelle 7,3 Mo / s
Vitesse actuelle 10 Mo / s
Vitesse actuelle 10,3 Mo / s
Vitesse actuelle 10 Mo / s
Vitesse actuelle 9,7 Mo / s
Vitesse actuelle 9,8 Mo / s
Vitesse actuelle 10,1 Mo / s
Vitesse actuelle 9,8 Mo / s
Vitesse actuelle 9,1 Mo / s
Vitesse actuelle 8,6 Mo / s
Vitesse actuelle 8,4 Mo / s
...
Conclusion
C'était intéressant de faire face à une tâche apparemment banale de compter la vitesse. Et même si le code fonctionne et donne quelques chiffres, je veux écouter les critiques - ce que j'ai manqué, comment pourrais-je faire mieux, peut-être qu'il existe des solutions toutes faites.
Je veux dire merci à Stack Overflow en russe et en particulier à VladD-exrabbit - bien qu'il y ait la moitié de la réponse dans une bonne question, tous les conseils et toute aide vous font toujours avancer.
Je veux vous rappeler qu'il s'agit d'un projet pour animaux de compagnie - c'est pourquoi la classe est statique et unique, donc la précision n'est pas vraiment. Je vois beaucoup de petites choses qui pourraient être mieux faites, mais ... il y a toujours autre chose à faire, donc pour l'instant je pense que c'est la vitesse et je pense que ce n'est pas une mauvaise option.