Net-Base Magazín

01.06.2026

Delphi WebSocket klient: robustně připojit, čistě ukončit, spolehlivě ladit

Delphi WebSocket klient je rychle „nějak připojen“ – ale v provozu rozhodují opětovná připojení, heartbeaty, čisté ukončení a možnost ladění. S praxí použitelným wrapperem založeným na System.Net.WebSockets (s fallbackem) a úryvkem zdrojového kódu pro vláknování a...

01.06.2026

Od tématu magazínu k projektové praxi

Vhodné stránky služeb a technické stránky k příspěvku

Proč je v praxi Delphi WebSocket Client více než „Connect“

Silný Delphi WebSocket Client se sice sestaví za minuty: URL, Connect, SendText, hotovo. V individuálním firemním softwaru a v procesně blízkých řešeních se ale problémy typicky projeví až v provozu: reverse proxy ukončí idle spojení, mobilní nebo VPN trasy mají krátké NAT-timeouty, certifikáty se mění a při ukončení se proces zasekne, protože Receive-Loop je stále zablokovaný. Navíc: WebSocket je dlouhodobý, stavem vázaný kanál — platí zde jiná pravidla než u klasického HTTP/REST (Request/Response, krátkodobé).

V tomto úryvku kódu nejde o „Hello WebSocket“, ale o provozně použitelný client-wrapper s:

  • čistým start/stop (bez zaseknutí při ukončování),
  • Receive-Loopem s Cancellation (signál zrušení) místo „Thread kill“,
  • reconnectem s backoff (řízené opětovné připojování),
  • heartbeat jako aplikačním vzorem (protože Ping/Pong není všude dostupné),
  • debug a trace hooky, které v servisních případech skutečně pomáhají.

Realizace vychází z System.Net.WebSockets (Delphi RTL; WebSocket klientské API s TClientWebSocket). Kde tato vrstva RTL ve starších verzích není dostupná nebo je příliš omezená, je často rozumné použít fallback přes knihovnu (např. ICS) — níže je k tomu zařazení.

Náčrt architektury: wrapper místo roztroušených WebSocket volání

Častá chyba v rostoucích Delphi aplikacích: UI formuláře nebo servisní moduly „komunikují přímo přes WebSocket“ a všude se objeví timery, thready a ošetření výjimek. Lepší je jasný komponent s dobře definovanými událostmi a malým stavovým automatem.

Termíny stručně vysvětleny: Backoff znamená čekací dobu, která se po chybách postupně prodlužuje (např. 1s, 2s, 4s …), aby nedošlo k zahlcení serveru a sítě. CancellationToken je signál zrušení z .NET světa; v Delphi neexistuje identický vzor, ale lze jej napodobit pomocí TEvent a příznaku „StopRequested“. TThread.Queue plánuje kód k provedení v hlavním vlákně (UI), aniž by blokoval worker; Synchronize blokuje a často je v ukončovacích cestách příčinou deadlocků.

Ukázka kódu: Delphi WebSocket klient se Stop, Reconnect a Message-Dispatch

Následující kód je záměrně postaven jako „provozní modul“: třída, kterou lze podobně použít ve VCL/FMX nebo v Windows- a Windows- a Linux-Services (podle verze/platformy Delphi). Jádrem je worker-thread, který drží receive-loop a hlásí události do aplikace.

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žitečné proti idle-timeoutům za proxy servery
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 neodpověděl během timeoutu; možný blok v síťovém zásobníku‘);
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 blokovat v závislosti na DNS/TLS.
// Proto to tady běží ve workeru.
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 jako aplikační zpráva, protože Ping/Pong není v každé Delphi-verzi spolehlivě vystaven.
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;

// Přijímání: založeno na framích, proto StringBuilder pro fragmentaci.
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 mnoha podnikových protokolech je standardem text/JSON.
// Binární data lze zde podobně bufferovat nebo je předat přímo.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Krátký sleep, aby se při velmi rychlém cyklu šetřilo CPU.
// Ne příliš velký, jinak se zhorší latence.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Znovupřipojení po čistém uzavření nebo 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.

Co kód záměrně dělá „jinak“ než typické příklady

  • Stop bez násilí: Místo zabíjení vláken nastaví Stop událost. Worker ukončí smyčky na definovaných místech. To snižuje zasekávání při ukončování a zabraňuje únikům prostředků ve vrstvě socketů.
  • Queue místo Synchronize: Protokolování a události jdou přes TThread.Queue do hlavního vlákna. To je důležité, pokud Stop/Shutdown pochází z UI nebo z handlerů Service-Control. Synchronize se může zablokovat, pokud hlavní vlákno právě čeká.
  • Fragmentaci zohledněno: WebSocket-Text může přijít rozdělený do rámců. Proto TStringBuilder a kontrola EndOfMessage.
  • Heartbeat jako aplikační protokol: Mnoho nasazení ztroskotá na Idle-timeoutech (Load Balancer, nginx, Cloud WAF). Lehký textový „ping“ je jako provozní páka často efektivnější než spoléhat na „TCP keepalive“ nebo na Ping/Pong‑API, které není všude dostupné.

Okolní podmínky a úskalí v provozu

1) DNS, TLS und Proxy: Connect může blokovat

TClientWebSocket.Connect je synchronní. Podle DNS‑resoluce, TLS‑handshaku, kontroly certifikátu nebo proxy prostředí to může trvat několik sekund. Kód to záměrně umisťuje do workeru. Pokud navíc potřebujete přísné timeouty, musíte na úrovni API ověřit, zda vaše Delphi‑verze poskytuje možnosti timeoutu, nebo obalit Connect do separátního vlákna a ukončovat ho přes procesní logiku. Důležité: „zrušení“ zde obvykle znamená „označit připojení jako poškozené a znovu spustit worker“, nikoli „okamžitě zabít socketovou operaci“.

2) Idle‑Timeouts: proč je Heartbeat často nutností

V podnikových sítích bývá WebSocket často ukončen za reverzním proxy (nginx, IIS ARR) nebo za load balancerem. Mnoho těchto komponent uzavírá spojení, pokud delší dobu neprotečou žádná data. TCP‑keepalive není vždy nakonfigurován na dostatečně krátký interval (a pod Windows bývá spíše v řádu minut než sekund). Heartbeat na aplikační vrstvě je proto stabilní workaround. Dbejte na to, aby server i klient používaly stejný koncept (např. „ping“/„pong“ jako text nebo JSON).

3) Vlákna a UI: události musí zůstat oddělené

Pokud je zpracování OnText náročné (JSON‑parsování, přístupy do DB s BDE-Ablosung mit nativer Anbindung, aktualizace UI), nemělo by to vše blokovat hlavní vlákno. Wrapper dodává pouze zprávu. Typický vzor je: OnText vloží payload do fronty (např. TThreadedQueue<string>), samostatný worker zpracovává se zpětným tlakem (tj. omezená délka fronty). To zabraňuje, aby se při nárazovém zatížení UI zaseklo nebo aby příjem přestal plynule fungovat.

Debugging: co byste měli logovat, když to „občas“ přestane fungovat

WebSockety jsou proslulé tím, že „běží dny, pak náhle přestanou“. Bez logování je to těžké lokalizovat. Užitené logovací body:

  • Časové razítko (UTC), URL a změny stavu (connecting/open/closed).
  • Close‑Reason, pokud dostupné (server požaduje Close vs. chyba sítě).
  • Chyby odeslání heartbeat a výjimky při přijmu včetně typu výjimky.
  • Volitelně: velikosti přijímaných zpráv (nikoli obsah), abyste odhalili datové exploze.

Pokud terminujete přes TLS: zkontrolujte navíc, zda změny certifikátu (vypršení, nový issuer) časově nekorelují s chybami. V zabezpečených (hardenovaných) prostředích jsou v podezření také proxy a DPI boxy (Deep Packet Inspection).

Varianty: kdy System.Net.WebSockets stačí – a kdy ne

System.Net.WebSockets je pro mnoho integračních případů dostatečný, zvláště pokud jde o text/JSON, střední zátěž a jasné reconnect strategie. Hranice se projeví v závislosti na verzi Delphi a cílové platformě:

  • Chybějící/omezená podpora Ping/Pong: V takovém případě zůstává App-Heartbeat robustním vzorem.
  • Chybějící time‑outy/možnost cancellation při Connect/Receive: Pak musíte architekturu navrhnout tak, aby visící worker zůstal izolovaný a aplikace se přesto čistě ukončila (např. pomocí procesního watchdogu nebo oddělených instancí workeru).
  • Vysoká zátěž nebo binární streamy: V tom případě se vyplatí silnější framing/buffering koncept (např. ring buffer, separátní Binary-Event, Message-Assembler s limity).

Pro legacy situace (starší generace Delphi, velmi specifické TLS/Proxy požadavky) jsou knihovny jako ICS v některých projektech pragmatičtější. Důležitější než „která knihovna“ je, že k tématům Shutdown, Reconnect a Observability (logs/metriky) přistupujete jako k primárním požadavkům.

Závěr: Delphi WebSocket klient je provozní stavební prvek – s jasnými omezeními

WebSocket se výborně hodí pro push události, živý stav, strojní nebo procesní hlášení a jako zpětný kanál pro portály a služby. Ukázaný wrapper se soustředí na body, které v digitálních podnikových řešeních často dělají rozdíl: kontrolovaný reconnect, heartbeat proti idle-timeoutům, zpracování textu odolné vůči fragmentaci a cesta ukončení, která se při nasazení nebo aktualizaci nezasekne.

Hranice použití zůstávají: pokud potřebujete tvrdé záruky pro přerušení Connect/Receive v velmi úzkých časových oknech nebo provozujete extrémně vysoké datové toky, musíte hlouběji řešit time‑outy, specifika platformy a případně alternativní stacky. Pro většinu integračních a modernizačních scénářů je však čistě enkapsulovaný, dobře logovaný klient, jako výše, solidní základ, který lze integrovat do existujících Delphi systémů.

Pokud chcete takový stavební prvek zapracovat do stávající architektury (např. Layer-3 architektura s jasnými vrstvami služeb a UI) nebo potřebujete debugovat sporadická odpojení za reálných podmínek, můžeme to s vámi cíleně posoudit: Kontaktujte nás.

V odborném kontextu hraje také důležitou roli Heartbeat Ping/Pong, pokud mají integrace, datové toky a další vývoj spolu hladce fungovat.

Projekt nebo modernizační záměr probrat s Net-Base.

Další krok

Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz včas posuzovány společně.

Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.

  • Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
  • REST, přístup k datům, portály a nasazení nebudou odkládány na později.
  • Vidíte včas, která cesta je ekonomicky i provozně životaschopná.

Sdílet příspěvek

Sdílet tento příspěvek přímo

LinkedIn, X, XING, Facebook, WhatsApp a e-mail jsou ihned k dispozici. Pro Instagram připravíme odkaz a stručný text.

E-mail

Instagram se otevře v nové záložce. Odkaz a krátký text budou předtím zkopírovány do schránky.