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:
- Un 200 con
text/event-streamabre el stream. Las redirecciones se siguen. - Un 204 No Content significa que el servidor quiere que el cliente se detenga. Se cierra sin error y no reconecta.
- Cualquier otro estado, o un tipo de contenido incorrecto, dispara
OnErrory luegoOnClose, y el cliente no reconecta. Reintentar un 401 para siempre no ayuda a nadie. - Un error de red o un timeout de lectura dispara
OnError, y luego el cliente reconecta. Cuando el servidor simplemente termina el stream, el cliente también reconecta.
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.
- Delphi:
Demos\20.HTTP_Protocol\17.SSE_Client_Component - .NET:
demos\20.HTTP_Protocol\17.SSE_Client_Component, que además tiene un modificador/selftestque comprueba por sí solo el orden, los duplicados y la cabecera de reanudación
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.
