Net-Base Žurnalas

01.06.2026

Delphi WebSocket klientas: stabiliai prisijungti, tvarkingai sustabdyti, patikimai derinti

Delphi WebSocket klientas greitai „kažkaip prisijungęs“ – tačiau eksploatacijoje svarbūs Reconnect, Heartbeats, tvarkingas sustabdymas ir derinimo galimybės. Su pritaikomu wrapper'iu, paremtu System.Net.WebSockets (su fallback), ir kodo ištrauka, skirta Threading ir...

01.06.2026

Nuo žurnalo temos iki projekto įgyvendinimo

Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui

Kodėl ein Delphi WebSocket Client praktiškai yra daugiau nei „Connect“

Ein Delphi WebSocket Client surenkamas per kelias minutes: URL, Connect, SendText, ir pasiruošta. Tačiau individualioje įmonių programinėje įrangoje ir procesams artimose sprendimuose problemos dažniausiai išryškėja tik eksploatacijos metu: Reverse Proxy nutraukia neaktyvias jungtis, mobilios ar VPN trasos turi trumpus NAT-timeout’us, sertifikatai keičiasi, o proceso užbaigimas užstringa, nes Receive-Loop vis dar blokuoja. Be to: WebSocket yra ilgaamžis, būseną turintis kanalas – todėl galioja kitokios taisyklės nei klasikinio HTTP/REST (Request/Response, trumpalaikis).

Šiame Source-Schnipsel nekalbama apie „Hello WebSocket“, o apie pritaikomą kliento apvalkalą su:

  • švariu Start/Stop (be užstrigimų išjungimo metu),
  • Receive-Loop su Cancellation (nutraukimo signalas) vietoje „Thread kill“,
  • Reconnect su Backoff (kontroliuojamas pakartotinis prisijungimas),
  • Heartbeat kaip taikymo modelis (nes Ping/Pong ne visur prieinamas),
  • Debug- ir Trace-Hooks, kurie iš tikrųjų padeda palaikymo atvejais.

Įgyvendinimas remiasi System.Net.WebSockets (Delphi RTL; WebSocket-Client-API su TClientWebSocket). Ten, kur ši RTL-sluoksnis senesnėse versijose nėra prieinamas arba per daug ribotas, dažnai prasminga naudoti fallback per biblioteką (pvz., ICS) – apie tai žemiau yra aiškesnė įvertinimo pastaba.

Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe

Dažna klaida auginamose Delphi-programose: UI-Formulare arba servisų moduliai „kalba tiesiogiai su WebSocket“ ir tuomet visur pasklinda Timer’ai, Thread’ai ir išimčių tvarkymai. Geriau turėti aiškų komponentą su gerai apibrėžtais įvykiais ir nedidele būsenos mašina.

Trumpas terminų išaiškinimas: Backoff reiškia laukimo intervalą, kuris po klaidų palaipsniui auga (pvz., 1s, 2s, 4s …), kad nebūtų užplūstas serveris ar tinklas. CancellationToken yra nutraukimo signalas iš .NET pasaulio; Delphi nėra identiško modelio, bet tai galime atkurti su TEvent ir „StopRequested“ žymeniu. TThread.Queue planuoja kodą vykdyti pagrindiniame gijoje (UI), neužblokavus worker’io; Synchronize blokuoja ir dažnai yra užstrigimų (deadlock) priežastis išjungimo keliuose.

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

Toliau pateiktas kodas sąmoningai sukonstruotas kaip „Betriebs-Baustein“: klasė, kurią panašiai galima naudoti VCL/FMX arba viename Windows- ir Windows- und Linux-Services (priklausomai nuo Delphi versijos / platformos). Branduolys yra Worker-Thread, kuris palaiko Receive-Loop ir per įvykius praneša į programą.

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: naudingas apsisaugoti nuo neaktyvumo laiko limitų už proxy serverių
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 neatsako per nustatytą laiko tarpą; galimas blokavimas tinklo steke‘);
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
// Pastaba: TClientWebSocket.Connect yra sinchroninė ir gali užstrigti priklausomai nuo DNS/TLS.
// Todėl šis kodas vykdomas workerio gijoje.
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 siunčiamas kaip programos žinutė, nes Ping/Pong ne visuomet yra tinkamai eksponuojami kiekvienoje Delphi versijoje.
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;

// Gavimas: paremtas rėmeliais (frames), todėl StringBuilder naudojamas fragmentacijai.
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
// Dvejetainius duomenis čia galima panašiai buferizuoti arba perduoti tiesiogiai.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Trumpa pauzė (Mini-Sleep), kad esant labai greitam ciklui būtų tausojamas CPU.
// Ne per ilga, kitaip pablogės delsos laikas.
TThread.Sleep(1);
end;
finally
SB.Free;
end;

SafeClose;
State(‚closed‘);

finally
WS.Free;
end;

if StopRequested then
Break;

// Perjungimas (reconnect) po tvarkingo uždarymo arba po klaidų
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.

Kuo kodas sąmoningai skiriasi nuo tipinių pavyzdžių

  • „Stop“ be prievartos: Užuot „nužudžius“ Threads, Stop sukelia įvykį. Worker baigia ciklus apibrėžtose vietose. Tai sumažina užstrigimus uždarant ir išvengia resursų nutekėjimo Socket-Stack.
  • Queue vietoje Synchronize: Logging ir įvykiai perduodami per TThread.Queue į Mainthread. Tai svarbu, kai Stop/Shutdown inicijuojamas iš UI arba iš Service-Control-handlerių. Synchronize gali blokuoti, jei Mainthread šiuo metu laukia.
  • Atsižvelgiama į fragmentaciją: WebSocket teksto žinutės gali būti sufragmentuotos į rėmelius (Frames). Todėl naudojamas TStringBuilder ir tikrinamas EndOfMessage.
  • Heartbeat kaip programos protokolas: Daugelis konfigūracijų žlunga dėl Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Lengvas „ping“ tekstas kaip veiklos svirtis dažnai veiksmingesnis už pasitikėjimą „TCP keepalive“ arba už API, kurios Ping/Pong ne visur prieinamos.

Eksploatavimo sąlygos ir spąstai

1) DNS, TLS und Proxy: Connect kann blockieren

TClientWebSocket.Connect yra sinchroninis. Priklausomai nuo DNS sprendimo, TLS rankinio susitarimo, sertifikato patikros ar proxy aplinkos tai gali užtrukti kelias sekundes. Kode tai sąmoningai perkeliama į Worker. Jei reikia papildomų griežtų timeout73, turite API lygiu patikrinti, ar jūsų Delphi versija suteikia timeout parinktis, arba apsukti Connect į atskirą thread01 ir nutraukti per proceso logiką. Svarbu: „nutraukimas“ čia dažniausiai reiškia „pažymėti ryšį kaip sugadintą ir iš naujo paleisti Worker“, o ne „socket operacijos akimirksniu nutraukimą“.

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

Įmonių tinkluose WebSocket dažnai terminamas už Reverse Proxy (nginx, IIS ARR) arba Load Balancer. Daugelis šių komponentų uždaro jungtis, jei per ilgesnį laiką nepraeina duomenys. TCP-Keepalive ne visada sukonfigūruotas pakankamai trumpai (ir po Windows dažniau matuojamas minutėmis nei sekundėmis). Todėl programos lygio Heartbeat yra patikima išeitis. Užtikrinkite, kad serveris ir klientas naudotų tą patį modelį (pvz. „ping“/„pong“ kaip tekstą arba JSON).

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

Jei OnText apdorojimas yra sunkus (JSON-parsing, DB prieigos su BDE-Ablosung mit nativer Anbindung, UI atnaujinimai), jis neturėtų blokuoti visko Mainthread2f. Wrapperis pateikia tik žinutę. Tipinis modelis: OnText deda payload į Queue (pvz. TThreadedQueue<string>), atskiras Worker apdoroja su backpressure (t. y. ribotos Queue ilgio). Tai neleidžia, kad gausos sprogimo metu UI užšaltų arba priėmimas prarastų ritmą.

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

WebSocket73 elgsena dažnai yra „veikia dienomis, o paskui staiga nustoja“. Be registravimo (logging) tai sunkiai lokalizuojama. Naudingi registravimo taškai:

  • Laiko žyma (UTC), URL ir būsenos pokyčiai (connecting/open/closed).
  • Close-Reason, jei prieinama (Server iškelia Close vs. tinklo klaida).
  • Heartbeat siuntimo klaidos ir gavimo išimtys, įskaitant Exception tipą.
  • Papildomai: gautų žinučių dydžiai (ne turinys), kad būtų aptiktas duomenų sprogimas.

Jei TLS yra terminuojamas: papildomai patikrinkite, ar sertifikatų pasikeitimai (pasibaigimas, naujas issuer) laike koreliuoja su klaidomis. Griežtoje aplinkoje taip pat verta tikrinti proxy ir DPI dėžes (Deep Packet Inspection) kaip potencialius kaltininkus.

Variantai: kada užtenka System.Net.WebSockets – o kada ne

System.Net.WebSockets daugeliui integracijos atvejų yra pakankamas, ypač kai kalbama apie tekstą/JSON, vidutinę apkrovą ir aiškias pakartotinio prisijungimo strategijas. Ribos atsiskleidžia priklausomai nuo Delphi versijos ir taikomosios platformos:

  • Trūksta arba ribotas Ping/Pong palaikymas: Tokiu atveju patikima schema lieka App-Heartbeat.
  • Trūksta timeout9ų/atšaukimo mechanizm9ų Connect/Receive operacijose: Tuomet architektūrą reikia suprojektuoti taip, kad užstrigęs worker būtų izoliuotas ir programa vis tiek galėtų tvarkingai užsidaryti (pvz., per proceso watchdog arba atskiras worker instancijas).
  • Aukšta apkrova arba dvejetainiai srautai: Tokiu atveju verta taikyti griežtesnį framing/buffering koncepciją (pvz., žiedinis buferis, atskiras Binary-Event, pranešimų surinkėjas su apribojimais).

Legacy situacijose (senesn17s Delphi kartos, labai specifiniai TLS/Proxy reikalavimai) bibliotekos, tokios kaip ICS, kai kuriuose projektuose yra pragmati61kesnis sprendimas. Svarbiau ne „kuri biblioteka“, o kad išjungimas, pakartotinis prisijungimas ir observability (logai/metrikos) būtų traktuojami kaip prioritetiniai klausimai.

I61vada: ein Delphi WebSocket Client ist ein Betriebsbaustein 03 93 mit ai61kiomis ribomis

WebSocket puikiai tinka push-įvykiams, gyviems būsenos pranešimams, įrenginių ar procesų signalizacijai ir kaip atgalinis kanalas portalams bei paslaugoms. Pateiktas wrapperis koncentruojasi į aspektus, kurie verslo sprendimuose dažnai lemia skirtumą: kontroliuojamas pakartotinis prisijungimas, heartbeat prieš idle-timeout9us, fragmentų saugi teksto apdorojimas ir sustabdymo kelias, kuris neįstringa diegimo ar atnaujinimo metu.

Išlieka naudojimo ribos: jei jums reikia griežtų garantijų dėl Connect/Receive nutraukimo labai trumpuose laiko langeliuose arba dirbate su itin didelėmis duomenų spautomis, turėsite gilintis į timeout9us, platformos ypatumus ir, esant reikalui, alternatyvius stack9us. Tačiau daugumai integracijos ir modernizacijos scenarijų švariai inkapsuliuotas, gerai loguojamas klientas, kaip parodyta aukščiau, yra tvirta bazė, kurią galima integruoti į brand73 Delphi sistemas.

Jei norite įterpti tokį komponentą į esamą architektūrą (pvz. Layer-3 architektūrą su aiškiomis paslaugų ir UI sluoksniais) arba reikia derinti sporadinius atjungimus realiomis sąlygomis, mes galime tai tiksliai įvertinti kartu su jumis: susisiekite.

Profesiniame kontekste taip pat svarb 99 vaidmen ed atlieka heartbeat / Ping/Pong, kai integracijos, duomenų srautai ir tolesnė pl17tra turi veikti darniai.

Aptarti projekt05 ar modernizacijos iniciatyv05 su Net-Base.

Kitas žingsnis

Kai tema virsta realiu projektu, architektūra, esami sprendimai ir eksploatavimas turėtų būti nagrinėjami kartu nuo pat pradžių.

Mes padedame ne tik pavienėse užklausose, bet ir tuomet, kai iš šaltinio kodo fragmentų, paveldėtų temų ar portalo idėjų turi tapti patikimas įmonės projektas.

  • Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
  • REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
  • Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.

Pasidalinti įrašu

Tiesiogiai pasidalinti šiuo įrašu

LinkedIn, X, XING, Facebook, WhatsApp ir el. paštas yra iš karto prieinami. Instagramui paruošiame nuorodą ir trumpą tekstą iš karto.

El. paštas

Instagram atidaromas naujame skirtuke. Nuoroda ir trumpas tekstas iš anksto nukopijuojami į iškarpinę.