Net-Base Časopis

01.06.2026

Delphi WebSocket klijent: robustno povezivanje, uredno zaustavljanje, pouzdano otklanjanje pogrešaka

Jedan Delphi WebSocket klijent brzo je 'nekako povezan' — ali u radu su presudni Reconnect, Heartbeats, uredno zaustavljanje i mogućnost debugiranja. S praktičnim wrapperom temeljenim na System.Net.WebSockets (s fallbackom) i isječkom izvornog koda za Threading i...

01.06.2026

Od teme magazina do projektne prakse

Povezane stranice usluga i tehnologije za članak

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

Ein Delphi WebSocket Client sastavljen je za nekoliko minuta: URL, Connect, SendText, gotovo. U individualnom poslovnom softveru i softverskim rješenjima bliskim procesu problem se obično pojavi tek u radu: Der Reverse Proxy prekida idle-veze, mobilne ili VPN-rute imaju kratke NAT-Timeouts, certifikati se mijenjaju, i pri gašenju proces zapne jer je ein Receive-Loop još blokiran. Osim toga: WebSocket je dugotrajan, stanje-držeći kanal – prema tome vrijede druga pravila nego kod klasičnog HTTP/REST (Request/Response, kratkotrajan).

U ovom Source-Schnipsel-u ne radi se o „Hello WebSocket“, nego o praktičnom Client-Wrapperu sa:

  • urednim pokretanjem/zaustavljanjem (bez zastoja pri gašenju),
  • Receive-Loop s Cancellation (signal za prekid) umjesto „Thread kill“,
  • Reconnect s Backoffom (kontrolirano ponovno povezivanje),
  • Heartbeat kao obrazac primjene (jer Ping/Pong nije svugdje dostupan),
  • Debug- i Trace-Hookovi koji u slučajevima podrške zaista pomažu.

Implementacija se temelji na System.Net.WebSockets (Delphi RTL; WebSocket-Client-API s TClientWebSocket). Gdje taj RTL-sloj u starijim verzijama nije dostupan ili je previše ograničen, fallback preko biblioteke (npr. ICS) često je smislen – o tome niže slijedi razrada.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Česta pogreška u razrađenim Delphi-aplikacijama: UI-obrasci ili servisni moduli „sprechen direkt WebSocket“ i imaju raspoređene timere, threadove i obradu iznimki posvuda. Bolje je imati jasnu komponentu s dobro definiranim eventima i malim automatom stanja.

Pojmovi ukratko: Backoff znači vrijeme čekanja koje nakon pogreš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 u shutdown-patovima razlog za deadlockove.

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

Sljedeći kod je namjerno strukturiran kao „Betriebs-Baustein“: klasa koju se može koristiti u VCL/FMX ili u einem Windows- und Windows- und Linux-Services (ovisno o Delphi-verziji/platformi) na sličan način. Jezgra je worker-thread koji održava Receive-Loop i preko eventa prijavljuje 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 Idle-timeoutova 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 u zadanom vremenu; moguća blokada 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 sinkron i može blokirati ovisno o DNS/TLS.
// Zato se ovo izvršava u Worker niti.
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: bazirano na frameovima, zato 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 poruke se ovdje mogu na sličan način međuspremiti ili proslijediti izravno.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Kratko spavanje kako bi se pri vrlo brzom loopu smanjila potrošnja CPU-a.
// 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 pogreš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.

Was der Code absichtlich „anders“ macht als typische Beispiele

  • Stop ohne Gewalt: Statt Threads abzuschießen, setzt Stop ein Event. Der Worker beendet Loops an definierten Stellen. Das reduziert Hänger beim Beenden und vermeidet Ressourcenlecks im Socket-Stack.
  • Queue statt Synchronize: Logging und Events gehen per TThread.Queue in den Mainthread. Das ist wichtig, wenn Stop/Shutdown aus dem UI oder aus Service-Control-Handlern kommt. Synchronize kann blockieren, wenn der Mainthread gerade wartet.
  • Fragmentierung berücksichtigt: WebSocket-Text kann in Frames fragmentiert kommen. Deshalb der TStringBuilder und das Prüfen von EndOfMessage.
  • Heartbeat als App-Protokoll: Viele Setups sterben an Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Ein leichtgewichtiger „ping“-Text ist als Betriebshebel oft effektiver als die Hoffnung auf „TCP keepalive“ oder ein nicht überall verfügbares Ping/Pong-API.

Randbedingungen und Stolperfallen im Betrieb

1) DNS, TLS und Proxy: Connect kann blockieren

TClientWebSocket.Connect ist synchron. Je nach DNS-Auflösung, TLS-Handshake, Zertifikatsprüfung oder Proxy-Umgebung kann das mehrere Sekunden dauern. Der Code legt das bewusst in einen Worker. Wenn Sie zusätzlich harte Timeouts brauchen, müssen Sie auf API-Ebene prüfen, ob Ihre Delphi-Version Timeout-Optionen bereitstellt, oder Sie kapseln Connect in einen separaten Thread und brechen über Prozesslogik ab. Wichtig: Ein „abbrechen“ heißt hier meist „Verbindung als kaputt markieren und Worker neu aufsetzen“, nicht „Socket-Operation sofort killen“.

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

In Unternehmensnetzwerken ist ein WebSocket oft hinter einem Reverse Proxy (nginx, IIS ARR) oder einem Load Balancer terminiert. Viele dieser Komponenten schließen Verbindungen, wenn über längere Zeit keine Daten fließen. TCP-Keepalive ist nicht immer kurz genug konfiguriert (und unter Windows oft eher Minuten als Sekunden). Ein Heartbeat auf Anwendungsebene ist deshalb ein stabiler Workaround. Achten Sie darauf, dass Server und Client das gleiche Konzept haben (z. B. „ping“/„pong“ als Text oder JSON).

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

Wenn die OnText-Verarbeitung schwer ist (JSON-Parsing, DB-Zugriffe mit BDE-Ablosung mit nativer Anbindung, UI-Updates), sollte sie nicht im Mainthread alles blockieren. Der Wrapper liefert nur die Nachricht. Ein typisches Muster ist: OnText legt die Payload in eine Queue (z. B. TThreadedQueue<string>), ein separater Worker verarbeitet mit Backpressure (also begrenzter Queue-Länge). Das verhindert, dass bei Burst-Last die UI einfriert oder der Empfang aus dem Tritt kommt.

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

WebSockets sind berüchtigt für „läuft tagelang, dann nicht mehr“. Ohne Logging ist das kaum einzugrenzen. Sinnvolle Logpunkte:

  • Zeitstempel (UTC), URL, und Zustandswechsel (connecting/open/closed).
  • Close-Reason, sofern verfügbar (Server fordert Close vs. Netzwerkfehler).
  • Heartbeat-Sendefehler und Receive-Ausnahmen inkl. Exception-Typ.
  • Optional: Größen der empfangenen Nachrichten (nicht die Inhalte), um Datenexplosion zu erkennen.

Wenn Sie über TLS terminieren: Prüfen Sie zusätzlich, ob Zertifikatswechsel (Ablauf, neuer Issuer) zeitlich mit Fehlern korrelieren. In gehärteten Umgebungen sind auch Proxy- und DPI-Boxen (Deep Packet Inspection) Kandidaten.

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

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

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

Za legacy-situacije (starije Delphi-generacije, vrlo specifični TLS/Proxy-zahtjevi) biblioteke poput ICS-a u nekim projektima znače pragmatičniji pristup. Važnije je manje „koja biblioteka“, a više da tretirate Shutdown, Reconnect i Observability (logove/metrike) kao teme prve klase.

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

WebSocket je vrlo prikladan za push-događaje, live-status, strojna ili procesna izvješća te 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-timeouta, fragmentno-sigurna obrada teksta i put za zaustavljanje koji se pri deploymentu ili updateu ne zablokira.

Ograničenja ostaju: ako trebate stroge garancije za prekid Connect/Receive-a u vrlo kratkim vremenskim okvirima ili imate ekstremno visoke brzine prijenosa podataka, morate se dublje pozabaviti timeoutima, specifičnostima platforme i eventualno alternativnim stackovima. Za većinu scenarija integracije i modernizacije, međutim, čisto enkapsulirani, dobro logirani klijent kao gore naveden predstavlja solidnu osnovu koja se može integrirati u postojeće Delphi-sustave.

Ako želite uklopiti takav element u postojeću arhitekturu (npr. Layer-3 arhitektura s jasnim servisnim i UI-slojima) ili debugirati sporadične disconnect-e u realnim uvjetima, možemo to ciljano razmotriti s vama: Kontaktirajte nas.

U stručnom kontekstu Heartbeat Ping/Pong također igra važnu ulogu kad integracije, tokovi podataka i daljnji razvoj moraju besprijekorno surađivati.

Razgovarajte o projektu ili modernizacijskom pothvatu s Net-Base.

Sljedeći korak

Kad se tema pretvori u stvarni projekt, arhitektura, postojeći sustav i operativni rad trebaju se rano sagledati zajedno.

Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.

  • Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
  • REST, pristup podacima, portali i Rollout neće biti odgođeni kao kasne posljedice.
  • Vidite rano koji je put ekonomski i operativno održiv.

Podijeli objavu

Izravno proslijedite ovu objavu

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

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.