Net-Base Žurnāls

01.06.2026

Delphi WebSocket klients: robusti pieslēgties, korekti apturēt, uzticami atkļūdot

Delphi WebSocket klients ātri var izskatīties „kaut kā pieslēgts“ — taču ekspluatācijā būtiskas ir atkārtotas pieslēgšanās mehānikas, heartbeat signāli, korekta apturēšana un atkļūdošanas iespējas. Ar praksi pārbaudītu wrapperi, balstītu uz System.Net.WebSockets (ar fallback) un avota koda fragmentu pavedienu pārvaldībai un...

01.06.2026

No žurnāla tēmas līdz projektu praksei

Atbilstošas pakalpojumu un tehniskās lapas rakstam

Kāpēc ein Delphi WebSocket Client praksē ir vairāk nekā „Connect“

Ein Delphi WebSocket Client ir salikts minūtēs: URL, Connect, SendText, gatavs. Tomēr individuālajā uzņēmumu programmatūrā un ar procesiem saistītos risinājumos problēmas parasti parādās tikai ekspluatācijā: Reverse Proxy pārtrauc inaktīvās savienojumus, mobilās vai VPN līnijas ir ar īsiem NAT-Timeouts, sertifikāti tiek mainīti, un izbeidzot procesu tas var iestrēgt, jo Receive-Loop joprojām bloķē. Papildus: WebSocket ir ilglaicīgs, stāvokli saglabājošs kanāls — tam ir citas likumsakarības nekā klasiskajam HTTP/REST (Request/Response, īslaicīgs).

Šajā Source-Schnipsel nav runa par „Hello WebSocket“, bet gan par praksē izmantojamu klienta apvalku ar:

  • sakārtotu Start/Stop (bez iestrēgšanas izslēgšanas laikā),
  • Receive-Loop ar Cancellation (pārtraukšanas signāls) nevis „Thread kill“,
  • Reconnect ar Backoff (kontrolēta atkārtota pieslēgšanās),
  • Heartbeat kā lietojuma modelis (jo Ping/Pong nav pieejams visur),
  • Debug- un Trace-Hooks, kas patiesi palīdz atbalsta gadījumos.

Realizācija balstīta uz System.Net.WebSockets (Delphi RTL; WebSocket-Client-API ar TClientWebSocket). Ja šī RTL slāņa vecākās versijās nav pieejama vai tā ir pārāk ierobežota, bieži lietderīgs ir fallback, izmantojot bibliotēku (piem., ICS) — par to zemāk sniegts skaidrojums.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Bieži sastopama kļūda krietni audzētās Delphi-lietotnēs: UI-formas vai servisa moduļi „sprechen direkt WebSocket“ un tad visur ir izkaisīti Timer, Threads un izņēmumu apstrāde. Labāka pieeja ir skaidrs komponents ar labi definētiem Events un nelielu stāvokļa mašīnu.

Terminu īss skaidrojums: Backoff nozīmē gaidīšanas laiku, kas pēc kļūdām pakāpeniski palielinās (piem., 1s, 2s, 4s …), lai neuzlādētu serveri un tīklu. CancellationToken ir pārtraukšanas signāls no .NET pasaules; in Delphi nav identiska modeļa, bet to var atdarināt ar TEvent un „StopRequested“ karodziņu. TThread.Queue plāno kodu izpildei galvenajā pavedienā (UI), nekavējot worker; Synchronize bloķē un bieži shutdown ceļos ir deadlock cēlonis.

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

Sekojošais kods apzināti veidots kā „Betriebs-Baustein“: klase, kuru var līdzīgi izmantot VCL/FMX vai Windows- un Windows- und Linux-Services (atkarībā no Delphi versijas/platformas). Kodols ir worker-thread, kas uztur Receive-Loop un caur Events paziņo lietotnei.

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: noderīgs pret inaktivitātes timeoutiem aiz starpniekserveriem
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: Darba pavediens nereaģē robežas laikā; iespējams bloķējums tīkla stekā');
    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
        // Piezīme: TClientWebSocket.Connect ir sinhrons un var bloķēt atkarībā no DNS/TLS.
        // Tāpēc tas tiek izpildīts šeit darba pavedienā.
        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 kā lietotnes ziņa, jo Ping/Pong ne vienmēr ir korekti pieejams katrā Delphi versijā.
          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;

          // Saņemšana: rāmju bāzēta, tāpēc TStringBuilder fragmentācijai.
          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
                // Daudzos biznesa protokolos teksts/JSON ir noklusējums.
                // Bināros datus šeit var līdzīgi buferēt vai nodot tālāk tieši.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

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

          // Īsa pauze, lai ļoti ātrā ciklā saudzētu CPU.
          // Nepārāk gara, citādi pasliktinās latentums.
          TThread.Sleep(1);
        end;
      finally
        SB.Free;
      end;

      SafeClose;
      State('closed');

    finally
      WS.Free;
    end;

    if StopRequested then
      Break;

    // Reconnect nach sauberem Close oder nach Fehlern
    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.

Kas kods apzināti „citādāk” dara nekā tipiskie piemēri

  • Stop bez vardarbības: Tā vietā, lai pavedienus nogalinātu, Stop iestata Event. Darba pavediens pabeidz cilpas definētās vietās. Tas samazina iesaldēšanu izbeigšanas laikā un novērš resursu noplūdes Socket-Stack.
  • Queue nevis Synchronize: Žurnālu ieraksti un notikumi tiek virzīti uz galveno pavedienu, izmantojot TThread.Queue. Tas ir svarīgi, ja Stop/Shutdown tiek izsaukts no UI vai no Service-Control handleriem. Synchronize var bloķēt, ja galvenais pavediens pašlaik gaida.
  • Ņemot vērā fragmentāciju: WebSocket teksts var tikt fragmentēts pa frame’iem. Tāpēc tiek izmantots TStringBuilder un tiek pārbaudīts EndOfMessage.
  • Heartbeat kā aplikācijas protokols: Daudzi risinājumi mirst dēļ idle-timeoutiem (Load Balancer, nginx, Cloud WAF). Viegls „ping” teksts kā darbības sviras bieži vien ir efektīvāks nekā cerēt uz „TCP keepalive” vai uz Ping/Pong‑API, kas nav pieejami visur.

Darbības nosacījumi un riski ekspluatācijā

1) DNS, TLS un Proxy: Connect var bloķēt

TClientWebSocket.Connect ir sinhrons. Atkarībā no DNS atrisināšanas, TLS rokasspiediena, sertifikāta pārbaudes vai proxy vides tas var aizņemt vairākas sekundes. Kods apzināti izvieto šo operāciju worker. Ja nepieciešami stingri timeoutu ierobežojumi, API līmenī pārbaudiet, vai jūsu Delphi‑versija nodrošina timeout opcijas, vai arī iesaiņojiet Connect atsevišķā pavedienā un pārtrauciet to ar procesu loģiku. Svarīgi: „atcelt” parasti nozīmē „atzīmēt savienojumu kā bojātu un uzsākt worker no jauna”, nevis „nekavējoties nogalināt socket‑operāciju”.

2) Idle-Timeouts: kāpēc Heartbeat bieži vien ir obligāts

Uzņēmumu tīklos WebSocket bieži tiek termiņēts aiz reversā proxy (nginx, IIS ARR) vai load balancera. Daudzas no šīm komponentēm slēdz savienojumus, ja ilgstoši nenotiek datu plūsma. TCP‑keepalive ne vienmēr ir konfigurēts ar pietiekami īsu intervālu (un zem Windows bieži tas ir drīzāk minūtēs nekā sekundēs). Tāpēc Heartbeat lietojuma līmenī ir stabils risinājums. Pārliecinieties, ka serveris un klients izmanto vienu un to pašu konceptu (piem., „ping”/„pong” kā tekstu vai JSON).

3) Threading und UI: Notikumiem jāpaliek atdalītiem

Ja OnText apstrāde ir smaga (JSON parsēšana, DB piekļuve ar BDE-aizvietošana ar vietēju pieslēgumu, UI atjauninājumi), tā nedrīkst bloķēt galveno pavedienu. Wrapperis nodod tikai ziņu. Tipisks paraugs: OnText noliek payload rindā (piem., TThreadedQueue<string>), atsevišķs worker to apstrādā ar backpressure (t.i., ierobežotas rindas garuma). Tas novērš, ka pie burst‑slodzes UI iesaldējas vai saņemšana iziet no ritma.

Atkļūdošana: ko jālogē, ja tas „reizēm” pārtrauc darboties

WebSockets ir bēdīgi slaveni ar situāciju „strādā dienām, tad vairs ne”. Bez žurnāla to gandrīz nevar ierobežot. Jēdzīgi logpunkti:

  • Laika zīmogs (UTC), URL, un stāvokļa pārejas (connecting/open/closed).
  • Close‑Reason, ja pieejams (serveris iniciē Close vs tīkla kļūme).
  • Heartbeat sūtīšanas kļūdas un saņemšanas izņēmumi, tostarp izņēmuma tips.
  • Pēc izvēles: saņemto ziņu izmēri (ne to saturs), lai konstatētu datu eksploziju.

Ja TLS tiek terminēts pie jums: papildus pārbaudiet, vai sertifikātu maiņas (derīguma beigas, jauns izdevējs) laika ziņā korelē ar kļūdām. Cietinātās vidēs arī proxy un DPI iekārtas (Deep Packet Inspection) ir iespējamie cēloņi.

Varianti: kad System.Net.WebSockets pietiek — un kad nē

System.Net.WebSockets ir pietiekams daudziem integrācijas gadījumiem, īpaši, ja runa ir par tekstu/JSON, mērenu slodzi un skaidrām savienojuma atjaunošanas stratēģijām. Ierobežojumi parādās atkarībā no Delphi versijas un mērķplatformas:

  • Trūkstošs/ierobežots Ping/Pong atbalsts: Tad kā robusts risinājums saglabājas lietotnes heartbeat.
  • Trūkstoši laika ierobežojumi/atcelšana Connect/Receive operācijās: Tad arhitektūru jāveido tā, lai iestrēdzis workers būtu izolēts un lietojumprogramma tomēr varētu tīri pabeigt darbu (piem., ar procesa watchdog vai atsevišķām worker instancēm).
  • Liela slodze vai binārie straumi: Tad atmaksājas stingrāks framing/buffering koncepts (piem., ring buffer, atsevišķs Binary-Event, Message-Assembler ar limita kontroli).

Legacy situācijām (vecākas Delphi paaudzes, ļoti specifiskas TLS/Proxy prasības) bibliotēkas kā ICS dažos projektos ir pragmatiskākas. Svarīgāk nav „kura bibliotēka“, bet gan tas, ka jūs aplūkojat izslēgšanu (shutdown), savienojuma atjaunošanu (reconnect) un novērošanu (logs/metriki) kā pirmklasīgas tēmas.

Sekojums: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen

WebSocket ir ļoti piemērots push-notikumiem, tiešā statusa atjauninājumiem, mašīnu vai procesu paziņojumiem un kā atpakaļkanāls portāliem un servisiem. Demonstrētais wrapper fokusējas uz punktiem, kas digitālajos uzņēmuma risinājumos bieži nosaka atšķirību: kontrolēta savienojuma atjaunošana, heartbeat pret idle-timeoutiem, fragmentu droša teksta apstrāde un apturēšanas ceļš, kas neveido iestrēgumu izvietošanas vai atjaunināšanas laikā.

Ierobežojumi saglabājas: ja jums nepieciešamas stingras garantijas Connect/Receive pārtraukumam ļoti īsos laika logus vai ja darbināt ārkārtīgi lielas datu caurlaidspējas, jums jāiedziļinās laika ierobežojumos, platformas īpatnībās un, ja nepieciešams, alternatīvos stekos. Tomēr lielākajai daļai integrācijas un modernizācijas scenāriju labi izolēts, labi žurnālots klients kā augstāk attēlots ir stabils pamats, ko var integrēt esošos Delphi-sistemas.

Ja jums jāiebūvē šāds būvbloks esošā arhitektūrā (piem., Layer-3 arhitektūra ar skaidrām servisa un UI slāņiem) vai jādebugo sporādiskas atslēgšanās reālajos apstākļos, mēs to varam mērķtiecīgi kontekstualizēt: sazinieties ar mums.

Tehniskajā kontekstā arī heartbeat Ping/Pong spēlē nozīmīgu lomu, ja integrācijām, datu plūsmām un turpmākajai izstrādei jādarbojas skaidri un saskaņoti.

Apspriest projektu vai modernizācijas ieceri ar Net-Base.

Nākamais solis

Ja no tēmas rodas reāls projekts, arhitektūra, esošais stāvoklis un ekspluatācija būtu jāizskata kopā jau agri.

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

  • Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
  • REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
  • Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.

Kopīgot ierakstu

Kopīgot šo ierakstu tieši

LinkedIn, X, XING, Facebook, WhatsApp un e-pasts ir uzreiz pieejami. Instagramam saiti un īsu tekstu sagatavosim nekavējoties.

E-pasts

Instagram atveras jaunā cilnē. Saite un īss teksts tiek iepriekš nokopēti starpliktuvē.