Net-Base Magasin

01.06.2026

Delphi WebSocket-klient: robust koble til, ryddig stoppe, pålitelig feilsøke

En Delphi WebSocket-klient er raskt «på en måte tilkoblet» – men i drift er det reconnect, heartbeats, ryddig nedstengning og feilsøkbarhet som teller. Med en praktisk wrapper basert på System.Net.WebSockets (med fallback) og et kildekodeeksempel for trådbehandling og...

01.06.2026

Fra magasinetema til prosjektpraksis

Egnede tjeneste- og tekniske sider for innlegget

Hvorfor en Delphi WebSocket-klient i praksis er mer enn „Connect“

En Delphi WebSocket Client kan settes sammen på minutter: URL, Connect, SendText, ferdig. I skreddersydd bedriftsprogramvare og proksime prosessnære løsninger oppstår problemene som regel først i drift: Reverse Proxy bryter idle-tilkoblinger, mobile- eller VPN-lenker har korte NAT-timeouts, sertifikater skiftes, og ved avslutning henger prosessen fordi en receive-loop fortsatt blokkerer. I tillegg: En WebSocket er en langvarig, tilstandsbevarende kanal – derfor gjelder andre regler enn for klassisk HTTP/REST (Request/Response, kortlivet).

I dette kildekodeeksemplet handler det ikke om „Hello WebSocket“, men om en praksisnær klient-wrapper med:

  • ryddig start/stop (uten heng ved nedstengning),
  • Receive-Loop med Cancellation (avbruddssignal) i stedet for „Thread kill“,
  • Reconnect med Backoff (kontrollert gjenoppretting av tilkobling),
  • Heartbeat som applikasjonsmønster (fordi Ping/Pong ikke er tilgjengelig overalt),
  • Debug- og Trace-Hooks som faktisk hjelper i supporttilfeller.

Implementasjonen er basert på System.Net.WebSockets (Delphi RTL; WebSocket-klient-API med TClientWebSocket). Hvor dette RTL-laget i eldre versjoner ikke er tilgjengelig eller er for begrenset, er en fallback via et bibliotek (f.eks. ICS) ofte fornuftig – nedenfor følger en avklaring.

Arkitekturskisse: en Wrapper i stedet for spredte WebSocket-kall

En vanlig feil i modne Delphi-applikasjoner: UI-skjemaer eller servicemoduler „snakker direkte med WebSocket“ og ender opp med timere, tråder og unntakshåndtering spredt overalt. Bedre er en tydelig komponent med veldefinerte events og en liten tilstandsmaskin.

Begreper kort forklart: Backoff betyr en ventetid som øker trinnvis etter feil (f.eks. 1s, 2s, 4s …) for å unngå å overbelaste server eller nettverk. CancellationToken er et avbruddssignal fra .NET-verdenen; i Delphi finnes ikke et identisk mønster, men vi kan etterligne det med TEvent og et „StopRequested“-flagg. TThread.Queue planlegger kode for kjøring i hovedtråden (UI) uten å blokkere workeren; Synchronize blokkerer og er ofte årsaken til deadlocks i nedstengningssekvenser.

Kildekodeeksempel: Delphi WebSocket-klient med Stop, Reconnect og Message-Dispatch

Følgende kode er bevisst utformet som en „driftskomponent“: en klasse som kan brukes på lignende måte i VCL/FMX eller i en Windows- og Windows- og Linux-tjenester (avhengig av Delphi-versjon/plattform). Kjernen er en worker-tråd som holder receive-løkken og melder via events til applikasjonen.

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: nyttig mot inaktivitetstimeouts bak 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, ‚Stopp: Worker reagerer ikke innen tidsavbruddet; mulig blokkering i nettverksstakken‘);
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
// Merk: TClientWebSocket.Connect er synkron og kan blokkere avhengig av DNS/TLS.
// Derfor kjører 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 app-melding, fordi Ping/Pong ikke er konsekvent eksponert i alle Delphi-versjoner.
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;

// Mottak: rammebasert, derfor StringBuilder for 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.
// Binære data kan buffres på samme måte her eller videresendes direkte.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Kort pause for å redusere CPU-bruk ved svært rask løkke.
// 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;

// Gjenkobling etter korrekt lukking eller etter feil
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.

Hva koden bevisst gjør «annerledes» enn typiske eksempler

  • Stop uten tvang: I stedet for å drepe tråder setter Stop et event. Worker avslutter løkker på definerte steder. Det reduserer heng ved avslutning og unngår ressurslekkasjer i socket-stacken.
  • Queue i stedet for Synchronize: Logging og hendelser sendes via TThread.Queue til hovedtråden. Dette er viktig når Stop/Shutdown kommer fra UI eller fra service-control-handlere. Synchronize kan blokkere hvis hovedtråden venter.
  • Tar hensyn til fragmentering: WebSocket-tekst kan komme fragmentert i frames. Derfor brukes TStringBuilder og kontroll av EndOfMessage.
  • Heartbeat som applikasjonsprotokoll: Mange oppsett dør av idle-timeouts (load balancer, nginx, Cloud WAF). En lettvekts «ping»-tekst er ofte et mer pålitelig driftshåndtak enn å håpe på «TCP keepalive» eller et ikke-universelt tilgjengelig Ping/Pong-API.

Rammebetingelser og fallgruver i drift

1) DNS, TLS og Proxy: Connect kan blokkere

TClientWebSocket.Connect er synkron. Avhengig av DNS-oppløsning, TLS-handshake, sertifikatvalidering eller proxy-miljø kan dette ta flere sekunder. Koden plasserer dette bevisst i en Worker. Hvis du i tillegg trenger harde timeouts, må du på API-nivå sjekke om din Delphi-versjon tilbyr timeout-alternativer, eller kapsle Connect i en separat tråd og avbryte via prosesslogikk. Viktig: Et «avbryt» betyr vanligvis å markere forbindelsen som ødelagt og sette opp Worker på nytt, ikke å drepe socket-operasjonen umiddelbart.

2) Idle-Timeouts: hvorfor Heartbeat ofte er påkrevd

I bedriftsnettverk termineres en WebSocket ofte bak en reverse proxy (nginx, IIS ARR) eller en load balancer. Mange av disse komponentene lukker forbindelser når det ikke flyter data over lengre tid. TCP-keepalive er ikke alltid konfigurert så kort (og under Windows er det ofte minutter snarere enn sekunder). Et Heartbeat på applikasjonsnivå er derfor et stabilt arbeidsomgangspunkt. Sørg for at server og klient har samme konsept (f.eks. «ping»/«pong» som tekst eller JSON).

3) Trådhåndtering og UI: Hendelser må være frakoblet

Hvis OnText-behandlingen er tung (JSON-parsing, DB-tilgang med BDE-Ablosung mit nativer Anbindung, UI-oppdateringer), bør den ikke blokkere alt i hovedtråden. Wrapperen leverer bare meldingen. Et typisk mønster er: OnText legger payload i en kø (f.eks. TThreadedQueue<string>), en separat Worker behandler med backpressure (altså begrenset kø-lengde). Det forhindrer at UI fryser eller at mottaket kommer ut av takt ved burst-last.

Debugging: hva du bør logge når det ’noen ganger‘ avbryter

WebSockets er beryktet for «kjører i dagevis, så slutter det». Uten logging er dette vanskelig å avgrense. Fornuftige loggpunkter:

  • Tidsstempel (UTC), URL, og tilstandsskifter (connecting/open/closed).
  • Close-Reason, hvis tilgjengelig (server initierer Close vs. nettverksfeil).
  • Heartbeat-sendefeil og mottaksunntak inkl. unntakstype.
  • Valgfritt: størrelser på mottatte meldinger (ikke innholdet), for å oppdage datamengdeeksplosjon.

Hvis du terminerer over TLS: sjekk i tillegg om sertifikatbytter (utløp, ny issuer) korrelerer tidsmessig med feil. I hardede miljøer er også proxy- og DPI-bokser (Deep Packet Inspection) aktuelle kandidater.

Varianter: når System.Net.WebSockets er tilstrekkelig – og når ikke

System.Net.WebSockets er tilstrekkelig for mange integrasjonsscenarier, spesielt når det gjelder tekst/JSON, moderat last og klare reconnect-strategier. Begrensninger viser seg avhengig av Delphi-versjon og målplattform:

  • Manglende/begrenset Ping/Pong-støtte: Da forblir App-Heartbeat det robuste mønsteret.
  • Manglende timeouts/cancellation ved Connect/Receive: Da må du bygge arkitekturen slik at en hengende worker forblir isolert og applikasjonen likevel kan termineres ryddig (f.eks. via prosess-watchdog eller separate worker-instans­er).
  • Høy last eller binære strømmer: Da lønner det seg med et sterkere framing/buffering-konsept (f.eks. ring buffer, separat Binary-Event, Message-Assembler med begrensninger).

For legacy-situasjoner (eldre Delphi-generasjoner, svært spesifikke TLS/Proxy-krav) er biblioteker som ICS i enkelte prosjekter mer pragmatiske. Viktigere enn «hvilket bibliotek» er at du behandler Shutdown, Reconnect og Observability (logs/metrikker) som førsteklasses temaer.

Konklusjon: en Delphi WebSocket-klient er en driftskomponent – med klare begrensninger

En WebSocket egner seg godt for push-events, live-status, maskin- eller prosessmeldinger og som tilbakekanal for portaler og tjenester. Den viste wrapperen fokuserer på punktene som ofte avgjør i digitale virksomhetsløsninger: kontrollert Reconnect, Heartbeat mot idle-timeouts, fragment-sikker tekstbehandling og en stoppsti som ikke henger ved deployment eller oppdatering.

Bruksgrensene består: Hvis du trenger harde garantier for avbrudd av Connect/Receive innen svært korte tidvinduer eller kjører ekstremt høye datarater, må du gå dypere inn i timeouts, plattformspesifikke forhold og eventuelt alternative stacks. For majoriteten av integrasjons- og moderniseringsscenarier er likevel en godt kapslet, godt logget klient som ovenfor et solid grunnlag som lar seg integrere i eksisterende Delphi-systemer.

Hvis du skal innpasse en slik komponent i en eksisterende arkitektur (f.eks. Layer-3 Architektur med klare service- og UI-lag) eller må debugge sporadiske disconnects under reelle forhold, kan vi hjelpe med en målrettet vurdering: Ta kontakt.

I faglig sammenheng spiller også Heartbeat Ping/Pong en viktig rolle når integrasjoner, datastreamer og videreutvikling må fungere sømløst sammen.

Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

Neste steg

Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift tidlig vurderes samlet.

Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.

  • Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
  • REST, datatilgang, portaler og utrulling blir ikke utsatt som sene følger.
  • Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e‑post er umiddelbart tilgjengelig. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.