Frá tímaritsþema til verkefnaframkvæmdar
Viðeigandi þjónustu- og tæknisíður fyrir greinina
Af hverju er Delphi WebSocket Client í rekstri meira en „Connect“
Ein Delphi WebSocket Client er settur saman á nokkrum mínútum: URL, Connect, SendText, tilbúið. Í sérsniðnum fyrirtækjakerfum og ferlapróxímálum kemur málið þó yfirleitt fram í rekstri: Reverse Proxy rýfur óvirk tengsl, farsíma- eða VPN-tengingar hafa stutt NAT-Timeouts, vottorð breytast, og við lokun hangir ferlið vegna þess að Receive-Loop er enn blokkaður. Enn fremur: WebSocket er langlífur, ástandsmiðaður rás — því gilda aðrar reglur en fyrir hefðbundið HTTP/REST (Request/Response, skammvinnt).
Í þessum Source-Schnipsel snýst það ekki um „Hello WebSocket“, heldur um rekstrarhæfan Client-Wrapper með:
- hreinum Start/Stop (engin hang á lokun),
- Receive-Loop með Cancellation (stöðvunarmerki) í stað „Thread kill“,
- Reconnect með Backoff (stýrð endurtenging),
- Heartbeat sem viðmótsmynstur (því Ping/Pong er ekki alltaf tiltækt),
- Debug- og Trace-Hooks sem hjálpa raunverulega í stuðningsmálum.
Útfærslan byggir á System.Net.WebSockets (Delphi RTL; WebSocket-Client-API með TClientWebSocket). Þar sem þetta RTL-lag er ekki tiltækt í eldri útgáfum eða of takmarkað er oft skynsamlegt að nota fallback í formi bókasafns (t.d. ICS) — nánari umfjöllun kemur síðar.
Arkitektúrskissa: einn Wrapper í stað dreifðra WebSocket-kalla
Algeng villa í vaxandi Delphi-forritum: UI-form eða þjónustumodúl „tala beint við WebSocket“ og hafa þá víða timers, þræði og undantekningameðhöndlun dreifða. Betra er skýr eining með vel skilgreindum atburðum og litla ástandsvél.
Hugtök stutt til skýringa: Backoff merkir biðtíma sem eykst stigvaxandi eftir villur (t.d. 1s, 2s, 4s …) til að ofhlaða ekki þjón eða net. CancellationToken er stöðvunarmerki úr .NET-heiminum; í Delphi er ekki nákvæmlega samsvarandi mynstur, en það má herma með TEvent og „StopRequested“-fána. TThread.Queue skipuleggur kóða til keyrslu í aðalþræðinum (UI) án þess að hindra Worker; Synchronize læsist og er í lokunarstefnum oft orsök fyrir deadlocks.
Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch
Eftirfarandi kóði er viljandi uppsettur sem „rekstrar-eining“: klasi sem má nota svipað í VCL/FMX eða í Windows- og Windows- og Linux-Services (eftir Delphi-útgáfu/kerfi). Kjarni lausnarinnar er Worker-Thread sem heldur Receive-Loop og tilkynnir um atburði inn í forritið.
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: gagnlegt gegn óvirkutímatakmörkum hjá proxyjum
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 ekki innan tímataks; möguleg stífla í netstakknum');
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
// Athugið: TClientWebSocket.Connect er samstillt og getur, undir áhrifum DNS/TLS, hindrað keyrslu.
// Þess vegna keyrir þetta í Worker-þræði.
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 sem forritaskilaboð, því Ping/Pong er ekki vel aðgengilegt í öllum Delphi-útgáfum.
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;
// Móttaka: rammabundin, því StringBuilder er notaður fyrir fragmentun.
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
// Í mörgum fyrirtækjaprótókollum er texti/JSON staðlaður.
// Tvíundagögn (binary) er hægt að búffa hér á svipaðan hátt eða senda beint áfram.
Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, 'Server requested close');
Break;
end;
end;
// Stutt svefn til að minnka CPU-notkun í mjög hraðri lykkju.
// Ekki of langt, annars versnar biðtími.
TThread.Sleep(1);
end;
finally
SB.Free;
end;
SafeClose;
State('closed');
finally
WS.Free;
end;
if StopRequested then
Break;
// Endurtengja eftir hreint lok eða eftir villur
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.
Hvað kóðinn gerir viljandi „annað“ en dæmigerð dæmi
- Stop án ofbeldis: Í stað þess að slökkva á þræði setur Stop atburð. Vinnsluþráðurinn lýkur lykkjum á skilgreindum stöðum. Þetta dregur úr hang-tilvikum við lokun og kemur í veg fyrir auðlindalekkar í socket-stakknum.
- Queue í stað Synchronize: Skráningar og atburðir fara með TThread.Queue í aðalþráðinn. Þetta er mikilvægt þegar Stop/Shutdown kemur úr UI eða úr Service-Control-handlerum. Synchronize getur blokkerað ef aðalþráðurinn er í biðstöðu.
- Tekið tillit til fragmentunar: WebSocket-texti getur borist í ramma (fragment). Þess vegna er notaður TStringBuilder og athugað EndOfMessage.
- Heartbeat sem forrits-protókoll: Mörg uppsett umhverfi deyja vegna idle-timeouts (Load Balancer, nginx, Cloud WAF). Léttur „ping“-texti er oft virkari rekstrarhandfang en að treysta á „TCP keepalive“ eða á þeirri von að Ping/Pong-API sé til staðar alls staðar.
Skilyrði og gildrur í rekstri
1) DNS, TLS og Proxy: Connect getur hindrast
TClientWebSocket.Connect er samstillt. Fer eftir DNS-upplausn, TLS-handshake, vottunarathugun eða proxy-umhverfi getur þetta tekið nokkrar sekúndur. Kóðinn setur þetta af ásettu ráði í vinnsluþráð. Ef þörf er á harðari tímalokum þarf að kanna á API-stigi hvort útgáfa af Delphi bjóði upp á timeout-valkosti, eða innpakka Connect í sértækan þræði og hætta með ferlalógík. Mikilvægt: „að hætta/afrita“ þýðir hér yfirleitt „merkja tengingu sem skemmd og endurræsa vinnsluþráðinn“, ekki „drepa socket-aðgerðina samstundis“.
2) Idle-Timeouts: hvers vegna Heartbeat er oft nauðsyn
Í fyrirtækjanetum endar WebSocket yfirleitt aftan reverse proxy (nginx, IIS ARR) eða load balancer. Margar slíkar einingar loka tengingum ef engin gögn flæða í langan tíma. TCP-Keepalive er ekki alltaf stillt með stutt bil (og undir Windows er það oft frekar í mínútum en sekúndum). Heartbeat á forritsstigi er því stöðugur og áreiðanlegur leiðréttingarhnífur. Gætið þess að þjónn og viðskiptavinur hafi sama fyrirkomulag (t.d. „ping“/„pong“ sem texti eða JSON).
3) Þráðastjórnun og UI: Atburðir verða að vera óháðir
Ef OnText-meðhöndlun er þung (JSON-parsing, gagnagrunnsaðgerðir með BDE-lausn með innfæddri tengingu, UI-uppfærslur), ætti hún ekki að loka aðalþræðinum. Wrapperinn skilar einungis skilaboðunum. Algengt mynstur er að OnText setji payload í röð (t.d. TThreadedQueue<string>), og sértækur vinnsluþráður með bakþrýstingi (þ.e. takmörkuð biðröðarlengd) vinni úr þeim. Þetta kemur í veg fyrir að UI frjósi eða að móttaka fari úr skorðum við álagsköst.
Rökgreining: hvað þú ættir að skrá ef það bilar „stundum“
WebSockets eru þekkt fyrir að „gangi í marga daga og hætti svo“. Án logging er nánast ómögulegt að þrengja niður. Hagnýtir loggpunkter:
- Tímasetning (UTC), URL og ástandsskipti (connecting/open/closed).
- Close-Reason (lokunarástæða), ef tiltækt (þjónn krefst Close vs. netvilla).
- Heartbeat-sendivillur og móttöku-undantekningar, þ.m.t. undantekningartegund.
- Valfrjálst: stærðir móttekinna skilaboða (ekki efni þeirra), til að greina gagnasprengingu.
Ef þú terminierar yfir TLS: athugaðu einnig hvort breytingar á vottorðum (gildistími, nýr Issuer) samræmist villum tímabundið. Í harðgerðum umhverfum geta proxy- og DPI-boxar (Deep Packet Inspection) einnig verið sökudólgar.
Útfæringar: hvenær System.Net.WebSockets dugar – og hvenær ekki
System.Net.WebSockets er fyrir mörg samþættingartilvik nægjanlegt, sérstaklega þegar um Text/JSON, hóflegt álag og skýrar endurtengingarstefnur er að ræða. Takmarkanir koma í ljós eftir Delphi-útgáfu og markmiði markkerfisins:
- Takmarkaður eða enginn Ping/Pong-stuðningur: Þá er App-Heartbeat áfram traust kerfismynstur.
- Skortur á timeouts/afpöntun við Connect/Receive: Þá verður þú að hanna arkitektúrinn þannig að fastur worker haldist einangraður og forritið lokist samt hreint (t.d. með ferilvakta eða aðskildum worker-tilvikum).
- Mikið álag eða binærstraumar: Þá borgar sig sterkara framing-/buffering-hugmyndafræði (t.d. ring buffer, aðskilið Binary-Event, Message-Assembler með takmörkum).
Fyrir legacy-aðstæður (eldri Delphi-kynslóðir, mjög sértækar TLS/Proxy-kröfur) eru bókasöfn eins og ICS í sumum verkefnum hagnýtari. Mikilvægara er ekki „hvort bókasafn“, heldur að meðhöndla Shutdown, Reconnect og Observability (logs/metrík) sem forgangsmál.
Niðurstaða: Delphi WebSocket-klient er rekstrarhluti – með skýrum mörkum
WebSocket hentar vel fyrir push-atburði, rauntíma stöðu, véla- eða feriltilkynningar og sem bakrás fyrir gáttir og þjónustur. Sá wrapper sem sýndur er einbeitir sér að þeim atriðum sem oft gera muninn í stafrænum fyrirtækjalausnum: stjórnað endurtenging, Heartbeat gegn idle-timeouts, fragment-örugg textavinnsla og stöðvunarstígur sem festist ekki við deployment eða uppfærslu.
Takmörk við notkun standa þó: Ef þú þarft strangar tryggingar um að Connect/Receive verði rofin innan mjög stuttra tímaglugga eða keyrir mjög háan gagnahraða, þarf að kafa dýpra í timeouts, sérkenni platformar og mögulega aðra stakka. Fyrir meirihluta samþættingar- og nútímavæðingarsena er hreinn, vel skráður client eins og sýndur hér þó traustur grunnur sem samþættist rótgrónum Delphi-kerfum.
Ef þú ætlar að innlima slíkan þátt í fyrirliggjandi arkitektúr (t.d. Layer-3 arkitektúr með skýrum þjónustu- og UI-lögum) eða þarft að greina sporadíska aftengingu undir raunskilyrðum, getum við hjálpað þér að flokka þetta markvisst: Hafðu samband við okkur.
Í faglegu samhengi gegna Heartbeat Ping/Pong einnig mikilvægu hlutverki þegar samþættingar, gagnastraumar og áframhaldandi þróun þurfa að spila vel saman.
Næsta skref
Þegar úr málinu verður raunverulegt verkefni ber að skoða arkitektúr, núverandi kerfi og rekstur snemma saman.
Við styðjum ekki aðeins við einstakar spurningar, heldur einnig þegar úr kóðabútum, eldri kerfum eða gáttahugmyndum þarf að verða traust fyrirtækjaverkefni.
- Núverandi staða, markmynd og tæknileg áhætta eru metin saman.
- REST, gagnaaðgangur, gáttir og innleiðing eru ekki skildir eftir til síðar.
- Það sést snemma hvaða leið er fjárhagslega og rekstrarlega sjálfbær.