Net-Base Revistă

01.06.2026

Delphi WebSocket Client: conectare robustă, oprire curată, depanare fiabilă

Un Delphi WebSocket Client se conectează „cumva” rapid – dar în exploatare contează reconectarea, heartbeats, oprirea ordonată și depanarea. Cu un wrapper potrivit pentru producție bazat pe System.Net.WebSockets (cu fallback) și un fragment de cod sursă pentru gestionarea firelor de execuție și...

01.06.2026

De la tema din revistă la practica în proiecte

Pagini relevante de servicii și pagini tehnice pentru articol

De ce un client WebSocket Delphi în practică este mai mult decât „Connect”

Un Delphi WebSocket Client este asamblat în câteva minute: URL, Connect, SendText, gata. În software‑ul individual pentru companii și în soluțiile apropiate de proces, însă, problema apare de obicei în funcționare: Reverse Proxy separă conexiunile inactive, legăturile mobile sau VPN au timeout‑uri NAT scurte, certificatele se schimbă, iar la închidere procesul rămâne blocat deoarece o buclă de recepție încă blochează. În plus: un WebSocket este un canal de durată, cu stare — se aplică reguli diferite față de HTTP/REST clasic (Request/Response, efemer).

În acest fragment de cod nu este vorba despre „Hello WebSocket”, ci despre un wrapper de client pregătit pentru producție, cu:

  • pornire/oprire curată (fără blocări la închidere),
  • buclă de recepție cu Cancellation (semnal de anulare) în loc de „Thread kill”,
  • reconectare cu Backoff (reatasare controlată),
  • heartbeat ca model de aplicație (deoarece Ping/Pong nu este disponibil peste tot),
  • hook‑uri de debug și trace care ajută efectiv în cazuri de suport.

Implementarea se bazează pe System.Net.WebSockets (Delphi RTL; API client WebSocket cu TClientWebSocket). Acolo unde acest strat RTL nu este disponibil în versiunile mai vechi sau este prea restricționat, un fallback printr-o bibliotecă (de ex. ICS) este adesea util — mai jos urmează o încadrare.

Schiță de arhitectură: un Wrapper în locul apelurilor WebSocket răspândite

O greșeală frecventă în aplicații Delphi mature: formularele UI sau modulele de serviciu „vorbesc direct cu WebSocket” și astfel ajung să aibă timer‑e, thread‑uri și tratare a excepțiilor distribuite peste tot. Mai bine este un modul clar cu evenimente bine definite și o mică mașină de stări.

Termeni, scurt: Backoff înseamnă o durată de așteptare care crește treptat după erori (de ex. 1s, 2s, 4s …), pentru a nu inunda serverul și rețeaua. CancellationToken este un semnal de anulare din lumea .NET; în Delphi nu există un pattern identic, dar îl putem reproduce cu TEvent și un flag „StopRequested”. TThread.Queue programează cod pentru execuție în thread‑ul principal (UI), fără a bloca worker‑ul; Synchronize blochează și este adesea cauza deadlock‑urilor în căile de shutdown.

Fragment de cod: Delphi WebSocket Client cu Stop, Reconnect și distribuire a mesajelor

Codul următor este conceput intenționat ca un „Betriebs‑Baustein”: o clasă pe care o puteți utiliza în mod similar în VCL/FMX sau într-un Windows- și Windows- und Linux-Services (în funcție de versiunea/platforma Delphi). Nucleul este un worker‑thread care întreține bucla de recepție și raportează către aplicație prin evenimente.

Delphi
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 la nivel de aplicație: util împotriva timeout-urilor idle din spatele proxy-urilor
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 nu răspunde în intervalul de Timeout; posibil blocaj în stack-ul de rețea');
    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
        // Observație: TClientWebSocket.Connect este sincron și se poate bloca în funcție de DNS/TLS.
        // De aceea rulează aici în 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 ca mesaj la nivel de aplicație, deoarece Ping/Pong nu este expus corect în fiecare versiune 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: bazat pe cadre (frames), prin urmare StringBuilder pentru fragmentare.
          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
                // În multe protocoale business, Text/JSON este standard.
                // Binarele pot fi tamponate similar aici sau retransmise direct.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

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

          // Pauză scurtă (mini-sleep) pentru a proteja CPU în bucle foarte rapide.
          // Să nu fie prea mare, altfel latenta se deteriorează.
          TThread.Sleep(1);
        end;
      finally
        SB.Free;
      end;

      SafeClose;
      State('closed');

    finally
      WS.Free;
    end;

    if StopRequested then
      Break;

    // Reconnect după închidere curată sau după erori
    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.

Was der Code absichtlich „anders“ macht als typische Beispiele

  • Stop ohne Gewalt: Statt Threads abzuschießen, setzt Stop ein Event. Der Worker beendet Loops an definierten Stellen. Das reduziert Hänger beim Beenden und vermeidet Ressourcenlecks im Socket-Stack.
  • Queue statt Synchronize: Logging und Events gehen per TThread.Queue in den Mainthread. Das ist wichtig, wenn Stop/Shutdown aus dem UI oder aus Service-Control-Handlern kommt. Synchronize kann blockieren, wenn der Mainthread gerade wartet.
  • Fragmentierung berücksichtigt: WebSocket-Text kann in Frames fragmentiert kommen. Deshalb der TStringBuilder und das Prüfen von EndOfMessage.
  • Heartbeat als App-Protokoll: Viele Setups sterben an Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Ein leichtgewichtiger „ping“-Text ist als Betriebshebel oft effektiver als die Hoffnung auf „TCP keepalive“ oder ein nicht überall verfügbares Ping/Pong-API.

Randbedingungen und Stolperfallen im Betrieb

1) DNS, TLS und Proxy: Connect kann blockieren

TClientWebSocket.Connect ist synchron. Je nach DNS-Auflösung, TLS-Handshake, Zertifikatsprüfung oder Proxy-Umgebung kann das mehrere Sekunden dauern. Der Code legt das bewusst in einen Worker. Wenn Sie zusätzlich harte Timeouts brauchen, müssen Sie auf API-Ebene prüfen, ob Ihre Delphi-Version Timeout-Optionen bereitstellt, oder Sie kapseln Connect in einen separaten Thread und brechen über Prozesslogik ab. Wichtig: Ein „abbrechen“ heißt hier meist „Verbindung als kaputt markieren und Worker neu aufsetzen“, nicht „Socket-Operation sofort killen“.

2) Idle-Timeouts: warum Heartbeat häufig Pflicht ist

In Unternehmensnetzwerken ist ein WebSocket oft hinter einem Reverse Proxy (nginx, IIS ARR) oder einem Load Balancer terminiert. Viele dieser Komponenten schließen Verbindungen, wenn über längere Zeit keine Daten fließen. TCP-Keepalive ist nicht immer kurz genug konfiguriert (und unter Windows oft eher Minuten als Sekunden). Ein Heartbeat auf Anwendungsebene ist deshalb ein stabiler Workaround. Achten Sie darauf, dass Server und Client das gleiche Konzept haben (z. B. „ping“/„pong“ als Text oder JSON).

3) Threading und UI: Ereignisse müssen entkoppelt bleiben

Wenn die OnText-Verarbeitung schwer ist (JSON-Parsing, DB-Zugriffe mit BDE-Ablosung mit nativer Anbindung, UI-Updates), sollte sie nicht im Mainthread alles blockieren. Der Wrapper liefert nur die Nachricht. Ein typisches Muster ist: OnText legt die Payload in eine Queue (z. B. TThreadedQueue<string>), ein separater Worker verarbeitet mit Backpressure (also begrenzter Queue-Länge). Das verhindert, dass bei Burst-Last die UI einfriert oder der Empfang aus dem Tritt kommt.

Debugging: was Sie loggen sollten, wenn es „manchmal“ abbricht

WebSockets sind berüchtigt für „läuft tagelang, dann nicht mehr“. Ohne Logging ist das kaum einzugrenzen. Sinnvolle Logpunkte:

  • Zeitstempel (UTC), URL, und Zustandswechsel (connecting/open/closed).
  • Close-Reason, sofern verfügbar (Server fordert Close vs. Netzwerkfehler).
  • Heartbeat-Sendefehler und Receive-Ausnahmen inkl. Exception-Typ.
  • Optional: Größen der empfangenen Nachrichten (nicht die Inhalte), um Datenexplosion zu erkennen.

Wenn Sie über TLS terminieren: Prüfen Sie zusätzlich, ob Zertifikatswechsel (Ablauf, neuer Issuer) zeitlich mit Fehlern korrelieren. In gehärteten Umgebungen sind auch Proxy- und DPI-Boxen (Deep Packet Inspection) Kandidaten.

Variante: când System.Net.WebSockets este suficient – și când nu

System.Net.WebSockets este suficient pentru multe cazuri de integrare, mai ales când este vorba de text/JSON, încărcare moderată și strategii clare de reconnect. Limitele apar în funcție de versiunea Delphi și de platforma țintă:

  • Suport Ping/Pong absent sau limitat: În aceste situații, App-Heartbeat rămâne modelul robust.
  • Lipsa timeout-urilor/cancelării la Connect/Receive: Atunci trebuie să proiectați arhitectura astfel încât un worker blocat să rămână izolat și aplicația să se încheie curat (de exemplu printr-un watchdog de proces sau instanțe de worker separate).
  • Sarcină mare sau stream-uri binare: Atunci merită un concept mai solid de framing/buffering (de ex. ring buffer, eveniment binar separat, asamblor de mesaje cu limite).

Pentru situații legacy (generații mai vechi de Delphi, cerințe TLS/Proxy foarte specifice) unele proiecte găsesc biblioteci precum ICS mai pragmatice. Important nu este atât „ce bibliotecă”, cât să tratați Shutdown, Reconnect și Observability (logs/metrice) ca subiecte de primă clasă.

Concluzie: un client WebSocket Delphi este un component operațional – cu limite clare

Un WebSocket se potrivește foarte bine pentru push-events, status live, mesaje de la maşini sau procese și ca canal de retur pentru portaluri și servicii. Wrapper‑ul prezentat se concentrează pe punctele care, în soluțiile digitale pentru companii, fac adesea diferența: reconnect controlat, heartbeat contra idle‑timeouturilor, procesare a textelor sigură la fragmentare și o cale de oprire care nu blochează la implementare sau actualizare.

Persistă limitele de utilizare: dacă aveți nevoie de garanții stricte pentru întreruperea Connect/Receive în ferestre de timp foarte scurte sau rulați rate de date extrem de mari, trebuie să aprofundați timeout‑urile, particularitățile platformei și, eventual, stack‑uri alternative. Pentru majoritatea scenariilor de integrare și modernizare, totuși, un client bine încapsulat și bine logat ca cel de mai sus reprezintă o bază solidă, care se poate integra în sisteme Delphi dezvoltate în timp.

Dacă trebuie să integrați un astfel de component într‑o arhitectură existentă (de ex. Layer-3 Architektur cu straturi clare de service și UI) sau să depanați disconnect‑uri sporadice în condiții reale, putem evalua situația împreună: Contactați‑ne.

În contextul profesional, Heartbeat Ping/Pong joacă de asemenea un rol important când integrațiile, fluxurile de date și evoluția continuă trebuie să funcționeze corect împreună.

Discutați un proiect sau un demers de modernizare cu Net-Base.

Următorul pas

Când o temă devine un proiect real, arhitectura, infrastructura existentă și operarea trebuie analizate împreună de la început.

Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.

  • Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
  • REST, accesul la date, portalurile și Rollout nu sunt amânate ca consecințe ulterioare.
  • Veți vedea din timp ce cale este viabilă din punct de vedere economic și operațional.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.