O problema do tempo real
Dois funcionários estão vendo a lista de pagamentos de um sistema. Um deles aprova um pagamento. Como o outro fica sabendo?
Na comunicação HTTP tradicional, quem começa a conversa é sempre o cliente. O servidor só responde. Então o navegador precisa ficar perguntando de tempos em tempos "mudou alguma coisa?". Isso se chama polling. Se a consulta é feita a cada 5 segundos, a notícia pode chegar com até 5 segundos de atraso, e a maior parte das consultas volta com a resposta "nada mudou".
Com o SignalR, a conexão fica aberta e o servidor envia a mensagem assim que o evento acontece. Esse modelo é usado em chats, notificações, painéis operacionais, placares ao vivo e no acompanhamento de processamentos demorados.
SignalR e arquitetura
- Cliente: navegador, aplicativo ou outro programa conectado.
- Servidor: a aplicação ASP.NET Core que gerencia a comunicação.
- Hub: a classe C# que é o ponto de encontro entre clientes e servidor.
- Conexão: o canal que fica aberto entre cada cliente e o servidor.
- Transporte: como os dados viajam. O preferido é WebSockets; se não estiver disponível, o SignalR usa alternativas automaticamente.
O SignalR não substitui a API REST: ela continua sendo o caminho natural para consultar e
alterar dados. O SignalR complementa a API quando o servidor precisa avisar os clientes.
No projeto da aula, os dois convivem: GET /api/pagamentos carrega a lista e o Hub avisa
quando algo muda.
Como funciona a aula prática
Até agora, cada um rodava seu programa isolado. Hoje precisamos de um servidor e de vários clientes em máquinas diferentes. A dinâmica será assim:
Login: para entrar, selecione o seu nome na lista e digite o seu RA. Todas as mensagens enviadas ficam registradas no servidor com nome, RA e horário.
| Parte | O que acontece | Onde roda |
|---|---|---|
| 1. Polling | A tela de pagamentos sem tempo real: o navegador fica perguntando | Servidor do professor |
| 2. Tempo real | A mesma tela com SignalR, lado a lado com a de polling | Servidor do professor |
| 3. Quem recebe? | All, Others, Caller e Group na prática | Servidor do professor |
| 4. Streaming | Importação com barra de progresso e fila | Servidor do professor |
| 5. Exercício A | Você implementa um Hub de chat | Sua máquina (várias abas) |
Parte 1 · Polling: o navegador fica perguntando
Com o endereço do projetor, abra no navegador:
http://IP-DO-PROFESSOR:5000/polling.html
Essa tela não usa tempo real. A cada 5 segundos ela faz um GET /api/pagamentos e redesenha a
lista. O professor vai aprovar alguns pagamentos. Observe:
- Quanto tempo a sua tela demora para mostrar a aprovação? Todos da turma veem ao mesmo tempo?
- No projetor, o contador de requisições de polling não para de crescer. Quantas dessas consultas trouxeram alguma novidade?
- E se o intervalo fosse de 1 segundo? A tela ficaria mais rápida, mas o que aconteceria com o servidor?
O polling tem um dilema sem saída: intervalo curto deixa a tela mais atual, mas sobrecarrega o servidor com perguntas inúteis; intervalo longo alivia o servidor, mas a informação chega atrasada. O problema é que só o cliente pode começar a conversa.
Parte 2 · Tempo real: o servidor avisa
Mantenha a aba de polling aberta e abra, ao lado, a versão com SignalR:
http://IP-DO-PROFESSOR:5000/
Faça login (seu nome + RA) e aprove um pagamento. Compare as duas abas: na de SignalR a mudança aparece na hora, para todos; na de polling, só na próxima consulta. A aba de SignalR não fica perguntando nada: a conexão permanece aberta e o servidor empurra o evento quando ele acontece.
Parte 3 · Notificações: quem recebe a mensagem?
Um método do Hub é chamado pelo cliente. Dentro dele, o servidor escolhe para quem enviar um evento. Cada destinatário é um botão na página da aula:
| Destinatário | Quem recebe | Na página |
|---|---|---|
Clients.All | Todos os conectados, inclusive quem chamou | Aprovar pagamento |
Clients.Others | Todos, menos quem chamou | Avisar os outros |
Clients.Caller | Só quem chamou | Eco |
Clients.Group("financeiro") | Só quem está no grupo | Grupos |
No servidor (trecho de PagamentosHub.cs):
public class PagamentosHub(RepositorioPagamentos pagamentos, Participantes participantes) : Hub
{
public async Task AprovarPagamento(int id)
{
Pagamento? aprovado = pagamentos.Aprovar(id, NomeAtual);
if (aprovado is null)
throw new HubException($"O pagamento {id} não existe.");
await Clients.All.SendAsync("PagamentoAprovado", aprovado);
}
public Task AvisarOutros(string texto) =>
Clients.Others.SendAsync("Aviso", NomeAtual, texto);
public Task Eco(string texto) =>
Clients.Caller.SendAsync("Eco", texto);
public async Task EntrarNoGrupo(string grupo)
{
await Groups.AddToGroupAsync(Context.ConnectionId, grupo);
await Clients.Group(grupo).SendAsync("AvisoGrupo", grupo, "Sistema", $"{NomeAtual} entrou no grupo");
}
}
No cliente (JavaScript), cada evento precisa de um "ouvinte", e os métodos do Hub são chamados com invoke:
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hub")
.withAutomaticReconnect()
.build();
// servidor → cliente: o nome do evento é o 1º argumento de SendAsync
connection.on("PagamentoAprovado", pagamento => {
console.log(`Pagamento ${pagamento.id} aprovado por ${pagamento.aprovadoPor}`);
});
await connection.start();
// cliente → servidor: chama o método AprovarPagamento do Hub
await connection.invoke("AprovarPagamento", 3);
Teste: combine com um colega e cliquem em "Aprovar" no mesmo pagamento ao mesmo tempo.
Só um consegue. O repositório protege a lista com lock, como na aula de Concorrência, e o
outro recebe uma HubException, que é a única exceção cuja mensagem chega até o cliente.
Um Hub é criado a cada chamada. Por isso, nada de guardar estado em campos do Hub: o estado
compartilhado fica em serviços singleton injetados no construtor. E só guardamos estado
quando existe uma necessidade de negócio (o nome do participante, o grupo dele). Conexões, grupos e
reconexões o SignalR já gerencia sozinho.
Parte 4 · Streaming
Uma notificação informa um acontecimento ("o pagamento 3 foi aprovado"). Já o streaming
envia uma sequência de valores ao longo do tempo: 0%, 5%, 10%... 100%. No SignalR, basta o
método do Hub retornar IAsyncEnumerable<T>: cada yield return chega ao
cliente assim que é produzido.
Para deixar mais real, o servidor só processa 3 importações ao mesmo tempo, controladas por um
SemaphoreSlim. Quando a turma inteira clicar em "Importar" junto, o projetor mostra quem
está processando e quem está na fila.
public class PainelImportacoes
{
private readonly SemaphoreSlim _vagas = new(3, 3);
public async IAsyncEnumerable<Progresso> Executar(
string nome, int registros, [EnumeratorCancellation] CancellationToken cancelamento)
{
bool conseguiuVaga = false;
try
{
yield return new Progresso("Aguardando", 0, "Na fila...");
await _vagas.WaitAsync(cancelamento);
conseguiuVaga = true;
for (int percentual = 0; percentual <= 100; percentual += 5)
{
yield return new Progresso("Processando", percentual, $"{registros * percentual / 100} de {registros}");
await Task.Delay(400, cancelamento);
}
}
finally
{
// roda no fim normal E quando o cliente cancela (fechou a aba, clicou em Cancelar...)
if (conseguiuVaga)
_vagas.Release();
}
}
}
No cliente, em vez de invoke, usamos stream:
const assinatura = connection.stream("Importar", 10000).subscribe({
next: progresso => atualizarBarra(progresso.percentual),
complete: () => console.log("Concluído"),
error: erro => console.error(erro)
});
// Cancelar: o CancellationToken do método no servidor é cancelado
assinatura.dispose();
Para discutir: uma tela precisa mostrar o andamento da importação de 10 mil registros. É melhor enviar uma notificação isolada ou transmitir o progresso? Depende do comportamento desejado. A notificação informa que algo aconteceu; o streaming permite acompanhar uma sequência de resultados.
Parte 5 · Exercício A: chat em tempo real (na sua máquina)
Baixe tempo-real-exercicios.zip,
descompacte e abra a pasta ExercicioA no VS Code. Funciona com .NET 9 ou 10.
cd ExercicioA
dotnet run
Abra http://localhost:5050 em 2 ou 3 abas e use um nome diferente em cada uma:
cada aba é um cliente. A página já está pronta. Seu trabalho é implementar os métodos de
Hubs/ChatHub.cs. Enquanto um TODO não estiver feito, o erro aparece em vermelho na página.
- TODO 1:
EnviarParaTodos: envie o evento"MensagemRecebida"(nome, texto, "todos")para todos. - TODO 2:
EnviarParaOutros: o mesmo, mas para todos menos quem enviou. Confira que a aba que enviou não recebe. - TODO 3:
EntrarNaSala: adicione a conexão ao grupo e avise a sala que alguém entrou. Dica:Context.ConnectionId. - TODO 4:
EnviarParaSala: só quem está na sala recebe. Teste com uma aba fora da sala. - TODO 5: em
OnConnectedAsync/OnDisconnectedAsync, mantenha o total de conectados e envie"UsuariosOnline"para todos. Feche uma aba e veja o número cair. Atenção: várias conexões chegam ao mesmo tempo. UseInterlocked. - TODO 6:
Contagem: um stream que conta dedeaté 0, um número por segundo. Clique em "Cancelar" no meio e confirme que o servidor para.
Desafios:
- Mostre "fulano está digitando..." para os outros (método
Digitando, evento"UsuarioDigitando"). - Implemente
SairDaSala. - Em dupla: no
Program.cs, troquelocalhostpor0.0.0.0e peça para o colega acessarhttp://SEU-IP:5050. Descubra seu IP comipconfig.
Se algo não funcionar
- Não consigo abrir o endereço do professor: confira se digitou
http://(sem S), o IP e a porta:5000. - O colega não acessa o meu servidor (desafio em dupla): o Firewall do Windows bloqueia
conexões de entrada. Nesse caso, faça o exercício com várias abas em
localhost. - "Address already in use": já existe um
dotnet runaberto usando a porta. Feche-o com Ctrl+C.
Para refletir
- O servidor guarda o nome e o grupo de cada participante em um
ConcurrentDictionary. Por que não usar um campo comum dentro do Hub? - Quando a conexão cai e o
withAutomaticReconnectreconecta, o cliente precisa chamarEntrarde novo. Por quê? O que acontece com os grupos dele? - O
SemaphoreSlimda importação e olockdo repositório de pagamentos fazem papéis diferentes. Relacione com a regra da aula de Concorrência: capacidade x consistência. - A tela de polling consulta a cada 5 segundos. Com 40 alunos durante 1 hora, quantas requisições o servidor recebe? Quantas delas trouxeram alguma novidade?
Projeto completo para estudo
Todos os projetos da aula: o servidor da demonstração (página do aluno, painel do projetor e versão
com polling) e a resolução do Exercício A. Veja o LEIA-ME.md dentro do ZIP.
Para estudar sozinho, rode o servidor (cd Servidor e dotnet run) e abra várias abas em
http://localhost:5000. O painel fica em /painel.html.