Ein Server-Sent-Events-Client für Delphi und .NET

· Komponenten
Ein Server-Sent-Events-Client für Delphi und .NET

Server-Sent Events waren früher der stille Cousin von WebSockets: eine einfache HTTP-Antwort, die nie endet, wobei der Server ein kleines Textereignis nach dem anderen schreibt. Plötzlich sind sie überall. Die gestreamten Antworten der LLM-APIs kommen als SSE an, MCP-Server sprechen es über Streamable HTTP, und Dashboards, Benachrichtigungen und Job-Fortschrittsfeeds nutzen es, weil es durch jeden Proxy geht, der HTTP durchlässt.

sgcWebSockets 2026.10 fügt TsgcSSEClient hinzu, einen EventSource-Client für Delphi, C++Builder und .NET. Er liest den Stream in einem Hintergrundthread, liefert Ihnen jedes Ereignis mit seinem Typ, seinen Daten und seiner ID, verbindet sich bei einem Verbindungsabbruch von selbst neu und setzt mit Last-Event-ID fort, sodass dazwischen nichts verloren geht. Die Komponente ist nicht Teil des aktuellen Downloads. Sie kommt mit dem Release 2026.10, in jeder Edition.

TsgcSSEClient empfängt typisierte Ereignisse, verliert die Verbindung und setzt mit Last-Event-ID fort. Auch auf YouTube.

Warum SSE jetzt wichtig ist

Ein SSE-Stream ist eine text/event-stream-Antwort aus kurzen Blöcken: einer optionalen event:-Zeile mit dem Typ, einer oder mehreren data:-Zeilen, einer optionalen id: und einer Leerzeile, die das Ereignis beendet. Ein Server kann auch retry: senden, um dem Client mitzuteilen, wie lange er vor einer Wiederverbindung warten soll. Das Format ist einfach, aber ein korrekter Client muss Fragmente verarbeiten, die eine Zeile an beliebiger Stelle teilen, mehrzeilige Daten, CR-, LF- und CRLF-Zeilenenden, und eine Wiederverbindung, die sich merkt, wo sie aufgehört hat. Genau das übernimmt TsgcSSEClient für Sie.

Schnellstart

Fügen Sie die Unit sgcHTTP_SSE_Client hinzu, setzen Sie die URL, behandeln Sie OnEvent und rufen Sie Open auf. Anfrage-Header wie ein Bearer-Token kommen in Headers, jeweils eine Zeile Name: Value, und sie werden bei jeder Verbindung gesendet, Wiederverbindungen eingeschlossen.

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;

Jedes Ereignis ist typisiert. EventType trägt den Namen aus event:, sodass ein einziger Handler alert, tick oder jeden von Ihrem Server erfundenen Typ weiterleiten kann. In einer VCL- oder FireMonkey-Anwendung werden die Ereignisse standardmäßig an den Hauptthread geliefert, sodass die Handler die Oberfläche direkt anfassen können. NotifyEvents ändert das. OnOpen und OnClose teilen Ihnen mit, wann der Stream beginnt und endet, und ReadyState meldet sseConnecting, sseOpen oder sseClosed.

Der Client reagiert auf den Server genauso wie das EventSource eines Browsers:

Mit Last-Event-ID fortsetzen

Wenn der Server seinen Ereignissen ein id:-Feld gibt, speichert der Client das letzte in LastEventId und sendet es bei jeder Wiederverbindung im Header Last-Event-ID. Der Server liest den Header und setzt nach diesem Ereignis fort, sodass eine abgebrochene Verbindung keine Nachricht kostet und keine wiederholt. Innerhalb einer Sitzung braucht das überhaupt keinen Code.

Um nach einem Neustart der Anwendung fortzusetzen, speichern Sie LastEventId beim Stoppen und stellen Sie es vor Open wieder her. Die Eigenschaft behält ihren Wert nach Close, und ein vor Open gesetzter Wert wird mit der allerersten Anfrage gesendet.

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;

Zum Fortsetzen muss der Server mitspielen: Er muss id: mit seinen Ereignissen senden und den Header Last-Event-ID beachten.

Eine Wiederverbindungsrichtlinie, die Sie steuern

Die Wiederverbindung ist standardmäßig aktiviert, mit einer Verzögerung von 3 Sekunden. ReconnectOptions macht daraus die Richtlinie, die Ihr Server verdient: exponentielles Backoff mit einem Multiplikator, eine Obergrenze für die Verzögerung, zufälliger Jitter, damit nicht tausend Clients in derselben Millisekunde zurückkommen, und ein Limit für die Versuche.

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 zählt aufeinanderfolgende Fehlschläge, und der Zähler beginnt jedes Mal von neuem, wenn eine Verbindung geöffnet wird, sodass einem Client, der wochenlang mit dem gelegentlichen Netzwerkaussetzer läuft, nie die Versuche ausgehen. Der Server hat ebenfalls ein Wort mitzureden: Ein retry:-Feld ersetzt Interval für den Rest der Sitzung. Das letzte Wort haben Sie, in OnReconnect, das vor jedem Versuch mit der berechneten Verzögerung ausgelöst wird:

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 und Authentifizierung

Der Stream wird mit einem TsgcHTTP1Client gelesen, und OnBeforeConnect übergibt ihn Ihnen vor jedem Verbindungsversuch. Alles, was der HTTP-Client kann, kann auch der SSE-Client: TLS-Optionen, ein Proxy, Basisauthentifizierung oder ein längeres Lese-Timeout.

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 und OnReconnect laufen immer im Hintergrundthread, halten Sie die Oberfläche also aus ihnen heraus.

Der Parser für LLM-Streams und MCP

Oft brauchen Sie gar kein langlebiges EventSource. Eine LLM-Chat-Vervollständigung mit stream: true ist ein POST, dessen Antwort SSE ist, und eine Streamable-HTTP-Antwort von MCP kann ebenfalls SSE sein. Dafür ist der Parser im Client öffentlich: TsgcSSEParser. Füttern Sie ihn mit rohen Bytes oder Text in Fragmenten beliebiger Größe, genau wie das Netzwerk sie liefert, und er löst OnEvent für jedes vollständige Ereignis und OnRetry für jedes gültige retry:-Feld aus. Ein Fragment kann mitten in einer Zeile oder mitten in einem UTF-8-Zeichen enden, und der Parser behält den Rest bis zum nächsten Aufruf.

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;

Diese drei Fragmente erzeugen zwei Ereignisse: content_block_delta mit dem vollständigen JSON {"delta":{"text":"Hello"}}, und message_stop. Reset verwirft jedes halb gelesene Ereignis vor der nächsten Antwort, und LastEventId verrät Ihnen die letzte ID, die der Parser gesehen hat.

C# für .NET

Die .NET-Edition von sgcWebSockets hat dieselbe Komponente mit denselben Namen, im Namespace esegece.sgcWebSockets. Headers ist eine Liste von Name: Value-Zeichenfolgen, und die Ereignisse sind normale .NET-Ereignisse.

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 ist auch dort, mit Feed-Überladungen für ein byte[], einen Ausschnitt davon oder eine Zeichenfolge.

Die Demos ausprobieren

Beide Editionen enthalten eine Demo, die vollständig offline läuft. Sie hostet einen kleinen SSE-Server innerhalb derselben Anwendung auf 127.0.0.1, Port 5580, der nummerierte Ereignisse mit einer id: streamt, die Typen message, tick und alert abwechselt, ein retry:-Feld sendet und nach der empfangenen Last-Event-ID fortsetzt. Trennen Sie die Verbindung und sehen Sie zu, wie der Client sich neu verbindet und ab der nächsten ID fortsetzt, ohne Lücke und ohne Duplikat.

Verfügbarkeit

TsgcSSEClient und TsgcSSEParser kommen in sgcWebSockets 2026.10, für Delphi, C++Builder und .NET, in jeder Edition. Sie sind nicht in der Version enthalten, die Sie heute herunterladen können. Wenn 2026.10 erscheint, wird es auf der Download-Seite stehen, und die Komponente wird in der SGC-HTTP-Palettenregisterkarte registriert.

Die vollständige Referenz, mit jeder Eigenschaft, jedem Ereignis und dem Leitfaden zum Fortsetzen, steht in der TsgcSSEClient-Hilfe. Für alles andere, was sgcWebSockets mit Server-Sent Events macht, siehe die SSE-Produktseite.

Weiterlesen

Fragen, Feedback oder Hilfe bei der Migration? Nehmen Sie Kontakt auf. Sie erhalten eine Antwort von den Leuten, die den Code geschrieben haben.