Vom Magazinthema zur Projektpraxis
Passende Leistungs- und Technikseiten zum Beitrag
Warum ein Delphi WebSocket Client in der Praxis mehr als „Connect“ ist
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).
In diesem Source-Schnipsel geht es nicht um „Hello WebSocket“, sondern um einen praxistauglichen Client-Wrapper mit:
- sauberem Start/Stop (ohne Hänger beim Shutdown),
- Receive-Loop mit Cancellation (Abbruchsignal) statt „Thread kill“,
- Reconnect mit Backoff (kontrollierte Wiederanbindung),
- Heartbeat als Anwendungsmuster (weil Ping/Pong nicht überall verfügbar ist),
- Debug- und Trace-Hooks, die bei Supportfällen wirklich helfen.
Die Umsetzung basiert auf System.Net.WebSockets (Delphi RTL; WebSocket-Client-API mit TClientWebSocket). Wo diese RTL-Schicht in älteren Versionen nicht verfügbar oder zu eingeschränkt ist, ist ein Fallback über eine Bibliothek (z. B. ICS) oft sinnvoll – dazu weiter unten eine Einordnung.
Architektur-Skizze: ein Wrapper statt verstreuter WebSocket-Aufrufe
Ein häufiger Fehler in gewachsenen Delphi-Anwendungen: UI-Formulare oder Service-Module „sprechen direkt WebSocket“ und haben dann überall Timer, Threads und Ausnahmebehandlungen verteilt. Besser ist ein klarer Baustein mit wohldefinierten Events und einer kleinen Zustandsmaschine.
Begriffe kurz eingeordnet: Backoff meint eine Wartezeit, die nach Fehlern schrittweise wächst (z. B. 1s, 2s, 4s …), um Server und Netzwerk nicht zu fluten. CancellationToken ist ein Abbruchsignal aus .NET-Welt; in Delphi gibt es kein identisches Pattern, aber wir können es mit TEvent und einem „StopRequested“-Flag nachbilden. TThread.Queue plant Code zur Ausführung im Hauptthread (UI), ohne den Worker zu blockieren; Synchronize blockiert und ist in Shutdown-Pfaden oft der Grund für Deadlocks.
Source-Schnipsel: Delphi WebSocket Client mit Stop, Reconnect und Message-Dispatch
Der folgende Code ist bewusst als „Betriebs-Baustein“ aufgebaut: eine Klasse, die man in VCL/FMX oder in einem Windows- und Windows- und Linux-Services (je nach Delphi-Version/Plattform) ähnlich nutzen kann. Der Kern ist ein Worker-Thread, der den Receive-Loop hält und über Events in die Anwendung meldet.
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: nützlich gegen Idle-Timeouts hinter Proxies
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 reagiert nicht innerhalb Timeout; möglicher Block im Netzwerk-Stack');
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
// Hinweis: TClientWebSocket.Connect ist synchron und kann abhängig von DNS/TLS blockieren.
// Deshalb läuft das hier im 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 als App-Nachricht, weil Ping/Pong nicht in jeder Delphi-Version sauber exponiert ist.
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: Frame-basiert, daher StringBuilder für Fragmentierung.
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
// In vielen Business-Protokollen ist Text/JSON Standard.
// Binary kann man hier ähnlich puffern oder direkt weiterreichen.
Log(llDebug, 'Binary frame received: ' + Received.BytesReceived.ToString + ' bytes');
end;
TWebSocketMessageKind.Close:
begin
Log(llInfo, 'Server requested close');
Break;
end;
end;
// Mini-Sleep, um bei sehr schnellem Loop CPU zu schonen.
// Nicht zu groß, sonst verschlechtert sich Latenz.
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.
Was der Code absichtlich „anders“ macht als typische Beispiele
- Stop ohne Gewalt: Statt Threads abzuschießen, setzt Stop ein Event. Der Worker beendet Loops an definierten Stellen. Das reduziert Hänger beim Beenden und vermeidet Ressourcenlecks im Socket-Stack.
- Queue statt Synchronize: Logging und Events gehen per TThread.Queue in den Mainthread. Das ist wichtig, wenn Stop/Shutdown aus dem UI oder aus Service-Control-Handlern kommt. Synchronize kann blockieren, wenn der Mainthread gerade wartet.
- Fragmentierung berücksichtigt: WebSocket-Text kann in Frames fragmentiert kommen. Deshalb der TStringBuilder und das Prüfen von EndOfMessage.
- Heartbeat als App-Protokoll: Viele Setups sterben an Idle-Timeouts (Load Balancer, nginx, Cloud WAF). Ein leichtgewichtiger „ping“-Text ist als Betriebshebel oft effektiver als die Hoffnung auf „TCP keepalive“ oder ein nicht überall verfügbares Ping/Pong-API.
Randbedingungen und Stolperfallen im Betrieb
1) DNS, TLS und Proxy: Connect kann blockieren
TClientWebSocket.Connect ist synchron. Je nach DNS-Auflösung, TLS-Handshake, Zertifikatsprüfung oder Proxy-Umgebung kann das mehrere Sekunden dauern. Der Code legt das bewusst in einen Worker. Wenn Sie zusätzlich harte Timeouts brauchen, müssen Sie auf API-Ebene prüfen, ob Ihre Delphi-Version Timeout-Optionen bereitstellt, oder Sie kapseln Connect in einen separaten Thread und brechen über Prozesslogik ab. Wichtig: Ein „abbrechen“ heißt hier meist „Verbindung als kaputt markieren und Worker neu aufsetzen“, nicht „Socket-Operation sofort killen“.
2) Idle-Timeouts: warum Heartbeat häufig Pflicht ist
In Unternehmensnetzwerken ist ein WebSocket oft hinter einem Reverse Proxy (nginx, IIS ARR) oder einem Load Balancer terminiert. Viele dieser Komponenten schließen Verbindungen, wenn über längere Zeit keine Daten fließen. TCP-Keepalive ist nicht immer kurz genug konfiguriert (und unter Windows oft eher Minuten als Sekunden). Ein Heartbeat auf Anwendungsebene ist deshalb ein stabiler Workaround. Achten Sie darauf, dass Server und Client das gleiche Konzept haben (z. B. „ping“/„pong“ als Text oder JSON).
3) Threading und UI: Ereignisse müssen entkoppelt bleiben
Wenn die OnText-Verarbeitung schwer ist (JSON-Parsing, DB-Zugriffe mit BDE-Ablosung mit nativer Anbindung, UI-Updates), sollte sie nicht im Mainthread alles blockieren. Der Wrapper liefert nur die Nachricht. Ein typisches Muster ist: OnText legt die Payload in eine Queue (z. B. TThreadedQueue<string>), ein separater Worker verarbeitet mit Backpressure (also begrenzter Queue-Länge). Das verhindert, dass bei Burst-Last die UI einfriert oder der Empfang aus dem Tritt kommt.
Debugging: was Sie loggen sollten, wenn es „manchmal“ abbricht
WebSockets sind berüchtigt für „läuft tagelang, dann nicht mehr“. Ohne Logging ist das kaum einzugrenzen. Sinnvolle Logpunkte:
- Zeitstempel (UTC), URL, und Zustandswechsel (connecting/open/closed).
- Close-Reason, sofern verfügbar (Server fordert Close vs. Netzwerkfehler).
- Heartbeat-Sendefehler und Receive-Ausnahmen inkl. Exception-Typ.
- Optional: Größen der empfangenen Nachrichten (nicht die Inhalte), um Datenexplosion zu erkennen.
Wenn Sie über TLS terminieren: Prüfen Sie zusätzlich, ob Zertifikatswechsel (Ablauf, neuer Issuer) zeitlich mit Fehlern korrelieren. In gehärteten Umgebungen sind auch Proxy- und DPI-Boxen (Deep Packet Inspection) Kandidaten.
Varianten: wann System.Net.WebSockets reicht – und wann nicht
System.Net.WebSockets ist für viele Integrationsfälle ausreichend, vor allem wenn es um Text/JSON, moderate Last und klare Reconnect-Strategien geht. Grenzen zeigen sich je nach Delphi-Version und Plattformziel:
- Fehlende/limitierte Ping/Pong-Unterstützung: Dann bleibt App-Heartbeat das robuste Muster.
- Fehlende Timeouts/Cancellation im Connect/Receive: Dann müssen Sie die Architektur so bauen, dass ein hängender Worker isoliert bleibt und die Anwendung trotzdem sauber beendet (z. B. per Prozesswatchdog oder getrennten Worker-Instanzen).
- Hohe Last oder binäre Streams: Dann lohnt ein stärkeres Framing/Buffering-Konzept (z. B. ring buffer, separates Binary-Event, Message-Assembler mit Limits).
Für Legacy-Situationen (ältere Delphi-Generationen, sehr spezifische TLS/Proxy-Anforderungen) sind Bibliotheken wie ICS in manchen Projekten pragmatischer. Wichtig ist weniger „welche Library“, sondern dass Sie Shutdown, Reconnect und Observability (Logs/Metriken) als Erstklass-Themen behandeln.
Fazit: ein Delphi WebSocket Client ist ein Betriebsbaustein – mit klaren Grenzen
Ein WebSocket eignet sich hervorragend für Push-Events, Live-Status, Maschinen- oder Prozessmeldungen und als Rückkanal für Portale und Services. Der gezeigte Wrapper fokussiert auf die Punkte, die in digitalen Unternehmenslösungen oft den Unterschied machen: kontrollierter Reconnect, Heartbeat gegen Idle-Timeouts, fragment-sichere Textverarbeitung und ein Stop-Pfad, der beim Deployment oder Update nicht hängen bleibt.
Einsatzgrenzen bleiben: Wenn Sie harte Garantien für Connect/Receive-Abbruch in sehr knappen Zeitfenstern brauchen oder extrem hohe Datenraten fahren, müssen Sie tiefer in Timeouts, Plattformbesonderheiten und ggf. alternative Stacks einsteigen. Für die Mehrheit der Integrations- und Modernisierungsszenarien ist ein sauber gekapselter, gut geloggter Client wie oben jedoch eine solide Basis, die sich in gewachsene Delphi-Systeme integrieren lässt.
Wenn Sie so einen Baustein in eine bestehende Architektur (z. B. Layer-3 Architektur mit klaren Service- und UI-Schichten) einpassen oder bei sporadischen Disconnects unter Realbedingungen debuggen müssen, können Sie das gezielt mit uns einordnen: Kontakt aufnehmen.
Im fachlichen Umfeld spielen auch Heartbeat Ping/Pong eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Nächster Schritt
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Bestand, Zielbild und technische Risiken werden zusammen bewertet.
- REST, Datenzugriff, Portale und Rollout werden nicht als Spaetfolgen verschoben.
- Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.