Net-Base Magazín

01.06.2026

Delphi WebSocket klient: robustne sa pripojiť, čisto ukončiť, spoľahlivo ladiť

Klient Delphi WebSocket sa rýchlo „nejako pripojí“ – ale v prevádzke záleží na obnovení spojenia (Reconnect), heartbeatov, kontrolovanom ukončení a možnosti ladenia. S praktickým wrapperom založeným na System.Net.WebSockets (s fallbackom) a útržkom zdrojového kódu pre správu vlákien a...

01.06.2026

Od témy magazínu k projektovej praxi

Súvisiace stránky služieb a technológií k príspevku

Prečo je Delphi WebSocket klient v praxi viac než „Connect“

Jednoduchý Delphi WebSocket Client sa poskladá za minúty: URL, Connect, SendText, hotovo. V individuálnom firemnom softvéri a v procesne blízkych riešeniach sa problém väčšinou prejaví až v prevádzke: Reverse Proxy prerušuje idle pripojenia, mobilné alebo VPN trasy majú krátke NAT-Timeouts, certifikáty sa menia a pri ukončovaní proces visí, pretože Receive-Loop je stále zablokovaný. Okrem toho: WebSocket je dlhodobý, stavový kanál – naň platia iné pravidlá než na klasické HTTP/REST (Request/Response, krátkodobé).

V tomto úryvku zdrojového kódu nejde o „Hello WebSocket“, ale o prevádzkyschopný klientský Wrapper s:

  • čistým Start/Stop (bez zaseknutia pri Shutdown),
  • Receive-Loop s Cancellation (Abbruchsignal) namiesto „Thread kill“,
  • Reconnect s Backoff (kontrolované Wiederanbindung),
  • Heartbeat ako aplikačný vzor (pretože Ping/Pong nie je všade dostupný),
  • Debug- und Trace-Hooks, ktoré pri podporných prípadoch skutočne pomáhajú.

Implementácia je založená na System.Net.WebSockets (Delphi RTL; WebSocket-Client-API s TClientWebSocket). Kde táto RTL-vrstva v starších verziách nie je dostupná alebo je príliš obmedzená, často dáva zmysel fallback cez knižnicu (napr. ICS) – k tomu nižšie zaradenie.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Bežná chyba v rastúcich Delphi aplikáciách: UI-formuláre alebo servisné moduly „kommunikujú priamo cez WebSocket“ a majú potom roztrúsené Timers, Threads a spracovanie výnimiek. Lepšie je mať jasný komponent s dobre definovanými Events a malým stavovým strojom.

Krátke zaradenie pojmov: Backoff označuje čakaciu dobu, ktorá po chybách postupne rastie (napr. 1s, 2s, 4s …), aby sa server a sieť nezaplavili. CancellationToken je Abbruchsignal z .NET sveta; v Delphi neexistuje identický pattern, ale môžeme ho napodobniť pomocou TEvent a príznaku „StopRequested“. TThread.Queue plánuje kód na vykonanie v hlavnom vlákne (UI) bez blokovania workeru; Synchronize blokuje a často je v shutdown-cestách príčinou deadlockov.

Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch

Nasledujúci kód je zámerne postavený ako „Betriebs-Baustein“: trieda, ktorú možno použiť v VCL/FMX alebo v Windows- a Windows- und Linux-Services (podľa verzie/Platformy Delphi). Jadrom je Worker-Thread, ktorý udržiava Receive-Loop a hlási udalosti do aplikácie.

Delphi
unit Net.WebSocketClientEx;

interface

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs,
  System.Generics.Collections,
  System.Net.URLClient,
  System.Net.WebSockets;

type
  TWsLogLevel = (llDebug, llInfo, llWarn, llError);

  TWsLogEvent = reference to procedure(Level: TWsLogLevel; const Msg: string);
  TWsTextEvent = reference to procedure(const Text: string);
  TWsStateEvent = reference to procedure(const State: string);

  TDelphiWebSocketClient = class
  private
    FUrl: string;
    FOnLog: TWsLogEvent;
    FOnText: TWsTextEvent;
    FOnState: TWsStateEvent;

    FStopEvent: TEvent;
    FWorker: TThread;

    FMinBackoffMs: Integer;
    FMaxBackoffMs: Integer;
    FHeartbeatSec: Integer;

    procedure Log(Level: TWsLogLevel; const Msg: string);
    procedure State(const S: string);

    procedure Run;
    function NextBackoffMs(const Prev: Integer): Integer;
    function NowUtcStr: string;
  public
    constructor Create(const AUrl: string);
    destructor Destroy; override;

    procedure Start;
    procedure Stop(TimeoutMs: Cardinal = 5000);

    property OnLog: TWsLogEvent read FOnLog write FOnLog;
    property OnText: TWsTextEvent read FOnText write FOnText;
    property OnState: TWsStateEvent read FOnState write FOnState;

    property MinBackoffMs: Integer read FMinBackoffMs write FMinBackoffMs;
    property MaxBackoffMs: Integer read FMaxBackoffMs write FMaxBackoffMs;
    property HeartbeatSec: Integer read FHeartbeatSec write FHeartbeatSec;
  end;

implementation

type
  TBytesBuffer = record
    Data: TBytes;
    Len: Integer;
  end;

{ TDelphiWebSocketClient }

constructor TDelphiWebSocketClient.Create(const AUrl: string);
begin
  inherited Create;
  FUrl := AUrl;
  FStopEvent := TEvent.Create(nil, True, False, '');

  FMinBackoffMs := 500;
  FMaxBackoffMs := 15000;
  FHeartbeatSec := 20; // App-Heartbeat: užitočné proti timeoutom nečinnosti za proxy
end;

destructor TDelphiWebSocketClient.Destroy;
begin
  Stop;
  FStopEvent.Free;
  inherited;
end;

procedure TDelphiWebSocketClient.Start;
begin
  if Assigned(FWorker) then
    Exit;

  FStopEvent.ResetEvent;
  FWorker := TThread.CreateAnonymousThread(
    procedure
    begin
      Run;
    end);
  FWorker.FreeOnTerminate := False;
  FWorker.Start;
end;

procedure TDelphiWebSocketClient.Stop(TimeoutMs: Cardinal);
var
  W: TThread;
begin
  FStopEvent.SetEvent;

  W := FWorker;
  if Assigned(W) then
  begin
    if W.WaitFor(TimeoutMs) = wrTimeout then
      Log(llWarn, 'Stop: Worker neodpovedá v rámci timeoutu; možná blokácia v sieťovom stacku');
    FreeAndNil(FWorker);
  end;
end;

procedure TDelphiWebSocketClient.Log(Level: TWsLogLevel; const Msg: string);
begin
  if Assigned(FOnLog) then
    TThread.Queue(nil,
      procedure
      begin
        FOnLog(Level, NowUtcStr + ' ' + Msg);
      end);
end;

procedure TDelphiWebSocketClient.State(const S: string);
begin
  if Assigned(FOnState) then
    TThread.Queue(nil,
      procedure
      begin
        FOnState(S);
      end);
end;

function TDelphiWebSocketClient.NowUtcStr: string;
begin
  Result := FormatDateTime('yyyy-mm-dd"T"hh:nn:ss.zzz"Z"', TTimeZone.Local.ToUniversalTime(Now));
end;

function TDelphiWebSocketClient.NextBackoffMs(const Prev: Integer): Integer;
var
  N: Integer;
begin
  if Prev <= 0 then
    Exit(FMinBackoffMs);

  N := Prev * 2;
  if N < FMinBackoffMs then N := FMinBackoffMs;
  if N > FMaxBackoffMs then N := FMaxBackoffMs;
  Result := N;
end;

procedure TDelphiWebSocketClient.Run;
var
  WS: TClientWebSocket;
  Backoff: Integer;
  LastHeartbeat: UInt64;
  Msg: string;
  Buf: TBytes;
  Received: TWebSocketReceiveResult;
  SB: TStringBuilder;
  WaitRes: TWaitResult;

  function StopRequested: Boolean;
  begin
    Result := (FStopEvent.WaitFor(0) = wrSignaled);
  end;

  procedure SafeClose;
  begin
    try
      if WS.State = TWebSocketState.Open then
        WS.Close(TWebSocketCloseStatus.NormalClosure, 'client shutdown');
    except
      on E: Exception do
        Log(llDebug, 'Close: ' + E.ClassName + ': ' + E.Message);
    end;
  end;

begin
  Backoff := 0;
  LastHeartbeat := 0;

  while not StopRequested do
  begin
    WS := TClientWebSocket.Create;
    try
      State('connecting');
      Log(llInfo, 'Connect to ' + FUrl);

      try
        // Poznámka: TClientWebSocket.Connect je synchronná a môže blokovať v závislosti od DNS/TLS.
        // Preto to beží tu vo worker-vlákne.
        WS.Connect(FUrl);
      except
        on E: Exception do
        begin
          State('connect_failed');
          Log(llWarn, 'Connect failed: ' + E.ClassName + ': ' + E.Message);
          Backoff := NextBackoffMs(Backoff);
          WaitRes := FStopEvent.WaitFor(Backoff);
          if WaitRes = wrSignaled then Break;
          Continue;
        end;
      end;

      State('open');
      Backoff := 0;

      SetLength(Buf, 16 * 1024);
      SB := TStringBuilder.Create;
      try
        while (WS.State = TWebSocketState.Open) and (not StopRequested) do
        begin
          // Heartbeat ako aplikačná správa, pretože Ping/Pong nie je v každej Delphi-verzii spoľahlivo dostupný.
          if (FHeartbeatSec > 0) then
          begin
            if (LastHeartbeat = 0) or (TThread.GetTickCount64 - LastHeartbeat >= UInt64(FHeartbeatSec) * 1000) then
            begin
              try
                WS.Send('ping');
                LastHeartbeat := TThread.GetTickCount64;
                Log(llDebug, 'Heartbeat ping sent');
              except
                on E: Exception do
                begin
                  Log(llWarn, 'Heartbeat send failed: ' + E.Message);
                  Break;
                end;
              end;
            end;
          end;

          // Príjem: založený na rámcoch, preto StringBuilder pre fragmentáciu.
          try
            Received := WS.Receive(Buf);
          except
            on E: Exception do
            begin
              Log(llWarn, 'Receive failed: ' + E.ClassName + ': ' + E.Message);
              Break;
            end;
          end;

          case Received.Kind of
            TWebSocketMessageKind.Text:
              begin
                SB.Append(TEncoding.UTF8.GetString(Buf, 0, Received.BytesReceived));
                if Received.EndOfMessage then
                begin
                  Msg := SB.ToString;
                  SB.Clear;

                  if Assigned(FOnText) then
                    TThread.Queue(nil,
                      procedure
                      begin
                        FOnText(Msg);
                      end);
                end;
              end;

            TWebSocketMessageKind.Binary:
              begin
                // V mnohých podnikových protokoloch je štandardom text/JSON.
                // Binárne dáta možno tu podobne bufferovať alebo priamo odovzdať.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

            TWebSocketMessageKind.Close:
              begin
                Log(llInfo, 'Server requested close');
                Break;
              end;
          end;

          // Krátke spanie (mini-sleep), aby sa pri veľmi rýchlom cykle šetrilo CPU.
          // Príliš dlhé by však zhoršilo latenciu.
          TThread.Sleep(1);
        end;
      finally
        SB.Free;
      end;

      SafeClose;
      State('closed');

    finally
      WS.Free;
    end;

    if StopRequested then
      Break;

    // Znovupripojenie po čistom zavretí alebo po chybách
    Backoff := NextBackoffMs(Backoff);
    Log(llInfo, 'Reconnect in ' + Backoff.ToString + ' ms');
    WaitRes := FStopEvent.WaitFor(Backoff);
    if WaitRes = wrSignaled then
      Break;
  end;

  State('stopped');
  Log(llInfo, 'Worker stopped');
end;

end.

Čo kód zámerne robí „inak“ než typické príklady

  • Stop bez násilia: Namiesto násilného ukončovania vlákien nastaví Stop udalosť. Worker ukončí slučky v definovaných bodoch. Znižuje to zasekávanie pri ukončení a zabraňuje únikom zdrojov v socket-stacku.
  • Queue namiesto Synchronize: Logovanie a udalosti idú cez TThread.Queue do hlavného vlákna. To je dôležité, keď Stop/Shutdown prichádza z UI alebo zo Service-Control-Handlerov. Synchronize môže blokovať, ak hlavné vlákno práve čaká.
  • Zohľadnenie fragmentácie: Text WebSocketu môže prichádzať fragmentovaný vo frámach. Preto TStringBuilder a kontrola EndOfMessage.
  • Heartbeat ako aplikačný protokol: Mnohé nasadenia hynú na idle-timeoutoch (Load Balancer, nginx, Cloud WAF). Ľahký textový „ping“ ako prevádzková páka je často efektívnejší než spoliehanie sa na „TCP keepalive“ alebo na Ping/Pong-API, ktoré nie je všade dostupné.

Hraničné podmienky a úskalia v prevádzke

1) DNS, TLS a proxy: Connect môže blokovať

TClientWebSocket.Connect je synchronný. V závislosti od DNS-resolúcie, TLS-handshake, kontroly certifikátu alebo proxy prostredia to môže trvať niekoľko sekúnd. Kód to zámerne umiestňuje do workeru. Ak potrebujete prísne timeouty, musíte na úrovni API skontrolovať, či vaša Delphi-verzia poskytuje možnosti timeoutu, alebo obaliť Connect do samostatného vlákna a prerušiť ho cez procesnú logiku. Dôležité: „prerušenie“ tu zvyčajne znamená „označiť pripojenie za poškodené a znovu spustiť Workera“, nie „ihneď ukončiť socketovú operáciu“.

2) Idle-timeouty: prečo je Heartbeat často povinný

V podnikových sieťach je WebSocket často terminovaný za reverzným proxy (nginx, IIS ARR) alebo Load Balancerom. Mnohé z týchto komponentov zatvárajú pripojenia, ak dlhší čas neprechádzajú žiadne dáta. TCP-Keepalive nie je vždy nakonfigurovaný na dosť krátku hodnotu (a pod Windows je to často skôr minúty než sekundy). Heartbeat na úrovni aplikácie je preto spoľahlivý pracovný obchvat. Dbajte, aby server a klient mali rovnaké koncepty (napr. „ping“/„pong“ ako text alebo JSON).

3) Vlákna a UI: udalosti musia zostať oddelené

Ak je spracovanie OnText ťažké (parsovanie JSON, prístupy k DB s BDE-náhrada s natívnym prepojením, aktualizácie UI), nemalo by to všetko blokovať v hlavnom vlákne. Wrapper dodáva len správu. Typický vzor je: OnText vloží payload do fronty (napr. TThreadedQueue<string>), samostatný worker spracováva s backpressure (t. j. obmedzená dĺžka fronty). To zabráni, aby pri špičkovej záťaži UI zamrzla alebo príjem prestal fungovať.

Debugging: čo by ste mali logovať, keď sa to „niekedy“ preruší

WebSockety sú známe tým, že „bežia dni, potom prestanú“. Bez logovania sa to ťažko lokalizuje. Zmysluplné logovacie body:

  • Časová značka (UTC), URL a zmeny stavu (connecting/open/closed).
  • Close-Reason, ak je dostupné (server požiada o Close vs. sieťová chyba).
  • Chyby pri odosielaní heartbeat a výnimky pri prijímaní vrátane typu výnimky.
  • Voliteľne: veľkosti prijatých správ (nie ich obsah), aby ste odhalili dátové explózie.

Ak terminujete cez TLS: skontrolujte tiež, či zmeny certifikátov (expirácia, nový vydavateľ certifikátu) časovo korelujú s chybami. V prísne zabezpečených prostrediach sú kandidátmi aj proxy a DPI boxy (Deep Packet Inspection).

Varianty: kedy postačuje System.Net.WebSockets – a kedy nie

System.Net.WebSockets je pre mnohé integračné prípady postačujúci, najmä ak ide o text/JSON, mierne zaťaženie a jasné stratégie opätovného pripojenia. Hranice sa prejavujú v závislosti od verzie Delphi a cieľovej platformy:

  • Chýbajúca/obmedzená podpora Ping/Pong: V tom prípade zostáva App-Heartbeat robustným vzorom.
  • Chýbajúce Timeouts/Cancellation pri Connect/Receive: V takom prípade musíte architektúru navrhnúť tak, aby zablokovaný worker zostal izolovaný a aplikácia sa napriek tomu mohla korektne ukončiť (napr. prostredníctvom procesného watchdogu alebo oddelených inštancií workerov).
  • Vysoké zaťaženie alebo binárne streamy: V takom prípade má zmysel silnejší koncept framingu/buffering (napr. ring buffer, separátne Binary-Event, Message-Assembler s limitmi).

Pre legacy situácie (staršie generácie Delphi, veľmi špecifické požiadavky na TLS/proxy) sú knižnice ako ICS v niektorých projektoch pragmatickejšie. Dôležitejšie nie je „ktorá knižnica“, ale aby ste Shutdown, Reconnect a Observability (logy/metriky) riešili ako prvotriedne témy.

Záver: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen

WebSocket sa výborne hodí na push-udalosti, live stav, hlásenia strojov alebo procesov a ako spätný kanál pre portály a služby. Predvedený Wrapper sa sústreďuje na aspekty, ktoré v digitálnych podnikových riešeniach často rozhodujú: kontrolovaný Reconnect, Heartbeat proti Idle-Timeouts, spracovanie textu odolné voči fragmentácii a mechanizmus zastavenia, ktorý sa pri nasadení alebo aktualizácii nezaseká.

Zostávajú obmedzenia nasadenia: ak potrebujete tvrdé záruky na prerušenie Connect/Receive v veľmi tesných časových oknách alebo dosahujete extrémne vysoké dátové toky, musíte sa hlbšie venovať timeoutom, špecifikám platformy a prípadne alternatívnym stackom. Pre väčšinu integračných a modernizačných scenárov je však čisto enkapsulovaný, dobre logovaný klient ako vyššie uvedený solídnym základom, ktorý sa dá integrovať do existujúcich Delphi systémov.

Ak chcete takýto stavebný blok zapracovať do existujúcej architektúry (napr. Layer-3 architektúry s jasnými vrstvami Service a UI) alebo potrebujete debugovať sporadické Disconnects v reálnych podmienkach, môžeme to s vami cielene zanalyzovať: kontaktujte nás.

V odbornom kontexte majú Heartbeat Ping/Pong tiež dôležitú úlohu, keď musia integrácie, dátové toky a ďalší vývoj navzájom hladko spolupracovať.

Prediskutovať projekt alebo modernizačný zámer s Net-Base.

Ďalší krok

Keď sa téma stane reálnym projektom, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.

Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.

  • Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
  • REST, prístup k dátam, portály a Rollout nebudú odložené na neskôr.
  • Včas zistíte, ktorá cesta je ekonomicky a prevádzkovo životaschopná.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.