Atualizações em tempo real utilizando server-sent events

Leonardo Ikeda
Geekie Educação
Published in
12 min readMar 14, 2024
Mulher adulta segurando um tablet e olhando para informações na tela
Imagem de freepik

Por que usar e o que são server-sent events?

Durante o desenvolvimento de alguma aplicação, é comum nos depararmos com situações em que seria muito vantajoso conseguir lidar com dados em tempo real. Por tempo real, nos referimos à capacidade da aplicação de processar dados e causar atualizações de maneira quase instantânea, assim que eles são gerados. Para aplicações que disponibilizam uma análise de dados para seus usuários, por exemplo, pode ser essencial que isso seja feito em tempo real e que atualizações aconteçam de forma automática, conforme novos dados são gerados, para que um usuário possa realizar ações com base nos dados de maneira rápida e fácil.

Ao procurar por soluções eficientes para lidar com dados em tempo real, é comum que se recomende a utilização do WebSocket, um protocolo de comunicação que permite uma comunicação bidirecional, entre cliente e servidor, por meio de uma única conexão de longa duração. Isso permite que cliente e servidor se comuniquem de maneira eficiente quando comparado a protocolos mais usuais, como o HTTP, uma vez que a conexão duradoura permite que a troca de informações ocorra de forma contínua, removendo a necessidade de se estabelecer e fechar conexões constantemente.

Embora essas características tornem o WebSocket uma opção atrativa para lidar com dados em tempo real, muitos problemas costumam ser encontrados ao utilizá-lo, e lidar com certas situações passa ser mais complexo do que o usual, principalmente em comparação ao HTTP. Não é nosso foco entrar em muitos detalhes, mas os tipos de problemas enfrentados envolvem diversos tópicos, que vão desde como será feita a escalabilidade e infraestrutura do sistema, até como lidar com questões relacionadas à segurança. Todos esses fatores contribuem para que, ao se adotar a utilização do WebSocket, sejam necessários cuidados redobrados em relação ao planejamento inicial e manutenção do sistema.

Dada essa visão geral, será que existe alguma alternativa que ainda nos daria a capacidade de lidar com dados em tempo real de maneira eficiente, sem ter que passar por todos os problemas enfrentados ao utilizar o WebSocket como solução? Dependendo dos seus requisitos, sim, essa solução existe!

Uma opção menos conhecida do que as demais para desenvolver aplicações de tempo real é a de utilizar server-sent events (em português, eventos enviados pelo servidor). Server-sent events é uma tecnologia da web que também permite uma conexão contínua entre servidor e cliente, mas unidirecional, diferente do WebSocket. Como o próprio nome diz, essa unidirecionalidade vem do fato que somente o servidor é capaz de enviar mensagens utilizando essa conexão, enquanto que o cliente só as recebe. Como o protocolo utilizado pelo server-sent events é construído em cima do padrão HTML, os problemas com os quais precisamos lidar ao adotá-lo são menos complexos do que aqueles encontrados ao utilizar o WebSocket, e muitas das funcionalidades presentes no padrão HTML podem ser aproveitadas de forma direta.

Mas, antes de entrarmos em maiores detalhes, vamos dar um passo para trás. Antes de mais nada, é preciso entender se a utilização de server-sent events é uma alternativa viável para o tipo de problema que se deseja resolver.

Limitações de uso

Podemos dizer que o grande problema colocado pela utilização de server-sent events está nas suas limitações técnicas. Aqui, entraremos em mais detalhes somente acerca de alguns pontos mais limitantes, que possuem maior chance de inviabilizar essa solução dependendo do seu caso de uso esperado:

  • A unidirecionalidade da conexão
  • Limite no número de conexões simultâneas
  • Falta de suporte nativo para dados binários

Unidirecionalidade

Essa é certamente a maior das limitações impostas pelo uso de server-sent events. Dizemos que a conexão estabelecida com o uso de server-sent events é uma conexão unidirecional, pois somente o servidor é capaz de enviar mensagens para os clientes. Caso o cliente precise se comunicar com o servidor de maneira proativa, será necessário estabelecer outra conexão para isso, o que traz consigo todo o overhead associado a esse tipo de operação.

Ao considerar essa solução, o importante é analisar como se dá o fluxo de dados e o que realmente precisa ser feito em tempo real. Por exemplo, em um feed de atualizações de uma rede social, o cliente costuma ser um mero consumidor de dados, tornando essa solução bem adequada para esse tipo de problema. Por outro lado, para um jogo on-line essa solução pode não ser tão viável, uma vez que tanto servidor como cliente estão constantemente se comunicando entre si — a todo instante, o servidor precisa enviar dados sobre o estado do jogo para todos os jogadores, enquanto os clientes precisam enviar ao servidor quais ações estão sendo tomadas pelo jogador.

Entretanto, mesmo que sua aplicação possua um fluxo de dados bidirecional, isso por si só pode não inviabilizar totalmente a solução. Pode ser que esse fluxo de dados, apesar de bidirecional, seja fortemente assimétrico — é capaz que o servidor seja responsável por enviar a maior parte das mensagens, enquanto o cliente só precisa fazer isso de forma esporádica. Em uma aplicação de apostas online, por exemplo, um cliente pode estar constantemente recebendo atualizações sobre o estado de diferentes apostas, e apenas ocasionalmente precisar se comunicar com o servidor para gerar uma nova aposta. Em um caso como esse, ainda é interessante de se considerar o uso de server-sent events, uma vez que permite que os dados estejam disponíveis para os clientes em tempo real e de maneira eficiente.

Limite no número de conexões simultâneas

A maioria dos navegadores possui um limite para o número máximo de conexões simultâneas que podem ser estabelecidas para um mesmo domínio. Este é um limite imposto pelo padrão HTML/1.1, então essa restrição não é específica para server-sent events, podendo acontecer para outros tipos de conexões. Atualmente, a maioria dos navegadores possui um limite máximo de até 6 conexões simultâneas.

Com isso, caso você tenha uma aplicação que possibilite e seja comum que um mesmo cliente se conecte com o servidor mais de uma vez (por exemplo, com a abertura de uma nova aba), pode ser que sua aplicação não funcione como esperado.

Este é um problema que não é enfrentado ao se utilizar o WebSocket, por exemplo. Por não usar o protocolo HTTP, é possível estabelecer um número ilimitado de conexões simultâneas para essa solução.

Apesar disso, existem algumas estratégias para lidar com esse problema, como o uso de diferentes domínios ou o multiplex introduzido com o HTTP/2, que permite que o número máximo de conexões simultâneas exceda esse limite previamente citado. De qualquer forma, é importante ter em mente que esse é um problema em potencial, e que pode ser preciso pensar nisso.

Falta de suporte nativo para dados binários

Outra limitação importante está no fato de que server-sent events só dão suporte nativo para mensagens de texto. Isso pode ser um fator limitante caso seja necessário que dados em formatos binários, tais como fotos e vídeos, precisem ser enviados nas mensagens do servidor.

Existe a possibilidade de codificar os dados para um formato de texto e decodificá-los no cliente, mas é uma solução que não é recomendada caso isso precise ser feito com muitos arquivos grandes. A codificação aumenta o tamanho dos dados e gera um overhead adicional, fazendo com que o desempenho da aplicação seja afetado negativamente.

Como usar server-sent events

Uma vez decidido que a utilização de server-sent events atende aos requisitos de sua aplicação, cabe agora ver o que é preciso fazer para que ele possa ser utilizado.

Nos exemplos de código dados abaixo, utilizaremos o Javascript como linguagem, tanto na implementação do cliente como do servidor.

Exemplo de implementação em um cliente

Em Javascript, a implementação realmente pode ser feita de maneira bem simples, e utiliza a API de EventSource. Basta instanciar um EventSource com a URL da API do seu servidor, e o tratamento dos eventos é feito de maneira muito semelhante a como é feito para outros tipos de eventos mais comuns:

const eventSource = new EventSource('<URL-do-servidor>');
eventSource.onopen = (_) => console.log('Conexão SSE estabelecida');
eventSource.onmessage = (e) => {
// Lidar com os dados recebidos
const data = e.data;
}

No exemplo acima, a partir do momento em que a variável eventSource é instanciada, uma conexão HTTP persistente é feita com o servidor. Caso necessário, é possível fechar essa conexão com o método eventSource.close().

Uma vez estabelecida a conexão, o cliente espera por eventos do servidor. A cada vez que um evento é recebido do servidor, a função que definimos no onmessage da instância é executada, fazendo o tratamento dos dados recebidos pelos eventos.

Também é possível esperar por eventos nomeados, definidos no servidor. Isso é feito adicionando um campo “event” com o nome do evento na mensagem enviada. Nestes casos, é preciso utilizar um listener para cada tipo de evento:

const eventSource = new EventSource('<URL-do-servidor>');
eventSource.addEventListener('<nome-do-evento>', e => {
// Lidar com os dados recebidos
const data = e.data;
}

Ao fazer isso, somente eventos com esse nome são capturados, fazendo a função passada ao método addEventListener ser executada. Uma pequena nota aqui é que o nome de evento message é usado para um caso especial, que captura também eventos enviados sem nome. Este caso é justamente o exemplo mais simples dado anteriormente, em que usamos o método eventSource.onmessage().

Como é possível de se imaginar, essa pequena funcionalidade de poder escutar eventos de tipos específicos nos permite organizar a lógica da aplicação de uma forma mais modular.

Exemplo de implementação no servidor

Como citamos anteriormente, o protocolo utilizado por server-sent events é construído por cima do protocolo HTTP. A comunicação entre servidor e cliente utiliza o modelo de requisições e respostas do HTTP, usualmente bem conhecido pela maior parte dos desenvolvedores. O servidor deve enviar mensagens de texto, utilizando o MIME type text/event-stream, que é o que sinaliza ao cliente que os dados seguirão um formato específico.

Isso significa que as mensagens enviadas pelo servidor precisam seguir algumas regras:

  • Cada evento é separado por duas quebras de linha \n\n
  • Um evento é composto por vários campos, na forma campo: valor , opcionais e separados por uma única quebra de linha. Esses campos incluem:
  • data: → o conteúdo da mensagem
  • id: → identificador do evento
  • event: → nome/tipo do evento, usado para definir eventos nomeados, como citado anteriormente
  • retry: → configura o tempo, em segundos, que deve ser esperado antes de uma tentativa de reconexão (veremos mais detalhes sobre isso logo abaixo)

Ou seja, um exemplo de uma mensagem enviada por um servidor pode ser algo na linha de:

id: 1
data: A primeira mensagem

id: 2
data: A segunda mensagem, de um tipo específico
event: myEvent

data: A terceira mensagem, sem um id,
data: cujo conteúdo é composto por mais de uma linha
retry: 6

Sobre o campo id, é interessante pontuar que, além de poder ser usado como identificador de cada mensagem, ele traz à tona uma das funcionalidades mais interessantes de server-sent events: a reconexão automática. Quando a conexão com o servidor é perdida, o cliente automaticamente tenta se reconectar. Nessa situação, o header Last-Event-ID é utilizado pelo cliente, e como o próprio nome diz, seu valor é o do último campo id recebido. Isso permite que, feita a reconexão, o servidor resuma o envio de mensagens partindo desse ponto. Com isso, garantimos que o cliente não perderá nenhuma mensagem que possa ter sido enviada durante esse período de desconexão.

Além do que citamos acima, é esperado que a conexão seja uma conexão contínua. Com todos esses fatores em mente, podemos escrever uma implementação básica para a função do servidor que servirá como controladora, que faz duas coisas básicas — configura a conexão e escreve a mensagem a ser enviada:

function sseHandler(response) {
// Configuração da conexão com headers de resposta
response.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
})

// Receber os dados relevantes de alguma forma
const data = getData()

// Exemplo de construção (e envio!) de uma mensagem
response.write('id: <algum-id>\n')
response.write('retry: 120\n')
response.write('event: myEvent\n')
response.write('data: ' + data + '\n\n')
}

Apesar de ser um exemplo bem simplificado, é possível ver como é simples escrever um código que faça o server-sent events funcionar.

Sobre os headers de resposta utilizados na configuração da conexão, o Content-Type: 'text/event-stream' serve para sinalizar que a resposta seguirá o formato citado anteriormente, enquanto os demais servem para garantir que o tempo real aconteça como esperado: Cache-Control: 'no-cache' impede o que a resposta seja cacheada e o Connection: 'keep-alive' determina que a conexão seja de longa duração.

Em relação a construção e envio das mensagens, o exemplo é bem simples, mas serve mais para ilustrar como as coisas podem ser feitas. Em um caso de uso real, é provável que teremos muitas outras etapas no meio desse processo. Por exemplo, para uma aplicação que faça a análise de dados do servidor, provavelmente teríamos alguma arquitetura responsável por fazer com que a cada novo dado inserido no banco de dados, uma lógica responsável pela construção de um determinado tipo de mensagem seja disparado, disponibilizando ao cliente tudo o que ele necessitaria acerca desse novo dado para gerar as atualizações necessárias.

Quanto ao envio das mensagens, ele acontece assim que duas quebras de linha são escritas em sequência. Ou seja, assim que a última linha de código for executada, a mensagem será enviada. Por isso, também é preciso tomar cuidado exatamente com o tipo de mensagem sendo construída, para evitar que uma mensagem incompleta seja acidentalmente enviada por ter \n\n no meio do conteúdo dela.

Um pouco mais sobre infraestrutura

Como o objetivo principal deste artigo é trazer mais visibilidade a opção de se utilizar server-sent events para lidar com dados em tempo real de maneira simples e eficiente, não nos aprofundaremos muito em questões de infraestrutura. Dito isso, podemos passar muito brevemente por alguns tópicos importantes relacionados, que também são bons exemplos do que server-sent events são capazes de proporcionar de maneira bem simples.

Como citamos anteriormente, um dos grandes benefícios do fato de server-sent events serem construídos por cima do protocolo HTTP está no fato de que é possível aproveitar muitas funcionalidades próprias do HTTP.

Para exemplificar isso, podemos citar funcionalidades como a compressão e o multiplexing. A compressão HTTP faz com que os dados sejam comprimidos antes de serem enviados pelo servidor, otimizando a velocidade e uso de recursos de rede. O multiplexing, introduzido com o HTTP/2, faz otimizações semelhantes ao permitir que múltiplas requisições sejam processadas por uma mesma conexão. Ambas essas funcionalidades podem ser facilmente utilizadas com server-sent events, enquanto uma solução como o WebSocket requer que funcionalidades desse tipo sejam implementadas especificamente para a solução.

Outro benefício relacionado a infraestrutura está no fato de que muitas das soluções de infraestrutura existentes, tendo sido pensadas para o protocolo HTTP, funcionam sem maiores problemas. Assim como acontece para as funcionalidades HTTP citadas acima, coisas como proxies, load balancers e content delivery networks, em geral, tem seu uso facilitado com server-sent events quando comparado a algo como o WebSocket, pelos mesmos motivos.

Ou seja, a situação é a de que server-sent events é uma opção que, além de ser inerentemente simples e fácil de se utilizar, também trás consigo uma série de simplificações por herdar muito desse ambiente HTML — por poder se utilizar de muitas das ferramentas e soluções construídas para esse padrão sem muitos problemas.

Considerações finais

Com esse artigo, esperamos ter conseguido dar maior visibilidade para a opção de se utilizar o server-sent events como uma solução eficiente aos problemas encontrados envolvendo dados em tempo real.

Nosso objetivo principal foi apresentar essa solução, uma vez que é muito comum que o WebSocket seja rapidamente citado quando surge a necessidade de lidar com dados em tempo real. Entretanto, embora o WebSocket seja uma ferramenta poderosa, ele traz consigo uma série de complicações que podem fazer dele uma solução muito custosa para muitos casos de uso. Nesse contexto, o server-sent events se mostra como uma alternativa interessante, e que provavelmente deve ser considerada, dada sua simplicidade.

Esperamos ter conseguido mostrar com clareza essa simplicidade. Embora tenhamos utilizado exemplos muito simples e minimalistas, a ideia básica por trás do protocolo utilizado por essa tecnologia é realmente simples. Podemos, inclusive, dizer que muito da simplicidade do código vem da simplicidade da própria solução.

Passamos brevemente por assuntos mais complexos, envolvendo a infraestrutura. Caso você se interesse mais por esse tema, recomendo a leitura deste artigo de Martin Chaov, assim como sua apresentação no JSFest 2018. Quanto ao assunto de tempo real de forma mais ampla, existem algumas alternativas que valem a pena ser olhadas. O GraphQL, por exemplo, dá suporte a algumas funcionalidades de tempo real por meio de Subscriptions. O WebRTC é uma tecnologia muito utilizada para streaming em tempo real de áudio e vídeo. Por fim, embora ainda esteja em desenvolvimento e não esteja disponível para muitos dos browsers atuais, o WebTransport é um protocolo que pode ser descrito como uma espécie de melhoria sobre o WebSocket, buscando trazer mais funcionalidades para ele.

--

--