Van magazinethema naar projectpraktijk
Relevante dienst- en technische pagina's bij het artikel
Waarom een Delphi WebSocket Client in de praktijk meer is dan „Connect“
Een Delphi WebSocket Client is in enkele minuten in elkaar gezet: URL, Connect, SendText, klaar. In individuele bedrijfssoftware en procesnabije oplossingen doet het onderwerp zich echter vaak pas in productie voor: de reverse proxy verbreekt inactieve verbindingen, mobiele of VPN-routes hebben korte NAT-timeouts, certificaten wisselen, en bij het afsluiten blijft het proces hangen omdat een Receive-Loop nog geblokkeerd is. Daar komt bij: een WebSocket is een langlevend, statusgebonden kanaal – er gelden andere regels dan bij klassiek HTTP/REST (Request/Response, kortlevend).
In dit Source-Schnipsel gaat het niet om „Hello WebSocket“, maar om een praktijkgeschikte client-wrapper met:
- zorgvuldig Start/Stop (zonder vastlopen bij shutdown),
- Receive-Loop met Cancellation (annuleringssignaal) in plaats van „Thread kill“,
- Reconnect met Backoff (gecontroleerde herverbinding),
- Heartbeat als toepassingspatroon (omdat Ping/Pong niet overal beschikbaar is),
- Debug- en Trace-Hooks die in supportgevallen echt helpen.
De implementatie is gebaseerd op System.Net.WebSockets (Delphi RTL; WebSocket-Client-API met TClientWebSocket). Waar deze RTL-laag in oudere versies niet beschikbaar of te beperkt is, is een fallback via een bibliotheek (bijv. ICS) vaak zinvol – hierover verderop een toelichting.
Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe
Een veelgemaakte fout in volwassen Delphi-toepassingen: UI-formulieren of servicemodules „sprechen direkt WebSocket“ en hebben dan overal timers, threads en uitzonderingafhandeling verspreid. Beter is een duidelijke component met goed gedefinieerde events en een kleine toestandsmachine.
Begriffe kurz eingeordnet: Backoff betekent een wachttijd die na fouten stapsgewijs toeneemt (bijv. 1s, 2s, 4s …), om server en netwerk niet te overspoelen. CancellationToken is een annuleringssignaal uit de .NET-wereld; in Delphi bestaat er geen identiek patroon, maar we kunnen het nabootsen met TEvent en een „StopRequested“-flag. TThread.Queue plant code voor uitvoering in de hoofdthread (UI), zonder de worker te blokkeren; Synchronize blokkeert en is in shutdown-paden vaak de oorzaak van deadlocks.
Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch
De volgende code is bewust opgezet als „Betriebs-Baustein“: een klasse die men vergelijkbaar kan gebruiken in VCL/FMX of in een Windows- und Windows- und Linux-Services (afhankelijk van Delphi-versie/Platform) . De kern is een worker-thread die de Receive-Loop onderhoudt en via events aan de applicatie meldt.
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: nuttig tegen idle-timeouts achter proxies
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 reageert niet binnen timeout; mogelijke blokkade in de netwerkstack‘);
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
// Opmerking: TClientWebSocket.Connect is synchroon en kan, afhankelijk van DNS/TLS, blokkeren.
// Daarom wordt dit hier in de worker uitgevoerd.
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 als app-bericht, omdat Ping/Pong niet in elke Delphi-versie netjes geëxposeerd is.
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;
// Ontvangst: frame-gebaseerd; daarom StringBuilder voor fragmentatie.
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
// In veel businessprotocollen is tekst/JSON de standaard.
// Binary kan hier op vergelijkbare wijze gebufferd of direct doorgegeven worden.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, ‚Server requested close‘);
Break;
end;
end;
// Kleine pauze om bij zeer snelle loops de CPU te sparen.
// Niet te groot, anders verslechtert de latentie.
TThread.Sleep(1);
end;
finally
SB.Free;
end;
SafeClose;
State(‚closed‘);
finally
WS.Free;
end;
if StopRequested then
Break;
// Reconnect nach sauberem Close oder nach Fehlern
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.
Wat de code opzettelijk ‚anders‘ doet dan typische voorbeelden
- Stop zonder geweld: In plaats van threads af te schieten, zet Stop een event. De Worker beëindigt loops op gedefinieerde punten. Dat vermindert vastlopers bij het afsluiten en voorkomt resource-lekken in de socket-stack.
- Queue in plaats van Synchronize: Logging en events gaan via TThread.Queue naar de mainthread. Dat is belangrijk wanneer Stop/Shutdown uit de UI of uit service-control-handlers komt. Synchronize kan blokkeren als de mainthread op dat moment wacht.
- Rekening houden met fragmentatie: WebSocket-tekst kan in frames gefragmenteerd binnenkomen. Daarom de TStringBuilder en het controleren van EndOfMessage.
- Heartbeat als applicatieprotocol: Veel opstellingen sterven aan idle-timeouts (Load Balancer, nginx, Cloud WAF). Een lichtgewicht ‚ping‘-tekst is als operationeel middel vaak effectiever dan hopen op ‚TCP keepalive‘ of een niet overal beschikbaar Ping/Pong-API.
Randvoorwaarden en valkuilen in de operatie
1) DNS, TLS en proxy: Connect kan blokkeren
TClientWebSocket.Connect is synchroon. Afhankelijk van DNS-resolutie, TLS-handshake, certificaatcontrole of proxy-omgeving kan dat meerdere seconden duren. De code plaatst dit bewust in een Worker. Als u daarnaast harde timeouts nodig heeft, moet u op API-niveau controleren of uw Delphi-versie timeout-opties biedt, of u kapselt Connect in een aparte thread en breekt via proceslogica af. Belangrijk: een „afbreken“ betekent hier meestal „verbinding als kapot markeren en de Worker herstarten“, niet „socket-operatie onmiddellijk killen“.
2) Idle-timeouts: waarom Heartbeat vaak verplicht is
In bedrijfsnetwerken wordt een WebSocket vaak beëindigd achter een reverse proxy (nginx, IIS ARR) of een load balancer. Veel van deze componenten sluiten verbindingen als er langere tijd geen data stroomt. TCP-Keepalive is niet altijd kort genoeg geconfigureerd (en onder Windows vaak eerder minuten dan seconden). Een heartbeat op applicatieniveau is daarom een robuuste workaround. Zorg dat server en client hetzelfde concept hanteren (bijv. ‚ping’/’pong‘ als tekst of JSON).
3) Threading en UI: gebeurtenissen moeten ontkoppeld blijven
Als de OnText-verwerking zwaar is (JSON-parsing, DB-toegangen met BDE-Ablosung met native koppeling, UI-updates), mag die niet de mainthread volledig blokkeren. De wrapper levert alleen het bericht. Een typisch patroon is: OnText plaatst de payload in een queue (bijv. TThreadedQueue<string>), een aparte Worker verwerkt met backpressure (dus beperkte queue-lengte). Dat voorkomt dat bij burst-load de UI bevriest of de ontvangst uit het ritme raakt.
Debugging: wat u moet loggen als het „soms“ afbreekt
WebSockets hebben de reputatie „dagenlang te lopen en dan ineens niet meer“. Zonder logging is het nauwelijks te duiden. Zinnige logpunten:
- Tijdstempel (UTC), URL en statuswisselingen (connecting/open/closed).
- Close-Reason, indien beschikbaar (server-initieerde Close versus netwerkfout).
- Heartbeat-sendefouten en receive-excepties inclusief exception-type.
- Optioneel: groottes van ontvangen berichten (niet de inhoud), om explosies in datavolume te detecteren.
Als u TLS terminateert: controleer daarnaast of certificaatwisselingen (vervaldatum, nieuwe issuer) temporeel correleren met fouten. In geharde omgevingen zijn ook proxy- en DPI-boxen (Deep Packet Inspection) kandidaten.
Varianten: wanneer System.Net.WebSockets volstaat – en wanneer niet
System.Net.WebSockets is voor veel integraties toereikend, vooral bij Text/JSON, matige belasting en duidelijke herverbindingsstrategieën. Grenzen worden zichtbaar afhankelijk van de Delphi-versie en het doelsysteem:
- Ontbrekende/beperkte Ping/Pong-ondersteuning: Dan blijft app-heartbeat het robuuste patroon.
- Ontbrekende timeouts/cancellation bij Connect/Receive: Dan moet u de architectuur zo opzetten dat een vastgelopen worker geïsoleerd blijft en de applicatie toch netjes kan afsluiten (bijv. via een proces-watchdog of gescheiden worker-instanties).
- Hoge belasting of binaire streams: Dan loont een sterker framing-/buffering-concept (bijv. ring buffer, apart Binary-Event, Message-Assembler met limieten).
Voor legacy-situaties (oudere Delphi-generaties, zeer specifieke TLS/Proxy-eisen) zijn bibliotheken zoals ICS in sommige projecten pragmatischer. Belangrijker dan “welke library” is dat u Shutdown, Reconnect en Observability (logs/metrics) als eersteklas onderwerpen behandelt.
Conclusie: een Delphi WebSocket-client is een operationele bouwsteen – met duidelijke grenzen
Een WebSocket is uitstekend geschikt voor push-events, live-status, machine- of procesmeldingen en als terugkanaal voor portalen en services. De getoonde wrapper richt zich op de aspecten die in digitale bedrijfsoplossingen vaak het verschil maken: gecontroleerde herverbinding, heartbeat tegen idle-timeouts, fragment-veilige tekstverwerking en een stoppad dat bij deployment of update niet blijft hangen.
Toepassingsgrenzen blijven bestaan: als u harde garanties nodig heeft voor het afbreken van Connect/Receive binnen zeer krappe tijdvensters of extreem hoge datastromen heeft, moet u dieper ingaan op timeouts, platformeigenaardigheden en eventueel alternatieve stacks. Voor de meerderheid van integratie- en moderniseringsscenario’s is een netjes gekapselde, goed gelogde client zoals hierboven echter een solide basis die zich in bestaande Delphi-systemen laat integreren.
Als u zo’n bouwsteen in een bestaande architectuur wilt inpassen (bijv. Layer-3 Architektur met duidelijke service- en UI-lagen) of sporadische disconnects onder reële omstandigheden moet debuggen, kunnen wij dat gericht voor u classificeren: Kontakt aufnehmen.
In het vakmatige speelveld spelen ook heartbeat Ping/Pong een belangrijke rol wanneer integraties, datastromen en doorontwikkeling netjes moeten samenwerken.
Volgende stap
Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.
We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.
- Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
- REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
- U ziet vroeg welke weg economisch en operationeel houdbaar is.