No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
Kāpēc ein Delphi WebSocket Client praksē ir vairāk nekā „Connect“
Ein Delphi WebSocket Client ir salikts minūtēs: URL, Connect, SendText, gatavs. Tomēr individuālajā uzņēmumu programmatūrā un ar procesiem saistītos risinājumos problēmas parasti parādās tikai ekspluatācijā: Reverse Proxy pārtrauc inaktīvās savienojumus, mobilās vai VPN līnijas ir ar īsiem NAT-Timeouts, sertifikāti tiek mainīti, un izbeidzot procesu tas var iestrēgt, jo Receive-Loop joprojām bloķē. Papildus: WebSocket ir ilglaicīgs, stāvokli saglabājošs kanāls — tam ir citas likumsakarības nekā klasiskajam HTTP/REST (Request/Response, īslaicīgs).
Šajā Source-Schnipsel nav runa par „Hello WebSocket“, bet gan par praksē izmantojamu klienta apvalku ar:
- sakārtotu Start/Stop (bez iestrēgšanas izslēgšanas laikā),
- Receive-Loop ar Cancellation (pārtraukšanas signāls) nevis „Thread kill“,
- Reconnect ar Backoff (kontrolēta atkārtota pieslēgšanās),
- Heartbeat kā lietojuma modelis (jo Ping/Pong nav pieejams visur),
- Debug- un Trace-Hooks, kas patiesi palīdz atbalsta gadījumos.
Realizācija balstīta uz System.Net.WebSockets (Delphi RTL; WebSocket-Client-API ar TClientWebSocket). Ja šī RTL slāņa vecākās versijās nav pieejama vai tā ir pārāk ierobežota, bieži lietderīgs ir fallback, izmantojot bibliotēku (piem., ICS) — par to zemāk sniegts skaidrojums.
Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe
Bieži sastopama kļūda krietni audzētās Delphi-lietotnēs: UI-formas vai servisa moduļi „sprechen direkt WebSocket“ un tad visur ir izkaisīti Timer, Threads un izņēmumu apstrāde. Labāka pieeja ir skaidrs komponents ar labi definētiem Events un nelielu stāvokļa mašīnu.
Terminu īss skaidrojums: Backoff nozīmē gaidīšanas laiku, kas pēc kļūdām pakāpeniski palielinās (piem., 1s, 2s, 4s …), lai neuzlādētu serveri un tīklu. CancellationToken ir pārtraukšanas signāls no .NET pasaules; in Delphi nav identiska modeļa, bet to var atdarināt ar TEvent un „StopRequested“ karodziņu. TThread.Queue plāno kodu izpildei galvenajā pavedienā (UI), nekavējot worker; Synchronize bloķē un bieži shutdown ceļos ir deadlock cēlonis.
Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch
Sekojošais kods apzināti veidots kā „Betriebs-Baustein“: klase, kuru var līdzīgi izmantot VCL/FMX vai Windows- un Windows- und Linux-Services (atkarībā no Delphi versijas/platformas). Kodols ir worker-thread, kas uztur Receive-Loop un caur Events paziņo lietotnei.
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: noderīgs pret inaktivitātes timeoutiem aiz starpniekserveriem
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: Darba pavediens nereaģē robežas laikā; iespējams bloķējums tīkla stekā');
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
// Piezīme: TClientWebSocket.Connect ir sinhrons un var bloķēt atkarībā no DNS/TLS.
// Tāpēc tas tiek izpildīts šeit darba pavedienā.
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 kā lietotnes ziņa, jo Ping/Pong ne vienmēr ir korekti pieejams katrā Delphi versijā.
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;
// Saņemšana: rāmju bāzēta, tāpēc TStringBuilder fragmentācijai.
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
// Daudzos biznesa protokolos teksts/JSON ir noklusējums.
// Bināros datus šeit var līdzīgi buferēt vai nodot tālāk tieši.
Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, 'Server requested close');
Break;
end;
end;
// Īsa pauze, lai ļoti ātrā ciklā saudzētu CPU.
// Nepārāk gara, citādi pasliktinās latentums.
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.
Kas kods apzināti „citādāk” dara nekā tipiskie piemēri
- Stop bez vardarbības: Tā vietā, lai pavedienus nogalinātu, Stop iestata Event. Darba pavediens pabeidz cilpas definētās vietās. Tas samazina iesaldēšanu izbeigšanas laikā un novērš resursu noplūdes Socket-Stack.
- Queue nevis Synchronize: Žurnālu ieraksti un notikumi tiek virzīti uz galveno pavedienu, izmantojot TThread.Queue. Tas ir svarīgi, ja Stop/Shutdown tiek izsaukts no UI vai no Service-Control handleriem. Synchronize var bloķēt, ja galvenais pavediens pašlaik gaida.
- Ņemot vērā fragmentāciju: WebSocket teksts var tikt fragmentēts pa frame’iem. Tāpēc tiek izmantots TStringBuilder un tiek pārbaudīts EndOfMessage.
- Heartbeat kā aplikācijas protokols: Daudzi risinājumi mirst dēļ idle-timeoutiem (Load Balancer, nginx, Cloud WAF). Viegls „ping” teksts kā darbības sviras bieži vien ir efektīvāks nekā cerēt uz „TCP keepalive” vai uz Ping/Pong‑API, kas nav pieejami visur.
Darbības nosacījumi un riski ekspluatācijā
1) DNS, TLS un Proxy: Connect var bloķēt
TClientWebSocket.Connect ir sinhrons. Atkarībā no DNS atrisināšanas, TLS rokasspiediena, sertifikāta pārbaudes vai proxy vides tas var aizņemt vairākas sekundes. Kods apzināti izvieto šo operāciju worker. Ja nepieciešami stingri timeoutu ierobežojumi, API līmenī pārbaudiet, vai jūsu Delphi‑versija nodrošina timeout opcijas, vai arī iesaiņojiet Connect atsevišķā pavedienā un pārtrauciet to ar procesu loģiku. Svarīgi: „atcelt” parasti nozīmē „atzīmēt savienojumu kā bojātu un uzsākt worker no jauna”, nevis „nekavējoties nogalināt socket‑operāciju”.
2) Idle-Timeouts: kāpēc Heartbeat bieži vien ir obligāts
Uzņēmumu tīklos WebSocket bieži tiek termiņēts aiz reversā proxy (nginx, IIS ARR) vai load balancera. Daudzas no šīm komponentēm slēdz savienojumus, ja ilgstoši nenotiek datu plūsma. TCP‑keepalive ne vienmēr ir konfigurēts ar pietiekami īsu intervālu (un zem Windows bieži tas ir drīzāk minūtēs nekā sekundēs). Tāpēc Heartbeat lietojuma līmenī ir stabils risinājums. Pārliecinieties, ka serveris un klients izmanto vienu un to pašu konceptu (piem., „ping”/„pong” kā tekstu vai JSON).
3) Threading und UI: Notikumiem jāpaliek atdalītiem
Ja OnText apstrāde ir smaga (JSON parsēšana, DB piekļuve ar BDE-aizvietošana ar vietēju pieslēgumu, UI atjauninājumi), tā nedrīkst bloķēt galveno pavedienu. Wrapperis nodod tikai ziņu. Tipisks paraugs: OnText noliek payload rindā (piem., TThreadedQueue<string>), atsevišķs worker to apstrādā ar backpressure (t.i., ierobežotas rindas garuma). Tas novērš, ka pie burst‑slodzes UI iesaldējas vai saņemšana iziet no ritma.
Atkļūdošana: ko jālogē, ja tas „reizēm” pārtrauc darboties
WebSockets ir bēdīgi slaveni ar situāciju „strādā dienām, tad vairs ne”. Bez žurnāla to gandrīz nevar ierobežot. Jēdzīgi logpunkti:
- Laika zīmogs (UTC), URL, un stāvokļa pārejas (connecting/open/closed).
- Close‑Reason, ja pieejams (serveris iniciē Close vs tīkla kļūme).
- Heartbeat sūtīšanas kļūdas un saņemšanas izņēmumi, tostarp izņēmuma tips.
- Pēc izvēles: saņemto ziņu izmēri (ne to saturs), lai konstatētu datu eksploziju.
Ja TLS tiek terminēts pie jums: papildus pārbaudiet, vai sertifikātu maiņas (derīguma beigas, jauns izdevējs) laika ziņā korelē ar kļūdām. Cietinātās vidēs arī proxy un DPI iekārtas (Deep Packet Inspection) ir iespējamie cēloņi.
Varianti: kad System.Net.WebSockets pietiek — un kad nē
System.Net.WebSockets ir pietiekams daudziem integrācijas gadījumiem, īpaši, ja runa ir par tekstu/JSON, mērenu slodzi un skaidrām savienojuma atjaunošanas stratēģijām. Ierobežojumi parādās atkarībā no Delphi versijas un mērķplatformas:
- Trūkstošs/ierobežots Ping/Pong atbalsts: Tad kā robusts risinājums saglabājas lietotnes heartbeat.
- Trūkstoši laika ierobežojumi/atcelšana Connect/Receive operācijās: Tad arhitektūru jāveido tā, lai iestrēdzis workers būtu izolēts un lietojumprogramma tomēr varētu tīri pabeigt darbu (piem., ar procesa watchdog vai atsevišķām worker instancēm).
- Liela slodze vai binārie straumi: Tad atmaksājas stingrāks framing/buffering koncepts (piem., ring buffer, atsevišķs Binary-Event, Message-Assembler ar limita kontroli).
Legacy situācijām (vecākas Delphi paaudzes, ļoti specifiskas TLS/Proxy prasības) bibliotēkas kā ICS dažos projektos ir pragmatiskākas. Svarīgāk nav „kura bibliotēka“, bet gan tas, ka jūs aplūkojat izslēgšanu (shutdown), savienojuma atjaunošanu (reconnect) un novērošanu (logs/metriki) kā pirmklasīgas tēmas.
Sekojums: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen
WebSocket ir ļoti piemērots push-notikumiem, tiešā statusa atjauninājumiem, mašīnu vai procesu paziņojumiem un kā atpakaļkanāls portāliem un servisiem. Demonstrētais wrapper fokusējas uz punktiem, kas digitālajos uzņēmuma risinājumos bieži nosaka atšķirību: kontrolēta savienojuma atjaunošana, heartbeat pret idle-timeoutiem, fragmentu droša teksta apstrāde un apturēšanas ceļš, kas neveido iestrēgumu izvietošanas vai atjaunināšanas laikā.
Ierobežojumi saglabājas: ja jums nepieciešamas stingras garantijas Connect/Receive pārtraukumam ļoti īsos laika logus vai ja darbināt ārkārtīgi lielas datu caurlaidspējas, jums jāiedziļinās laika ierobežojumos, platformas īpatnībās un, ja nepieciešams, alternatīvos stekos. Tomēr lielākajai daļai integrācijas un modernizācijas scenāriju labi izolēts, labi žurnālots klients kā augstāk attēlots ir stabils pamats, ko var integrēt esošos Delphi-sistemas.
Ja jums jāiebūvē šāds būvbloks esošā arhitektūrā (piem., Layer-3 arhitektūra ar skaidrām servisa un UI slāņiem) vai jādebugo sporādiskas atslēgšanās reālajos apstākļos, mēs to varam mērķtiecīgi kontekstualizēt: sazinieties ar mums.
Tehniskajā kontekstā arī heartbeat Ping/Pong spēlē nozīmīgu lomu, ja integrācijām, datu plūsmām un turpmākajai izstrādei jādarbojas skaidri un saskaņoti.
Nākamais solis
Ja no tēmas rodas reāls projekts, arhitektūra, esošais stāvoklis un ekspluatācija būtu jāizskata kopā jau agri.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
- Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.