Un cliente Server-Sent Events para Delphi y .NET

· Componentes
Un cliente Server-Sent Events para Delphi y .NET

Los Server-Sent Events eran el primo silencioso de los WebSockets: una simple respuesta HTTP que nunca termina, con el servidor escribiendo un pequeño evento de texto tras otro. De repente están en todas partes. Las respuestas en streaming de las API de LLM llegan como SSE, los servidores MCP lo hablan sobre Streamable HTTP, y los paneles, las notificaciones y los feeds de progreso de trabajos lo usan porque atraviesa cualquier proxy que deje pasar HTTP.

sgcWebSockets 2026.10 añade TsgcSSEClient, un cliente EventSource para Delphi, C++Builder y .NET. Lee el stream en un hilo en segundo plano, te entrega cada evento con su tipo, datos e id, se reconecta por sí solo cuando la conexión se cae, y reanuda con Last-Event-ID para que nada se pierda entre medias. El componente no forma parte de la descarga actual. Llega con la versión 2026.10, en todas las ediciones.

TsgcSSEClient recibiendo eventos tipados, perdiendo la conexión y reanudando con Last-Event-ID. También en YouTube.

Por qué SSE importa ahora

Un stream SSE es una respuesta text/event-stream formada por bloques cortos: una línea event: opcional con el tipo, una o más líneas data:, un id: opcional, y una línea en blanco que termina el evento. Un servidor también puede enviar retry: para indicar al cliente cuánto esperar antes de reconectar. El formato es simple, pero un cliente correcto tiene que manejar fragmentos que cortan una línea en cualquier punto, datos multilínea, finales de línea CR, LF y CRLF, y una reconexión que recuerde dónde se quedó. Eso es lo que TsgcSSEClient hace por ti.

Inicio rápido

Añade la unidad sgcHTTP_SSE_Client, establece la URL, gestiona OnEvent y llama a Open. Las cabeceras de la petición, como un token bearer, van en Headers, una línea Name: Value cada una, y se envían en cada conexión, reconexiones incluidas.

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 está tipado. EventType lleva el nombre event:, así que un único manejador puede dirigir alert, tick o cualquier tipo que tu servidor invente. En una aplicación VCL o FireMonkey los eventos se entregan al hilo principal por defecto, así que los manejadores pueden tocar la interfaz directamente. NotifyEvents cambia eso. OnOpen y OnClose te dicen cuándo empieza y termina el stream, y ReadyState informa de sseConnecting, sseOpen o sseClosed.

El cliente reacciona al servidor igual que lo hace el EventSource de un navegador:

Reanudar con Last-Event-ID

Cuando el servidor da a sus eventos un campo id:, el cliente guarda el último en LastEventId y lo envía en la cabecera Last-Event-ID en cada reconexión. El servidor lee la cabecera y continúa después de ese evento, así que una conexión caída no cuesta ningún mensaje ni repite ninguno. Dentro de una misma sesión esto no necesita nada de código.

Para reanudar tras reiniciar la aplicación, guarda LastEventId cuando pares y restáuralo antes de Open. La propiedad conserva su valor después de Close, y un valor establecido antes de Open se envía con la primera petición.

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;

Reanudar necesita que el servidor coopere: debe enviar id: con sus eventos y respetar la cabecera Last-Event-ID.

Una política de reconexión que controlas tú

La reconexión está activada por defecto, con un retardo de 3 segundos. ReconnectOptions la convierte en la política que tu servidor merezca: backoff exponencial con un multiplicador, un techo para el retardo, jitter aleatorio para que mil clientes no vuelvan en el mismo milisegundo, y un límite de intentos.

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 cuenta fallos consecutivos, y el contador vuelve a empezar cada vez que se abre una conexión, así que un cliente que lleva semanas funcionando con algún que otro problema de red nunca se queda sin intentos. El servidor también tiene voz: un campo retry: sustituye a Interval durante el resto de la sesión. La última palabra es tuya, en OnReconnect, que se dispara antes de cada intento con el retardo 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 y autenticación

El stream se lee con un TsgcHTTP1Client, y OnBeforeConnect te lo entrega antes de cada intento de conexión. Todo lo que puede hacer el cliente HTTP, lo puede hacer el cliente SSE: opciones TLS, un proxy, autenticación básica o un timeout de lectura más largo.

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 y OnReconnect siempre se ejecutan en el hilo en segundo plano, así que mantén la interfaz fuera de ellos.

El parser para streams de LLM y MCP

A menudo no necesitas un EventSource de larga duración. Una compleción de chat de un LLM con stream: true es un POST cuya respuesta es SSE, y una respuesta Streamable HTTP de MCP también puede ser SSE. Para esos casos, el parser dentro del cliente es público: TsgcSSEParser. Aliméntalo con bytes o texto en fragmentos de cualquier tamaño, exactamente como los entrega la red, y dispara OnEvent por cada evento completo y OnRetry por cada campo retry: válido. Un fragmento puede terminar en mitad de una línea o de un carácter UTF-8, y el parser conserva el resto hasta la siguiente llamada.

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;

Esos tres fragmentos producen dos eventos: content_block_delta con el JSON completo {"delta":{"text":"Hello"}}, y message_stop. Reset descarta cualquier evento a medio leer antes de la siguiente respuesta, y LastEventId te dice el último id que vio el parser.

C# para .NET

La edición .NET de sgcWebSockets tiene el mismo componente con los mismos nombres, en el espacio de nombres esegece.sgcWebSockets. Headers es una lista de cadenas Name: Value y los eventos son eventos .NET normales.

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 también está ahí, con sobrecargas de Feed para un byte[], un slice de uno, o una cadena.

Prueba las demos

Ambas ediciones incluyen una demo que funciona completamente sin conexión. Aloja un pequeño servidor SSE dentro de la misma aplicación en 127.0.0.1, puerto 5580, que envía eventos numerados con un id:, rota los tipos message, tick y alert, envía un campo retry: y reanuda tras el Last-Event-ID que recibe. Corta la conexión y observa cómo el cliente reconecta y continúa desde el siguiente id, sin huecos ni duplicados.

Disponibilidad

TsgcSSEClient y TsgcSSEParser llegan en sgcWebSockets 2026.10, para Delphi, C++Builder y .NET, en todas las ediciones. No están en la versión que puedes descargar hoy. Cuando salga la 2026.10, estará en la página de descargas, y el componente quedará registrado en la pestaña de paleta SGC HTTP.

La referencia completa, con cada propiedad, evento y la guía de reanudación, está en la ayuda de TsgcSSEClient. Para todo lo demás que sgcWebSockets hace con Server-Sent Events, consulta la página de producto SSE.

Sigue leyendo

¿Preguntas, comentarios o ayuda con la migración? Ponte en contacto. Recibirás respuesta de las mismas personas que escribieron el código.