Fra magasinets tema til projektpraksis
Passende service- og tekniske sider til artiklen
Hvorfor en Delphi WebSocket-klient i praksis er mere end „Connect“
En Delphi WebSocket Client kan samles på få minutter: URL, Connect, SendText, færdig. I individuel virksomhedssoftware og procesnære softwareløsninger opstår problemet dog som regel først i drift: Reverse-proxyen afbryder inaktive forbindelser, mobile- eller VPN-forbindelser har korte NAT-timeouts, certifikater udskiftes, og ved nedlukning hænger processen, fordi en Receive-Loop stadig er blokeret. Derudover: En WebSocket er en langvarig, tilstandsfuld kanal – derfor gælder andre regler end ved klassisk HTTP/REST (Request/Response, kortlivet).
I dette kildekodeudsnit handler det ikke om „Hello WebSocket“, men om en praktisk client-wrapper med:
- ordnet start/stop (uden fastlåsning ved nedlukning),
- Receive-Loop med Cancellation (afbrudssignal) i stedet for „Thread kill“,
- Reconnect med Backoff (kontrolleret genforbindelse),
- Heartbeat som anvendelsesmønster (fordi Ping/Pong ikke altid er tilgængeligt),
- Debug- og Trace-Hooks, der reelt hjælper i supporttilfælde.
Implementeringen er baseret på System.Net.WebSockets (Delphi RTL; WebSocket-klient-API med TClientWebSocket). Hvor dette RTL-lag i ældre versioner ikke er tilgængeligt eller er for begrænset, er et fallback via et bibliotek (f.eks. ICS) ofte fornuftigt – nedenfor en vurdering.
Arkitektur-skitse: en Wrapper i stedet for spredte WebSocket-opkald
En hyppig fejl i modne Delphi-applikationer: UI-formularer eller service-moduler „taler direkte med WebSocket“ og har derefter timere, tråde og undtagelseshåndtering spredt overalt. Bedre er en tydelig komponent med veldefinerede events og en lille tilstandsmaskine.
Begreber kort placeret: Backoff betyder en ventetid, der trinvis øges efter fejl (f.eks. 1s, 2s, 4s …) for ikke at overbelaste server og netværk. CancellationToken er et afbrudssignal fra .NET-verdenen; i Delphi findes der ikke et identisk mønster, men vi kan efterligne det med TEvent og et „StopRequested“-flag. TThread.Queue planlægger kode til udførelse i hovedtråden (UI) uden at blokere workeren; Synchronize blokerer og er i shutdown-stier ofte årsagen til deadlocks.
Kildeudsnit: Delphi WebSocket Client med Stop, Reconnect und Message-Dispatch
Følgende kode er bevidst opbygget som en „drifts-komponent“: en klasse, som man kan bruge på lignende vis i VCL/FMX eller i en Windows- og Windows- og Linux-Services (afhængig af Delphi-version/platform). Kernen er en worker-thread, der holder Receive-Loop’en og rapporterer til applikationen via events.
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: nyttigt mod idle-timeouts bag proxyer
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 reagerer ikke inden for timeout; mulig blokering i netværksstakken‘);
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
// Bemærk: TClientWebSocket.Connect er synkron og kan blokere afhængigt af DNS/TLS.
// Derfor køres dette i worker-tråden.
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 som applikationsbesked, fordi Ping/Pong ikke er ordentligt eksponeret i alle Delphi-versioner.
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: Rammebaseret, derfor StringBuilder til håndtering af fragmentering.
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
// I mange forretningsprotokoller er tekst/JSON standard.
// Binary kan her bufferes tilsvarende eller videresendes direkte.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, ‚Server requested close‘);
Break;
end;
end;
// Mini-sleep for at skåne CPU’en ved meget hurtige loops.
// Ikke for stor, ellers forringes latenstiden.
TThread.Sleep(1);
end;
finally
SB.Free;
end;
SafeClose;
State(‚closed‘);
finally
WS.Free;
end;
if StopRequested then
Break;
// Genforbindelse efter ordentlig lukning eller efter fejl
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.
Hvad koden bevidst gør „anderledes“ end typiske eksempler
- Stop uden tvang: I stedet for at dræbe tråde sætter Stop et event. Workeren afslutter loops på definerede steder. Det reducerer hængere ved afslutning og undgår ressourcelæk i socket-stakken.
- Queue i stedet for Synchronize: Logging og events sendes via TThread.Queue til maintråden. Det er vigtigt, når Stop/Shutdown kommer fra UI eller fra Service-Control-handlers. Synchronize kan blokere, hvis maintråden lige nu venter.
- Fragmentering håndteres: WebSocket-tekst kan komme fragmenteret i frames. Derfor bruges TStringBuilder og kontrol af EndOfMessage.
- Heartbeat som applikationsprotokol: Mange opsætninger dør på idle-timeouts (Load Balancer, nginx, Cloud WAF). En letvægts „ping“-tekst fungerer ofte bedre som driftmekanisme end at håbe på „TCP keepalive“ eller et ikke-universelt Ping/Pong-API.
Randbetingelser og faldgruber i drift
1) DNS, TLS og Proxy: Connect kan blokere
TClientWebSocket.Connect er synkron. Afhængigt af DNS-opløsning, TLS-handshake, certifikatvalidering eller proxy-miljø kan det tage flere sekunder. Koden placerer det bevidst i en worker. Hvis I har brug for hårde timeouts, skal I på API-niveau tjekke, om jeres Delphi-version tilbyder timeout-optioner, eller I kapsler Connect i en separat tråd og afbryder via proceslogik. Vigtigt: Et „afbryd“ betyder her typisk „markér forbindelsen som defekt og genstart workeren“, ikke „kill socket-operationen øjeblikkeligt“.
2) Idle-timeouts: hvorfor Heartbeat ofte er påkrævet
I erhvervsnetværk termineres en WebSocket ofte bag en reverse proxy (nginx, IIS ARR) eller en load balancer. Mange af disse komponenter lukker forbindelser, når der ikke flyder data i længere tid. TCP-Keepalive er ikke altid konfigureret til korte intervaller (og under Windows er det ofte minutter fremfor sekunder). Et heartbeat på applikationsniveau er derfor et robust workaround. Sørg for, at server og client har samme koncept (fx „ping“/“pong“ som tekst eller JSON).
3) Threading og UI: Begivenheder skal være løst koblet
Hvis OnText-behandlingen er tung (JSON-parsing, DB-adgang med BDE-erstatning med native tilslutning, UI-opdateringer), bør den ikke blokere maintråden fuldstændigt. Wrapperen leverer kun beskeden. Et typisk mønster er: OnText lægger payload i en kø (fx TThreadedQueue<string>), en separat worker behandler med Backpressure (dvs. begrænset kølængde). Det forhindrer, at UI fryser eller modtagelsen kommer ud af takt ved burst-load.
Debugging: hvad I bør logge, når det „nogle gange“ afbryder
WebSockets er berygtede for „kører i dagevis, så stopper de«. Uden logging er det svært at indkredse. Relevante logpunkter:
- Tidsstempel (UTC), URL og tilstandsskift (connecting/open/closed).
- Lukningsårsag, hvis tilgængelig (server initierer Close vs. netværksfejl).
- Heartbeat-sendefejl og modtage-undtagelser inklusive undtagelsestype.
- Valgfrit: størrelser på modtagne beskeder (ikke indholdet), for at opdage dataeksplosioner.
Hvis I terminerer ved TLS: Kontroller desuden, om certifikatændringer (udløb, ny Issuer) tidsmæssigt korrelerer med fejlene. I hårdt sikrede miljøer er også proxy- og DPI-boxe (Deep Packet Inspection) kandidater.
Varianter: hvornår System.Net.WebSockets er tilstrækkeligt – og hvornår ikke
System.Net.WebSockets er tilstrækkeligt i mange integrationsscenarier, især når det drejer sig om tekst/JSON, moderat belastning og klare genopkoblingsstrategier. Begrænsninger afhænger af Delphi-version og platformsmål:
- Manglende/begrænset Ping/Pong-understøttelse: Så forbliver applikations-heartbeat det robuste mønster.
- Manglende timeouts/cancellation i Connect/Receive: Så skal I designe arkitekturen, så en hængende worker isoleres, og applikationen alligevel kan lukkes ordentligt (f.eks. via proces-watchdog eller separate worker-instanser).
- Høj belastning eller binære streams: Så er et stærkere framing/buffering-koncept relevant (f.eks. ring buffer, separat Binary-Event, message-assembler med grænser).
For legacy-situationer (ældre Delphi-generationer, meget specifikke TLS/Proxy-krav) er biblioteker som ICS i nogle projekter mere pragmatiske. Væsentligere er ikke „hvilket bibliotek“, men at I håndterer Shutdown, Reconnect og Observability (Logs/Metrikker) som førsteklasses emner.
Konklusion: en Delphi WebSocket-klient er en driftskomponent – med klare begrænsninger
En WebSocket er velegnet til push-events, live-status, maskin- eller procesmeddelelser og som returkanal for portaler og services. Den viste wrapper fokuserer på de punkter, der ofte gør forskellen i digitale virksomhedsløsninger: kontrolleret Reconnect, Heartbeat mod Idle-Timeouts, fragmentsikker tekstbehandling og en stopsti, der ikke hænger ved deployment eller update.
Begrænsninger består: Hvis I har brug for hårde garantier for afbrydelse af Connect/Receive inden for meget snævre tidsvinduer, eller hvis I kører ekstremt høje datahastigheder, må I gå dybere ned i timeouts, platformspecifikke forhold og eventuelt alternative stacks. For størstedelen af integrations- og moderniseringsscenarier er en velindkapslet, godt logget klient som ovenfor dog et solidt grundlag, der kan integreres i eksisterende Delphi-systemer.
Hvis I skal indpasse en sådan komponent i en eksisterende arkitektur (f.eks. Layer-3 Architektur med klare service- og UI-lag) eller debugge sporadiske disconnects under realforhold, kan vi hjælpe med at vurdere det målrettet: Kontakt os.
I faglige sammenhænge spiller også Heartbeat Ping/Pong en væsentlig rolle, når integrationer, datastrømme og videreudvikling skal spille sammen på en ordentlig måde.
Næste trin
Når et emne bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.
Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.
- Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
- REST, dataadgang, portaler og idrulning bliver ikke udskudt som eftertanker.
- I ser tidligt, hvilken vej der er økonomisk og driftsmæssigt holdbar.