Klient Server-Sent Events dla Delphi i .NET

· Komponenty
Klient Server-Sent Events dla Delphi i .NET

Server-Sent Events bywało cichym kuzynem WebSocketów: zwykła odpowiedź HTTP, która nigdy się nie kończy, a serwer zapisuje w niej kolejno jedno małe zdarzenie tekstowe po drugim. Nagle jest wszędzie. Strumieniowane odpowiedzi API LLM przychodzą jako SSE, serwery MCP mówią nim przez Streamable HTTP, a dashboardy, powiadomienia i strumienie postępu zadań korzystają z niego, bo przechodzi przez każdy proxy, który przepuszcza HTTP.

sgcWebSockets 2026.10 dodaje TsgcSSEClient, klienta EventSource dla Delphi, C++Buildera i .NET. Odczytuje strumień w wątku w tle, dostarcza każde zdarzenie wraz z jego typem, danymi i identyfikatorem, sam ponownie łączy się, gdy połączenie zostanie zerwane, i wznawia za pomocą Last-Event-ID, dzięki czemu nic nie ginie po drodze. Komponent nie jest częścią obecnej wersji do pobrania. Pojawi się wraz z wydaniem 2026.10, w każdej edycji.

TsgcSSEClient odbiera typowane zdarzenia, traci połączenie i wznawia je za pomocą Last-Event-ID. Również w serwisie YouTube.

Dlaczego SSE ma teraz znaczenie

Strumień SSE to odpowiedź text/event-stream złożona z krótkich bloków: opcjonalna linia event: z typem, jedna lub więcej linii data:, opcjonalne id:, oraz pusta linia kończąca zdarzenie. Serwer może też wysłać retry:, aby powiedzieć klientowi, jak długo czekać przed ponownym połączeniem. Format jest prosty, ale poprawny klient musi radzić sobie z fragmentami, które dzielą linię w dowolnym miejscu, danymi rozłożonymi na wiele linii, zakończeniami linii CR, LF i CRLF, oraz ponownym połączeniem, które pamięta, gdzie się zatrzymało. To właśnie tę część TsgcSSEClient robi za ciebie.

Szybki start

Dodaj jednostkę sgcHTTP_SSE_Client, ustaw adres URL, obsłuż OnEvent i wywołaj Open. Nagłówki żądania, takie jak token bearer, trafiają do Headers, każdy jako linia Nazwa: Wartość, i są wysyłane przy każdym połączeniu, łącznie z ponownymi połączeniami.

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;

Każde zdarzenie jest typowane. EventType przenosi nazwę z event:, dzięki czemu jeden handler może kierować alert, tick lub dowolny typ, jaki wymyśli twój serwer. W aplikacji VCL lub FireMonkey zdarzenia są domyślnie dostarczane do wątku głównego, więc handlery mogą bezpośrednio dotykać interfejsu użytkownika. NotifyEvents to zmienia. OnOpen i OnClose informują cię, kiedy strumień się zaczyna i kończy, a ReadyState zgłasza sseConnecting, sseOpen lub sseClosed.

Klient reaguje na serwer tak samo, jak robi to EventSource w przeglądarce:

Wznawianie za pomocą Last-Event-ID

Gdy serwer nadaje swoim zdarzeniom pole id:, klient zapamiętuje ostatnie w LastEventId i wysyła je w nagłówku Last-Event-ID przy każdym ponownym połączeniu. Serwer odczytuje ten nagłówek i kontynuuje od tamtego zdarzenia, więc zerwane połączenie nie kosztuje ani jednej wiadomości i żadnej nie powtarza. W obrębie jednej sesji nie potrzeba do tego żadnego kodu.

Aby wznowić po ponownym uruchomieniu aplikacji, zapisz LastEventId, gdy zatrzymujesz strumień, i przywróć go przed wywołaniem Open. Właściwość zachowuje swoją wartość po Close, a wartość ustawiona przed Open jest wysyłana już z pierwszym żądaniem.

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;

Wznawianie wymaga współpracy serwera: musi wysyłać id: ze swoimi zdarzeniami i respektować nagłówek Last-Event-ID.

Zasady ponownego łączenia, które kontrolujesz

Ponowne łączenie jest domyślnie włączone, z opóźnieniem 3 sekund. ReconnectOptions zamienia to w dowolną politykę, na jaką zasługuje twój serwer: wykładniczy backoff z mnożnikiem, górny limit opóźnienia, losowy jitter, aby tysiąc klientów nie wróciło w tej samej milisekundzie, oraz limit liczby prób.

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 liczy kolejne niepowodzenia, a licznik zaczyna się od nowa za każdym razem, gdy połączenie zostaje otwarte, więc klient działający tygodniami, z rzadka trafiając na zakłócenie sieci, nigdy nie wyczerpie prób. Serwer też ma coś do powiedzenia: pole retry: zastępuje Interval do końca sesji. Ostatnie słowo należy do ciebie, w OnReconnect, które wywoływane jest przed każdą próbą wraz z obliczonym opóźnieniem:

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 i uwierzytelnianie

Strumień jest odczytywany za pomocą TsgcHTTP1Client, a OnBeforeConnect przekazuje go tobie przed każdą próbą połączenia. Wszystko, co potrafi klient HTTP, potrafi też klient SSE: opcje TLS, proxy, uwierzytelnianie podstawowe lub dłuższy czas oczekiwania na odczyt.

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 i OnReconnect zawsze działają w wątku w tle, więc trzymaj z dala od nich interfejs użytkownika.

Parser dla strumieni LLM i MCP

Często w ogóle nie potrzebujesz długo żyjącego EventSource. Uzupełnienie czatu LLM z stream: true to żądanie POST, którego odpowiedzią jest SSE, a odpowiedź MCP Streamable HTTP też może być SSE. Do tego służy publiczny parser wewnątrz klienta: TsgcSSEParser. Podawaj mu surowe bajty lub tekst w porcjach dowolnej wielkości, dokładnie tak, jak dostarcza je sieć, a on wywoła OnEvent dla każdego kompletnego zdarzenia i OnRetry dla każdego prawidłowego pola retry:. Porcja może kończyć się w środku linii lub w środku znaku UTF-8, a parser zachowuje resztę do następnego wywołania.

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;

Te trzy porcje dają dwa zdarzenia: content_block_delta z kompletnym JSON-em {"delta":{"text":"Hello"}} oraz message_stop. Reset odrzuca każde niedoczytane zdarzenie przed kolejną odpowiedzią, a LastEventId podaje ostatni identyfikator, jaki zobaczył parser.

C# dla .NET

Edycja .NET sgcWebSockets ma ten sam komponent pod tymi samymi nazwami, w przestrzeni nazw esegece.sgcWebSockets. Headers to lista łańcuchów Nazwa: Wartość, a zdarzenia są zwykłymi zdarzeniami .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 też tam jest, z przeciążeniami Feed dla byte[], jego fragmentu lub ciągu znaków.

Wypróbuj demo

Obie edycje zawierają demo, które działa całkowicie offline. Hostuje ono mały serwer SSE wewnątrz tej samej aplikacji na 127.0.0.1, port 5580, który strumieniuje ponumerowane zdarzenia z id:, rotuje typy message, tick i alert, wysyła pole retry: i wznawia po otrzymanym Last-Event-ID. Zerwij połączenie i zobacz, jak klient łączy się ponownie i kontynuuje od kolejnego identyfikatora, bez luki i bez duplikatu.

Dostępność

TsgcSSEClient i TsgcSSEParser pojawią się w sgcWebSockets 2026.10, dla Delphi, C++Buildera i .NET, w każdej edycji. Nie ma ich w wersji, którą można pobrać dzisiaj. Gdy 2026.10 zostanie wydane, trafi na stronę pobierania, a komponent zostanie zarejestrowany na karcie palety SGC HTTP.

Pełna dokumentacja referencyjna, z każdą właściwością, zdarzeniem i przewodnikiem po wznawianiu, znajduje się w pomocy TsgcSSEClient. Wszystko inne, co sgcWebSockets robi z Server-Sent Events, opisuje strona produktu SSE.

Czytaj dalej

Masz pytania, uwagi lub potrzebujesz pomocy przy migracji? Skontaktuj się z nami. Otrzymasz odpowiedź od osób, które napisały ten kod.