Net-Base Revistë

01.06.2026

Delphi Klienti WebSocket: lidhje e qëndrueshme, mbyllje e pastër, debug i besueshëm

Një klient WebSocket Delphi shpejt duket „i lidhur“ – por gjatë operimit kanë rëndësi rikonektimi, heartbeat-et, ndalimi i pastër dhe mundësia për debug. Me një wrapper praktik bazuar në System.Net.WebSockets (me fallback) dhe një fragment kodi për menaxhimin e thread-eve dhe...

01.06.2026

Nga tema e revistës në praktikën e projektit

Faqe shërbimi dhe teknike të përshtatshme për artikullin

Pse një Delphi WebSocket Client në praktikë është më shumë se „Connect“

Një Delphi WebSocket Client montehet brenda disa minutash: URL, Connect, SendText, gati. Në softuerin individual të ndërmarrjes dhe në zgjidhjet e afërta me proceset, problemi zakonisht shfaqet vetëm në funksionim: Reverse Proxy ndan lidhjet inaktive, rrugët mobile ose VPN kanë NAT-Timeout-e të shkurtra, certifikatat ndryshojnë, dhe gjatë mbylljes procesi ngec sepse një Receive-Loop ende bllokohet. Për më tepër: një WebSocket është një kanal afatgjatë dhe me gjendje – ndaj zbatohen rregulla të ndryshme sesa për HTTP/REST klasik (Request/Response, afatshkurtër).

Në këtë copë kodi nuk bëhet fjalë për „Hello WebSocket“, por për një client-wrapper të përshtatshëm për përdorim praktik me:

  • nisje/ndalim të pastër (pa ngecje gjatë mbylljes),
  • Receive-Loop me Cancellation (sinjal ndërprerjeje) në vend të „Thread kill“,
  • Reconnect me Backoff (ri-lidhje e kontrolluar),
  • Heartbeat si model aplikimi (sepse Ping/Pong nuk është i disponueshëm kudo),
  • Debug- dhe Trace-Hooks që ndihmojnë me të vërtetë në raste të mbështetjes.

Zbatimi bazohet në System.Net.WebSockets (Delphi RTL; WebSocket-Client-API me TClientWebSocket). Kur kjo shtresë RTL në versione më të vjetra nuk është e disponueshme ose është e kufizuar, një fallback përmes një biblioteke (p.sh. ICS) shpesh është i arsyeshëm – për këtë më poshtë jepet një vlerësim.

Skica e arkitekturës: një Wrapper në vend të thirrjeve të shpërndara WebSocket

Një gabim i zakonshëm në aplikacionet e konsoliduara Delphi: formularët UI ose modulet e shërbimit „flasin direkt me WebSocket“ dhe pastaj kanë kudo Timer, Threads dhe trajtime për jashtëzakonitë të shpërndara. Më e mira është një komponent i qartë me Events të mirë të përcaktuara dhe një makineri të vogël gjendjesh.

Termat, shpjegim i shkurtër: Backoff do të thotë një kohë pritjeje që pas gabimeve rritet hap pas hapi (p.sh. 1s, 2s, 4s …), për të mos mbingarkuar serverin dhe rrjetin. CancellationToken është një sinjal ndërprerjeje nga bota .NET; në Delphi nuk ka një model identik, por mund ta imitojmë me TEvent dhe një flag „StopRequested“. TThread.Queue planifikon kod për ekzekutim në thread-in kryesor (UI), pa bllokuar worker-in; Synchronize bllokon dhe shpesh është shkaku i deadlock-eve në rrugët e mbylljes.

Copë kodi: Delphi WebSocket Client me Stop, Reconnect dhe Message-Dispatch

Kodi vijues është qëllimisht ndërtuar si një „komponent operativ“: një klasë që mund të përdoret në VCL/FMX ose në një Windows- dhe Windows- und Linux-Services (sipas versionit/platformës Delphi). Bërthama është një Worker-Thread që mban Receive-Loop dhe raporton në aplikacion përmes 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; // Heartbeat i aplikacionit: i dobishëm kundër Timeout-eve të idle pas proxy-ve
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 nuk reagoi brenda Timeout-it; mund të ketë bllokim në stack-un e rrjetit‘);
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
// Vërejtje: TClientWebSocket.Connect është sinkron dhe mund të bllokojë në varësi të DNS/TLS.
// Prandaj kjo ekzekutohet këtu në 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 si mesazh i aplikacionit, sepse Ping/Pong nuk është ekspozuar qartë në çdo version të Delphi.
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;

// Receive: i bazuar në frame, prandaj StringBuilder për fragmentim.
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
// Në shumë protokolle biznesi, tekst/JSON është standard.
// Binari mund të buferohet njësoj këtu ose të përcillet direkt.
Log(llDebug, ‚Binary frame received: ‚ + Received.BytesReceived.ToString + ‚ bytes‘);
end;

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

// Mini-sleep për të kursyer CPU në loop-e shumë të shpejta.
// Mos e rritni shumë, përndryshe latenca përkeqësohet.
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.

Çfarë kodi bën qëllimisht ndryshe nga shembujt tipikë

  • Ndalesë pa forcë: Në vend që të mbyllen Threads, Stop vendos një event. Worker përfundon Loops në pika të përcaktuara. Kjo redukton ngecjet gjatë mbylljes dhe shmang rrjedhjet e burimeve në Socket-Stack.
  • Queue në vend të Synchronize: Logging dhe eventet shkojnë përmes TThread.Queue në Mainthread. Kjo është e rëndësishme kur Stop/Shutdown vjen nga UI ose nga handler-et e Service-Control. Synchronize mund të bllokojë nëse Mainthread është aktualisht duke pritur.
  • Fragmentimi merret parasysh: Teksti i WebSocket mund të vijë i fragmentuar në frame. Prandaj përdoret TStringBuilder dhe verifikimi i EndOfMessage.
  • Heartbeat si protokoll aplikacioni: Shumë konfigurime ndërpriten nga Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Një tekst i lehtë „ping“ është shpesh si mjet operimi më efektiv sesa shpresa tek „TCP keepalive“ ose një API Ping/Pong që nuk është i disponueshëm kudo.

Kushtet kornizë dhe kurthet në operim

1) DNS, TLS dhe Proxy: Connect mund të bllokohet

TClientWebSocket.Connect është sinkron. Varësisht nga zgjidhja e DNS-it, TLS-Handshake, verifikimi i certifikatës ose mjedisi i proxy-t, kjo mund të zgjasë disa sekonda. Kodi e vendos atë qëllimisht në një Worker. Nëse ju nevojiten timeoute të forta, duhet të kontrolloni në nivel API nëse versioni juaj Delphi ofron opsione Timeout, ose të kapsuloni Connect në një thread të veçantë dhe të abortoni përmes logjikës së procesit. E rëndësishme: një „abbrechen“ këtu zakonisht do të thotë „shënoni lidhjen si të prishur dhe ngrini përsëri Worker-in“, jo „vrisni menjëherë operacionin e socket-it“.

2) Idle-Timeouts: pse Heartbeat shpesh është i detyrueshëm

Në rrjetet e ndërmarrjeve, një WebSocket shpesh përfundon pas një Reverse Proxy (nginx, IIS ARR) ose një Load Balancer. Shumë nga këto komponente mbyllin lidhjet kur nuk rrjedhin të dhëna për një periudhë të gjatë. TCP-Keepalive nuk konfigurohet gjithmonë mjaft shkurt (dhe nën Windows shpesh është më shumë pjesë e minutave sesa sekondave). Një Heartbeat në nivel aplikacioni është për pasojë një zgjidhje e qëndrueshme. Sigurohuni që serveri dhe klienti të kenë të njëjtin koncept (p.sh. „ping“/„pong“ si tekst ose JSON).

3) Threading dhe UI: ngjarjet duhet të qëndrojnë të shkëputura

Nëse procesimi i OnText është i rëndë (JSON-Parsing, akseset DB me BDE-zëvendësim me lidhje native, UI-Updates), ai nuk duhet të bllokojë gjithçka në Mainthread. Wrapper-i jep vetëm mesazhin. Një model tipik është: OnText vendos payload-in në një Queue (p.sh. TThreadedQueue<string>), një Worker i veçantë përpunon me Backpressure (pra gjatësi të kufizuar të Queue). Kjo parandalon që gjatë një ngarkese Burst UI-ja të ngecë ose marrja të dalë nga ritmi.

Debugging: çfarë duhet të logoni kur ndonjëherë ndërpritet

WebSocket-et njihen për drejtimin „punon ditë me radhë, pastaj ndalon“. Pa regjistrim (logging) është vështirë ta kufizosh problemin. Pikat e regjistrimit që kanë kuptim:

  • Koha (UTC), URL, dhe ndryshimet e gjendjes (connecting/open/closed).
  • Close-Reason, nëse i disponueshëm (server kërkon Close vs. gabim rrjeti).
  • Gabimet e dërgimit të Heartbeat-it dhe përjashtimet gjatë pranimit, duke përfshirë tipin e Exception.
  • Opsionale: madhësitë e mesazheve të pranuara (jo përmbajtjet), për të zbuluar shpërthime të të dhënave.

Nëse bëni terminim mbi TLS: kontrolloni gjithashtu nëse ndryshimet e certifikatave (skadim, autor i ri) korrelacionojnë kohësisht me gabimet. Në ambiente të fortifikuara, edhe kuti proxy dhe DPI-Boxen (Deep Packet Inspection) janë kandidatë.

Variantet: kur mjafton System.Net.WebSockets – dhe kur jo

System.Net.WebSockets mjafton për shumë raste integrimi, veçanërisht kur bëhet fjalë për tekst/JSON, ngarkesë të moderuar dhe strategji të qarta për reconnect. Kufizimet shfaqen sipas Delphi-versionit dhe platformës së synuar:

  • Mungesë/mbështetje e kufizuar për Ping/Pong: Atëherë App-Heartbeat mbetet modeli i qëndrueshëm.
  • Mungesa e Timeouts/Cancellation në Connect/Receive: Atëherë duhet të ndërtoni arkitekturën në mënyrë që një worker i bllokuar të mbetet i izoluar dhe aplikacioni përsëri të mbyllet në mënyrë të pastër (p.sh. me një watchdog të procesit ose instancia worker të ndara).
  • Ngarkesë e lartë ose stream-e binare: Atëherë vlen të përdoret një koncept më i fortë për framing/buffering (p.sh. ring buffer, event binar i ndarë, Message-Assembler me kufizime).

Për situata legacy (gjenerata të vjetra të Delphi, kërkesa shumë specifike për TLS/Proxy) bibliotekat si ICS në disa projekte janë më pragmatike. Më e rëndësishme nuk është “cila bibliotekë”, por që të trajtoni Shutdown, Reconnect dhe Observability (Logs/Metriken) si tema me prioritet të lartë.

Përfundim: një Delphi WebSocket Client është një bllok ndërtimi për operacionet – me kufizime të qarta

Një WebSocket përshtatet mirë për push-events, status live, njoftime makinerish ose procesesh dhe si kanal kthimi për portale dhe shërbime. Wrapper-i i treguar fokusohet te pikat që shpesh bëjnë ndryshimin në zgjidhjet digjitale për bizneset: reconnect i kontrolluar, Heartbeat kundër idle-timeout, përpunim teksti i sigurt ndaj fragmentimit dhe një rrugë ndalimi që gjatë deployment-it ose update-it nuk ngec.

Vazhdojnë kufizimet e përdorimit: Nëse keni nevojë për garanci të forta për ndërprerje Connect/Receive brenda dritareve shumë të shkurtra kohore ose për transmetime me shpejtësi jashtëzakonisht të larta të të dhënave, duhet të zhyteni më thellë në timeouts, veçoritë e platformës dhe, nëse e nevojshme, stack-e alternative. Për shumicën e skenarëve të integrimit dhe modernizimit, një klient i pastërisht i kapsulluar dhe mirë i log-uar si më sipër mbetet një bazë solide që mund të integrohet në sistemet ekzistuese Delphi.

Nëse doni të përshtatni një komponent të tillë në një arkitekturë ekzistuese (p.sh. Layer-3 arkitekturë me shtresa të qarta shërbimi dhe UI) ose të debug-oni disconnect-e sporadike në kushte reale, mund ta kategorizoni këtë me ne në mënyrë të synuar: Merrni kontakt.

Në ambientin profesional, Heartbeat Ping/Pong luajnë gjithashtu një rol të rëndësishëm kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të punojnë së bashku në mënyrë të pastër.

Diskutoni projektin ose iniciativën e modernizimit me Net-Base.

Hapi tjetër

Kur nga një temë bëhet një projekt real, arkitektura, sistemi ekzistues dhe operimi duhet të merren në konsideratë së bashku që në fazat e hershme.

Ne nuk mbështesim vetëm në çështje të veçanta, por edhe kur nga fragmente të kodit burimor, temat legacy ose idetë për portale duhet të zhvillohen në një projekt korporativ të qëndrueshëm.

  • Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
  • REST, akses në të dhëna, portalet dhe Rollout nuk shtyhen si pasoja të mëvonshme.
  • Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.