Een Server-Sent Events-client voor Delphi en .NET

· Componenten
Een Server-Sent Events-client voor Delphi en .NET

Server-Sent Events was ooit de stille neef van WebSockets: een gewoon HTTP-antwoord dat nooit eindigt, waarbij de server het ene kleine tekstevent na het andere schrijft. Ineens is het overal. De gestreamde antwoorden van de LLM-API's komen binnen als SSE, MCP-servers spreken het via Streamable HTTP, en dashboards, meldingen en voortgangsfeeds van taken gebruiken het omdat het door elke proxy heen komt die HTTP doorlaat.

sgcWebSockets 2026.10 voegt TsgcSSEClient toe, een EventSource-client voor Delphi, C++Builder en .NET. Hij leest de stream op een achtergrondthread, geeft je elk event met zijn type, data en id, herverbindt vanzelf wanneer de verbinding wegvalt, en hervat met Last-Event-ID zodat er tussendoor niets verloren gaat. De component maakt geen deel uit van de huidige download. Hij komt met de 2026.10-release, in elke editie.

TsgcSSEClient ontvangt getypeerde events, verliest de verbinding en hervat met Last-Event-ID. Ook op YouTube.

Waarom SSE er nu toe doet

Een SSE-stream is een text/event-stream-antwoord dat is opgebouwd uit korte blokken: een optionele event:-regel met het type, een of meer data:-regels, een optionele id:, en een lege regel die het event afsluit. Een server kan ook retry: sturen om de client te vertellen hoe lang hij moet wachten voordat hij opnieuw verbinding maakt. Het formaat is eenvoudig, maar een correcte client moet omgaan met chunks die een regel op elke willekeurige plek doorknippen, data die over meerdere regels loopt, CR-, LF- en CRLF-regeleinden, en een herverbinding die onthoudt waar hij gebleven was. Dat is het deel dat TsgcSSEClient voor je doet.

Aan de slag

Voeg de unit sgcHTTP_SSE_Client toe, stel de URL in, handel OnEvent af en roep Open aan. Requestheaders zoals een bearer-token gaan in Headers, elk als een regel Naam: Waarde, en ze worden bij elke verbinding meegestuurd, herverbindingen inbegrepen.

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;

Elk event is getypeerd. EventType bevat de naam uit event:, zodat één handler alert, tick of elk type dat jouw server verzint kan routeren. In een VCL- of FireMonkey-toepassing worden de events standaard afgeleverd op de hoofdthread, zodat de handlers de UI rechtstreeks kunnen aanraken. NotifyEvents verandert dat. OnOpen en OnClose laten je weten wanneer de stream begint en stopt, en ReadyState meldt sseConnecting, sseOpen of sseClosed.

De client reageert op de server zoals de EventSource van een browser dat doet:

Hervatten met Last-Event-ID

Wanneer de server zijn events een id:-veld meegeeft, bewaart de client de laatste in LastEventId en stuurt hij die bij elke herverbinding mee in de Last-Event-ID-header. De server leest de header en gaat verder na dat event, zodat een verbroken verbinding geen enkel bericht kost en er ook geen herhaalt. Binnen één sessie is hier helemaal geen code voor nodig.

Om te hervatten nadat de toepassing opnieuw is opgestart, sla je LastEventId op wanneer je stopt en herstel je hem voordat je Open aanroept. De property behoudt zijn waarde na Close, en een waarde die vóór Open is ingesteld wordt met het allereerste request meegestuurd.

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;

Hervatten vereist medewerking van de server: hij moet id: meesturen met zijn events en de Last-Event-ID-header respecteren.

Een herverbindingsbeleid dat jij bepaalt

Herverbinden staat standaard aan, met een vertraging van 3 seconden. ReconnectOptions maakt daar het beleid van dat jouw server verdient: exponentiële backoff met een vermenigvuldigingsfactor, een bovengrens op de vertraging, willekeurige jitter zodat duizend clients niet in dezelfde milliseconde terugkomen, en een limiet op het aantal pogingen.

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 telt opeenvolgende mislukkingen, en de teller begint opnieuw elke keer dat een verbinding wordt geopend, zodat een client die wekenlang draait met af en toe een netwerkhikje nooit door zijn pogingen heen raakt. De server heeft ook iets te zeggen: een retry:-veld vervangt Interval voor de rest van de sessie. Het laatste woord is aan jou, in OnReconnect, dat vóór elke poging afgaat met de berekende vertraging:

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 en authenticatie

De stream wordt gelezen met een TsgcHTTP1Client, en OnBeforeConnect geeft die aan jou voordat elke verbindingspoging wordt gedaan. Alles wat de HTTP-client kan, kan de SSE-client ook: TLS-opties, een proxy, basisauthenticatie of een langere leestime-out.

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 en OnReconnect draaien altijd op de achtergrondthread, dus houd de UI erbuiten.

De parser voor LLM-streams en MCP

Vaak heb je helemaal geen langlevende EventSource nodig. Een LLM chat completion met stream: true is een POST waarvan het antwoord SSE is, en ook een MCP Streamable HTTP-antwoord kan SSE zijn. Daarvoor is de parser binnen in de client openbaar: TsgcSSEParser. Voed hem ruwe bytes of tekst in brokken van elke grootte, precies zoals het netwerk ze aflevert, en hij activeert OnEvent voor elk compleet event en OnRetry voor elk geldig retry:-veld. Een brok kan eindigen midden in een regel of midden in een UTF-8-teken, en de parser bewaart de rest tot de volgende aanroep.

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;

Die drie brokken leveren twee events op: content_block_delta met de complete JSON {"delta":{"text":"Hello"}}, en message_stop. Reset verwerpt elk half gelezen event voordat het volgende antwoord begint, en LastEventId vertelt je de laatste id die de parser heeft gezien.

C# voor .NET

De .NET-editie van sgcWebSockets heeft dezelfde component met dezelfde namen, in de namespace esegece.sgcWebSockets. Headers is een lijst van Naam: Waarde-strings en de events zijn gewone .NET-events.

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 is er ook, met Feed-overloads voor een byte[], een deel daarvan, of een string.

Probeer de demo's

Beide edities worden geleverd met een demo die volledig offline draait. Die host een kleine SSE-server binnen dezelfde toepassing op 127.0.0.1, poort 5580, die genummerde events met een id: streamt, de types message, tick en alert laat rouleren, een retry:-veld stuurt en hervat na de Last-Event-ID die hij ontvangt. Verbreek de verbinding en kijk hoe de client herverbindt en verdergaat vanaf de volgende id, zonder gat en zonder duplicaat.

Beschikbaarheid

TsgcSSEClient en TsgcSSEParser komen in sgcWebSockets 2026.10, voor Delphi, C++Builder en .NET, in elke editie. Ze zitten niet in de versie die je vandaag kunt downloaden. Zodra 2026.10 verschijnt, staat hij op de downloadpagina, en wordt de component geregistreerd op het SGC HTTP-palettetabblad.

De volledige referentie, met elke property, elk event en de hervattingsgids, staat in de TsgcSSEClient-help. Voor al het andere dat sgcWebSockets doet met Server-Sent Events, zie de SSE-productpagina.

Lees verder

Vragen, feedback of hulp bij migratie? Neem contact op. Je krijgt een antwoord van de mensen die de code hebben geschreven.