Um Cliente de Server-Sent Events para Delphi e .NET

· Componentes
Um Cliente de Server-Sent Events para Delphi e .NET

Server-Sent Events costumava ser o primo quieto dos WebSockets: uma resposta HTTP simples que nunca termina, com o servidor escrevendo um pequeno evento de texto após outro. De repente, está em toda parte. As respostas em stream das APIs de LLM chegam como SSE, servidores MCP falam esse protocolo por Streamable HTTP, e dashboards, notificações e feeds de progresso de tarefas o usam porque ele passa por qualquer proxy que deixe o HTTP passar.

O sgcWebSockets 2026.10 adiciona o TsgcSSEClient, um cliente EventSource para Delphi, C++Builder e .NET. Ele lê o stream em uma thread em segundo plano, entrega cada evento com seu tipo, dados e id, reconecta sozinho quando a conexão cai, e retoma com Last-Event-ID para que nada se perca no meio do caminho. O componente não faz parte da versão disponível para download atualmente. Ele chega com o lançamento 2026.10, em todas as edições.

O TsgcSSEClient recebendo eventos tipados, perdendo a conexão e retomando com Last-Event-ID. Também no YouTube.

Por que o SSE importa agora

Um stream SSE é uma resposta text/event-stream composta por blocos curtos: uma linha opcional event: com o tipo, uma ou mais linhas data:, um id: opcional, e uma linha em branco que encerra o evento. Um servidor também pode enviar retry: para dizer ao cliente quanto tempo esperar antes de reconectar. O formato é simples, mas um cliente correto precisa lidar com blocos que dividem uma linha em qualquer ponto, dados em várias linhas, quebras de linha CR, LF e CRLF, e uma reconexão que lembra onde parou. É essa parte que o TsgcSSEClient faz por você.

Início Rápido

Adicione a unit sgcHTTP_SSE_Client, defina a URL, trate OnEvent e chame Open. Cabeçalhos de requisição, como um token bearer, vão em Headers, cada um como uma linha Nome: Valor, e são enviados em toda conexão, incluindo reconexões.

procedure TForm1.FormCreate(Sender: TObject);
begin
  oSSE := TsgcSSEClient.Create(nil);
  oSSE.URL := 'https://www.example.com/events';
  oSSE.Headers.Add('Authorization: Bearer ' + FToken);
  oSSE.OnEvent := OnSSEEvent;
  oSSE.OnError := OnSSEError;
  oSSE.Open;
end;

procedure TForm1.OnSSEEvent(Sender: TObject; const aEvent: TsgcSSEEvent);
begin
  if aEvent.EventType = 'alert' then
    ShowMessage(aEvent.Data)
  else
    Memo1.Lines.Add(aEvent.EventType + ' #' + aEvent.Id + ': ' + aEvent.Data);
end;

procedure TForm1.OnSSEError(Sender: TObject; const aError: string);
begin
  Memo1.Lines.Add('error: ' + aError);
end;

Cada evento é tipado. EventType carrega o nome de event:, de modo que um único handler pode rotear alert, tick ou qualquer tipo que o seu servidor inventar. Em uma aplicação VCL ou FireMonkey, os eventos são entregues à thread principal por padrão, então os handlers podem tocar a interface diretamente. NotifyEvents muda isso. OnOpen e OnClose avisam quando o stream começa e termina, e ReadyState informa sseConnecting, sseOpen ou sseClosed.

O cliente reage ao servidor da mesma forma que o EventSource de um navegador:

Retomando com Last-Event-ID

Quando o servidor dá aos seus eventos um campo id:, o cliente guarda o último em LastEventId e o envia no cabeçalho Last-Event-ID em toda reconexão. O servidor lê o cabeçalho e continua depois daquele evento, de modo que uma conexão perdida não custa nenhuma mensagem e não repete nenhuma. Dentro de uma única sessão, isso não exige nenhum código.

Para retomar depois que a aplicação reiniciar, salve LastEventId quando parar e restaure-o antes de Open. A propriedade mantém seu valor depois de Close, e um valor definido antes de Open é enviado já na primeira requisição.

procedure TForm1.StartStream;
begin
  if FileExists('sse_last_id.txt') then
    oSSE.LastEventId := Trim(TFile.ReadAllText('sse_last_id.txt'));
  oSSE.Open;
end;

procedure TForm1.StopStream;
begin
  oSSE.Close;
  TFile.WriteAllText('sse_last_id.txt', oSSE.LastEventId);
end;

Retomar exige a cooperação do servidor: ele precisa enviar id: com seus eventos e respeitar o cabeçalho Last-Event-ID.

Uma política de reconexão que você controla

A reconexão vem ativada por padrão, com um atraso de 3 segundos. ReconnectOptions transforma isso na política que o seu servidor merecer: backoff exponencial com um multiplicador, um teto para o atraso, jitter aleatório para que mil clientes não voltem no mesmo milissegundo, e um limite de tentativas.

procedure TForm1.SetupReconnect;
begin
  oSSE.ReconnectOptions.Interval := 1000;
  oSSE.ReconnectOptions.Backoff := True;
  oSSE.ReconnectOptions.BackoffMultiplier := 2.0;
  oSSE.ReconnectOptions.MaxInterval := 30000;
  oSSE.ReconnectOptions.Jitter := 0.2;
  oSSE.ReconnectOptions.MaxAttempts := 10;
  oSSE.OnReconnect := OnSSEReconnect;
end;

MaxAttempts conta falhas consecutivas, e o contador recomeça sempre que uma conexão é aberta, então um cliente que roda por semanas com um ou outro soluço de rede nunca fica sem tentativas. O servidor também tem voz ativa: um campo retry: substitui Interval pelo resto da sessão. A última palavra é sua, em OnReconnect, que dispara antes de cada tentativa com o atraso calculado:

procedure TForm1.OnSSEReconnect(Sender: TObject; aAttempt: Integer;
  var aDelay: Integer; var aCancel: Boolean);
begin
  // stop after the fifth failed attempt, otherwise wait at least 2 seconds
  if aAttempt > 5 then
    aCancel := True
  else if aDelay < 2000 then
    aDelay := 2000;
end;

TLS, proxy e autenticação

O stream é lido com um TsgcHTTP1Client, e OnBeforeConnect o entrega a você antes de cada tentativa de conexão. Tudo que o cliente HTTP pode fazer, o cliente SSE também pode: opções de TLS, um proxy, autenticação básica ou um timeout de leitura mais longo.

procedure TForm1.OnSSEBeforeConnect(Sender: TObject;
  const aHTTP: TsgcHTTP1Client);
begin
  aHTTP.TLSOptions.Version := tls1_2;
  aHTTP.Proxy.Enabled := True;
  aHTTP.Proxy.Host := '192.168.1.10';
  aHTTP.Proxy.Port := 8080;
  aHTTP.ReadTimeout := 60000;
end;

OnBeforeConnect e OnReconnect sempre rodam na thread em segundo plano, então mantenha a interface fora deles.

O parser para streams de LLM e MCP

Muitas vezes você nem precisa de um EventSource de vida longa. Uma chat completion de LLM com stream: true é um POST cuja resposta é SSE, e uma resposta MCP Streamable HTTP também pode ser SSE. Para esses casos, o parser dentro do cliente é público: TsgcSSEParser. Alimente-o com bytes brutos ou texto em blocos de qualquer tamanho, exatamente como a rede os entrega, e ele dispara OnEvent para cada evento completo e OnRetry para cada campo retry: válido. Um bloco pode terminar no meio de uma linha ou de um caractere UTF-8, e o parser guarda o restante até a próxima chamada.

procedure TForm1.ParseStream;
begin
  FParser := TsgcSSEParser.Create;
  FParser.OnEvent := OnParserEvent;
  // chunks arrive as the network delivers them, split anywhere
  FParser.Feed('event: content_block_delta'#10'data: {"delta":{"text":"Hel');
  FParser.Feed('lo"}}'#10#10'event: message_stop'#10);
  FParser.Feed(TEncoding.UTF8.GetBytes('data: {}'#10#10));
  // start again for the next response
  FParser.Reset;
end;

procedure TForm1.OnParserEvent(Sender: TObject; const aEvent: TsgcSSEEvent);
begin
  Memo1.Lines.Add(aEvent.EventType + ' ' + aEvent.Data);
end;

Esses três blocos produzem dois eventos: content_block_delta com o JSON completo {"delta":{"text":"Hello"}}, e message_stop. Reset descarta qualquer evento lido pela metade antes da próxima resposta, e LastEventId informa o último id que o parser viu.

C# para .NET

A edição .NET do sgcWebSockets tem o mesmo componente com os mesmos nomes, no namespace esegece.sgcWebSockets. Headers é uma lista de strings Nome: Valor e os eventos são eventos .NET comuns.

using esegece.sgcWebSockets;

string token = args.Length > 0 ? args[0] : "";

var sse = new TsgcSSEClient();
sse.URL = "https://www.example.com/events";
sse.Headers.Add("Authorization: Bearer " + token);
if (File.Exists("sse_last_id.txt"))
    sse.LastEventId = File.ReadAllText("sse_last_id.txt").Trim();
sse.ReconnectOptions.Backoff = true;
sse.ReconnectOptions.MaxInterval = 30000;
sse.OnEvent += (sender, e) => Console.WriteLine($"{e.EventType} #{e.Id}: {e.Data}");
sse.OnError += (sender, error) => Console.WriteLine("error: " + error);
sse.Open();

Console.ReadLine();
sse.Close();
File.WriteAllText("sse_last_id.txt", sse.LastEventId);

TsgcSSEParser também está lá, com sobrecargas de Feed para um byte[], um trecho dele, ou uma string.

Experimente as demos

As duas edições vêm com uma demo que roda totalmente offline. Ela hospeda um pequeno servidor SSE dentro da mesma aplicação em 127.0.0.1, porta 5580, que envia eventos numerados em stream com um id:, alterna os tipos message, tick e alert, envia um campo retry: e retoma depois do Last-Event-ID que recebe. Derrube a conexão e veja o cliente reconectar e continuar a partir do próximo id, sem lacuna e sem duplicata.

Disponibilidade

TsgcSSEClient e TsgcSSEParser chegam no sgcWebSockets 2026.10, para Delphi, C++Builder e .NET, em todas as edições. Eles não estão na versão que você pode baixar hoje. Quando o 2026.10 sair, ele estará na página de download, e o componente será registrado na aba SGC HTTP da paleta.

A referência completa, com cada propriedade, evento e o guia de retomada, está na ajuda do TsgcSSEClient. Para tudo mais que o sgcWebSockets faz com Server-Sent Events, veja a página do produto SSE.

Leia também

Dúvidas, feedback ou ajuda com a migração? Entre em contato. Você receberá uma resposta das pessoas que escreveram o código.