Net-Base Magasin

01.06.2026

Delphi WebSocket-klient: ansluta robust, stänga ner säkert, felsöka pålitligt

En Delphi WebSocket-klient är snabbt 'på något sätt ansluten' – men i drift räknas återanslutning, heartbeats, ordnad avstängning och felsökningsbarhet. Med en praktisk wrapper baserad på System.Net.WebSockets (med fallback) och ett källkodsexempel för trådhantering och...

01.06.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

Varför en Delphi WebSocket Client i praktiken mer än „Connect“ är

En Delphi WebSocket Client monteras på några minuter: URL, Connect, SendText, klart. I individuell företagsprogramvara och processnära lösningar uppstår problemen ofta först i drift: Reverse Proxy bryter idle‑förbindelser, mobil‑ eller VPN‑sträckor har korta NAT‑Timeouts, certifikat byts, och vid avslut fastnar processen eftersom en Receive-Loop fortfarande blockerar. Dessutom: en WebSocket är en långlivad, tillståndsburen kanal – andra regler gäller än för klassisk HTTP/REST (Request/Response, kortlivad).

I detta Source-Schnipsel handlar det inte om „Hello WebSocket“, utan om en driftssäker klient‑wrapper med:

  • ren start/stop (utan hängningar vid nedstängning),
  • Receive-Loop med Cancellation (avbrottssignal) istället för „Thread kill“,
  • Reconnect med Backoff (kontrollerad återanslutning),
  • Heartbeat som applikationsmönster (eftersom Ping/Pong inte är tillgängligt överallt),
  • debug‑ och trace‑hooks som verkligen hjälper vid supportärenden.

Implementeringen bygger på System.Net.WebSockets (Delphi RTL; WebSocket‑klient‑API med TClientWebSocket). Där denna RTL‑nivå i äldre versioner inte är tillgänglig eller är för begränsad, är en fallback via ett bibliotek (t.ex. ICS) ofta lämplig – nedan finns en bedömning.

Architektur‑Skizze: en Wrapper istället för utspridda WebSocket‑anrop

Ett vanligt fel i befintliga Delphi‑applikationer: UI‑formulär eller servicemoduler „pratar direkt WebSocket“ och får då timers, trådar och undantagshantering utspritt överallt. Bättre är en tydlig komponent med väldefinierade Events och en liten tillståndsmaskin.

Begrepp kort förklarade: Backoff avser en väntetid som efter fel växer stegvis (t.ex. 1s, 2s, 4s …) för att inte översvämma server eller nätverk. CancellationToken är ett avbrottssignal från .NET‑världen; i Delphi finns inget identiskt mönster, men vi kan efterlikna det med TEvent och en „StopRequested“‑flagga. TThread.Queue schemalägger kod för körning i huvudtråden (UI) utan att blockera workern; Synchronize blockerar och är i nedstängningsvägar ofta orsaken till deadlocks.

Source‑Schnipsel: Delphi WebSocket Client med Stop, Reconnect och Message‑Dispatch

Följande kod är medvetet uppbyggd som en „Betriebs‑Baustein“: en klass som kan användas liknande i VCL/FMX eller i en Windows‑ och Windows‑ och Linux‑Services (beroende på Delphi‑version/plattform). Kärnan är en worker‑thread som håller Receive‑Loop igång och rapporterar till applikationen via Events.

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: användbart för att undvika idle-timeouts bakom proxies
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, ‚Stopp: Worker svarar inte inom timeout; möjlig blockering i nätverksstacken‘);
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
// Obs: TClientWebSocket.Connect är synkront och kan blockera beroende på DNS/TLS.
// Därför körs detta i 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 som app-meddelande, eftersom Ping/Pong inte är tydligt exponerat i varje Delphi-version.
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;

// Mottagning: ram-baserad, därför StringBuilder för 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 många affärsprotokoll är Text/JSON standard.
// Binary kan buffras här på liknande sätt eller vidarebefordras direkt.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Mini-sleep för att skona CPU vid mycket snabba loopar.
// Inte för stort, annars försämras latensen.
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.

Vad koden medvetet gör „annorlunda“ än typiska exempel

  • Stop utan våld: Istället för att skjuta ner trådar sätter Stop ett Event. Worker avslutar loopar på definierade ställen. Det minskar hängningar vid avslut och förhindrar resursläckor i socket-stacken.
  • Queue istället för Synchronize: Loggning och events skickas via TThread.Queue till huvudtråden. Det är viktigt när Stop/Shutdown kommer från UI eller från Service-Control-handler. Synchronize kan blockera om huvudtråden för närvarande väntar.
  • Fragmentering beaktas: WebSocket-text kan komma fragmenterad i frames. Därför används TStringBuilder och kontroll av EndOfMessage.
  • Heartbeat som applikationsprotokoll: Många installationer dör av idle-timeouter (Load Balancer, nginx, Cloud WAF). En lättviktig „ping“-text är ofta en mer effektiv driftåtgärd än att förlita sig på „TCP keepalive“ eller ett inte allmänt tillgängligt Ping/Pong-API.

Driftsförutsättningar och fallgropar i drift

1) DNS, TLS och proxy: Connect kan blockera

TClientWebSocket.Connect är synkron. Beroende på DNS-upplösning, TLS-handshake, certifikatverifiering eller proxy-miljö kan det ta flera sekunder. Koden placerar det medvetet i en Worker. Om ni behöver hårda timeouter måste ni kontrollera på API-nivå om er Delphi-version tillhandahåller timeout-alternativ, eller kapsla Connect i en separat tråd och avbryta via processlogik. Viktigt: Ett „avbryt“ betyder här oftast „markera anslutningen som korrupt och sätta upp en ny Worker“, inte „döda socket-operationen omedelbart“.

2) Idle-timeouter: varför Heartbeat ofta är nödvändigt

I företagsnätverk termineras en WebSocket ofta bakom en reverse proxy (nginx, IIS ARR) eller en Load Balancer. Många av dessa komponenter stänger anslutningar om ingen data flödar under längre tid. TCP-Keepalive är inte alltid konfigurerat tillräckligt kort (och under Windows är det ofta snarare minuter än sekunder). Ett Heartbeat på applikationsnivå är därför en robust workaround. Se till att server och klient har samma koncept (t.ex. „ping“/„pong“ som text eller JSON).

3) Trådning och UI: Händelser måste hållas löst kopplade

Om OnText-bearbetningen är tung (JSON-parsning, DB-åtkomst med BDE-ersättning med native anslutning, UI-uppdateringar) bör den inte blockera allt i huvudtråden. Wrappern levererar bara meddelandet. Ett typiskt mönster är: OnText lägger payloaden i en kö (t.ex. TThreadedQueue<string>), en separat Worker bearbetar med backpressure (det vill säga begränsad kölängd). Det förhindrar att UI:n fryser eller att mottagningen störs vid burst-last.

Debugging: vad ni bör logga när det „ibland“ avbryts

WebSockets är ökända för att „fungerar i dagar, sedan inte längre“. Utan loggning är det svårt att avgränsa. Meningsfulla loggpunkter:

  • Tidsstämpel (UTC), URL och tillståndsövergångar (connecting/open/closed).
  • Close-Reason, om tillgängligt (server begär Close vs. nätverksfel).
  • Heartbeat-sändfel och mottagningsundantag inklusive Exception-Typ.
  • Valfritt: storlekar på mottagna meddelanden (inte innehållet), för att upptäcka dataexplosion.

Om ni terminerar över TLS: kontrollera dessutom om certifikatbyten (utgång, ny Issuer) korrelerar tidsmässigt med felen. I hårt säkrade miljöer är även proxy- och DPI-boxar (Deep Packet Inspection) kandidater.

Varianter: när System.Net.WebSockets räcker – och när inte

System.Net.WebSockets räcker i många integrationsfall, särskilt när det gäller text/JSON, måttlig belastning och tydliga reconnect-strategier. Begränsningar uppträder beroende på Delphi-version och målplattform:

  • Saknat/begränsat Ping/Pong-stöd: Då förblir App-Heartbeat det robusta mönstret.
  • Saknade timeouts/cancellation vid Connect/Receive: Då måste du bygga arkitekturen så att en hängande worker förblir isolerad och applikationen ändå kan stängas ner ordnat (t.ex. via processwatchdog eller separata worker-instanser).
  • Hög belastning eller binära strömmar: Då lönar sig ett tydligare framing/buffering-koncept (t.ex. ring buffer, separat Binary-Event, Message-Assembler med begränsningar).

För legacy-situationer (äldre Delphi-generationer, mycket specifika TLS/Proxy-krav) är bibliotek som ICS i vissa projekt mer pragmatiska. Viktigare än „vilket bibliotek“ är att ni behandlar shutdown, reconnect och observability (loggar/metriker) som förstklassiga ämnen.

Slutsats: en Delphi WebSocket Client är en driftkomponent – med tydliga gränser

En WebSocket lämpar sig väl för push-event, live-status, maskin- eller processmeddelanden och som återkanal för portaler och tjänster. Den visade wrappern fokuserar på de punkter som ofta gör skillnad i digitala företagslösningar: kontrollerad reconnect, heartbeat mot idle-timeouts, fragment-säker textbehandling och en stoppsekvens som inte fastnar vid deployment eller uppdatering.

Begränsningar kvarstår: Om ni behöver hårda garantier för avbrott av Connect/Receive inom mycket snäva tidsfönster eller kör extremt höga datapassager måste ni gå djupare i timeouts, plattformssärdrag och eventuellt alternativa stackar. För majoriteten av integrations- och moderniseringsscenarier är dock en väl kapslad, väl loggad klient som ovan en solid bas som kan integreras i befintliga Delphi-system.

Om ni ska infoga en sådan komponent i en befintlig arkitektur (t.ex. Layer-3-arkitektur med tydliga service- och UI-skikt) eller behöver debugga sporadiska disconnects under verkliga förhållanden kan vi hjälpa er att bedöma det: ta kontakt.

I det tekniska sammanhanget spelar också Heartbeat Ping/Pong en viktig roll när integrationer, dataflöden och vidareutveckling måste fungera tillsammans på ett ordnat sätt.

Diskutera projekt eller moderniseringsinsatser med Net-Base.

Nästa steg

När ett ämne blir ett verkligt projekt bör arkitektur, befintliga system och drift behandlas gemensamt redan i ett tidigt skede.

Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
  • Ni ser tidigt vilken väg som är ekonomiskt och driftsmässigt bärkraftig.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.