A magazintémától a projektgyakorlatig
A bejegyzéshez tartozó szolgáltatási és technikai oldalak
Miért több egy Delphi WebSocket kliens a gyakorlatban, mint a „Connect”
Ein Delphi WebSocket Client ist in Minuten zusammengesteckt: URL, Connect, SendText, fertig. In individueller Unternehmenssoftware und prozessnahen Softwarelösungen kippt das Thema aber meist erst im Betrieb: Der Reverse Proxy trennt Idle-Verbindungen, Mobil- oder VPN-Strecken haben kurze NAT-Timeouts, Zertifikate wechseln, und beim Beenden hängt der Prozess, weil ein Receive-Loop noch blockiert. Dazu kommt: Ein WebSocket ist ein langlebiger, zustandsbehafteter Kanal – damit gelten andere Regeln als bei klassischem HTTP/REST (Request/Response, kurzlebig).
Ebben a forráskódrészletben nem a „Hello WebSocket” a célpont, hanem egy üzemeltetésre alkalmas kliens-wrapper, amely:
- tiszta Start/Stop viselkedést biztosít (nem akad le a leállításnál),
- Receive-Loop Cancellation-nel (megszakítási jel) a „Thread kill” helyett,
- Reconnect Backoff-pal (vezérelt újracsatlakozás),
- Heartbeat mint alkalmazási minta (mivel Ping/Pong nem mindenütt elérhető),
- debug- és trace-hookok, amelyek támogatási esetekben valódi segítséget nyújtanak.
Az implementáció a System.Net.WebSockets-re épül (Delphi RTL; WebSocket-Client-API TClientWebSocket-tel). Ha ez az RTL-réteg régebbi verziókban nem érhető el, vagy túlzottan korlátozott, gyakran célszerű egy külső könyvtárra (pl. ICS) váltani – erről lentebb található egy besorolás.
Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe
Egy gyakori hiba a felhalmozódott Delphi-alkalmazásokban: a felhasználói felület űrlapjai vagy szolgáltatásmodulok „közvetlenül WebSocket-ként beszélnek”, és ennek következtében mindenütt időzítők, threadek és kivételkezelések szóródnak. Jobb egy egyértelmű komponens jól definiált eseményekkel és egy kis állapotgéppel.
Fogalmak röviden: Backoff egy várakozási időt jelent, amely hibák után fokozatosan növekszik (pl. 1s, 2s, 4s …), hogy ne terheljük túl a szervert és a hálózatot. CancellationToken a .NET világából ismert megszakítási jel; Delphi-ban nincs pontosan azonos minta, de ezt TEvent-tel és egy „StopRequested” flaggel modellezhetjük. TThread.Queue kódot ütemez a főszálon (UI) végrehajtásra anélkül, hogy blokkolná a munkavégzőt; Synchronize blokkol, és a leállítási utakban gyakran okozza a deadlockokat.
Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch
A következő kód szándékosan „üzemi komponensként” van felépítve: egy osztály, amely VCL/FMX-ben vagy egy Windows- és Windows- und Linux-Services (a Delphi-verziótól/ -platformtól függően) hasonló módon használható. A magja egy worker-thread, amely fenntartja a receive-loopot és eseményeken keresztül jelenti az alkalmazásnak.
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; // Alkalmazás-heartbeat: hasznos a proxytól eredő inaktivitási timeoutok ellen
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: A worker nem reagál a megadott időkorláton belül; lehetséges blokkolás a hálózati stackben');
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
// Megjegyzés: a TClientWebSocket.Connect szinkron, és DNS/TLS-től függően blokkolhat.
// Ezért ez itt a worker szálon fut.
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 alkalmazásüzenetként, mert a Ping/Pong nem érhető el megbízhatóan minden Delphi-verzióban.
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;
// Fogadás: frame-alapú, ezért StringBuilder a fragmentálás kezelésére.
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
// Sok üzleti protokollban a Text/JSON a szabvány.
// Binary-t hasonlóan lehet itt pufferelni vagy közvetlenül továbbadni.
Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, 'Server requested close');
Break;
end;
end;
// Rövid szünet, hogy nagyon gyors ciklusnál kímélje a CPU-t.
// Ne legyen túl nagy, különben romlik a késleltetés.
TThread.Sleep(1);
end;
finally
SB.Free;
end;
SafeClose;
State('closed');
finally
WS.Free;
end;
if StopRequested then
Break;
// Újracsatlakozás tiszta lezárás vagy hibák után
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.
Miért teszi a kód szándékosan „másképp”, mint a tipikus példák
- Stop ohne Gewalt: Ahelyett, hogy szálakat kilőnénk, a Stop egy eseményt állít be. A Worker meghatározott helyeken fejezi be a ciklusokat. Ez csökkenti a lefagyásokat leállításkor és elkerüli az erőforrás-szivárgásokat a socket-stackben.
- Queue statt Synchronize: A naplózás és az események a TThread.Queue-n keresztül jutnak el a főszálhoz. Ez fontos, ha a Stop/Shutdown az UI-ból vagy Service-Control-handlerből érkezik. A Synchronize blokkolhat, ha a főszál épp várakozik.
- Fragmentáció figyelembevétele: A WebSocket-szöveg frame-ekben atomonként érkezhet. Ezért van a TStringBuilder és az EndOfMessage ellenőrzése.
- Heartbeat mint alkalmazási protokoll: Sok környezet elbukik az idle-timeoutok miatt (Load Balancer, nginx, Cloud WAF). Egy könnyű „ping” szöveg üzemeltetési mechanizmusként gyakran hatékonyabb, mint a „TCP keepalive”-tól való remény vagy egy nem mindenhol elérhető Ping/Pong-API.
Üzemeltetési feltételek és buktatók
1) DNS, TLS und Proxy: Connect kann blockieren
TClientWebSocket.Connect szinkron. A DNS-felbontástól, TLS-kézfogástól, tanúsítvány-ellenőrzéstől vagy proxy-környezettől függően ez több másodpercig is eltarthat. A kód szándékosan egy Workerbe helyezi ezt. Ha további szigorú időkorlátokra van szükség, API-szinten ellenőrizze, hogy az Ön Delphi-verziója kínál-e timeout-opciókat, vagy csomagolja a Connect-et egy külön szálba, és szakítsa meg folyamatlogikával. Fontos: itt a „megszakítás” általában azt jelenti, hogy a kapcsolatot hibásnak jelölik és a Workert újra felépítik, nem pedig a socket-művelet azonnali kilövése.
2) Idle-Timeouts: warum Heartbeat häufig Pflicht ist
Vállalati hálózatokban a WebSocket gyakran egy reverse proxy (nginx, IIS ARR) vagy egy load balancer mögött végződik. Sok ilyen komponens lezárja a kapcsolatot, ha hosszabb ideig nem áramlik adat. A TCP-Keepalive nem mindig van elég rövidre konfigurálva (és Windows alatt gyakran inkább percekről van szó, mint másodpercekről). Ezért egy alkalmazási szintű Heartbeat stabil workaround. Ügyeljen rá, hogy a szerver és a kliens ugyanazt a koncepciót használja (pl. „ping”/„pong” szövegként vagy JSON-ként).
3) Threading und UI: Ereignisse müssen entkoppelt bleiben
Ha az OnText-feldolgozás erőforrásigényes (JSON-parszolás, adatbázis-hozzáférések BDE-kiváltás natív csatlakozással, UI-frissítések), akkor nem szabad, hogy mindent blokkoljon a főszálon. A wrapper csak az üzenetet adja át. Egy tipikus minta: az OnText a payloadot beteszi egy Queue-ba (pl. TThreadedQueue<string>), egy külön Worker backpressure-szel dolgozza fel (azaz korlátozott Queue-hosszal). Ez megakadályozza, hogy burst-terhelés esetén a UI lefagyjon vagy a fogadás kicsússzon a ritmusból.
Hibakeresés: mit naplózzon, ha „néha” megszakad
A WebSocketek híresek: „napokig fut, aztán egyszer csak megszakad”. Napló nélkül ezt alig lehet lokalizálni. Érdemi logpontok:
- Időbélyeg (UTC), URL és állapotváltások (connecting/open/closed).
- Lezárás oka (Close-Reason), ha elérhető (szerver kezdeményezi a Close vs. hálózati hiba).
- Heartbeat-küldési hibák és fogadási kivételek, a kivétel típusával együtt.
- Opcionális: a beérkező üzenetek méretei (nem a tartalom), hogy az adatrobbanás felismerhető legyen.
Ha TLS-nél terminál: vizsgálja azt is, hogy a tanúsítványcserék (lejárat, új issuer) időben korrelálnak-e a hibákkal. Megerősített környezetekben a proxy- és DPI-eszközök (Deep Packet Inspection) is gyanúsítottak.
Variánsok: mikor elegendő a System.Net.WebSockets – és mikor nem
System.Net.WebSockets sok integrációs esetben elegendő, különösen szöveg/JSON, mérsékelt terhelés és egyértelmű újracsatlakozási stratégiák esetén. A korlátok a Delphi-verziótól és a célplatformtól függően jelentkeznek:
- Hiányzó/korlátozott Ping/Pong-támogatás: Ilyenkor az alkalmazás-Heartbeat marad a robusztus minta.
- Hiányzó Timeoutok/Cancellation a Connect/Receive műveletekben: Ilyenkor úgy kell kialakítani az architektúrát, hogy egy beragadt worker izolált maradjon, és az alkalmazás mégis tisztán leállítható legyen (pl. folyamat-watchdoggal vagy külön worker-példányokkal).
- Nagy terhelés vagy bináris stream-ek esetén: Ilyenkor érdemes erősebb framing/buffering koncepciót alkalmazni (pl. ring buffer, külön Binary-Event, Message-Assembler korlátokkal).
Legacy helyzetekben (régebbi Delphi-generációk, nagyon specifikus TLS/Proxy-követelmények) bizonyos projektekben pragmatikusabbak lehetnek az olyan könyvtárak, mint az ICS. Fontosabb a „melyik Library“ kérdésénél, hogy a Shutdown, Reconnect és Observability (logok/metrikák) elsőrendű témaként legyen kezelve.
Következtetés: egy Delphi WebSocket Client egy üzemeltetési építőelem – világos korlátokkal
Egy WebSocket kiválóan alkalmas push eseményekre, élő állapotra, gépi vagy folyamatjelzésekre, valamint visszacsatornaként portálok és szolgáltatások számára. A bemutatott wrapper azokra a pontokra fókuszál, amelyek digitális vállalati megoldásoknál gyakran döntőek: kontrollált reconnect, heartbeat az idle-timeoutok ellen, fragmentbiztos szövegfeldolgozás és egy stop-útvonal, amely a deployment vagy update során nem akad meg.
Használati korlátok megmaradnak: ha szigorú garanciákra van szüksége a Connect/Receive megszakítására nagyon rövid időablakokban, vagy rendkívül nagy adatátviteli sebességgel dolgozik, mélyebben kell foglalkoznia a timeoutokkal, platform-specifikus sajátosságokkal és esetleg alternatív stackekkel. A legtöbb integrációs és modernizációs forgatókönyv számára azonban egy tisztán kapszulázott, jól logolt client, mint a fent bemutatott, szilárd alap, amely beilleszthető meglévő Delphi-rendszerekbe.
Ha egy ilyen építőelemet be kíván illeszteni egy meglévő architektúrába (pl. Layer-3 architektúra egyértelmű service- és UI-rétegekkel), vagy valós körülmények között fellépő időszakos disconnecteknél debugolni kell, ezt célzottan segítünk besorolni: Lépjen kapcsolatba velünk.
A szakmai környezetben a Heartbeat Ping/Pong is fontos szerepet játszik, ha az integrációknak, az adatfolyamoknak és a további fejlesztéseknek tisztán kell együttműködniük.
Projektet vagy modernizációs feladatot megvitatni Net-Base-vel.
Következő lépés
Ha egy témából valós projekt lesz, az architektúrát, a meglévő rendszert és az üzemeltetést korai fázisban együtt kell vizsgálni.
Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.
- A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
- REST, az adathozzáférést, a portálokat és a bevezetést nem halasztjuk későbbi fázisokra.
- Ön korán látja, melyik út gazdaságilag és üzemeltetési szempontból tartható.