Un client Server-Sent Events per Delphi e .NET

· Componenti
Un client Server-Sent Events per Delphi e .NET

Un tempo i Server-Sent Events erano il cugino silenzioso dei WebSocket: una semplice risposta HTTP che non finisce mai, con il server che scrive un piccolo evento di testo dopo l'altro. All'improvviso sono ovunque. Le risposte in streaming delle API LLM arrivano come SSE, i server MCP lo parlano su Streamable HTTP, e dashboard, notifiche e feed di avanzamento dei job lo usano perché attraversa qualsiasi proxy che lasci passare l'HTTP.

sgcWebSockets 2026.10 aggiunge TsgcSSEClient, un client EventSource per Delphi, C++Builder e .NET. Legge lo stream su un thread in background, ti consegna ogni evento con il suo tipo, i dati e l'id, si riconnette da solo quando la connessione cade, e riprende con Last-Event-ID così che nulla vada perso nel frattempo. Il componente non fa parte del download attuale. Arriva con la versione 2026.10, in ogni edizione.

TsgcSSEClient che riceve eventi tipizzati, perde la connessione e riprende con Last-Event-ID. Anche su YouTube.

Perché SSE conta adesso

Uno stream SSE è una risposta text/event-stream composta da blocchi brevi: una riga event: opzionale con il tipo, una o più righe data:, un id: opzionale, e una riga vuota che chiude l'evento. Un server può anche inviare retry: per dire al client quanto attendere prima di riconnettersi. Il formato è semplice, ma un client corretto deve gestire frammenti che spezzano una riga in qualsiasi punto, dati su più righe, fine riga CR, LF e CRLF, e una riconnessione che ricordi dove si era fermata. È questa la parte di cui si occupa TsgcSSEClient per te.

Avvio rapido

Aggiungi l'unit sgcHTTP_SSE_Client, imposta l'URL, gestisci OnEvent e chiama Open. Le intestazioni della richiesta, come un token bearer, vanno in Headers, una riga Name: Value ciascuna, e vengono inviate a ogni connessione, riconnessioni incluse.

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;

Ogni evento è tipizzato. EventType porta il nome event:, così un unico handler può instradare alert, tick o qualsiasi tipo inventato dal tuo server. In un'applicazione VCL o FireMonkey gli eventi vengono consegnati al thread principale per impostazione predefinita, così gli handler possono toccare l'interfaccia direttamente. NotifyEvents cambia questo comportamento. OnOpen e OnClose ti dicono quando lo stream inizia e finisce, e ReadyState riporta sseConnecting, sseOpen o sseClosed.

Il client reagisce al server proprio come fa l'EventSource di un browser:

Riprendere con Last-Event-ID

Quando il server assegna ai suoi eventi un campo id:, il client conserva l'ultimo in LastEventId e lo invia nell'intestazione Last-Event-ID a ogni riconnessione. Il server legge l'intestazione e continua dopo quell'evento, così una connessione caduta non costa nessun messaggio e non ne ripete nessuno. All'interno di una sessione questo non richiede alcun codice.

Per riprendere dopo il riavvio dell'applicazione, salva LastEventId quando ti fermi e ripristinalo prima di Open. La proprietà mantiene il suo valore dopo Close, e un valore impostato prima di Open viene inviato con la primissima richiesta.

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;

Riprendere richiede la collaborazione del server: deve inviare id: con i suoi eventi e rispettare l'intestazione Last-Event-ID.

Una politica di riconnessione che controlli tu

La riconnessione è attiva per impostazione predefinita, con un ritardo di 3 secondi. ReconnectOptions la trasforma nella politica che il tuo server merita: backoff esponenziale con un moltiplicatore, un tetto per il ritardo, jitter casuale perché mille client non tornino nello stesso millisecondo, e un limite di tentativi.

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 i fallimenti consecutivi, e il contatore riparte ogni volta che si apre una connessione, così un client che gira per settimane con qualche intoppo di rete non esaurisce mai i tentativi. Anche il server ha voce in capitolo: un campo retry: sostituisce Interval per il resto della sessione. L'ultima parola è tua, in OnReconnect, che scatta prima di ogni tentativo con il ritardo calcolato:

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 autenticazione

Lo stream viene letto con un TsgcHTTP1Client, e OnBeforeConnect te lo consegna prima di ogni tentativo di connessione. Tutto ciò che può fare il client HTTP, lo può fare il client SSE: opzioni TLS, un proxy, autenticazione di base o un timeout di lettura più lungo.

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 vengono sempre eseguiti sul thread in background, quindi tieni l'interfaccia fuori da essi.

Il parser per gli stream LLM e MCP

Spesso non ti serve affatto un EventSource di lunga durata. Un completamento di chat LLM con stream: true è un POST la cui risposta è SSE, e anche una risposta Streamable HTTP di MCP può essere SSE. Per questi casi, il parser all'interno del client è pubblico: TsgcSSEParser. Alimentalo con byte grezzi o testo in frammenti di qualsiasi dimensione, esattamente come li consegna la rete, e scatena OnEvent per ogni evento completo e OnRetry per ogni campo retry: valido. Un frammento può terminare in mezzo a una riga o a un carattere UTF-8, e il parser conserva il resto fino alla chiamata successiva.

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;

Questi tre frammenti producono due eventi: content_block_delta con il JSON completo {"delta":{"text":"Hello"}}, e message_stop. Reset scarta qualsiasi evento letto a metà prima della risposta successiva, e LastEventId ti dice l'ultimo id visto dal parser.

C# per .NET

L'edizione .NET di sgcWebSockets ha lo stesso componente con gli stessi nomi, nel namespace esegece.sgcWebSockets. Headers è un elenco di stringhe Name: Value e gli eventi sono normali eventi .NET.

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 c'è anche qui, con overload di Feed per un byte[], una porzione di uno, o una stringa.

Prova le demo

Entrambe le edizioni includono una demo che funziona completamente offline. Ospita un piccolo server SSE all'interno della stessa applicazione su 127.0.0.1, porta 5580, che invia in streaming eventi numerati con un id:, alterna i tipi message, tick e alert, invia un campo retry: e riprende dopo il Last-Event-ID che riceve. Interrompi la connessione e guarda il client riconnettersi e continuare dall'id successivo, senza vuoti e senza duplicati.

Disponibilità

TsgcSSEClient e TsgcSSEParser arrivano in sgcWebSockets 2026.10, per Delphi, C++Builder e .NET, in ogni edizione. Non sono nella versione che puoi scaricare oggi. Quando uscirà la 2026.10, sarà sulla pagina di download, e il componente verrà registrato nella scheda della palette SGC HTTP.

Il riferimento completo, con ogni proprietà, evento e la guida alla ripresa, è nella guida di TsgcSSEClient. Per tutto il resto che sgcWebSockets fa con i Server-Sent Events, vedi la pagina di prodotto SSE.

Continua a leggere

Domande, feedback o aiuto con la migrazione? Mettiti in contatto. Riceverai una risposta dalle persone che hanno scritto il codice.