Net-Base Magazine

01.06.2026

Delphi Client WebSocket : connexion robuste, arrêt propre, débogage fiable

Un client WebSocket Delphi est rapidement « connecté d'une manière ou d'une autre » — mais en exploitation, ce qui compte, ce sont la reconnexion, les heartbeats, l'arrêt propre et la capacité de débogage. Avec un wrapper opérationnel basé sur System.Net.WebSockets (avec fallback) et un extrait de code source pour le threading et...

01.06.2026

Du thème du magazine à la pratique des projets

Pages de services et techniques pertinentes pour l'article

Pourquoi un client WebSocket Delphi est en pratique plus que «Connect»

Un Delphi WebSocket Client se monte en quelques minutes : URL, Connect, SendText, terminé. Dans les logiciels d’entreprise sur mesure et les solutions logicielles proches du processus, le sujet devient toutefois généralement problématique en exploitation : le reverse proxy coupe les connexions inactives, les liaisons mobiles ou VPN ont des délais NAT courts, les certificats changent, et à l’arrêt le processus se bloque parce qu’une Receive-Loop reste bloquée. De plus : un WebSocket est un canal de longue durée, porteur d’état – il obéit donc à d’autres règles que le classique HTTP/REST (Request/Response, éphémère).

Dans cet extrait de code il ne s’agit pas de «Hello WebSocket», mais d’un client-wrapper opérationnel proposant :

  • démarrage/arrêt propres (sans blocage lors de l’arrêt),
  • boucle de réception avec Cancellation (signal d’arrêt) plutôt que «Thread kill»,
  • reconnexion avec Backoff (rebranchement contrôlé),
  • Heartbeat comme modèle applicatif (car Ping/Pong n’est pas disponible partout),
  • crochets de debug et de trace qui aident réellement lors d’interventions de support.

La mise en œuvre repose sur System.Net.WebSockets (Delphi RTL ; API client WebSocket avec TClientWebSocket). Lorsque cette couche RTL n’est pas disponible dans les versions anciennes ou est trop limitée, un fallback via une bibliothèque (p. ex. ICS) est souvent judicieux – une mise en perspective figure plus bas.

Esquisse d’architecture : un Wrapper plutôt que des appels WebSocket dispersés

Une erreur fréquente dans des applications Delphi évoluées : les formulaires UI ou les modules de service «parlent directement WebSocket» et se retrouvent avec des timers, threads et gestion d’exceptions disséminés partout. Mieux vaut un composant clair avec des événements bien définis et une petite machine à états.

Notions rapides : Backoff désigne un temps d’attente qui augmente progressivement après des erreurs (p. ex. 1s, 2s, 4s …) pour ne pas inonder le serveur et le réseau. CancellationToken est un signal d’arrêt issu du monde .NET ; dans Delphi il n’existe pas de pattern identique, mais on peut le reproduire avec TEvent et un flag «StopRequested». TThread.Queue planifie du code pour exécution dans le thread principal (UI) sans bloquer le worker ; Synchronize bloque et est souvent la cause de deadlocks dans les chemins d’arrêt.

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

Le code suivant est délibérément conçu comme un «composant d’exploitation» : une classe que l’on peut utiliser de manière similaire dans VCL/FMX ou dans des services Windows et Windows- und Linux-Services (selon la version/plateforme Delphi). Le noyau est un thread de travail qui maintient la boucle de réception et remonte les événements vers l’application.

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: utile contre les timeouts d’inactivité derrière des 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, ‚Arrêt: le Worker ne répond pas dans le délai imparti ; blocage possible dans la pile réseau‘);
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
// Remarque: TClientWebSocket.Connect est synchrone et peut bloquer en fonction de DNS/TLS.
// C’est pourquoi cela s’exécute dans le 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 comme message d’application, car Ping/Pong n’est pas exposé proprement dans toutes les versions de Delphi.
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;

// Réception: basé sur des frames, d’où l’usage de StringBuilder pour la fragmentation.
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
// Dans de nombreux protocoles métier, le texte/JSON est la norme.
// Le binaire peut être tamponné de façon analogue ici ou transmis directement.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Mini-sleep pour préserver le CPU en cas de boucle très rapide.
// Pas trop long, sinon la latence se dégrade.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Reconnect après une fermeture propre ou après des erreurs
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.

Ce que le code fait volontairement «différemment» par rapport aux exemples typiques

  • Stop sans brutalité : Au lieu de terminer brutalement les threads, Stop déclenche un événement. Le Worker termine les boucles à des points définis. Cela réduit les blocages lors de l’arrêt et évite les fuites de ressources dans la pile de sockets.
  • Queue au lieu de Synchronize : Le logging et les événements passent via TThread.Queue vers le Mainthread. C’est important lorsque Stop/Shutdown provient de l’UI ou des gestionnaires Service-Control. Synchronize peut bloquer si le Mainthread est en attente.
  • Prise en compte de la fragmentation : Le texte WebSocket peut arriver fragmenté en trames. D’où l’utilisation de TStringBuilder et la vérification de EndOfMessage.
  • Heartbeat comme protocole applicatif : Beaucoup de déploiements échouent à cause d’idle timeouts (Load Balancer, nginx, Cloud WAF). Un texte léger «ping» comme levier opérationnel est souvent plus efficace que d’espérer le «TCP keepalive» ou une API Ping/Pong non disponible partout.

Contraintes et pièges en exploitation

1) DNS, TLS et proxy : Connect peut se bloquer

TClientWebSocket.Connect est synchrone. Selon la résolution DNS, le handshake TLS, la vérification de certificat ou l’environnement proxy, cela peut prendre plusieurs secondes. Le code place cela volontairement dans un Worker. Si vous avez besoin de timeouts stricts, vérifiez au niveau de l’API si votre version Delphi propose des options de timeout, ou encapsulez Connect dans un thread séparé et interrompez via la logique de processus. Important : «annuler» signifie ici le plus souvent «marquer la connexion comme défaillante et recréer le Worker», pas «tuer immédiatement l’opération socket».

2) Idle-timeouts : pourquoi le Heartbeat est souvent obligatoire

Dans les réseaux d’entreprise, un WebSocket est souvent terminé derrière un reverse proxy (nginx, IIS ARR) ou un load balancer. Beaucoup de ces composants ferment les connexions si aucune donnée ne circule pendant une longue période. Le TCP-Keepalive n’est pas toujours configuré assez court (et sous Windows il est souvent plutôt en minutes qu’en secondes). Un heartbeat au niveau applicatif est donc un contournement stable. Veillez à ce que serveur et client partagent le même concept (par ex. «ping»/«pong» en texte ou en JSON).

3) Threading et UI : les événements doivent rester découplés

Si le traitement OnText est lourd (parsing JSON, accès DB avec BDE-remplacement avec intégration native, mises à jour UI), il ne doit pas tout bloquer dans le Mainthread. Le wrapper ne fournit que le message. Un modèle typique : OnText place la payload dans une queue (p. ex. TThreadedQueue<string>), un Worker séparé traite avec backpressure (c’est‑à‑dire une longueur de queue limitée). Cela évite que, lors de pics d’activité, l’UI ne se fige ou que la réception perde son rythme.

Debugging : ce que vous devez consigner dans les logs quand ça se coupe „parfois“

Les WebSockets sont réputés pour «fonctionner pendant des jours, puis s’arrêter». Sans journalisation, il est difficile de circonscrire le problème. Points de journalisation pertinents :

  • Horodatage (UTC), URL et transitions d’état (connecting/open/closed).
  • Close-Reason, si disponible (close demandé par le serveur vs. erreur réseau).
  • Erreurs d’envoi du heartbeat et exceptions Receive, y compris le type d’exception.
  • Optionnel : tailles des messages reçus (pas leur contenu), pour détecter une explosion de données.

Si vous terminez sur TLS : vérifiez en outre si des changements de certificats (expiration, nouvel issuer) coïncident temporellement avec des erreurs. Dans des environnements durcis, les boîtes proxy et DPI (Deep Packet Inspection) sont également des suspects.

Varianten : quand System.Net.WebSockets suffit — et quand il ne suffit pas

System.Net.WebSockets est suffisant pour de nombreux cas d’intégration, surtout pour du texte/JSON, des charges modérées et des stratégies de reconnexion claires. Des limites apparaissent selon la version Delphi et la cible plateforme :

  • Prise en charge Ping/Pong inexistante/limitée : dans ce cas, le pattern App-Heartbeat reste la solution robuste.
  • Absence de timeouts/annulation lors du Connect/Receive : vous devez alors concevoir l’architecture de façon qu’un worker bloqué reste isolé et que l’application puisse malgré tout s’arrêter proprement (p. ex. via un processus watchdog ou des instances worker séparées).
  • Charges élevées ou flux binaires : il est alors pertinent d’adopter un concept de framing/buffering plus robuste (p. ex. ring buffer, Binary-Event séparé, assembleur de messages avec limites).

Pour des situations legacy (générations Delphi plus anciennes, exigences TLS/Proxy très spécifiques), des bibliothèques comme ICS sont parfois plus pragmatiques dans certains projets. L’important n’est pas tant « quelle library » que de traiter le shutdown, la reconnexion et l’observability (logs/métriques) comme des sujets de première importance.

Conclusion : un client WebSocket Delphi est un composant d’exploitation — avec des limites claires

Un WebSocket convient parfaitement pour les push-events, l’état en direct, les messages machine ou de processus et comme canal de retour pour portails et services. Le wrapper présenté met l’accent sur les points qui font souvent la différence dans les solutions d’entreprise numériques : reconnexion contrôlée, Heartbeat contre les timeouts d’inactivité, traitement de texte sûr vis-à-vis des fragments et un chemin d’arrêt qui ne bloque pas lors d’un déploiement ou d’une mise à jour.

Des limites d’utilisation subsistent : si vous exigez des garanties strictes d’interruption de Connect/Receive dans des fenêtres temporelles très courtes, ou si vous traitez des débits de données extrêmement élevés, vous devrez approfondir les timeouts, les particularités de la plateforme et, le cas échéant, des stacks alternatifs. Pour la majorité des scénarios d’intégration et de modernisation, un client soigneusement encapsulé et bien loggé comme celui présenté constitue toutefois une base solide pouvant s’intégrer à des systèmes Delphi existants.

Si vous souhaitez intégrer un tel composant dans une architecture existante (p. ex. Layer-3 Architektur avec des couches service et UI clairement définies) ou déboguer des déconnexions sporadiques en conditions réelles, nous pouvons en discuter de manière ciblée : contactez-nous.

Dans le contexte métier, le Heartbeat Ping/Pong joue également un rôle important lorsque intégrations, flux de données et évolution doivent bien fonctionner ensemble.

Discuter d’un projet ou d’une modernisation avec Net-Base.

Étape suivante

Lorsque ce sujet devient un projet concret, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.

Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.

  • L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
  • REST, l'accès aux données, les portails et le déploiement ne sont pas repoussés en tant que conséquences ultérieures.
  • Vous identifiez tôt quelle voie est viable sur le plan économique et opérationnel.

Partager l'article

Partager directement cette publication

LinkedIn, X, XING, Facebook, WhatsApp et e-mail sont immédiatement disponibles. Pour Instagram, nous préparons directement le lien et un court texte.

Courriel

Instagram s'ouvre dans un nouvel onglet. Le lien et le court texte sont préalablement copiés dans le presse-papiers.