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.