10 errori comuni nell'uso di sgcWebSockets (e come correggerli)

· Componenti

Dopo molti anni a rispondere a ticket di supporto, lo stesso pugno di problemi rappresenta la stragrande maggioranza dei report "la mia connessione WebSocket si comporta in modo strano". Nessuno è un bug della libreria — sono tutti errori di configurazione che si correggono in cinque minuti una volta che sai che esistono. Questo post elenca i dieci più comuni, con il sintomo, la causa e l'one-liner che risolve ciascuno.

Se leggi un solo errore di questa lista, leggi il #2. Bloccare l'evento OnMessage è responsabile di più ticket "la libreria è lenta" che ogni altra categoria messa insieme, ed è invisibile finché non scali oltre poche centinaia di client concorrenti. Risparmiati la sessione di debug notturno in produzione e correggilo prima di spedire.

Errore 1: riconnessione WatchDog disabilitata

Sintomo: il client si connette all'avvio, gira bene per ore, poi smette silenziosamente di ricevere messaggi dopo un blip Wi-Fi, una riconnessione VPN o un riavvio del server.

Causa: la proprietà WatchDog è disattivata per default. La libreria non assume che tu voglia riconnessioni automatiche — alcune app vogliono un comportamento fail-fast. La maggior parte no.

oClient.WatchDog.Enabled  := True;
oClient.WatchDog.Interval := 10;   // try every 10 seconds
oClient.WatchDog.Attempts := 0;    // 0 = unlimited

Fire-and-forget. Combinalo con il logging di OnDisconnect così puoi vedere sul campo quanto spesso si attivano le riconnessioni. Per deployment mobile o laptop in cui l'utente si sposta tra reti, collega anche la notifica di cambio rete dell'OS (SystemEvents.NetworkAvailabilityChanged su .NET, la route WMI su Windows-Delphi) per scatenare una riconnessione immediata invece di aspettare il prossimo tick dell'intervallo.

Errore 2: bloccare l'evento OnMessage

Sintomo: il throughput crolla sotto carico. Il server si sente "bloccato". La CPU è bassa, ma i messaggi si accumulano.

Causa: OnMessage gira sul worker thread di I/O. Se chiami un database lento, un'API HTTP esterna o un parser long-running al suo interno, affami ogni altra connessione che condivide quel worker.

// BAD: blocks the worker
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
  oDb.Query('INSERT INTO events VALUES(?)', [Text]);     // 50 ms
  oHttp.Get('https://billing/track?event=' + Text);      // 200 ms
end;

// GOOD: hand off to a worker queue
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
  oWorkQueue.Push(TWorkItem.Create(Connection.Guid, Text));
end;

Errore 3: ignorare l'HeartBeat

Sintomo: si accumulano connessioni fantasma. Connections.Count continua a crescere anche se sai che i client sono morti.

Causa: senza heartbeat a livello applicativo, il timer di TCP keepalive dell'OS scatta dopo ~2 ore. A quel punto hai decine di migliaia di socket zombi che mangiano memoria.

oServer.HeartBeat.Enabled  := True;
oServer.HeartBeat.Interval := 30;
oServer.HeartBeat.Timeout  := 90;

Errore 4: versioni TLS non corrispondenti

Sintomo: il client riceve "handshake failed" o "SSL_ERROR_SYSCALL". La connessione funziona su una macchina di sviluppo ma fallisce in produzione dietro un proxy aziendale.

Causa: le opzioni SSL del client hanno come default TLS 1.0–1.2 per compatibilità all'indietro; i server moderni richiedono solo TLS 1.2/1.3. O le DLL OpenSSL sono vecchie.

oClient.TLSOptions.Version := tlsTLSv1_2;     // or tlsTLSv1_3
oClient.TLSOptions.OpenSSL_Options.LibPath := ExtractFilePath(ParamStr(0));
// Bundle the latest libcrypto-3.dll / libssl-3.dll with your installer.

Errore 5: non gestire messaggi frammentati

Sintomo: il server riceve la prima parte di un grande documento JSON, il tuo parser fallisce e la connessione viene chiusa.

Causa: per default, sgcWebSockets riassembla i frammenti per te e fa scattare OnMessage solo con il payload completo. Ma se hai impostato ReadOptions.FragmentMode := frgPartial per ragioni di memoria, devi riassemblare.

// Default (recommended for most apps)
oServer.ReadOptions.FragmentMode := frgComplete;

// If you opted into partial delivery, reassemble manually
procedure TForm1.OnFragment(Connection: TsgcWSConnection;
  const aPartial: TBytes; aIsFinal: Boolean);
begin
  Buffers[Connection.Guid].Append(aPartial);
  if aIsFinal then
    Process(Buffers[Connection.Guid].ToBytes);
end;

Errore 6: usare l'API sincrona nel thread GUI

Sintomo: la UI si blocca per diversi secondi alla connessione, o chiamando WriteData su un link lento.

Causa: chiamate bloccanti sul main thread. Usa sempre il pattern async nelle app VCL/FMX, o chiama da un worker thread.

// BAD: blocks the UI
oClient.Active := True;

// GOOD: connect asynchronously
TTask.Run(procedure
  begin
    oClient.Active := True;
  end);

// Or use the event-driven API
oClient.OnConnect := DoConnected;
oClient.Connect;  // returns immediately

Errore 7: dimenticare di selezionare un sotto-protocollo

Sintomo: la connessione ha successo ma il peer rifiuta ogni frame, o risponde con un formato diverso da quello atteso.

Causa: molti server WebSocket (MQTT-su-WS, STOMP, GraphQL-WS, Phoenix) richiedono un sotto-protocollo specifico nell'handshake. Senza, il server fa default a un protocollo diverso o ti scollega.

// MQTT over WebSocket
oClient.Specifications.Hixie76    := False;
oClient.Specifications.RFC6455    := True;
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: mqttv3.1');

// GraphQL over WS
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: graphql-ws');

Errore 8: dimensioni dei buffer troppo piccole (o troppo grandi)

Sintomo: CPU alta su un server che pusha molti messaggi piccoli, o out-of-memory su un server che pusha messaggi grandi.

Causa: i buffer di send/receive di default sono dimensionati per uso generale. Tarali sulla forma del tuo traffico.

// Small messages (chat, telemetry): smaller buffers reduce per-conn memory
oServer.IOHandler.RecvBufferSize := 4096;
oServer.IOHandler.SendBufferSize := 4096;

// Big messages (file transfer, media): larger buffers reduce syscalls
oServer.IOHandler.RecvBufferSize := 65536;
oServer.IOHandler.SendBufferSize := 65536;

Errore 9: nessun controllo dell'Origin su server pubblici

Sintomo: l'audit di sicurezza segnala "qualsiasi sito può connettersi al tuo WebSocket tramite il browser di un utente".

Causa: il protocollo WebSocket non impone la same-origin. Il tuo server deve validare l'header Origin.

procedure TForm1.ServerBeforeConnect(Connection: TsgcWSConnection;
  var Continue: Boolean);
var
  vOrigin: string;
begin
  vOrigin := Connection.Headers.Values['Origin'];
  Continue := (vOrigin = 'https://app.example.com')
           or (vOrigin = 'https://admin.example.com');
end;

Errore 10: loggare dati sensibili in OnMessage

Sintomo: l'auditor trova API key, JWT o PII nei file di log. Problema di compliance.

Causa: il facile LogMemo.Lines.Add(Text) in OnMessage scrive ogni payload su disco per sempre.

procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
  // Hash / redact before logging
  LogMemo.Lines.Add(Format('[%s] %d bytes (sha=%s)',
    [Connection.Guid, Length(Text), ShortHash(Text)]));
  Process(Text);
end;

Bonus: non leggere il history.txt

Ogni release è distribuita con un history.txt che elenca ogni cambiamento, fix e nota di breaking change dal 2013. Cinque minuti spesi a sfogliarlo dopo ogni upgrade risparmiano ore di debug "perché ha smesso di funzionare" più tardi.

Bonus 2: mescolare versioni di componenti tra progetti

Gli sviluppatori Delphi a volte copiano un singolo .pas da una release più recente di sgcWebSockets in un progetto più vecchio, "solo questo file". Funziona finché non funziona — il file dipende da tipi cambiati due release fa, e il linker fallisce misteriosamente o, peggio, linka ma crasha a runtime. Aggiorna sempre l'intera libreria insieme. I 30 secondi risparmiati copiando un file non valgono le quattro ore di debug quando qualcosa a valle si rompe.

Bonus 3: trattare WebSocket come fire-and-forget

WebSocket non è una message queue. È uno stream di byte bidirezionale su TCP. Se la rete cade a metà messaggio, il frame è perso e non viene mai riconsegnato automaticamente. Per messaggi business-critical devi aggiungere il tuo protocollo di acknowledgement in cima — di solito un UUID per messaggio, un ACK esplicito dal ricevitore e un resend dal mittente dopo un timeout. Saltare questo layer va bene per notifiche "user is typing" ed è fatale per "user paid for cart".

Bonus 4: lasciare che i memo perdano memoria

Sintomo: il memo diagnostico sulla tua form di debug è la prima cosa a mandare in OOM il tuo client dopo poche ore di traffico. Incolpi sgcWebSockets; sgcWebSockets è innocente.

Causa: un TMemo trattiene ogni riga mai aggiunta. A 100 righe al secondo sono 360.000 righe all'ora. Ogni riga alloca una stringa. La VCL renderizza migliaia di righe invisibili a ogni WM_PAINT. Il tuo processo si ferma mentre la libreria non sta facendo niente di male.

// Cap the diagnostic memo
procedure TForm1.LogLine(const aText: string);
const
  cMaxLines = 500;
begin
  TThread.Queue(nil, procedure
    begin
      while Memo1.Lines.Count > cMaxLines do
        Memo1.Lines.Delete(0);
      Memo1.Lines.Add(aText);
    end);
end;

Ancora meglio: logga su un file a rotazione via LoggerPro e mirra solo le ultime 200 righe nel memo per debug visivo. Il codice di produzione non dovrebbe mai scrivere su un controllo UI da un thread di rete.

Bonus 5: non chiudere il server prima di Application.Terminate

Sintomo: allo spegnimento dell'app il processo si appende per 30 secondi, o l'OS riporta eccezioni non gestite nei log dei client perché le connessioni sono state strappate in modo non grazioso.

Causa: il distruttore del server invia un frame di close a ogni connessione e aspetta che l'OS rilasci la porta in ascolto. Se chiami Application.Terminate prima di oServer.Active := False, le connessioni muoiono a metà handshake e la porta dell'OS resta in TIME_WAIT, bloccando un riavvio rapido.

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  if oServer.Active then
  begin
    oServer.Broadcast('{"event":"shutdown","reconnect_after":5}');
    Sleep(200);                  // let frames flush
    oServer.Active := False;     // graceful close
  end;
end;

Per server console, aggancia SetConsoleCtrlHandler su Windows o SIGTERM su Linux ed esegui la stessa sequenza di shutdown. Abbinalo a un loop HUP/restart nel tuo service manager e hai deploy con zero connessioni perse.

Il pattern dietro i pattern

La maggior parte di questi errori ha una radice comune: assumere che la rete sia affidabile. Non lo è. Connessioni TCP semi-aperte succedono. Le reti mobile cadono. I proxy aziendali rompono TLS. Il Wi-Fi roama. I server si riavviano. I load balancer cloud terminano connessioni idle dopo 60 secondi. Un'app WebSocket che non sopravvive a queste condizioni non è finita — è una demo del cammino felice. La buona notizia: la libreria espone un controllo per ognuno di questi scenari. La cattiva notizia: la maggior parte di essi è disattivata per default per compatibilità all'indietro. Due ore spese a leggere le pagine delle opzioni WatchDog, HeartBeat, Reconnect e TLS della documentazione sono l'assicurazione più economica che comprerai mai.

Pattern di secondo ordine: rispetta il thread di I/O. Qualunque cosa richieda più di un millisecondo — query al database, I/O su file, chiamata HTTP esterna, regex su una stringa lunga, parsing JSON su un payload da 100 KB — appartiene a un worker thread, non a OnMessage. Rendi questa regola assoluta. Gli sviluppatori junior la violeranno tra tre mesi; una checklist di code-review che includa "nessun blocco in OnMessage" la cattura prima che spedisca.

Dove andare dopo

Se stai ottimizzando un server ad alto traffico, leggi Tuning delle performance di sgcWebSockets dopo. Sei nuovo della libreria? Parti dall'hub Per Iniziare e collega il tuo primo WebSocket in cinque minuti.