Net-Base Tímarit

01.06.2026

Delphi WebSocket Client: að tengjast stöðugt, að stöðva hreint, að kemba villur áreiðanlega

Einn Delphi WebSocket-klienti er fljótur að vera „einhvern veginn tengdur“ – en í rekstri skipta endurtengingar (Reconnect), Heartbeats, hreint lokun og aðgengi að bilanaleit máli. Með hagnýtum Wrapper byggðum á System.Net.WebSockets (með Fallback) og kóðabút fyrir þráðavinnslu og...

01.06.2026

Frá tímaritsþema til verkefnaframkvæmdar

Viðeigandi þjónustu- og tæknisíður fyrir greinina

Af hverju er Delphi WebSocket Client í rekstri meira en „Connect“

Ein Delphi WebSocket Client er settur saman á nokkrum mínútum: URL, Connect, SendText, tilbúið. Í sérsniðnum fyrirtækjakerfum og ferlapróxímálum kemur málið þó yfirleitt fram í rekstri: Reverse Proxy rýfur óvirk tengsl, farsíma- eða VPN-tengingar hafa stutt NAT-Timeouts, vottorð breytast, og við lokun hangir ferlið vegna þess að Receive-Loop er enn blokkaður. Enn fremur: WebSocket er langlífur, ástandsmiðaður rás — því gilda aðrar reglur en fyrir hefðbundið HTTP/REST (Request/Response, skammvinnt).

Í þessum Source-Schnipsel snýst það ekki um „Hello WebSocket“, heldur um rekstrarhæfan Client-Wrapper með:

  • hreinum Start/Stop (engin hang á lokun),
  • Receive-Loop með Cancellation (stöðvunarmerki) í stað „Thread kill“,
  • Reconnect með Backoff (stýrð endurtenging),
  • Heartbeat sem viðmótsmynstur (því Ping/Pong er ekki alltaf tiltækt),
  • Debug- og Trace-Hooks sem hjálpa raunverulega í stuðningsmálum.

Útfærslan byggir á System.Net.WebSockets (Delphi RTL; WebSocket-Client-API með TClientWebSocket). Þar sem þetta RTL-lag er ekki tiltækt í eldri útgáfum eða of takmarkað er oft skynsamlegt að nota fallback í formi bókasafns (t.d. ICS) — nánari umfjöllun kemur síðar.

Arkitektúrskissa: einn Wrapper í stað dreifðra WebSocket-kalla

Algeng villa í vaxandi Delphi-forritum: UI-form eða þjónustumodúl „tala beint við WebSocket“ og hafa þá víða timers, þræði og undantekningameðhöndlun dreifða. Betra er skýr eining með vel skilgreindum atburðum og litla ástandsvél.

Hugtök stutt til skýringa: Backoff merkir biðtíma sem eykst stigvaxandi eftir villur (t.d. 1s, 2s, 4s …) til að ofhlaða ekki þjón eða net. CancellationToken er stöðvunarmerki úr .NET-heiminum; í Delphi er ekki nákvæmlega samsvarandi mynstur, en það má herma með TEvent og „StopRequested“-fána. TThread.Queue skipuleggur kóða til keyrslu í aðalþræðinum (UI) án þess að hindra Worker; Synchronize læsist og er í lokunarstefnum oft orsök fyrir deadlocks.

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

Eftirfarandi kóði er viljandi uppsettur sem „rekstrar-eining“: klasi sem má nota svipað í VCL/FMX eða í Windows- og Windows- og Linux-Services (eftir Delphi-útgáfu/kerfi). Kjarni lausnarinnar er Worker-Thread sem heldur Receive-Loop og tilkynnir um atburði inn í forritið.

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: gagnlegt gegn óvirkutímatakmörkum hjá proxyjum
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 svarar ekki innan tímataks; möguleg stífla í netstakknum');
    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
        // Athugið: TClientWebSocket.Connect er samstillt og getur, undir áhrifum DNS/TLS, hindrað keyrslu.
        // Þess vegna keyrir þetta í Worker-þræði.
        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 sem forritaskilaboð, því Ping/Pong er ekki vel aðgengilegt í öllum Delphi-útgáfum.
          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;

          // Móttaka: rammabundin, því StringBuilder er notaður fyrir fragmentun.
          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
                // Í mörgum fyrirtækjaprótókollum er texti/JSON staðlaður.
                // Tvíundagögn (binary) er hægt að búffa hér á svipaðan hátt eða senda beint áfram.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

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

          // Stutt svefn til að minnka CPU-notkun í mjög hraðri lykkju.
          // Ekki of langt, annars versnar biðtími.
          TThread.Sleep(1);
        end;
      finally
        SB.Free;
      end;

      SafeClose;
      State('closed');

    finally
      WS.Free;
    end;

    if StopRequested then
      Break;

    // Endurtengja eftir hreint lok eða eftir villur
    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.

Hvað kóðinn gerir viljandi „annað“ en dæmigerð dæmi

  • Stop án ofbeldis: Í stað þess að slökkva á þræði setur Stop atburð. Vinnsluþráðurinn lýkur lykkjum á skilgreindum stöðum. Þetta dregur úr hang-tilvikum við lokun og kemur í veg fyrir auðlindalekkar í socket-stakknum.
  • Queue í stað Synchronize: Skráningar og atburðir fara með TThread.Queue í aðalþráðinn. Þetta er mikilvægt þegar Stop/Shutdown kemur úr UI eða úr Service-Control-handlerum. Synchronize getur blokkerað ef aðalþráðurinn er í biðstöðu.
  • Tekið tillit til fragmentunar: WebSocket-texti getur borist í ramma (fragment). Þess vegna er notaður TStringBuilder og athugað EndOfMessage.
  • Heartbeat sem forrits-protókoll: Mörg uppsett umhverfi deyja vegna idle-timeouts (Load Balancer, nginx, Cloud WAF). Léttur „ping“-texti er oft virkari rekstrarhandfang en að treysta á „TCP keepalive“ eða á þeirri von að Ping/Pong-API sé til staðar alls staðar.

Skilyrði og gildrur í rekstri

1) DNS, TLS og Proxy: Connect getur hindrast

TClientWebSocket.Connect er samstillt. Fer eftir DNS-upplausn, TLS-handshake, vottunarathugun eða proxy-umhverfi getur þetta tekið nokkrar sekúndur. Kóðinn setur þetta af ásettu ráði í vinnsluþráð. Ef þörf er á harðari tímalokum þarf að kanna á API-stigi hvort útgáfa af Delphi bjóði upp á timeout-valkosti, eða innpakka Connect í sértækan þræði og hætta með ferlalógík. Mikilvægt: „að hætta/afrita“ þýðir hér yfirleitt „merkja tengingu sem skemmd og endurræsa vinnsluþráðinn“, ekki „drepa socket-aðgerðina samstundis“.

2) Idle-Timeouts: hvers vegna Heartbeat er oft nauðsyn

Í fyrirtækjanetum endar WebSocket yfirleitt aftan reverse proxy (nginx, IIS ARR) eða load balancer. Margar slíkar einingar loka tengingum ef engin gögn flæða í langan tíma. TCP-Keepalive er ekki alltaf stillt með stutt bil (og undir Windows er það oft frekar í mínútum en sekúndum). Heartbeat á forritsstigi er því stöðugur og áreiðanlegur leiðréttingarhnífur. Gætið þess að þjónn og viðskiptavinur hafi sama fyrirkomulag (t.d. „ping“/„pong“ sem texti eða JSON).

3) Þráðastjórnun og UI: Atburðir verða að vera óháðir

Ef OnText-meðhöndlun er þung (JSON-parsing, gagnagrunnsaðgerðir með BDE-lausn með innfæddri tengingu, UI-uppfærslur), ætti hún ekki að loka aðalþræðinum. Wrapperinn skilar einungis skilaboðunum. Algengt mynstur er að OnText setji payload í röð (t.d. TThreadedQueue<string>), og sértækur vinnsluþráður með bakþrýstingi (þ.e. takmörkuð biðröðarlengd) vinni úr þeim. Þetta kemur í veg fyrir að UI frjósi eða að móttaka fari úr skorðum við álagsköst.

Rökgreining: hvað þú ættir að skrá ef það bilar „stundum“

WebSockets eru þekkt fyrir að „gangi í marga daga og hætti svo“. Án logging er nánast ómögulegt að þrengja niður. Hagnýtir loggpunkter:

  • Tímasetning (UTC), URL og ástandsskipti (connecting/open/closed).
  • Close-Reason (lokunarástæða), ef tiltækt (þjónn krefst Close vs. netvilla).
  • Heartbeat-sendivillur og móttöku-undantekningar, þ.m.t. undantekningartegund.
  • Valfrjálst: stærðir móttekinna skilaboða (ekki efni þeirra), til að greina gagnasprengingu.

Ef þú terminierar yfir TLS: athugaðu einnig hvort breytingar á vottorðum (gildistími, nýr Issuer) samræmist villum tímabundið. Í harðgerðum umhverfum geta proxy- og DPI-boxar (Deep Packet Inspection) einnig verið sökudólgar.

Útfæringar: hvenær System.Net.WebSockets dugar – og hvenær ekki

System.Net.WebSockets er fyrir mörg samþættingartilvik nægjanlegt, sérstaklega þegar um Text/JSON, hóflegt álag og skýrar endurtengingarstefnur er að ræða. Takmarkanir koma í ljós eftir Delphi-útgáfu og markmiði markkerfisins:

  • Takmarkaður eða enginn Ping/Pong-stuðningur: Þá er App-Heartbeat áfram traust kerfismynstur.
  • Skortur á timeouts/afpöntun við Connect/Receive: Þá verður þú að hanna arkitektúrinn þannig að fastur worker haldist einangraður og forritið lokist samt hreint (t.d. með ferilvakta eða aðskildum worker-tilvikum).
  • Mikið álag eða binærstraumar: Þá borgar sig sterkara framing-/buffering-hugmyndafræði (t.d. ring buffer, aðskilið Binary-Event, Message-Assembler með takmörkum).

Fyrir legacy-aðstæður (eldri Delphi-kynslóðir, mjög sértækar TLS/Proxy-kröfur) eru bókasöfn eins og ICS í sumum verkefnum hagnýtari. Mikilvægara er ekki „hvort bókasafn“, heldur að meðhöndla Shutdown, Reconnect og Observability (logs/metrík) sem forgangsmál.

Niðurstaða: Delphi WebSocket-klient er rekstrarhluti – með skýrum mörkum

WebSocket hentar vel fyrir push-atburði, rauntíma stöðu, véla- eða feriltilkynningar og sem bakrás fyrir gáttir og þjónustur. Sá wrapper sem sýndur er einbeitir sér að þeim atriðum sem oft gera muninn í stafrænum fyrirtækjalausnum: stjórnað endurtenging, Heartbeat gegn idle-timeouts, fragment-örugg textavinnsla og stöðvunarstígur sem festist ekki við deployment eða uppfærslu.

Takmörk við notkun standa þó: Ef þú þarft strangar tryggingar um að Connect/Receive verði rofin innan mjög stuttra tímaglugga eða keyrir mjög háan gagnahraða, þarf að kafa dýpra í timeouts, sérkenni platformar og mögulega aðra stakka. Fyrir meirihluta samþættingar- og nútímavæðingarsena er hreinn, vel skráður client eins og sýndur hér þó traustur grunnur sem samþættist rótgrónum Delphi-kerfum.

Ef þú ætlar að innlima slíkan þátt í fyrirliggjandi arkitektúr (t.d. Layer-3 arkitektúr með skýrum þjónustu- og UI-lögum) eða þarft að greina sporadíska aftengingu undir raunskilyrðum, getum við hjálpað þér að flokka þetta markvisst: Hafðu samband við okkur.

Í faglegu samhengi gegna Heartbeat Ping/Pong einnig mikilvægu hlutverki þegar samþættingar, gagnastraumar og áframhaldandi þróun þurfa að spila vel saman.

Ræddu verkefni eða nútímavæðingaráform með Net-Base.

Næsta skref

Þegar úr málinu verður raunverulegt verkefni ber að skoða arkitektúr, núverandi kerfi og rekstur snemma saman.

Við styðjum ekki aðeins við einstakar spurningar, heldur einnig þegar úr kóðabútum, eldri kerfum eða gáttahugmyndum þarf að verða traust fyrirtækjaverkefni.

  • Núverandi staða, markmynd og tæknileg áhætta eru metin saman.
  • REST, gagnaaðgangur, gáttir og innleiðing eru ekki skildir eftir til síðar.
  • Það sést snemma hvaða leið er fjárhagslega og rekstrarlega sjálfbær.

Deila færslu

Deila þessari færslu beint

LinkedIn, X, XING, Facebook, WhatsApp og tölvupóstur eru strax tiltækir. Fyrir Instagram undirbúum við hlekk og stuttan texta beint.

Tölvupóstur

Instagram opnast í nýjum flipa. Tengill og stuttur texti eru afritaðir í klippiborðið á undan.