Antecedentes
Eu tenho um projeto pequeno e acolhedor para animais de estimação, que permite baixar arquivos da Internet. Os arquivos são agrupados e o usuário não mostra cada arquivo, mas sim alguns agrupamentos. E todo o processo de download (e a exibição desse processo) dependia muito dos dados. Os dados foram obtidos em tempo real, ou seja, o usuário começa a baixar e não há informações sobre o quanto você precisa baixar na realidade.
A implementação ingênua de pelo menos algum tipo de informação é simplificada - o progresso do download é exibido como a proporção entre o número de downloads e o número total. Não há muitas informações para o usuário - apenas uma faixa rastejante, mas isso é melhor que nada, e é notavelmente melhor que o mecanismo de carregamento atualmente popular sem indicar progresso.
E então um usuário aparece com um problema lógico - em um grupo grande, não está claro por que o progresso está apenas se arrastando - eu preciso baixar muitos arquivos ou uma velocidade baixa? Como mencionei acima - o número de arquivos não é conhecido antecipadamente. Por isso, decidi adicionar um contador de velocidade.
Análise
É uma boa prática ver quem já resolveu um problema semelhante para não reinventar a roda. Um software diferente fecha essas tarefas diferentes, mas a tela parece praticamente a mesma:
O ponto chave que identifiquei por mim mesmo é que a primeira exibição de velocidade é necessária no momento atual. Não qual velocidade era média, nem qual velocidade como um todo era média desde o momento em que começou, ou seja, qual é esse valor no momento atual. De fato, isso é importante quando eu chegar ao código - explicarei separadamente.
Portanto, precisamos de um dígito simples como 10 MB/s
ou algo assim. Como calculamos isso?
Teoria e Prática
A implementação de download existente usava o HttpWebRequest
e eu decidi não refazer o download em si - não toque no mecanismo de trabalho.
Portanto, a implementação inicial sem qualquer 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(); }
No nível dessa API, você só pode responder a um download completo de um arquivo; em pequenos grupos (ou mesmo em um único arquivo), não é possível calcular a velocidade. Seguimos o código- fonte CopyToAsync , copie e cole a lógica simples a partir daí:
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); }
Agora podemos responder a todos os buffers que nos foram dados pela rede.
Então, primeiro, o que fazemos em vez do CopyToAsync in a box:
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(); } }
A única coisa realmente adicionada é o NetworkSpeed.AddInfo
. E a única coisa que transmitimos é o número de bytes baixados.
O código em si para o download é semelhante a este:
var request = WebRequest.Create(uri); var response = await request.GetResponseAsync(); var array = await response.GetResponseStream().GetBytesAsync();
Opção para 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);
Opção para HttpClient var httpClient = new HttpClient(); var content = await httpClient.GetStreamAsync(uri); var array = await content.GetBytesAsync();
Bem, metade do problema está resolvido - sabemos o quanto baixamos. Nós nos voltamos para a velocidade.
De acordo com a wikipedia :
Taxa de transferência de dados - a quantidade de dados transmitidos por unidade de tempo.
Primeira abordagem ingênua
Nós temos um volume. O tempo pode ser gasto literalmente desde a inicialização e obtenha a diferença com o DateTime.Now
. Tirar e compartilhar?
Para utilitários de console como curl, isso é possível e faz sentido.
Mas se o seu aplicativo for um pouco mais complicado, literalmente o botão de pausa complicará drasticamente sua vida.
Um pouco sobre a pausa
Talvez eu seja muito ingênua, ou talvez a pergunta não seja tão simples - mas a pausa me faz pensar constantemente. Uma pausa durante o download pode se comportar de pelo menos três maneiras:
- interromper o upload do arquivo, comece novamente depois
- apenas não faça o download do arquivo, espere que o servidor continue após
- baixar arquivos já iniciados, não baixar novos, baixar novos depois
Como os dois primeiros levam à perda de informações já baixadas, eu uso o terceiro.
Um pouco mais alto, notei que a velocidade é necessária precisamente em um ponto no tempo. Portanto, uma pausa complica este assunto:
- você não pode calcular corretamente qual era a velocidade média, apenas tomando o volume por um tempo
- Uma pausa pode ter motivos externos que alteram a velocidade e o canal (reconectando-se à rede do provedor, alternando para VPN, encerrando o uTorrent que levou todo o canal), o que levará a uma alteração na velocidade real
De fato, uma pausa divide todos os indicadores em antes e depois dela. Isso não afeta particularmente o código abaixo, apenas um minuto de informações divertidas para se pensar.
Segunda abordagem ingênua
Adicione um cronômetro. O temporizador a cada período recolhe todas as informações mais recentes sobre o volume baixado e recalcula o indicador de velocidade. E se você definir o cronômetro por segundo, todas as informações recebidas para este segundo sobre o volume baixado serão iguais à velocidade desse segundo:
Toda a implementação da 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); } }
Comparado com a primeira opção, essa implementação começa a responder a uma pausa - a velocidade cai para 0 no próximo segundo após a chegada dos dados externos.
Mas também existem desvantagens. Estamos trabalhando com um buffer de 80kb, o que significa que o download iniciado em um segundo será exibido apenas no próximo. E com um grande fluxo de downloads paralelos, esses erros de medição exibirão qualquer coisa - eu tive um spread de até 30% dos números reais. Eu poderia não ter notado, mas exceder 100 Mbit parecia muito suspeito .
Terceira abordagem
A segunda opção já está próxima o suficiente da verdade, e seu erro foi observado mais no início do download, e não ao longo do ciclo de vida.
Portanto, uma solução simples é tomar como indicador, não o valor por segundo, mas a média nos últimos três segundos. Três aqui é uma constante mágica combinada a olho. Por um lado, queria uma exibição agradável do crescimento e declínio da velocidade, por outro - para que a velocidade estivesse mais próxima da verdade.
A implementação é um pouco complicada, mas, em geral, nada como isto:
Toda a implementação da 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); } }
Alguns pontos:
- no momento da implementação, não encontrei a fila finalizada com um limite no número de elementos e a levei à Internet, no código acima é
LimitedConcurrentQueue
. - em vez de implementar
INotifyPropertyChanged
por algum motivo, Action
, o uso é praticamente o mesmo, não me lembro dos motivos. A lógica é simples - o indicador está mudando, os usuários precisam ser notificados sobre isso. A implementação pode ser qualquer, mesmo IObservable
, para quem é mais conveniente.
E um pouco de legibilidade
A API fornece a velocidade em bytes, para facilitar a leitura, uma simples (usada na Internet) é útil
conversor public static string HumanizeByteSize(this long byteCount) { string[] suf = { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
Deixe-me lembrá-lo de que a velocidade em bytes, ou seja, por canal de 100 bits não deve emitir mais de 12,5 MB.
Como finalmente se parece:
Faça o download da imagem do ubuntuVelocidade atual 904.5KB / s
Velocidade atual 1.8MB / s
Velocidade atual 2.9MB / s
Velocidade atual 3.2MB / s
Velocidade atual 2.9MB / s
Velocidade atual 2.8MB / s
Velocidade atual 3MB / s
Velocidade atual 3.1MB / s
Velocidade atual 3.2MB / s
Velocidade atual 3.3MB / s
Velocidade atual 3,5MB / s
Velocidade atual 3.6MB / s
Velocidade atual 3.6MB / s
Velocidade atual 3.6MB / s
...
Bem, várias imagens de uma só vezVelocidade atual 1,2MB / s
Velocidade atual 3.8MB / s
Velocidade atual 7.3MB / s
Velocidade atual 10MB / s
Velocidade atual 10.3MB / s
Velocidade atual 10MB / s
Velocidade atual 9.7MB / s
Velocidade atual 9.8MB / s
Velocidade atual 10.1MB / s
Velocidade atual 9.8MB / s
Velocidade atual 9.1MB / s
Velocidade atual 8.6MB / s
Velocidade atual 8.4MB / s
...
Conclusão
Foi interessante lidar com uma tarefa aparentemente banal de contar velocidade. E mesmo que o código funcione e dê alguns números, quero ouvir os críticos - o que perdi, como poderia fazer melhor, talvez existam algumas soluções prontas.
Quero agradecer ao Stack Overflow em russo e especificamente ao VladD-exrabbit - embora exista metade da resposta em uma boa pergunta, qualquer dica e ajuda sempre o levam adiante.
Quero lembrá-lo de que este é um projeto de estimação - é por isso que a classe é estática e uma, de modo que a precisão não é realmente. Vejo muitas pequenas coisas que poderiam ser feitas melhor, mas ... sempre há mais algo a fazer, então, por enquanto, acho que é a velocidade e acho que essa não é uma má opção.