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.
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.
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.