Contando la velocidad de descarga en su aplicación

Antecedentes


Tengo un pequeño y acogedor proyecto para mascotas, que te permite descargar archivos de Internet. Los archivos se agrupan y el usuario no muestra cada archivo, sino algunos grupos. Y todo el proceso de descarga (y la visualización de este proceso) dependía mucho de los datos. Los datos se obtuvieron sobre la marcha, es decir. el usuario comienza a descargar y no hay información de cuánto tiene que descargar en realidad.


La implementación ingenua de al menos algún tipo de información se simplifica: el progreso de la descarga se muestra como la relación entre el número de descargas y el número total. No hay mucha información para el usuario, solo una tira progresiva, pero esto es mejor que nada, y es notablemente mejor que el mecanismo de carga actualmente popular sin indicar progreso.


Y luego aparece un usuario con un problema lógico: en un grupo grande no está claro por qué el progreso apenas se está arrastrando, ¿necesito descargar muchos archivos o baja velocidad? Como mencioné anteriormente, el número de archivos no se conoce de antemano. Por lo tanto, decidí agregar un contador de velocidad.


Análisis


Es una buena práctica ver a aquellos que ya han resuelto un problema similar para no reinventar la rueda. Un software diferente cierra estas tareas diferentes, pero la pantalla se ve más o menos igual:


uTorrentDownloadmaster
uTorrentDownloadmaster

El punto clave que he identificado por mí mismo es que la primera pantalla de velocidad es necesaria en el momento actual. No qué velocidad era promedio, no qué velocidad en general era promedio desde el momento en que comenzó, es decir, cuál es esta cifra en el momento actual. De hecho, esto es importante cuando llegue al código; lo explicaré por separado.


Entonces, necesitamos un dígito simple como 10 MB/s so algo así. ¿Cómo lo calculamos?


Teoría y práctica


La implementación de descarga existente utilizaba HttpWebRequest y decidí no rehacer la descarga en sí, no toque el mecanismo de trabajo.


Entonces, la implementación inicial sin ningún cálculo:


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

A nivel de dicha API, solo puede responder a una descarga de archivo completa, para grupos pequeños (o incluso para un archivo), la velocidad no se puede calcular realmente. Seguimos el código fuente de CopyToAsync , copiamos y pegamos la lógica simple desde allí:


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

Ahora podemos responder a cada búfer que se nos da a través de la red.


Entonces, en primer lugar, lo que hacemos en lugar del CopyToAsync en caja:


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

Lo único realmente agregado es NetworkSpeed.AddInfo . Y lo único que transmitimos es la cantidad de bytes descargados.


El código en sí para descargar se ve así:


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

Opción para cliente web
  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); 

Opción para HttpClient
  var httpClient = new HttpClient(); var content = await httpClient.GetStreamAsync(uri); var array = await content.GetBytesAsync(); 

Bueno, la mitad del problema está resuelto: sabemos cuánto hemos descargado. Pasamos a la velocidad.


De acuerdo con wikipedia :


Velocidad de transferencia de datos: la cantidad de datos transmitidos por unidad de tiempo.

Primer acercamiento ingenuo


Tenemos un volumen El tiempo puede tomarse literalmente desde el inicio y obtener la diferencia con DateTime.Now . Toma y comparte?
Para utilidades de consola como curl, esto es posible y tiene sentido.
Pero si su aplicación es un poco más complicada, entonces, literalmente, el botón de pausa complicará dramáticamente su vida.


Un poco sobre la pausa
Tal vez soy muy ingenuo, o tal vez la pregunta realmente no es tan simple, pero la pausa me hace pensar constantemente. Una pausa durante la descarga puede comportarse al menos de tres maneras:


  • interrumpir la carga del archivo, comenzar de nuevo después de
  • simplemente no descargue más el archivo, espero que el servidor continúe después de
  • descargar archivos ya iniciados, no descargar nuevos, descargar nuevos después

Como los dos primeros conducen a la pérdida de información ya descargada, utilizo el tercero.
Un poco más alto, noté que la velocidad es necesaria precisamente en un punto en el tiempo. Entonces, una pausa complica este asunto:


  • no puedes calcular correctamente cuál era la velocidad promedio, solo tomando el volumen por un tiempo
  • Una pausa puede tener razones externas que cambiarán la velocidad y el canal (volver a conectarse a la red del proveedor, cambiar a VPN, finalizar el uTorrent que tomó todo el canal), lo que conducirá a un cambio en la velocidad real
    De hecho, una pausa divide cualquier indicador en antes y después. Esto no afecta particularmente el código a continuación, solo un minuto de información divertida para pensar.

Segundo enfoque ingenuo


Agrega un temporizador. El temporizador de cada período de tiempo tomará toda la información más reciente sobre el volumen descargado y volverá a calcular el indicador de velocidad. Y si configura el temporizador por segundo, toda la información recibida para este segundo sobre el volumen descargado será igual a la velocidad de este segundo:


La implementación completa de la clase 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); } } 

En comparación con la primera opción, dicha implementación comienza a responder a una pausa: la velocidad cae a 0 en el siguiente segundo después de que llegan los datos externos.
Pero, también hay desventajas. Estamos trabajando con un búfer de 80 kb, lo que significa que la descarga iniciada en un segundo se mostrará solo en el siguiente. Y con una gran cantidad de descargas paralelas, tales errores de medición mostrarán cualquier cosa: tuve una extensión de hasta el 30% de los números reales. Puede que no me haya dado cuenta, pero más de 100 Mbit parecía demasiado sospechoso .


Tercer enfoque


La segunda opción ya está lo suficientemente cerca de la verdad, más su error se observó más al comienzo de la descarga, y no durante todo el ciclo de vida.
Por lo tanto, una solución simple es tomar como indicador no la cifra por segundo, sino el promedio de los últimos tres segundos. Tres aquí es una constante mágica combinada a simple vista. Por un lado, quería una muestra agradable del crecimiento y la disminución de la velocidad, por el otro, para que la velocidad estuviera más cerca de la verdad.


La implementación es un poco complicada, pero en general, nada como esto:


La implementación completa de la clase 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); } } 

Un par de puntos:


  • en el momento de la implementación, no encontré la cola terminada con un límite en el número de elementos y la tomé en Internet, en el código anterior es LimitedConcurrentQueue .
  • en lugar de implementar INotifyPropertyChanged por alguna razón, Action , el uso es prácticamente el mismo, no recuerdo las razones. La lógica es simple: el indicador está cambiando, los usuarios deben ser notificados al respecto. La implementación puede ser cualquiera, incluso IObservable , para quien sea más conveniente.

Y un poco de legibilidad


La API proporciona la velocidad en bytes, para facilitar la lectura es útil una simple (tomada en Internet)


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

Déjame recordarte que la velocidad en bytes, es decir por canal de 100mbit no debe emitir más de 12.5MB.


Cómo se ve en última instancia:


Descargar la imagen de ubuntu
Velocidad actual 904.5KB / s
Velocidad actual 1.8MB / s
Velocidad actual 2.9MB / s
Velocidad actual 3.2MB / s
Velocidad actual 2.9MB / s
Velocidad actual 2.8MB / s
Velocidad actual 3MB / s
Velocidad actual 3.1MB / s
Velocidad actual 3.2MB / s
Velocidad actual 3.3MB / s
Velocidad actual 3,5MB / s
Velocidad actual 3.6MB / s
Velocidad actual 3.6MB / s
Velocidad actual 3.6MB / s
...

Bueno, varias imágenes a la vez.
Velocidad actual 1,2MB / s
Velocidad actual 3.8MB / s
Velocidad actual 7.3MB / s
Velocidad actual 10MB / s
Velocidad actual 10.3MB / s
Velocidad actual 10MB / s
Velocidad actual 9.7MB / s
Velocidad actual 9.8MB / s
Velocidad actual 10.1MB / s
Velocidad actual 9.8MB / s
Velocidad actual 9.1MB / s
Velocidad actual 8.6MB / s
Velocidad actual 8.4MB / s
...

Conclusión


Fue interesante lidiar con una tarea aparentemente banal de contar la velocidad. Y aunque el código funciona y da algunos números, quiero escuchar a los críticos: lo que me perdí, cómo podría hacerlo mejor, tal vez hay algunas soluciones preparadas.


Quiero agradecer a Stack Overflow en ruso y específicamente a VladD-exrabbit , aunque hay la mitad de la respuesta en una buena pregunta, cualquier pista y ayuda siempre te hacen avanzar.


Quiero recordarles que este es un proyecto favorito, es por eso que la clase es estática y una en absoluto, por lo que la precisión no lo es realmente. Veo muchas pequeñas cosas que podrían hacerse mejor, pero ... siempre hay algo más que hacer, así que por ahora creo que esa es la velocidad y creo que esta no es una mala opción.

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


All Articles