Net-Base Magasin

01.06.2026

Delphi WebSocket-klient: robust tilkopling, ryddig stopping, påliteleg feilsøking

Ein Delphi WebSocket-klient kan raskt vere «på ein måte» tilkopla — men i drift tel gjenoppretting av tilkopling, heartbeats, ryddig stopping og feilsøkbarheit. Med ein praksisnær wrapper basert på System.Net.WebSockets (med fallback) og eit kildekodeutsnitt for trådhandtering og...

01.06.2026

Frå magasinetema til prosjektpraksis

Passande teneste- og tekniske sider til innlegget

Kvifor ein Delphi WebSocket‑klient i praksis er meir enn „Connect“

Ein Delphi WebSocket‑klient er sett saman på få minutt: URL, Connect, SendText, ferdig. I skreddarsydd bedriftsprogramvare og prosessnære løysingar oppstår problema som regel først i drift: Reverse Proxyen avsluttar idle‑tilkoplingar, mobil‑ eller VPN‑strekk har korte NAT‑timeoutar, sertifikata blir bytte, og ved avslutting heng prosessen fordi ein Receive‑Loop framleis er blokkert. I tillegg: Ein WebSocket er ein langlevande, tilstandsbasert kanal – då gjeld andre reglar enn for klassisk HTTP/REST (Request/Response, kortlevd).

I dette kodeeksempelet handlar det ikkje om «Hello WebSocket», men om ein praksisnær klient‑wrapper med:

  • ryddig start/stop (utan heng ved nedstenging),
  • Receive‑Loop med Cancellation (avbrotssignal) i staden for „Thread kill“,
  • Reconnect med Backoff (kontrollert gjentilkobling),
  • Heartbeat som applikasjonsmønster (fordi Ping/Pong ikkje er tilgjengeleg overalt),
  • Debug‑ og Trace‑Hooks som verkeleg hjelper i supporttilfelle.

Implementasjonen byggjer på System.Net.WebSockets (Delphi RTL; WebSocket‑klient‑API med TClientWebSocket). Der denne RTL‑skiktet i eldre versjonar ikkje er tilgjengeleg eller for avgrensa, er ein Fallback via ei bibliotekk (t.d. ICS) ofte nyttig – nedanfor følgjer ei innplassering.

Arkitekturskisse: ein wrapper i staden for spreidde WebSocket‑kall

Ein vanleg feil i veksande Delphi‑applikasjonar: UI‑skjema eller service‑modular «snakkar direkte WebSocket» og endar opp med timarar, trådar og unntakshandsaming spreidd overalt. Betre er ein tydeleg komponent med veldefinerte Events og ein liten tilstandsmaskin.

Kodesnutt: Delphi WebSocket‑klient med Stop, Reconnect og Message‑Dispatch

Den følgjande koden er medvite bygd som ein «drifts‑baustein»: ei klasse som ein kan bruke i VCL/FMX eller i eit Windows‑ og Windows‑ og Linux‑tenester (avhengig av Delphi‑versjon/plattform). Kjernen er ein worker‑tråd som held Receive‑Loopen og melder via Events inn i applikasjonen.

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: nyttig mot idle-timeoutar bak proxyar
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 ikkje innan timeout; mogleg blokkering i nettverksstakken');
    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
        // Merk: TClientWebSocket.Connect er synkront og kan blokkere avhengig av DNS/TLS.
        // Difor køyrer dette i Worker-tråden.
        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 som ein app-melding, fordi Ping/Pong ikkje er eksponert på ein stabil måte i alle Delphi-versjonar.
          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;

          // Mottak: rammebasert, derfor StringBuilder for fragmentering.
          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
                // I mange forretningsprotokollar er tekst/JSON standard.
                // Binære data kan her buffrast tilsvarande eller vidareførast direkte.
                Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
              end;

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

          // Kort søvn for å skåne CPU ved svært raske løkker.
          // Ikkje for lang, elles blir latenstida dårlegare.
          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.

Kva koden medvite gjer annleis enn typiske døme

  • Stop utan vald: I staden for å drepe trådar set Stop eit Event. Arbeidaren avsluttar løkker på definerte stadar. Det reduserer heng ved avslutting og unngår ressurslekkasjar i socket-stacken.
  • Queue i staden for Synchronize: Logging og hendingar går via TThread.Queue til hovudtråden. Det er viktig når Stop/Shutdown kjem frå UI eller frå Service-Control-handlarar. Synchronize kan blokkere dersom hovudtråden ventar.
  • Hensyn til fragmentering: WebSocket-tekst kan koma fragmentert i frames. Difor bruk av TStringBuilder og sjekk av EndOfMessage.
  • Heartbeat som applikasjonsprotokoll: Mange oppsett dør av idle-timeoutar (Load Balancer, nginx, Cloud WAF). Ein lettvekts «ping»-tekst er som driftsgrep ofte meir effektiv enn å håpe på «TCP keepalive» eller eit ikkje-universelt Ping/Pong-API.

Rammer og fallgruver i drift

1) DNS, TLS und Proxy: Connect kann blockieren

TClientWebSocket.Connect er synkron. Avhengig av DNS-oppløysing, TLS-handshake, sertifikatverifisering eller proxy-omgjevnad kan dette ta fleire sekund. Koden legg dette medvite i ein Worker. Hvis de i tillegg treng strenge timeouts, må de på API-nivå sjekke om dykkar Delphi-versjon tilbyr timeout-opsjonar, eller de kapslar Connect inn i ein separat tråd og avbrytar via prosesslogikk. Viktig: «avbryte» tyder her som regel «markere tilkoplinga som øydelagt og starte ny Worker», ikkje «drepe socket-operasjonen med ein gong».

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

I bedriftsnettverk er ein WebSocket ofte terminert bak ein reverse proxy (nginx, IIS ARR) eller ein Load Balancer. Mange av desse komponentane stengjer tilkoplingar når det over lengre tid ikkje flyt data. TCP-Keepalive er ikkje alltid konfigurert med korte intervall (og under Windows er det ofte heller minutt enn sekund). Derfor er eit Heartbeat på applikasjonsnivå eit robust omveg. Sørg for at server og klient har same konsept (t.d. «ping»/«pong» som tekst eller JSON).

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

Dersom OnText-handsaminga er tung (JSON-parsing, DB-tilgang med BDE-Ablosung med nativen tilkopling, UI-oppdateringar), bør ho ikkje blokkere hovudtråden. Wrapparen leverer berre meldinga. Eit typisk mønster er: OnText legg payloaden i ei kø (t.d. TThreadedQueue<string>), ein separat Worker handsamar med backpressure (altså avgrensa kølengd). Dette forhindrar at UI-en frys ved burst-belastning eller at mottaket kjem i ulage.

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

WebSockets er berykta for «køyrer i fleire døgn, så sluttar det å fungere». Utan logging er det vanskeleg å avgrense. Nyttige loggpunkter:

  • Tidsstempel (UTC), URL, og tilstandsendringar (connecting/open/closed).
  • Close-Reason, dersom tilgjengeleg (server ber om Close vs. nettverksfeil).
  • Heartbeat-sendefeil og mottaksunntak inkl. Exception-Typ.
  • Valfritt: storleikar på mottekne meldingar (ikkje innhaldet), for å oppdaga dataeksplosjonar.

Hvis de terminerer via TLS: sjekk i tillegg om sertifikatsskifte (utløp, ny issuer) tidsmessig korrelerer med feila. I hardna omgjevnader er også proxy- og DPI-boksar (Deep Packet Inspection) aktuelle kandidatpunkt.

Varianter: når System.Net.WebSockets held – og når ikkje

System.Net.WebSockets er tilstrekkeleg for mange integrasjonstilfelle, særleg ved tekst/JSON, moderat last og tydelege reconnect-strategiar. Grensene blir synlege avhengig av Delphi-versjon og plattformmål:

  • Manglande/avgrensa Ping/Pong-støtte: Då er App-Heartbeat det robuste mønsteret.
  • Manglande timeouts/cancellation i Connect/Receive: Då må du byggje arkitekturen slik at ein hengjande worker blir isolert og applikasjonen likevel kan avsluttast på reint vis (t.d. med prosess-watchdog eller separate worker-instansar).
  • Høg last eller binære straumar: Då lønner det seg med eit sterkare framing/buffering-konsept (t.d. ring buffer, separat Binary-Event, Message-Assembler med grenser).

For legacy-situasjonar (eldre Delphi-generasjonar, svært spesifikke TLS/Proxy-krav) er bibliotek som ICS i nokre prosjekt meir pragmatic. Viktigare enn «kva bibliotek» er at du handsamar shutdown, reconnect og observability (loggar/metrikkar) som førsteklasses tema.

Konklusjon: ein Delphi WebSocket-klient er ein driftsbyggestein – med klare grenser

Ein WebSocket eignar seg godt til push-hendingar, live-status, maskin- eller prosessmeldingar og som returkanal for portalar og tenester. Den viste wrapperen fokuserer på dei punkta som i digitale bedriftsløysingar ofte gjer skilnaden: kontrollert reconnect, Heartbeat mot idle-timeouts, fragmentsikker teksthandsaming og ein stopp-sti som ved deployment eller oppdatering ikkje heng seg opp.

Einsatzgrenser står att: Om du treng harde garantiar for avbrot av Connect/Receive innan svært korte tidsvindauge, eller køyrer ekstremt høge dataratar, må du gå djupare i timeouts, plattformspesifikke eigenskapar og ev. alternative stackar. For fleirtalet av integrasjons- og moderniseringsscenaria er likevel ein reint kapsla, godt logga klient som ovanfor ein solid basis som let seg integrere i etablerte Delphi-system.

Om du skal tilpasse ein slik byggestein i ei eksisterande arkitektur (t.d. Layer-3 arkitektur med klare service- og UI-lag) eller må feilsøkje sporadiske disconnects under reelle forhold, kan vi hjelpe deg med å vurdere dette konkret: ta kontakt.

I fagleg samanheng spelar også Heartbeat og Ping/Pong ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må spela godt saman.

Drøft prosjekt eller moderniseringstiltak med Net-Base.

Neste steg

Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift vurderast tidleg saman.

Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.

  • Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
  • REST, datatilgang, portalar og utrulling blir ikkje utsette til seinare som etterverknader.
  • De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.