Net-Base Časopis

01.06.2026

Delphi WebSocket klijent: robustno povezivanje, ispravno zaustavljanje, pouzdano otklanjanje grešaka

Jedan Delphi WebSocket klijent je brzo 'nekako povezan' – ali u radu su bitni ponovno uspostavljanje veze, heartbeati, uredno zaustavljanje i mogućnost debugiranja. Sa praktičnim wrapperom baziranim na System.Net.WebSockets (s fallbackom) i isječkom koda za upravljanje nitima i...

01.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

Zašto je Delphi WebSocket Client u praksi više od „Connect“

Jedan Delphi WebSocket Client se u minutama sastavi: URL, Connect, SendText, gotovo. U individualnom poslovnom softveru i softverskim rješenjima bliskim procesima problem se međutim obično pokaže tek u radu: der Reverse Proxy prekida neaktivne veze, mobilne ili VPN veze imaju kratke NAT-Timeouts, certifikati se mijenjaju, i pri gašenju proces zapne jer je Receive-Loop još blokiran. Osim toga: WebSocket je dugotrajan, kanal s održanim stanjem – za njega vrijede drugačija pravila nego za klasični HTTP/REST (Request/Response, kratkotrajan).

U ovom Source-Schnipsel-u ne radi se o „Hello WebSocket“, već o praksi primjenjivom Client-Wrapperu sa:

  • urednim Start/Stop (bez zastoja pri gašenju),
  • Receive-Loop s Cancellation (signal za prekid) umjesto „Thread kill“,
  • Reconnect s Backoff (kontrolisana ponovna veza),
  • Heartbeat kao aplikacijski obrazac (jer Ping/Pong nije svuda dostupan),
  • Debug- i Trace-Hooks koji u slučajevima podrške zaista pomažu.

Realizacija se zasniva na System.Net.WebSockets (Delphi RTL; WebSocket-Client-API s TClientWebSocket). Gdje ovaj RTL-sloj u starijim verzijama nije dostupan ili je previše ograničen, fallback preko biblioteke (npr. ICS) često ima smisla – dolje slijedi kratka Einordnung.

Skica arhitekture: jedan Wrapper umjesto razbacanih WebSocket-poziva

Česta greška u razrađenim Delphi aplikacijama: UI obrasci ili servisni moduli „govore direktno WebSocket“ i tada imaju svuda raspoređene timere, threadove i rukovanje izuzecima. Bolje je imati jasan modul s dobro definiranim Events i malom mašinom stanja.

Pojmovi ukratko: Backoff označava period čekanja koji nakon grešaka postupno raste (npr. 1s, 2s, 4s …), kako se ne bi preopteretio server i mreža. CancellationToken je signal za prekid iz .NET svijeta; u Delphi ne postoji identičan obrazac, ali ga možemo oponašati s TEvent i „StopRequested“-flagom. TThread.Queue planira kod za izvršenje u glavnom threadu (UI), bez blokiranja workera; Synchronize blokira i često je razlog za deadlockove u putovima gašenja.

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

Sljedeći kod je namjerno strukturiran kao „Betriebs-Baustein“: klasa koju možete koristiti u VCL/FMX ili u Windows- i Windows- und Linux-Services (ovisno o verziji/platformi Delphi). Srž je Worker-Thread koji održava Receive-Loop i putem Events prijavljuje poruke u aplikaciju.

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: koristan protiv isteka veze zbog neaktivnosti iza proxyja
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 ne reagira unutar vremenskog ograničenja; mogući blok u mrežnom 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
// Napomena: TClientWebSocket.Connect je sinhron i može blokirati ovisno o DNS/TLS.
// Zato ovo radi u radnoj niti (Worker).
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 kao aplikacijska poruka, jer Ping/Pong nije u svakoj Delphi-verziji pravilno izložen.
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;

// Receive: zasnovano na frame-ovima, zato se koristi StringBuilder za fragmentaciju.
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
// U mnogim poslovnim protokolima tekst/JSON je standard.
// Binarne podatke ovdje možete na sličan način buferirati ili proslijediti direktno.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Kratki sleep da se pri vrlo brzom petljanju uštedi CPU.
// Ne predugo, inače se pogoršava latencija.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Ponovno povezivanje nakon urednog zatvaranja ili nakon grešaka
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.

Šta kod namjerno radi „drugačije“ nego tipični primjeri

  • Stop ohne Gewalt: Umjesto da ubija dretve, Stop postavlja Event. Worker završava petlje na definisanim mjestima. To smanjuje zastoje pri gašenju i izbjegava curenje resursa u socket-stogu.
  • Queue statt Synchronize: Logiranje i eventi idu putem TThread.Queue u Mainthread. To je važno kada Stop/Shutdown dolazi iz UI-a ili iz Service-Control-Handlera. Synchronize može blokirati ako Mainthread trenutno čeka.
  • Fragmentierung berücksichtigt: WebSocket-tekst može stići fragmentiran u frame-ovima. Zato se koristi TStringBuilder i provjerava EndOfMessage.
  • Heartbeat als App-Protokoll: Mnogi sistemi umiru zbog Idle-Timeouta (Load Balancer, nginx, Cloud WAF). Lagan „ping“-tekst kao operativni mehanizam često je efikasniji nego oslanjanje na „TCP keepalive“ ili na Ping/Pong-API koje nije univerzalno dostupno.

Uslovi i zamke u radu

1) DNS, TLS und Proxy: Connect kann blockieren

TClientWebSocket.Connect je sinhron. U zavisnosti od DNS-razrješenja, TLS-handshakea, provjere certifikata ili proxy okoline, to može potrajati nekoliko sekundi. Kod namjerno smješta taj poziv u Workera. Ako trebate strože timeoute, morate na API-nivou provjeriti daje li vaša Delphi-verzija opcije za timeout, ili umotati Connect u poseban thread i prekinuti preko logike procesa. Važno: „abbrechen“ ovdje obično znači „označiti vezu kao pokvarenu i ponovo podići Workera“, ne „trenutno ubiti socket-operaciju“.

2) Idle-Timeouts: warum Heartbeat häufig Pflicht ist

U enterprise mrežama je WebSocket često terminiran iza reverse proxyja (nginx, IIS ARR) ili Load Balancera. Mnogi od tih komponenti zatvaraju konekcije ako za duži period nema prijenosa podataka. TCP-Keepalive nije uvijek konfigurisan dovoljno kratko (i pod Windows često izražen u minutama, a ne sekundama). Zato je Heartbeat na nivou aplikacije robusno rješenje. Vodite računa da server i klijent imaju isti koncept (npr. „ping“/„pong“ kao tekst ili JSON).

3) Threading und UI: Ereignisse müssen entkoppelt bleiben

Ako je OnText-obrada teška (JSON-parsing, DB-pristupi sa BDE-Ablosung mit nativer Anbindung, ažuriranja UI-a), ne bi smjela blockirati Mainthread. Wrapper samo dostavlja poruku. Tipičan obrazac je: OnText stavlja payload u queue (npr. TThreadedQueue<string>), poseban Worker procesuira s backpressureom (tj. ograničena dužina queue-a). To sprječava da pri burst-opterećenju UI zamrzne ili da prijem izađe iz ritma.

Debugging: was Sie loggen sollten, wenn es „manchmal“ abbricht

WebSockets su zloglasni po tome da „rade danima, pa se onda prekidaju“. Bez logovanja je teško suziti problem. Smisleni log-punktovi:

  • Timestamp (UTC), URL i promjene stanja (connecting/open/closed).
  • Close-Reason, ako je dostupan (server traži Close nasuprot mrežnoj grešci).
  • Greške pri slanju Heartbeata i izuzeci pri prijemu, uključujući tip Exception-a.
  • Opcionalno: veličine primljenih poruka (ne sadržaji), da se uoči eksplozija podataka.

Ako terminirate preko TLS-a: dodatno provjerite da li se promjene certifikata (istek, novi issuer) vremenski poklapaju s greškama. U zakučanim okruženjima su i proxy i DPI-boxovi (Deep Packet Inspection) kandidati za uzrok problema.

Varianti: kada System.Net.WebSockets zadovoljava – a kada ne

System.Net.WebSockets je dovoljan za mnoge integracijske slučajeve, posebno kad je riječ o tekstu/JSON, umjerenom opterećenju i jasnim strategijama ponovnog povezivanja. Ograničenja se pojavljuju ovisno o verziji Delphi i ciljnoj platformi:

  • Nedostatak/ograničena podrška za Ping/Pong: U tom slučaju App-Heartbeat ostaje robusni obrazac.
  • Nedostaju Timeouts/Cancellation pri Connect/Receive: Tada morate arhitekturu dizajnirati tako da zaglavljeni worker ostane izoliran, a aplikacija se ipak uredno zatvori (npr. putem proces-watchdoga ili odvojenih worker-instanci).
  • Veliko opterećenje ili binarni streamovi: U tom slučaju se isplati snažniji koncept framinga/buferiranja (npr. ring buffer, odvojeni Binary-Event, Message-Assembler s limitima).

Za legacy-situacije (starije generacije Delphi, vrlo specifični TLS/Proxy zahtjevi) biblioteke poput ICS u nekim projektima djeluju pragmatičnije. Važnije je manje „koja library“, a više da tretirate Shutdown, Reconnect i Observability (Logs/Metriken) kao prioritetne teme.

Zaključak: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen

WebSocket je prikladan za push-događaje, live-status, mašinske ili procesne poruke i kao povratni kanal za portale i servise. Prikazani Wrapper fokusira se na točke koje u digitalnim poslovnim rješenjima često čine razliku: kontrolirani Reconnect, Heartbeat protiv Idle-Timeouts, fragment-otporna obrada teksta i put za zaustavljanje koji se pri deploymentu ili updateu ne zaglavi.

Ograničenja ostaju: ako trebate čvrste garancije za prekid Connect/Receive u vrlo uskim vremenskim prozorima ili postižete ekstremno visoke podatkovne stope, morate dublje ući u timeoute, platformske posebnosti i eventualno alternativne stackove. Za većinu integracijskih i modernizacijskih scenarija, međutim, čisto enkapsulirani, dobro logirani klijent kao gore predstavlja solidnu osnovu koja se može integrirati u postojeće Delphi-sisteme.

Ako želite takav modul uklopiti u postojeću arhitekturu (npr. Layer-3 arhitekturu s jasnim servisnim i UI-slojevima) ili morate debugirati sporadične prekide u realnim uvjetima, možemo to ciljano s vama posložiti: Kontaktirajte nas.

U stručnom okruženju Heartbeat Ping/Pong također igra važnu ulogu kad integracije, tokovi podataka i daljnji razvoj moraju raditi usklađeno.

Razgovarajte o projektu ili modernizacijskom poduhvatu s Net-Base.

Sljedeći korak

Ako se tema pretvori u stvarni projekat, arhitekturu, postojeći sistem i operacije trebalo bi rano zajednički razmotriti.

Pružamo podršku ne samo pri pojedinačnim pitanjima, već i kada iz fragmenata izvornog koda, naslijeđenih sistema ili ideja za portal treba nastati robustan poslovni projekat.

  • Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće se odgađati za kasnije faze.
  • Pravovremeno prepoznajete koji pristup je ekonomski i operativno održiv.

Podijeli objavu

Ovu objavu direktno proslijediti

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novom tabu. Link i kratak tekst se prethodno kopiraju u međuspremnik.