Net-Base Revista

01.06.2026

Delphi Client WebSocket: connectar de forma robusta, aturar de manera ordenada, depurar amb fiabilitat

Un Delphi WebSocket Client queda ràpidament d'alguna manera connectat – però en producció compten la reconnexió, els heartbeats, el tancament ordenat i la capacitat de depuració. Amb un wrapper pràctic basat en System.Net.WebSockets (amb fallback) i un fragment de codi font per a threading i...

01.06.2026

Del tema de la revista a la pràctica del projecte

Pàgines de serveis i tècniques pertinents per a l'article

Per què un Delphi WebSocket Client a la pràctica és més que „Connect“

Un Delphi WebSocket Client es munta en minuts: URL, Connect, SendText, llest. En software empresarial a mida i solucions properes al procés, però, el tema sol esclatar sobretot en funcionament: el Reverse Proxy talla connexions inactives, enllaços mòbils o VPN tenen timeouts NAT curts, els certificats canvien, i en l’aturada el procés queda penjat perquè un bucle de recepció encara està bloquejat. A més: un WebSocket és un canal de llarga durada amb estat — per això s’hi apliquen regles diferents que en l’HTTP clàssic/REST (Request/Response, de curta durada).

En aquest fragment de codi no es tracta del „Hello WebSocket“, sinó d’un client-wrapper practicat amb:

  • arrencada/parada neta (sense bloquejos durant l’aturada),
  • bucle de recepció amb Cancellation (Abbruchsignal) en lloc de „Thread kill“,
  • Reconnect amb Backoff (reconexió controlada),
  • heartbeat com a patró d’aplicació (perquè Ping/Pong no està disponible a tot arreu),
  • hooks de debug i trace que realment ajuden en casos de suport.

La implementació es basa en System.Net.WebSockets (Delphi RTL; API de client WebSocket amb TClientWebSocket). On aquesta capa RTL no està disponible en versions antigues o és massa limitada, sovint té sentit un fallback mitjançant una biblioteca (p. ex. ICS) — a continuació hi ha una valoració.

Arquitectura: un Wrapper en lloc de crides WebSocket disperses

Un error freqüent en aplicacions Delphi madures: formularis UI o mòduls de servei „parlen directament amb WebSocket“ i tenen timers, threads i gestió d’excepcions dispersos per tot arreu. És preferible un component clar amb esdeveniments ben definits i una petita màquina d’estats.

Conceptes breument posicionats: Backoff designa un temps d’espera que creix de manera escalonada després d’errors (p. ex. 1s, 2s, 4s …), per no saturar servidor i xarxa. CancellationToken és un Abbruchsignal del món .NET; en Delphi no existeix un patró idèntic, però el podem simular amb TEvent i una bandera „StopRequested“. TThread.Queue programa codi per executar-se en el fil principal (UI) sense bloquejar el worker; Synchronize bloqueja i sovint és la causa de deadlocks en camins de tancament.

Fragment de codi: Delphi WebSocket Client amb parada, Reconnect i distribució de missatges

El codi següent està deliberadament dissenyat com a „bloc operatiu“: una classe que es pot utilitzar de forma similar en VCL/FMX o en Windows- i Windows- i Linux-Services (segons la versió/plataforma Delphi). El nucli és un fil de treball que manté el bucle de recepció i notifica l’aplicació via esdeveniments.

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: útil contra timeouts per inactivitat darrere de 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: el worker no respon dins del timeout; possible bloqueig a la pila de xarxa‘);
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
// Nota: TClientWebSocket.Connect és sincrònic i pot bloquejar depenent de DNS/TLS.
// Per això s’executa aquí dins del 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 com a missatge d’aplicació, perquè Ping/Pong no està exposat de forma neta en totes 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;

// Receive: basat en frames, per això s’usa StringBuilder per a la fragmentació.
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
// En molts protocols empresarials, Text/JSON és l’estàndard.
// Binary es pot buferar de manera similar aquí o reenviar-lo directament.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Mini-sleep per preservar la CPU en bucles molt ràpids.
// No massa gran, si no empitjora la latència.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Reconnect després d’un tancament correcte o després d’errors
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.

Què fa el codi deliberadament „diferent“ respecte dels exemples típics

  • Stop sense violència: En lloc de matar threads, Stop estableix un event. El Worker finalitza els bucles en punts definits. Això redueix els penjaments durant el tancament i evita fuites de recursos a la pila de sockets.
  • Queue en lloc de Synchronize: Els registres i els events passen al fil principal mitjançant TThread.Queue. Això és important quan Stop/Shutdown s’origina a la UI o als handlers de Service Control. Synchronize pot bloquejar si el fil principal està en espera.
  • Es té en compte la fragmentació: El text de WebSocket pot arribar fragmentat en frames. Per això TStringBuilder i la comprovació de EndOfMessage.
  • Heartbeat com a protocol d’aplicació: Moltes configuracions fallen per idle-timeouts (Load Balancer, nginx, Cloud WAF). Un text lleuger de „ping“ sovint és una palanca d’operació més efectiva que confiar en el „TCP keepalive“ o en una API Ping/Pong que no està disponible arreu.

Condicions marc i punts crítics en operació

1) DNS, TLS i proxy: Connect pot bloquejar-se

TClientWebSocket.Connect és síncron. Segons la resolució DNS, el TLS-handshake, la verificació del certificat o l’entorn de proxy, això pot durar diversos segons. El codi ho col·loca deliberadament dins d’un Worker. Si necessiteu timeouts estrictes addicionals, cal comprovar a nivell d’API si la vostra versió Delphi ofereix opcions de timeout, o encapsular Connect en un thread separat i abortar amb lògica de procés. Important: un „abortar“ aquí normalment vol dir „marcar la connexió com a trencada i refer el Worker“, no „matar immediatament l’operació del socket“.

2) Idle-timeouts: per què el Heartbeat sovint és obligatori

En xarxes corporatives, un WebSocket sovint està finalitzat darrere d’un reverse proxy (nginx, IIS ARR) o d’un load balancer. Molts d’aquests components tallen connexions si no flueixen dades durant períodes prolongats. El TCP-Keepalive no sempre està configurat amb intervals curts (i sota Windows sovint és més minuts que segons). Per això, un Heartbeat a nivell d’aplicació és un work-around robust. Assegureu-vos que servidor i client comparteixen el mateix concepte (per exemple, „ping“/“pong“ com a text o JSON).

3) Threading i UI: els esdeveniments han de romandre desacoblats

Si el processament de OnText és pesat (JSON-parsing, accessos a BD amb BDE-Ablosung amb connexió nativa, actualitzacions de UI), no hauria de bloquejar-ho tot en el fil principal. El wrapper només lliura el missatge. Un patró típic: OnText posa la payload en una cua (p. ex. TThreadedQueue<string>), i un Worker separat processa amb backpressure (és a dir, longitud de cua limitada). Això evita que, en pics de càrrega, la UI es bloquegi o que la recepció perdi el ritme.

Depuració: què heu de registrar quan es talla „de tant en tant“

Els WebSockets són famosos per „funcionar dies i després deixar de funcionar“. Sense logging és difícil acotar el problema. Punts de registre recomanats:

  • Marca temporal (UTC), URL i canvis d’estat (connecting/open/closed).
  • Motiu de tancament (Close-Reason), si està disponible (tancament sol·licitat pel servidor vs. error de xarxa).
  • Error d’enviament de Heartbeat i excepcions de recepció, inclòs el tipus d’Exception.
  • Opcional: mides dels missatges rebuts (no els continguts), per detectar explosions de dades.

Si feu terminació TLS: comproveu també si els canvis de certificat (caducitat, nou emissor) correlacionen temporalment amb els errors. En entorns endurets, les caixes de proxy i les unitats DPI (Deep Packet Inspection) també són candidats a investigar.

Varianten: wann System.Net.WebSockets reicht – und wann nicht

System.Net.WebSockets és suficient per a molts casos d’integració, sobretot quan es tracta de text/JSON, càrrega moderada i estratègies clares de reconnect. Les limitacions s’aprecien segons la versió de Delphi i la plataforma objectiu:

  • Suport de Ping/Pong absent o limitat: En aquests casos, App-Heartbeat continua sent el patró robust.
  • Absència de Timeouts/Cancellation en Connect/Receive: Cal dissenyar l’arquitectura perquè un worker penjat romangui aïllat i l’aplicació es pugui tancar netament (p. ex. amb un watchdog de procés o instàncies de worker separades).
  • Càrrega elevada o fluxos binaris: En aquests casos val la pena un concepte més sòlid de framing/buffering (p. ex. ring buffer, Binary-Event separat, Message-Assembler amb límits).

Per a situacions legacy (generacions antigues de Delphi, requisits TLS/Proxy molt específics) biblioteques com ICS són en alguns projectes més pragmàtiques. El que importa menys és «quina Library», i més que tracteu Shutdown, Reconnect i Observability (logs/mètriques) com a temes de primera classe.

Fazit: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen

Un WebSocket és idoni per a push-events, estats en viu, missatges de màquina o de procés i com a canal de retorn per a portals i serveis. El wrapper mostrat se centra en els punts que sovint marquen la diferència en solucions empresarials digitals: reconnect controlada, heartbeat contra idle-timeouts, processament de text segur respecte a fragments i un camí d’aturada que no es queda penjat durant el desplegament o l’actualització.

Persistixen límits d’ús: si necessiteu garanties estrictes per a l’abandonament de Connect/Receive en finestres de temps molt curtes o gestioneu ràtios de dades extremadament altes, cal aprofundir en timeouts, peculiaritats de la plataforma i, si escau, en stacks alternatius. Per a la majoria dels escenaris d’integració i modernització, però, un client ben aïllat i ben registrat és una base sòlida que es pot integrar en sistemes Delphi existents.

Si voleu encaixar un component així en una arquitectura existent (p. ex. Layer-3 arquitectura amb capes clares de servei i UI) o depurar disconnects esporàdics en condicions reals, podem ajudar-vos a prioritzar-ho: Poseu-vos en contacte.

En l’entorn tècnic, els heartbeat Ping/Pong també juguen un paper important quan integracions, fluxos de dades i desenvolupament han de funcionar de manera coordinada.

Parlar d’un projecte o iniciativa de modernització amb Net-Base.

Pas següent

Quan un tema esdevé un projecte real, l'arquitectura, l'entorn existent i les operacions s'haurien de considerar conjuntament des de bon començament.

No només donem suport en qüestions puntuals, sinó també quan, a partir de fragments de codi font, temes de sistemes heredats o idees de portal, ha de sorgir un projecte empresarial sòlid.

  • L'estat actual, la visió objectiu i els riscos tècnics s'avaluen conjuntament.
  • REST, l'accés a les dades, els portals i el desplegament no es releguen a fases posteriors.
  • Vostè veurà aviat quin camí és econòmicament i operativament viable.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.