DEV Community

Angelo Belchior
Angelo Belchior

Posted on

Comunicação em tempo real com Server-Sent Events em .NET!

Desenvolver sistemas não é simples. Desenvolver sistemas que necessitam de atualizações em tempo real então, é algo absurdamente complexo.

Não se deixe enganar, por mais que hoje tenhamos inúmeras bibliotecas para nos ajudar a fazer esse trabalho, o diabo mora nos detalhes. Enviar e/ou receber dados de um lado ou de outro é simples.

O grande problema é se manter conectado ao servidor enviando e recebendo dados de forma contínua! Dezenas ou centenas de atualizações por segundo! Com centenas ou milhares de clientes pendurados ao mesmo tempo!!!

É punk!

Você consegue imaginar como deveria ser a infraestrutura da sua aplicação para que seja possível manter uma conexão aberta por um longo tempo? Ou ainda, como efetuar a escalabilidade horizontal da sua aplicação?

É aí que a brincadeira começa a ficar muito séria...

No mundo .NET temos o SignalR.

Pra quem não conhece, o SignalR é uma biblioteca que simplifica a criação de aplicações que precisam de comunicação em tempo real, permitindo troca bidirecional de mensagens entre cliente e servidor de forma eficiente, usando WebSockets, Server-Sent Events ou outras tecnologias.
Seu uso é mamão com açúcar. Só que tem um porém: escalabilidade. Não é tão trivial, e só quem precisou suportar centenas de conexões abertas ao mesmo tempo sabe o tamanho do buraco.
De qualquer forma, temos a documentação da Microsoft nos dando uma certa luz em como resolver isso: https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-9.0.

Além disso temos a oportunidade de usar uma plataforma como serviço na nuvem (PaaS) como o próprio Azure SignalR Service que na teoria resolveria esse problema. Mas tem a questão do custo que pode ser uma pedra no sapato.

Mas enfim, eu fiz toda essa introdução para alinhar um pouco a expectativa com você, meu caro leitor, minha cara leitora: Eu não tenho como objetivo aqui explorar esse universo de escalabilidade/infraestrutura, quero apenas apresentar uma tecnologia que pode ser bem útil a você num cenário que demande o consumo de informações em real time.

Seguimos...

Quando se trata de real time, provavelmente você já deve ter ouvido falar em WebSockets, Pooling e Long Pooling, correto?

Esses termos são bem mais conhecidos do que o tema proposto nesse post. No geral nós temos:

  • WebSocket: É um protocolo de comunicação bidirecional e full-duplex que funciona sobre uma única conexão TCP. Ele permite que o cliente e o servidor troquem mensagens em tempo real, sem a necessidade de múltiplas requisições HTTP.

  • Pooling (ou Short Polling): É uma técnica em que o cliente faz requisições HTTP periódicas ao servidor para verificar se há novos dados disponíveis. É ineficiente, pois muitas requisições podem retornar sem novos dados além de gerar maior carga no servidor e na rede devido ao tráfego redundante.

  • Long Polling é uma variação mais eficiente do Pooling, onde o cliente mantém a conexão HTTP aberta até que o servidor tenha novos dados para enviar. Mas mesmo assim não é tão eficiente e pode sobrecarregar o servidor...

O WebSocket é um protocolo de comunicação, já Pooling e Long Polling são técnicas não tão eficientes para alcançarmos o tão famigerado real time...

Além dessas três tecnologias, temos mais uma, que eu particularmente gosto muito, e já tive a oportunidade de utilizar em produção: O nosso querido Server-Sent Events ou apenas SSE para os íntimos.

O que é Server-Sent Events (SSE)?

SSE (Server-Sent Events) é uma tecnologia que permite manter uma conexão aberta com um servidor HTTP, recebendo dados de forma contínua e em tempo real.

Diferentemente do WebSocket, que suporta comunicação bidirecional (cliente <-> servidor), o SSE é unidirecional, permitindo apenas o envio de informações do servidor para o cliente. Basicamente, enquanto o WebSocket possibilita troca de dados nos dois sentidos (enviar e receber), o SSE limita-se a transmissões contínuas a partir do servidor.

Essa tecnologia é especialmente útil em cenário como sistema de notificações, atualizações de status ou streaming de eventos. Por ser unidirecional, o SSE tende a ser mais eficiente que o WebSocket em muitos casos.

No caso da aplicação cliente, ela pode ser tanto um navegador web quanto um console application, desktop ou um app mobile.
O mais importante é que o SSE é suportado por todos os navegadores decentes e o ~Safari~: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#browser_compatibility.

Já no server temos duas importantes características:

Header

O content-type da mensagem de resposta precisa ser text/event-stream.

Body

O corpo da mensagem precisa estar num formato específico:

event: <<Nome do Evento>>
data: <<Conteúdo do evento>>
Enter fullscreen mode Exit fullscreen mode

O conteúdo do evento é uma string. Aliás, o corpo da mensagem é e só pode ser uma string.

Um exemplo mais concreto:

event: stockChanged
data: { "name": "MSFT", "value": "5.67",  "datetime": "2025-01-11T15:30:00" }
data: { "name": "AAPL", "value": "4.56",  "datetime": "2025-01-11T15:30:05" }
Enter fullscreen mode Exit fullscreen mode

Como podemos ver acima, é possível que um evento gere mais de um data, e não só isso, podemos, ainda, retornar mais de um evento:

event: stockChange
data: { "name": "MSFT", "value": "5.67", "datetime": "2025-01-11T15:30:00" }
data: { "name": "AAPL", "value": "4.56", "datetime": "2025-01-11T15:30:05" }

event: stockChange
data: { "name": "GOOGL", "value": "3.45", "datetime": "2025-01-11T15:30:08" }
Enter fullscreen mode Exit fullscreen mode

É muito comum que o conteúdo do data seja um json serializado, mas já vi casos onde valores separados por vírgula eram usados. Isso era feito dessa forma para que o body fosse menor. Esse é um ponto importante, já que o tráfego de dados pode se tornar um gargalo.

Para saber mais acesse https://html.spec.whatwg.org/multipage/server-sent-events.html

Certo, chegou a hora de codar!

Mãos à obra!

Nós vamos aqui simular um sistema que exibe as ações da bolsa de valores. Basicamente o servidor vai ficar enviando as atualizações das ações para o cliente.

A criação de uma aplicação servidor que envia dados pro cliente via SSE é bem simples. Utilizarei aqui Minimal APIs:

using System.Text.Json;

var builder = WebApplication
    .CreateBuilder(args);

var app = builder.Build();
app.MapGet("/stocks/{stock:alpha}", async (
        string stock, 
        HttpContext httpContext, 
        CancellationToken cancellationToken) =>
    {
        httpContext.Response.Headers.Append("Content-Type", "text/event-stream");

        while (!cancellationToken.IsCancellationRequested)
        {
            var stockItem = new
            {
                Stock = stock,
                Price = Random.Shared.Next(0, 1000),
                DateTime = DateTime.Now
            };

            await httpContext.Response.WriteAsync("event: stockChanged", cancellationToken: cancellationToken);
            await httpContext.Response.WriteAsync("\n", cancellationToken: cancellationToken);
            await httpContext.Response.WriteAsync("data: ", cancellationToken: cancellationToken);
            await JsonSerializer.SerializeAsync(httpContext.Response.Body, stockItem, cancellationToken: cancellationToken);
            await httpContext.Response.WriteAsync("\n\n", cancellationToken: cancellationToken);
            await httpContext.Response.Body.FlushAsync(cancellationToken);

            await Task.Delay(Random.Shared.Next(1000, 5000), cancellationToken);
        }
    })
    .WithName("GetWeatherForecast");

app.Run();
Enter fullscreen mode Exit fullscreen mode

Basicamente criamos um endpoint sse/{stock} e no seu response configuramos que o content-type é "text/event-stream". Isso vai informar ao cliente que tipo de informações estão sendo trafegadas.

Logo abaixo um famigerado e temido while (!cancellationToken.IsCancellationRequested). Isso indica apenas que os dados serão enviados ao cliente apenas se ele estiver conectado. Sim, é praticamente um looping infinito, e sim, eu tenho um post que fala sobre Cancellation Token.

Em seguida eu crio um objeto fake simulando uma ação com um determinado valor.

E as linhas a seguir escrevem no corpo da resposta a estrutura contendo o evento stockChanged e o dado serializado.

Em seguida simulo um pequeno delay apenas para deixar nossa demo mais legal...

Se tudo der certo e nada der errado, podemos executar nosso server e ao acessar a url http://localhost:<<PORTA>>/stocks/msft podemos ver os dados sendo enviados em tempo real:

Coisa linda, não é? Simples, Simples, Simples. Imagine as possibilidades... algo como, ouvir uma fila do RabbitMQ por exemplo... ou Kafka...

Agora precisamos criar uma aplicação cliente que se conecta ao nosso recém criado server. Para isso, crie um console application.

Uma das novidades que o .NET 9 trouxe foi uma nova biblioteca para podermos trabalhar com SSE. Ela não é nada revolucionária, já que se conectar ao server via HttpClient é algo bem tranquilo de se fazer. Porém, essa nova biblioteca visa facilitar o parser do body da mensagem.

Instale o pacote nuget: https://www.nuget.org/packages/System.Net.ServerSentEvents;

E o código:

using System.Net.ServerSentEvents;

const string url = "http://localhost:<<PORTA>>/stocks/";

Console.WriteLine("Informe o símbolo da ação:");
var stock = Console.ReadLine();

using var client = new HttpClient();
await using var stream = await client.GetStreamAsync(url + stock);
await foreach (var item in SseParser.Create(stream).EnumerateAsync())
{
    if (item.EventType == "stockChanged")
        Console.WriteLine(item.Data);
}

Console.WriteLine("Fim...");
Enter fullscreen mode Exit fullscreen mode

Basicamente a gente abre uma conexão http com o servidor e solicita o stream de dados. Em seguida, a biblioteca System.Net.ServerSentEvents nos ajuda disponibilizando a classe SseParser que pega esse stream contendo o body enviado e transforma numa estrutura onde temos duas propriedades: EventType e Data.

Não é sensacional? Eu, particularmente acho :)


Há um tempo eu criei um projeto usando SSE para simular uma listagem de valores de criptomoedas. Esse projeto é mais completo que o que eu apresentei nesse post já que tem interface visual.
Aliás, eu uso uma página HTML para acessar o servidor. Se no seu caso você precisar de algo parecido, acesse https://github.com/angelobelchior/MyCrypto-SSE.

Versionei o código da demo aqui: https://github.com/angelobelchior/SSE-DotNet

Era isso.

Muito obrigado e até a próxima!

Top comments (2)

Collapse
 
andre_lucas_6c746f1f230a7 profile image
Andre Lucas

Excelente artigo Ângelo, uma dúvida se estivermos trabalhando em um sistema multi-tenant, e precisamos direcionar uma mensagem para uma conexão específica, o que você sugeriria?

Collapse
 
angelobelchior profile image
Angelo Belchior

Muito obrigado Andre! No caso, entendo que a gente não direciona a mensagem, e sim o contrário. O usuário loga na sua aplicação e já entra dentro do tenant. O método se encarrega de obter - de alguma forma - essa mensagem.
Porém, acredito que a sua nessecidade seja origem da mensagem, algo como, uma mensageria publicar a mensagem e a pessoa logada num determinado tenant receber o dado correto. Seria isso?