Net-Base Магазин

01.06.2026

Delphi WebSocket клијент: робустно повезивање, уредно заустављање, поуздано отклањање грешака

Delphi WebSocket клијент се брзо „како-тако повезује“ — али у експлоатацији пресудни су поновно успостављање везе, heartbeats, коректно заустављање и могућност дебаговања. Уз практичан wrapper заснован на System.Net.WebSockets (са fallback-ом) и исечак кода за руковање нитима и...

01.06.2026

Од теме часописа до пројектне праксе

Одговарајуће странице услуга и техничке странице за чланак

Зашто је Delphi WebSocket клијент у пракси више од „Connect“

Ein Delphi WebSocket Client ist in Minuten zusammengesteckt: URL, Connect, SendText, fertig. У индивидуалном корпоративном софтверу и софтверским решењима блиским процесу, проблем обично постане видљив тек у раду: 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. Поред тога: Ein WebSocket ist ein langlebiger, zustandsbehafteter Kanal – damit gelten andere Regeln als bei klassischem HTTP/REST (Request/Response, kurzlebig).

У овом исечку кода не ради се о „Hello WebSocket“, sondern um einen praxistauglichen Client-Wrapper mit:

  • чистим Start/Stop (ohne Hänger beim Shutdown),
  • Receive-Loop mit Cancellation (Abbruchsignal) statt „Thread kill“,
  • Reconnect mit Backoff (kontrollierte Wiederanbindung),
  • Heartbeat als Anwendungsmuster (weil Ping/Pong nicht überall verfügbar ist),
  • Debug- und Trace-Hooks, die bei Supportfällen wirklich helfen.

Die Umsetzung basiert auf System.Net.WebSockets (Delphi RTL; WebSocket-Client-API mit TClientWebSocket). Где ова RTL-Schicht in älteren Versionen nicht verfügbar oder zu eingeschränkt ist, ist ein Fallback über eine Bibliothek (z. B. ICS) oft sinnvoll – dazu weiter unten eine Einordnung.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Честа грешка у разгранатим Delphi-Anwendungen: UI-Formulare oder Service-Module „sprechen direkt WebSocket“ und haben dann überall Timer, Threads und Ausnahmebehandlungen verteilt. Боље је један јасан компонент са хорошо дефинисаним Events и малом Zustandsmaschine.

Begriffe kurz eingeordnet: Backoff meint eine Wartezeit, die nach Fehlern schrittweise wächst (z. B. 1s, 2s, 4s …), um Server und Netzwerk nicht zu fluten. CancellationToken ist ein Abbruchsignal aus .NET-Welt; in Delphi gibt es kein identisches Pattern, aber wir können es mit TEvent und einem „StopRequested“-Flag nachbilden. TThread.Queue plant Code zur Ausführung im Hauptthread (UI), ohne den Worker zu blockieren; Synchronize blockiert und ist in Shutdown-Pfaden oft der Grund für Deadlocks.

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

Der folgende Code ist bewusst als „Betriebs-Baustein“ aufgebaut: eine Klasse, die man in VCL/FMX oder in einem Windows- und Windows- und Linux-Services (je nach Delphi-Version/Plattform) ähnlich nutzen kann. Језгро је ein Worker-Thread, der den Receive-Loop hält und über Events in die Anwendung meldet.

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; // App-Heartbeat: корисно против прекида сесије услед неактивности (idle timeout) иза проксија
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: радни тред није одговорио у оквиру временског ограничења; могућа блокада у мрежном стеку');
    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
        // Напомена: TClientWebSocket.Connect је синхрон и може блокирати у зависности од DNS/TLS.
        // Због тога ово ради у радничком треду (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 као апликацијска порука, јер Ping/Pong није у сваким верзијама 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;

          // Пријем: заснован на фрејмовима, због тога се користи TStringBuilder за фрагментацију.
          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
                // У многим бизнис-протоколима текст/JSON је стандард.
                // Binary се може овде слично баферирати или проследити директно.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

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

          // Мали паузни Sleep да се смањи оптерећење CPU-а при врло брзом петљирању.
          // Не превелик, иначе се латенција погоршава.
          TThread.Sleep(1);
        end;
      finally
        SB.Free;
      end;

      SafeClose;
      State('closed');

    finally
      WS.Free;
    end;

    if StopRequested then
      Break;

    // Поновно повезивање након чистог затварања или након грешака
    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.

Шта код намерно ради „другачје“ у односу на типичне примере

  • Stop без насилног гашења: Уместо да убија тредове, Stop поставља Event. Worker завршава петље на дефинисаним местима. То смањује заглављивање при затварању и избегава цурење ресурса у Socket-Stack.
  • Queue уместо Synchronize: Логовање и догађаји иду преко TThread.Queue у Mainthread. То је важно када Stop/Shutdown долази из UI или из Service-Control handlera. Synchronize може блокирати ако Mainthread тренутно чека.
  • Фрагментација узета у обзир: WebSocket-текст може доћи фрагментисан у фрејмовима. Због тога TStringBuilder и провера EndOfMessage.
  • Heartbeat као апликациони протокол: Многе конфигурације пропадају због Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Лаган „ping“-текст као оперативни механизам често је ефикаснији од наде у „TCP keepalive“ или API за Ping/Pong који није свуда доступан.

Ограничења и замке у раду

1) DNS, TLS и Proxy: Connect може блокирати

TClientWebSocket.Connect је синхрон. У зависности од DNS-резолуције, TLS-handshake-а, проверe сертификата или proxy-окружења то може трајати неколико секунди. Код свесно смешта то у Worker. Ако вам требају додатни строги timeouts, морате на нивоу API-ја проверити да ли ваша Delphi-верзија пружа опције за timeout, или обухватите Connect у посебан thread и прекинете преко процесне логике. Важно: „прекидање“ овде обично значи „означити везу као покварену и поново покренути Worker“, не „одмах убити socket-операцију“.

2) Idle-Timeouts: зашто је Heartbeat често обавезан

У корпоративним мрежама WebSocket је често терминован иза reverse proxy-ја (nginx, IIS ARR) или load balancer-а. Многе од тих компоненти затварају везе ако током дужег времена не протичу подаци. TCP-Keepalive није увек конфигурисан довољно кратко (и под Windows често пре у минутама него у секундама). Због тога је Heartbeat на нивоу апликације поуздан заобилазни механизам. Обратите пажњу да сервер и клијент користе исти концепт (нпр. „ping“/„pong“ као текст или JSON).

3) Threading и UI: Догађаји морају остати одвојени

Ако је обрада OnText тешка (JSON-парсирање, DB-приступи са BDE-Ablosung mit nativer Anbindung, ажурирања UI), не би требало да блокира све у Mainthread-у. Wrapper доставља само поруку. Типичан образац је: OnText ставља payload у Queue (нпр. TThreadedQueue<string>), посебан Worker обрађује са Backpressure-ом (односно ограниченом дужином Queue). То спречава да при burst-opterećenju UI заглави или да пријем изађе из ритма.

Debugging: шта треба да логујете када се то „повремено“ прекида

WebSockets су познати по „ради данима, па онда не ради“. Без логовања то је тешко дијагностиковати. Смислене тачке за логовање:

  • Временски жиг (UTC), URL и промене стања (connecting/open/closed).
  • Close-Reason, ако је доступан (server иницира Close naspram мрежне greške).
  • Грешке при слању Heartbeat-а и изузеци при пријему, укључујући тип изузетка.
  • Опционално: величине примљених порука (не њихов садржај), да би се детектовала експлозија података.

Ако терминате преко TLS-а: додатно проверите да ли се промена сертификата (истек, нови Issuer) временски поклапа са грешкама. У ојачаним окружењима и proxy и DPI-уређаји (Deep Packet Inspection) могу бити узрок.

Варијанте: када System.Net.WebSockets довољан – а када није

System.Net.WebSockets је довољан за многе интеграционе случајеве, нарочито када је реч о тексту/JSON-у, умереном оптерећењу и јасним стратегијама поновног повезивања. Ограничења се појављују у зависности од Delphi-верзије и циљне платформе:

  • Недостајућа/ограничена Ping/Pong подршка: у тим случајевима модел App-Heartbeat остаје робустан образац.
  • Недостајући Timeouts/Cancellation у Connect/Receive: онда морате архитектуру да дизајнирате тако да заглављени Worker буде изолован, а апликација ипак може чисто да се затвори (нпр. преко процес-watchdога или одвојених инстанци воркера).
  • Високо оптерећење или бинарни токови: онда се исплати снажнији концепт фрејмовања/баферисања (нпр. ring buffer, одвојени Binary-Event, Message-Assembler са лимитима).

За legacy ситуације (старије Delphi-генерације, веома специфични TLS/Proxy захтеви) библиотеке попут ICS у појединим пројектима могу бити прагматичније. Важно је мање „која библиотека“, а више да третирaте Shutdown, Reconnect и Observability (Logs/Metriken) као теме првог реда.

Закључак: Delphi WebSocket клиент је оперативни грађевни блок – са јасним границама

WebSocket се одлично слаже за push-догађаје, live-статус, машинске или процесне пријаве и као повратни канал за портале и сервисе. Приказани Wrapper се фокусира на тачке које у дигиталним корпоративним решењима често праве разлику: контролисани Reconnect, Heartbeat против Idle-Timeouts, фрагментно-сигурна обрада текста и путања за стоп који при деплоју или надоградњи не заглављују.

Ограничења остају: ако вам требају строге гаранције за прекид Connect/Receive у веома кратким временским прозорима или ако имате екстремно високе пропусне стопе, мораћете дубље да улетите у конфигурацију тайм-аута, посебности платформе и евентуално алтернативне стекове. За већину интеграционих и модернизационих сценарија, међутим, добро капсулирани, добро логовани клиент као горе представљен је солидна основа која се може интегрисати у постојеће Delphi системе.

Ако желите такав грађевни блок да ускладите са постојећом архитектуром (нпр. Layer-3 архитектура са јасним сервисним и UI слојевима) или да при спорадичним disconnect-овима у реалним условима радите дебаг, можемо то циљано са вама размотрити: Контактирајте нас.

У стручном контексту Heartbeat Ping/Pong такође игра важну улогу када интеграције, токови података и даљи развој морају да функционишу усклађено.

Пројекат или модернизациони захтев размотрити са Net-Base.

Следећи корак

Када тема прерасте у реалан пројекат, архитектуру, постојеће системе и операције треба рано разматрати заједно.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Постојеће стање, циљано стање и технички ризици оцењују се заједно.
  • REST, приступ подацима, портали и роллаут се неће одлагати као накнадне последице.
  • Ви рано видите који пут је економски и оперативно одржив.

Подели објаву

Поделите ову објаву директно

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта су одмах доступни. За Instagram припремамо линк и кратак текст.

Е-пошта

Инстаграм се отвара у новој картици. Линк и кратак текст се претходно копирају у међуспремник.