Net-Base Magazin

01.06.2026

Delphi WebSocket-kliens: robusztusan csatlakozni, rendezetten leállítani, megbízhatóan hibakeresni

Egy Delphi WebSocket-kliens gyorsan „valahogy csatlakozik” – de az üzemeltetés során a Reconnect, a Heartbeats, a tiszta leállítás és a hibakereshetőség számít. Egy gyakorlatias wrapper a System.Net.WebSockets alapjain (Fallback-megoldással) és egy forráskódrészlet a szálkezeléshez és...

01.06.2026

A magazintémától a projektgyakorlatig

A bejegyzéshez tartozó szolgáltatási és technikai oldalak

Miért több egy Delphi WebSocket kliens a gyakorlatban, mint a „Connect”

Ein Delphi WebSocket Client ist in Minuten zusammengesteckt: URL, Connect, SendText, fertig. In individueller Unternehmenssoftware und prozessnahen Softwarelösungen kippt das Thema aber meist erst im Betrieb: Der Reverse Proxy trennt Idle-Verbindungen, Mobil- oder VPN-Strecken haben kurze NAT-Timeouts, Zertifikate wechseln, und beim Beenden hängt der Prozess, weil ein Receive-Loop noch blockiert. Dazu kommt: Ein WebSocket ist ein langlebiger, zustandsbehafteter Kanal – damit gelten andere Regeln als bei klassischem HTTP/REST (Request/Response, kurzlebig).

Ebben a forráskódrészletben nem a „Hello WebSocket” a célpont, hanem egy üzemeltetésre alkalmas kliens-wrapper, amely:

  • tiszta Start/Stop viselkedést biztosít (nem akad le a leállításnál),
  • Receive-Loop Cancellation-nel (megszakítási jel) a „Thread kill” helyett,
  • Reconnect Backoff-pal (vezérelt újracsatlakozás),
  • Heartbeat mint alkalmazási minta (mivel Ping/Pong nem mindenütt elérhető),
  • debug- és trace-hookok, amelyek támogatási esetekben valódi segítséget nyújtanak.

Az implementáció a System.Net.WebSockets-re épül (Delphi RTL; WebSocket-Client-API TClientWebSocket-tel). Ha ez az RTL-réteg régebbi verziókban nem érhető el, vagy túlzottan korlátozott, gyakran célszerű egy külső könyvtárra (pl. ICS) váltani – erről lentebb található egy besorolás.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Egy gyakori hiba a felhalmozódott Delphi-alkalmazásokban: a felhasználói felület űrlapjai vagy szolgáltatásmodulok „közvetlenül WebSocket-ként beszélnek”, és ennek következtében mindenütt időzítők, threadek és kivételkezelések szóródnak. Jobb egy egyértelmű komponens jól definiált eseményekkel és egy kis állapotgéppel.

Fogalmak röviden: Backoff egy várakozási időt jelent, amely hibák után fokozatosan növekszik (pl. 1s, 2s, 4s …), hogy ne terheljük túl a szervert és a hálózatot. CancellationToken a .NET világából ismert megszakítási jel; Delphi-ban nincs pontosan azonos minta, de ezt TEvent-tel és egy „StopRequested” flaggel modellezhetjük. TThread.Queue kódot ütemez a főszálon (UI) végrehajtásra anélkül, hogy blokkolná a munkavégzőt; Synchronize blokkol, és a leállítási utakban gyakran okozza a deadlockokat.

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

A következő kód szándékosan „üzemi komponensként” van felépítve: egy osztály, amely VCL/FMX-ben vagy egy Windows- és Windows- und Linux-Services (a Delphi-verziótól/ -platformtól függően) hasonló módon használható. A magja egy worker-thread, amely fenntartja a receive-loopot és eseményeken keresztül jelenti az alkalmazásnak.

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; // Alkalmazás-heartbeat: hasznos a proxytól eredő inaktivitási timeoutok ellen
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: A worker nem reagál a megadott időkorláton belül; lehetséges blokkolás a hálózati stackben');
    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
        // Megjegyzés: a TClientWebSocket.Connect szinkron, és DNS/TLS-től függően blokkolhat.
        // Ezért ez itt a worker szálon fut.
        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 alkalmazásüzenetként, mert a Ping/Pong nem érhető el megbízhatóan minden Delphi-verzióban.
          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;

          // Fogadás: frame-alapú, ezért StringBuilder a fragmentálás kezelésére.
          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
                // Sok üzleti protokollban a Text/JSON a szabvány.
                // Binary-t hasonlóan lehet itt pufferelni vagy közvetlenül továbbadni.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

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

          // Rövid szünet, hogy nagyon gyors ciklusnál kímélje a CPU-t.
          // Ne legyen túl nagy, különben romlik a késleltetés.
          TThread.Sleep(1);
        end;
      finally
        SB.Free;
      end;

      SafeClose;
      State('closed');

    finally
      WS.Free;
    end;

    if StopRequested then
      Break;

    // Újracsatlakozás tiszta lezárás vagy hibák után
    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.

Miért teszi a kód szándékosan „másképp”, mint a tipikus példák

  • Stop ohne Gewalt: Ahelyett, hogy szálakat kilőnénk, a Stop egy eseményt állít be. A Worker meghatározott helyeken fejezi be a ciklusokat. Ez csökkenti a lefagyásokat leállításkor és elkerüli az erőforrás-szivárgásokat a socket-stackben.
  • Queue statt Synchronize: A naplózás és az események a TThread.Queue-n keresztül jutnak el a főszálhoz. Ez fontos, ha a Stop/Shutdown az UI-ból vagy Service-Control-handlerből érkezik. A Synchronize blokkolhat, ha a főszál épp várakozik.
  • Fragmentáció figyelembevétele: A WebSocket-szöveg frame-ekben atomonként érkezhet. Ezért van a TStringBuilder és az EndOfMessage ellenőrzése.
  • Heartbeat mint alkalmazási protokoll: Sok környezet elbukik az idle-timeoutok miatt (Load Balancer, nginx, Cloud WAF). Egy könnyű „ping” szöveg üzemeltetési mechanizmusként gyakran hatékonyabb, mint a „TCP keepalive”-tól való remény vagy egy nem mindenhol elérhető Ping/Pong-API.

Üzemeltetési feltételek és buktatók

1) DNS, TLS und Proxy: Connect kann blockieren

TClientWebSocket.Connect szinkron. A DNS-felbontástól, TLS-kézfogástól, tanúsítvány-ellenőrzéstől vagy proxy-környezettől függően ez több másodpercig is eltarthat. A kód szándékosan egy Workerbe helyezi ezt. Ha további szigorú időkorlátokra van szükség, API-szinten ellenőrizze, hogy az Ön Delphi-verziója kínál-e timeout-opciókat, vagy csomagolja a Connect-et egy külön szálba, és szakítsa meg folyamatlogikával. Fontos: itt a „megszakítás” általában azt jelenti, hogy a kapcsolatot hibásnak jelölik és a Workert újra felépítik, nem pedig a socket-művelet azonnali kilövése.

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

Vállalati hálózatokban a WebSocket gyakran egy reverse proxy (nginx, IIS ARR) vagy egy load balancer mögött végződik. Sok ilyen komponens lezárja a kapcsolatot, ha hosszabb ideig nem áramlik adat. A TCP-Keepalive nem mindig van elég rövidre konfigurálva (és Windows alatt gyakran inkább percekről van szó, mint másodpercekről). Ezért egy alkalmazási szintű Heartbeat stabil workaround. Ügyeljen rá, hogy a szerver és a kliens ugyanazt a koncepciót használja (pl. „ping”/„pong” szövegként vagy JSON-ként).

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

Ha az OnText-feldolgozás erőforrásigényes (JSON-parszolás, adatbázis-hozzáférések BDE-kiváltás natív csatlakozással, UI-frissítések), akkor nem szabad, hogy mindent blokkoljon a főszálon. A wrapper csak az üzenetet adja át. Egy tipikus minta: az OnText a payloadot beteszi egy Queue-ba (pl. TThreadedQueue<string>), egy külön Worker backpressure-szel dolgozza fel (azaz korlátozott Queue-hosszal). Ez megakadályozza, hogy burst-terhelés esetén a UI lefagyjon vagy a fogadás kicsússzon a ritmusból.

Hibakeresés: mit naplózzon, ha „néha” megszakad

A WebSocketek híresek: „napokig fut, aztán egyszer csak megszakad”. Napló nélkül ezt alig lehet lokalizálni. Érdemi logpontok:

  • Időbélyeg (UTC), URL és állapotváltások (connecting/open/closed).
  • Lezárás oka (Close-Reason), ha elérhető (szerver kezdeményezi a Close vs. hálózati hiba).
  • Heartbeat-küldési hibák és fogadási kivételek, a kivétel típusával együtt.
  • Opcionális: a beérkező üzenetek méretei (nem a tartalom), hogy az adatrobbanás felismerhető legyen.

Ha TLS-nél terminál: vizsgálja azt is, hogy a tanúsítványcserék (lejárat, új issuer) időben korrelálnak-e a hibákkal. Megerősített környezetekben a proxy- és DPI-eszközök (Deep Packet Inspection) is gyanúsítottak.

Variánsok: mikor elegendő a System.Net.WebSockets – és mikor nem

System.Net.WebSockets sok integrációs esetben elegendő, különösen szöveg/JSON, mérsékelt terhelés és egyértelmű újracsatlakozási stratégiák esetén. A korlátok a Delphi-verziótól és a célplatformtól függően jelentkeznek:

  • Hiányzó/korlátozott Ping/Pong-támogatás: Ilyenkor az alkalmazás-Heartbeat marad a robusztus minta.
  • Hiányzó Timeoutok/Cancellation a Connect/Receive műveletekben: Ilyenkor úgy kell kialakítani az architektúrát, hogy egy beragadt worker izolált maradjon, és az alkalmazás mégis tisztán leállítható legyen (pl. folyamat-watchdoggal vagy külön worker-példányokkal).
  • Nagy terhelés vagy bináris stream-ek esetén: Ilyenkor érdemes erősebb framing/buffering koncepciót alkalmazni (pl. ring buffer, külön Binary-Event, Message-Assembler korlátokkal).

Legacy helyzetekben (régebbi Delphi-generációk, nagyon specifikus TLS/Proxy-követelmények) bizonyos projektekben pragmatikusabbak lehetnek az olyan könyvtárak, mint az ICS. Fontosabb a „melyik Library“ kérdésénél, hogy a Shutdown, Reconnect és Observability (logok/metrikák) elsőrendű témaként legyen kezelve.

Következtetés: egy Delphi WebSocket Client egy üzemeltetési építőelem – világos korlátokkal

Egy WebSocket kiválóan alkalmas push eseményekre, élő állapotra, gépi vagy folyamatjelzésekre, valamint visszacsatornaként portálok és szolgáltatások számára. A bemutatott wrapper azokra a pontokra fókuszál, amelyek digitális vállalati megoldásoknál gyakran döntőek: kontrollált reconnect, heartbeat az idle-timeoutok ellen, fragmentbiztos szövegfeldolgozás és egy stop-útvonal, amely a deployment vagy update során nem akad meg.

Használati korlátok megmaradnak: ha szigorú garanciákra van szüksége a Connect/Receive megszakítására nagyon rövid időablakokban, vagy rendkívül nagy adatátviteli sebességgel dolgozik, mélyebben kell foglalkoznia a timeoutokkal, platform-specifikus sajátosságokkal és esetleg alternatív stackekkel. A legtöbb integrációs és modernizációs forgatókönyv számára azonban egy tisztán kapszulázott, jól logolt client, mint a fent bemutatott, szilárd alap, amely beilleszthető meglévő Delphi-rendszerekbe.

Ha egy ilyen építőelemet be kíván illeszteni egy meglévő architektúrába (pl. Layer-3 architektúra egyértelmű service- és UI-rétegekkel), vagy valós körülmények között fellépő időszakos disconnecteknél debugolni kell, ezt célzottan segítünk besorolni: Lépjen kapcsolatba velünk.

A szakmai környezetben a Heartbeat Ping/Pong is fontos szerepet játszik, ha az integrációknak, az adatfolyamoknak és a további fejlesztéseknek tisztán kell együttműködniük.

Projektet vagy modernizációs feladatot megvitatni Net-Base-vel.

Következő lépés

Ha egy témából valós projekt lesz, az architektúrát, a meglévő rendszert és az üzemeltetést korai fázisban együtt kell vizsgálni.

Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.

  • A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
  • REST, az adathozzáférést, a portálokat és a bevezetést nem halasztjuk későbbi fázisokra.
  • Ön korán látja, melyik út gazdaságilag és üzemeltetési szempontból tartható.

Bejegyzés megosztása

Ezt a bejegyzést közvetlenül megosztani

LinkedIn, X, XING, Facebook, WhatsApp és E-Mail azonnal elérhetők. Instagramra a linket és a rövid szöveget közvetlenül előkészítjük.

E-mail

Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.