Net-Base Revija

01.06.2026

Delphi WebSocket odjemalec: robustno vzpostavljanje povezave, urejeno zaustavljanje, zanesljivo odpravljanje napak

Delphi WebSocket odjemalec je hitro „nekako povezan“ – vendar v obratovanju šteje ponovno povezovanje, heartbeat-i, urejeno zaustavljanje in možnost razhroščevanja. S praktičnim ovojem na osnovi System.Net.WebSockets (z fallbackom) in izrezkom izvorne kode za upravljanje niti in...

01.06.2026

Od teme v reviji do projektne prakse

Ustrezne strani storitev in tehnični opisi k prispevku

Zakaj je Delphi WebSocket Client v praksi več kot „Connect“

Ein Delphi WebSocket Client je v nekaj minutah sestavljen: URL, Connect, SendText, končano. V individualni poslovni programski opremi in procesno bližjih rešitvah pa se težave večinoma pokažejo šele v obratovanju: Reverse Proxy prekinja neaktivne povezave, mobilne ali VPN povezave imajo kratke NAT-Timeoute, certifikati se zamenjajo, in ob zaustavitvi se proces zatakne, ker je Receive-Loop še vedno blokiran. Poleg tega je WebSocket dolgotrajen, z državo opremljen kanal – zato veljajo drugačna pravila kot pri klasičnem HTTP/REST (Request/Response, kratkotrajno).

V tem izseku kode ne gre za „Hello WebSocket“, temveč za praksi primeren odjemalski ovitek z:

  • čistim zagon/ustavitev (brez zatikov pri shutdown),
  • Receive-Loop z Cancellation (signal za prekinitev) namesto „Thread kill“,
  • ponovno povezavo z Backoff (kontrolirana ponovna vzpostavitev),
  • heartbeat kot vzorec v aplikaciji (ker Ping/Pong ni povsod na voljo),
  • Debug- in Trace-Hooks, ki pri podpornih primerih dejansko pomagajo.

Implementacija temelji na System.Net.WebSockets (Delphi RTL; WebSocket-Client-API z TClientWebSocket). Kjer ta RTL-sloj v starejših različicah ni na voljo ali je preveč omejen, je pogosto smiselno uporabiti fallback preko knjižnice (npr. ICS) – spodaj je pripisana kratka ocena.

Arhitekturni oris: ovitek namesto razpršenih WebSocket-klicev

Pogosta napaka v zrelih Delphi-aplikacijah: UI-obrazci ali servisni moduli „govorijo neposredno z WebSocket“ in imajo potem po vsej kodi razpršene timere, niti in obdelavo izjem. Bolje je enoten gradnik z jasno določenimi dogodki in majhnim avtomatom stanj.

Pojmi na kratko: Backoff pomeni čas čakanja, ki se po napakah postopoma povečuje (npr. 1s, 2s, 4s …), da se strežnik in omrežje ne preplavita. CancellationToken je signal za prekinitev iz sveta .NET; v Delphi ni popolnoma enakega vzorca, vendar ga lahko reproduciramo z TEvent in zastavico „StopRequested“. TThread.Queue načrtuje kodo za izvajanje v glavnem niti (UI), ne da bi blokiral worker; Synchronize blokira in je v shutdown-poteh pogosto razlog za deadlocke.

Izsek kode: Delphi WebSocket Client z Stop, Reconnect in Message-Dispatch

Naslednja koda je namenoma zasnovana kot „operativni gradnik“: razred, ki ga lahko podobno uporabite v VCL/FMX ali v Windows- in Windows- in Linux-servisih (odvisno od različice/ platforme Delphi). Jedro je worker-nit, ki vzdržuje Receive-Loop in preko dogodkov poroča aplikaciji.

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; // Aplikacijski heartbeat: uporaben proti prekinitvam zaradi idle-timeoutov za proxyje
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 ni odgovoril znotraj časovne omejitve; možna blokada v omrežnem 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
// Opozorilo: TClientWebSocket.Connect je sinhron in se lahko blokira zaradi DNS/TLS.
// Zato se to izvaja znotraj Workerja.
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 kot aplikacijsko sporočilo, ker Ping/Pong ni v vsaki Delphi-verziji pravilno izpostavljen.
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;

// Prejem: temelji na okvirjih (frame), zato StringBuilder za fragmentacijo.
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 mnogih poslovnih protokolih je standard tekst/JSON.
// Binarne podatke je mogoče tukaj podobno predpomniti ali jih posredovati neposredno.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Kratek premor (mini-sleep), da pri zelo hitrem zanki varčujemo z CPU.
// Ne predolgo, sicer se poslabša zakasnitev (latenca).
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Reconnect nach sauberem Close oder nach Fehlern
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.

Kaj koda namerno „drugače“ počne kot tipični primeri

  • Stop brez nasilja: Namesto prekinjanja nitk, Stop sproži dogodek. Worker zaključi zanke na določenih mestih. To zmanjša zatikanje pri zaustavljanju in prepreči uhajanje virov v sloju socketov.
  • Queue namesto Synchronize: Beleženje in dogodki gredo preko TThread.Queue v Mainthread. To je pomembno, kadar Stop/Shutdown prihajata iz UI ali iz Service-Control-handlerjev. Synchronize se lahko blokira, če Mainthread trenutno čaka.
  • Upoštevanje fragmentacije: WebSocket-tekst se lahko pojavi fragmentiran v okvirih. Zato TStringBuilder in preverjanje EndOfMessage.
  • Heartbeat kot protokol aplikacije: Veliko nastavitev odpove zaradi idle-timeoutov (Load Balancer, nginx, Cloud WAF). Lahkoten tekst „ping“ je kot operativni ukaz pogosto učinkovitejši od upanja na „TCP keepalive“ ali na Ping/Pong-API, ki ni na voljo povsod.

Pogoji in pasti v obratovanju

1) DNS, TLS und Proxy: Connect se lahko blokira

TClientWebSocket.Connect je sinhron. Odvisno od DNS-resolucije, TLS-handshake, preverjanja certifikatov ali proxy-okolja lahko to traja več sekund. Koda to zavestno izvaja v Workerju. Če potrebujete dodatne stroge timeoute, morate na ravni API preveriti, ali vaša Delphi-verzija ponuja možnosti time-outov, ali pa zapakirate Connect v ločeno nit in ga prek procesne logike prekinete. Pomembno: „preklic“ tukaj običajno pomeni „označiti povezavo kot pokvarjeno in znova zagnati Worker“, ne „takojšnje uničenje socket-operacije“.

2) Idle-timeouti: zakaj je Heartbeat pogosto obvezen

V podjetniških omrežjih je WebSocket pogosto terminiran za reverse proxyjem (nginx, IIS ARR) ali za Load Balancerjem. Veliko teh komponent zapre povezave, če dalj časa ne teče noben promet. TCP-Keepalive ni vedno nastavljen na dovolj kratek interval (in pod Windows gre pogosto za minute namesto sekund). Zato je Heartbeat na ravni aplikacije zanesljiva rešitev. Poskrbite, da imata strežnik in klient enako zasnovo (npr. „ping“/„pong“ kot tekst ali JSON).

3) Upravljanje niti in UI: dogodki morajo ostati ločeni

Če je obdelava OnText zahtevna (JSON-parsing, dostopi do DB z BDE-zamenjava z nativno povezavo, posodobitve UI), naj ne blokira vsega v Mainthreadu. Wrapper dostavi samo sporočilo. Tipičen vzorec je: OnText postavi payload v vrsto (npr. TThreadedQueue<string>), ločen Worker obdeluje z backpressureom (torej omejena dolžina vrste). To prepreči, da bi ob suneku prometa UI zamrznil ali da bi sprejem izgubil ritem.

Razhroščevanje: kaj logirati, če se občasno prekine

WebSocketi so znani po tem, da „delajo dneve, nato prenehajo“. Brez logiranja je težko zožiti vzrok. Smiselne točke za logiranje:

  • Časovni žig (UTC), URL in spremembe stanja (connecting/open/closed).
  • Razlog zaprtja, če je na voljo (strežnik zahteva Close vs. omrežna napaka).
  • Napake pri pošiljanju Heartbeata in izjeme pri sprejemu vključno s tipom Exception.
  • Opcijsko: velikosti prejetih sporočil (ne vsebina), da prepoznate eksplozijo podatkov.

Če terminirate preko TLS: dodatno preverite, ali se menjave certifikatov (potek, nov izdajatelj) časovno ujemajo z napakami. V zaostrenih okoljih so tudi proxy in DPI-naprave (Deep Packet Inspection) potencialni vzrok.

Varianten: wann System.Net.WebSockets reicht – und wann nicht

System.Net.WebSockets zadostuje za veliko integracijskih primerov, predvsem kadar gre za besedilo/JSON, zmerno obremenitev in jasne strategije ponovne vzpostavitve povezave. Meje se pokažejo glede na različico Delphi in ciljno platformo:

  • Odsotna/omejena podpora za Ping/Pong: V takih primerih ostaja App-Heartbeat robusten vzorec.
  • Odsotnost Timeouts/Cancellation pri Connect/Receive: V tem primeru morate arhitekturo načrtovati tako, da je zamrznjen Worker izoliran in se aplikacija vseeno čisto zaključi (npr. s procesnim watchdogom ali ločenimi instancami Workerjev).
  • Visoka obremenitev ali binarni tokovi: V tem primeru se izplača močnejši koncept framiranja/bufferinga (npr. ring buffer, ločen Binary-Event, Message-Assembler z omejitvami).

Za legacy-situacije (starejše Delphi-generacije, zelo specifične TLS/Proxy-zahteve) so knjižnice, kot je ICS, v nekaterih projektih pragmatičnejše. Pomembneje ni toliko „katera Library“, temveč da obravnavate Shutdown, Reconnect in Observability (Logi/Metrike) kot temeljne teme.

Fazit: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen

WebSocket je zelo primeren za Push-Events, Live-Status, strojna ali procesna sporočila in kot povratni kanal za portale in storitve. Prikazani Wrapper se osredotoča na točke, ki v digitalnih rešitvah podjetij pogosto naredijo razliko: kontrolirana Reconnect, Heartbeat proti Idle-Timeouts, fragment-sigurna obdelava besedila in pot za Stop, ki se pri Deploymentu ali Updateu ne zatakne.

Omejitve uporabe ostajajo: če potrebujete stroge garancije za prekinitev Connect/Receive v zelo kratkih časovnih oknih ali imate izjemno visoke podatkovne hitrosti, morate poglobljeno obravnavati Timeouts, platformne posebnosti in po potrebi alternativne stoge. Za večino scenarijev integracije in modernizacije pa je čisto kapsuliran, dobro logiran Client, kot zgoraj, stabilna osnova, ki jo je mogoče integrirati v obstoječe Delphi-sisteme.

Če želite takšen gradnik vključiti v obstoječo arhitekturo (npr. Layer-3 Architektur z jasnimi plastmi storitev in UI) ali morate pri občasnih Disconnects v realnih pogojih izvajati razhroščevanje, vam lahko to ciljno pomagamo ovrednotiti: Kontakt aufnehmen.

V strokovnem okolju igrajo tudi Heartbeat Ping/Pong pomembno vlogo, kadar morajo integracije, podatkovni tokovi in nadaljnji razvoj delovati usklajeno.

Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.

Naslednji korak

Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.

Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.

  • Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
  • REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
  • Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.

Deli objavo

Deli ta prispevek neposredno

LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram bomo neposredno pripravili povezavo in kratek opis.

E-pošta

Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.