Dal tema della rivista alla pratica di progetto
Pagine di servizi e tecniche correlate all'articolo
Perché un Delphi WebSocket Client nella pratica è più di «Connect»
Un Delphi WebSocket Client si mette insieme in pochi minuti: URL, Connect, SendText, fatto. Nelle soluzioni software aziendali e nelle applicazioni vicine ai processi però il problema si manifesta tipicamente in esercizio: il reverse proxy interrompe le connessioni idle, le tratte mobile o VPN hanno timeout NAT brevi, i certificati cambiano, e alla chiusura il processo resta appeso perché un receive-loop è ancora bloccato. In aggiunta: un WebSocket è un canale di lunga durata e con stato – perciò valgono regole diverse rispetto al classico HTTP/REST (request/response, di breve durata).
In questo frammento di codice non si tratta di «Hello WebSocket», ma di un client-wrapper pratico con:
- avvio/stop puliti (senza blocchi nello shutdown),
- receive-loop con Cancellation (segnale di aborto) invece di «thread kill»,
- reconnect con backoff (riattacco controllato),
- heartbeat come pattern applicativo (perché Ping/Pong non è disponibile ovunque),
- hook di debug e trace che aiutano realmente nei casi di supporto.
L’implementazione si basa su System.Net.WebSockets (Delphi RTL; API client WebSocket con TClientWebSocket). Dove questo strato RTL non è disponibile o è troppo limitato nelle versioni più vecchie, un fallback tramite una libreria (p. es. ICS) è spesso sensato – di questo sotto troverete un’ulteriore valutazione.
Schema architetturale: un Wrapper invece di chiamate WebSocket sparse
Un errore comune nelle applicazioni Delphi cresciute nel tempo: i form dell’interfaccia utente o i moduli di servizio «parlano direttamente con WebSocket» e finiscono per avere timer, thread e gestioni delle eccezioni distribuiti ovunque. È preferibile un componente chiaro con eventi ben definiti e una piccola macchina a stati.
Termini brevemente inquadrati: Backoff indica un tempo di attesa che dopo errori cresce progressivamente (es. 1s, 2s, 4s …), per non sovraccaricare server e rete. CancellationToken è un segnale di interruzione dal mondo .NET; in Delphi non esiste un pattern identico, ma lo si può riprodurre con TEvent e un flag «StopRequested». TThread.Queue programma codice per l’esecuzione nel thread principale (UI), senza bloccare il worker; Synchronize blocca ed è spesso la causa di deadlock nei percorsi di shutdown.
Frammento di codice: Delphi WebSocket Client con Stop, Reconnect e Message-Dispatch
Il codice seguente è volutamente strutturato come un «componente operativo»: una classe che si può usare in VCL/FMX o in servizi Windows- e Windows- e Linux-Services (a seconda della versione/piattaforma Delphi). Il nucleo è un worker-thread che mantiene il receive-loop e segnala all’applicazione tramite eventi.
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; // Heartbeat dell’app: utile contro timeout di inattività dietro i proxy
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: il worker non risponde entro il timeout; possibile blocco nello stack di rete‘);
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 è sincrono e può bloccare a seconda di DNS/TLS.
// Per questo viene eseguito qui nel 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 come messaggio applicativo, perché Ping/Pong non è esposto in modo affidabile in tutte le versioni di 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: basato su frame, quindi StringBuilder per la frammentazione.
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 molti protocolli business il testo/JSON è lo standard.
// Il binario può essere qui bufferizzato analogamente o passato direttamente.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, ‚Server requested close‘);
Break;
end;
end;
// Mini-sleep per ridurre il carico CPU in loop molto veloci.
// Non troppo lungo, altrimenti peggiora la latenza.
TThread.Sleep(1);
end;
finally
SB.Free;
end;
SafeClose;
State(‚closed‘);
finally
WS.Free;
end;
if StopRequested then
Break;
// Reconnect dopo chiusura pulita o dopo errori
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.
Cosa il codice fa intenzionalmente „diverso“ rispetto agli esempi tipici
- Stop senza terminazione forzata: Invece di terminare i thread brutalmente, Stop segnala un Event. Il worker chiude i loop in punti definiti. Questo riduce i blocchi alla chiusura ed evita perdite di risorse nello stack dei socket.
- Queue invece di Synchronize: Logging ed eventi vengono inviati al Mainthread tramite TThread.Queue. Questo è importante quando Stop/Shutdown proviene dall’UI o dai Service-Control-Handler. Synchronize può bloccare se il Mainthread è attualmente in attesa.
- Si tiene conto della frammentazione: Il testo WebSocket può arrivare frammentato in frame. Per questo si usa il TStringBuilder e si verifica EndOfMessage.
- Heartbeat come protocollo applicativo: Molte configurazioni falliscono per idle-timeout (Load Balancer, nginx, Cloud WAF). Un messaggio „ping“ leggero come testo è spesso una leva operativa più efficace della speranza nel „TCP keepalive“ o di un API Ping/Pong non sempre disponibile.
Vincoli e insidie operative
1) DNS, TLS e proxy: Connect può bloccarsi
TClientWebSocket.Connect è sincrono. A seconda della risoluzione DNS, dell’handshake TLS, della verifica del certificato o dell’ambiente proxy ciò può richiedere diversi secondi. Il codice pone intenzionalmente questa operazione in un worker. Se servono timeout stringenti, dovete verificare a livello di API se la vostra versione Delphi fornisce opzioni di timeout, oppure incapsulare Connect in un thread separato e abortare tramite logica di processo. Importante: un „annullamento“ qui di solito significa „marcare la connessione come danneggiata e ristanziare il worker“, non „uccidere immediatamente l’operazione socket“.
2) Idle-Timeouts: perché il Heartbeat è spesso obbligatorio
In reti aziendali un WebSocket è spesso terminato dietro un reverse proxy (nginx, IIS ARR) o un load balancer. Molti di questi componenti chiudono le connessioni se per un periodo prolungato non scorrono dati. TCP-Keepalive non è sempre configurato con intervalli brevi (e sotto Windows spesso è misurato in minuti anziché secondi). Un Heartbeat a livello applicativo è quindi un workaround stabile. Assicuratevi che server e client condividano lo stesso concetto (es. „ping“/“pong“ come testo o JSON).
3) Threading e UI: gli eventi devono restare disaccoppiati
Se l’elaborazione di OnText è pesante (parsing JSON, accessi DB con BDE-Ablosung mit nativer Anbindung, aggiornamenti UI), non dovrebbe bloccare il Mainthread. Il wrapper consegna solo il messaggio. Uno schema tipico è: OnText mette la payload in una coda (es. TThreadedQueue<string>), un worker separato la elabora applicando backpressure (ossia una lunghezza della coda limitata). Questo evita che, in caso di carichi a raffica, l’UI si blocchi o il ricevimento perda ritmo.
Debugging: cosa dovete loggare quando si interrompe „a volte“
I WebSocket sono noti per „funzionano per giorni, poi smettono“. Senza logging è difficile circoscrivere il problema. Punti di log utili:
- Timestamp (UTC), URL e cambi di stato (connecting/open/closed).
- Close-Reason, se disponibile (close richiesto dal server vs. errore di rete).
- Errori di invio Heartbeat ed eccezioni in ricezione, incluso il tipo di eccezione.
- Opzionale: dimensioni dei messaggi ricevuti (non i contenuti), per rilevare esplosioni di dati.
Se terminate su TLS: verificate inoltre se i cambi di certificato (scadenza, nuovo issuer) si correlano temporalmente con gli errori. In ambienti con hardening elevato anche proxy e appliance DPI (Deep Packet Inspection) sono candidati possibili.
Varianti: quando System.Net.WebSockets è sufficiente – e quando no
System.Net.WebSockets è adeguato per molti casi di integrazione, soprattutto quando si tratta di testo/JSON, carichi moderati e strategie di reconnessione chiare. I limiti emergono a seconda della versione di Delphi e della piattaforma target:
- Supporto Ping/Pong assente o limitato: in questi casi l’App-Heartbeat rimane il pattern robusto.
- Assenza di timeout/cancellazione in Connect/Receive: allora dovete progettare l’architettura in modo che un worker bloccato rimanga isolato e l’applicazione possa comunque terminare in modo pulito (es. mediante watchdog di processo o istanze worker separate).
- Alto carico o stream binari: conviene adottare un concetto più solido di framing/buffering (es. ring buffer, Binary-Event separato, Message-Assembler con limiti).
Per situazioni legacy (generazioni più vecchie di Delphi, requisiti TLS/proxy molto specifici) librerie come ICS risultano in alcuni progetti più pragmatiche. Ciò che conta meno è «quale libreria», e più che trattiate Shutdown, Reconnect e osservabilità (log/metriche) come temi di prima classe.
Conclusione: un Delphi WebSocket Client è un componente operativo – con limiti chiari
Un WebSocket è eccellente per eventi push, stato live, segnalazioni macchina o di processo e come canale di ritorno per portali e servizi. Il wrapper mostrato si concentra sui punti che nelle soluzioni aziendali digitali spesso fanno la differenza: reconnessione controllata, Heartbeat contro gli idle-timeout, elaborazione testuale sicura rispetto alla frammentazione e una procedura di Stop che non si blocca durante deployment o aggiornamenti.
Permangono limiti d’uso: se avete bisogno di garanzie rigide per l’aborto di Connect/Receive entro finestre temporali molto strette o gestite velocità di dati estremamente elevate, dovrete approfondire timeout, particolarità di piattaforma e, se necessario, stack alternativi. Per la maggior parte degli scenari di integrazione e modernizzazione un client ben incapsulato e ben loggato come quello sopra rappresentato è tuttavia una base solida, integrabile in sistemi Delphi esistenti.
Se dovete inserire un componente simile in un’architettura esistente (es. Layer-3 Architettura con chiare separazioni tra servizi e UI) o eseguire il debug di disconnessioni sporadiche in condizioni reali, possiamo aiutarvi a inquadrarlo in modo mirato: prenda contatto con noi.
Anche nel contesto tecnico i Heartbeat Ping/Pong svolgono un ruolo importante quando integrazioni, flussi dati e sviluppo devono interagire in modo ordinato.
Discutere progetto o iniziativa di modernizzazione con Net-Base.
Passo successivo
Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dall'inizio.
Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.
- Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
- REST, l'accesso ai dati, i portali e il rollout non vengono rimandati a fasi successive.
- Vede in anticipo quale percorso è economicamente ed operativamente sostenibile.