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.
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ť.
Ď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á.