Nach vielen Jahren Support-Tickets gehen die allermeisten „meine WebSocket-Verbindung verhält sich seltsam“-Meldungen auf dieselbe Handvoll Probleme zurück. Keines ist ein Bug der Bibliothek — alle sind Konfigurationsfehler, die fünf Minuten zum Beheben brauchen, sobald man sie kennt. Dieser Beitrag listet die zehn häufigsten mit Symptom, Ursache und dem Einzeiler, der jeden behebt.
Falls du nur einen Fehler liest, lies Nummer 2. Das Blockieren des OnMessage-Events ist für mehr „die Bibliothek ist langsam“-Tickets verantwortlich als alle anderen Kategorien zusammen, und es ist unsichtbar, bis du jenseits einiger Hundert gleichzeitiger Clients skalierst. Spar dir die nächtliche Produktions-Debug-Session und behebe es vor dem Release.
Fehler 1: WatchDog-Reconnect deaktiviert
Symptom: Der Client verbindet sich beim Start, läuft stundenlang und empfängt nach einem WLAN-Aussetzer, einem VPN-Reconnect oder einem Server-Restart still keine Nachrichten mehr.
Ursache: Die WatchDog-Eigenschaft ist standardmäßig aus. Die Bibliothek nimmt nicht an, dass du automatische Reconnects willst — manche Apps wollen Fail-Fast. Die meisten nicht.
oClient.WatchDog.Enabled := True; oClient.WatchDog.Interval := 10; // try every 10 seconds oClient.WatchDog.Attempts := 0; // 0 = unlimited
Fire-and-Forget. Kombiniere es mit Logging in OnDisconnect, damit du im Feld siehst, wie oft Reconnects feuern. Für Mobile- oder Laptop-Deployments, in denen der Nutzer zwischen Netzen wechselt, verdrahte zusätzlich die OS-Netzwerkwechsel-Benachrichtigung (SystemEvents.NetworkAvailabilityChanged in .NET, der WMI-Weg in Windows-Delphi), um sofort statt nach dem nächsten Intervall-Tick zu reconnecten.
Fehler 2: OnMessage-Event blockieren
Symptom: Der Durchsatz bricht unter Last ein. Der Server fühlt sich „hängend“ an. CPU ist niedrig, aber Nachrichten stauen sich.
Ursache: OnMessage läuft auf dem I/O-Worker-Thread. Wenn du dort eine langsame Datenbank, eine externe HTTP-API oder einen lang laufenden Parser aufrufst, hungert jede andere Verbindung aus, die sich diesen Worker teilt.
// BAD: blocks the worker
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oDb.Query('INSERT INTO events VALUES(?)', [Text]); // 50 ms
oHttp.Get('https://billing/track?event=' + Text); // 200 ms
end;
// GOOD: hand off to a worker queue
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
oWorkQueue.Push(TWorkItem.Create(Connection.Guid, Text));
end;
Fehler 3: HeartBeat ignorieren
Symptom: Geister-Verbindungen stapeln sich. Connections.Count wächst weiter, obwohl du weißt, dass Clients tot sind.
Ursache: Ohne Heartbeats auf Anwendungsebene feuert der OS-TCP-Keepalive-Timer erst nach ~2 Stunden. Bis dahin frisst du Zehntausende Zombie-Sockets im Speicher.
oServer.HeartBeat.Enabled := True; oServer.HeartBeat.Interval := 30; oServer.HeartBeat.Timeout := 90;
Fehler 4: Inkompatible TLS-Versionen
Symptom: Der Client bekommt „handshake failed“ oder „SSL_ERROR_SYSCALL“. Die Verbindung funktioniert auf der Dev-Box, scheitert aber in Produktion hinter einem Firmen-Proxy.
Ursache: Die Client-SSL-Optionen sind aus Rückwärtskompatibilität standardmäßig TLS 1.0–1.2; moderne Server verlangen nur TLS 1.2/1.3. Oder die OpenSSL-DLLs sind alt.
oClient.TLSOptions.Version := tlsTLSv1_2; // or tlsTLSv1_3 oClient.TLSOptions.OpenSSL_Options.LibPath := ExtractFilePath(ParamStr(0)); // Bundle the latest libcrypto-3.dll / libssl-3.dll with your installer.
Fehler 5: Fragmentierte Nachrichten nicht handhaben
Symptom: Der Server empfängt den ersten Teil eines großen JSON-Dokuments, dein Parser scheitert und die Verbindung wird geschlossen.
Ursache: Standardmäßig setzt sgcWebSockets Fragmente für dich zusammen und feuert OnMessage nur mit dem vollständigen Payload. Wenn du aber aus Speichergründen ReadOptions.FragmentMode := frgPartial gesetzt hast, musst du selbst zusammensetzen.
// Default (recommended for most apps)
oServer.ReadOptions.FragmentMode := frgComplete;
// If you opted into partial delivery, reassemble manually
procedure TForm1.OnFragment(Connection: TsgcWSConnection;
const aPartial: TBytes; aIsFinal: Boolean);
begin
Buffers[Connection.Guid].Append(aPartial);
if aIsFinal then
Process(Buffers[Connection.Guid].ToBytes);
end;
Fehler 6: Synchrone API im GUI-Thread
Symptom: Die UI friert beim Verbinden oder beim WriteData auf einer langsamen Leitung für mehrere Sekunden ein.
Ursache: Blockierende Aufrufe auf dem Main-Thread. Nutze in VCL/FMX-Apps immer das Async-Muster oder rufe aus einem Worker-Thread auf.
// BAD: blocks the UI
oClient.Active := True;
// GOOD: connect asynchronously
TTask.Run(procedure
begin
oClient.Active := True;
end);
// Or use the event-driven API
oClient.OnConnect := DoConnected;
oClient.Connect; // returns immediately
Fehler 7: Subprotokoll nicht wählen
Symptom: Die Verbindung klappt, aber der Peer lehnt jeden Frame ab oder antwortet in einem anderen Format als erwartet.
Ursache: Viele WebSocket-Server (MQTT-over-WS, STOMP, GraphQL-WS, Phoenix) verlangen ein bestimmtes Subprotokoll im Handshake. Ohne das fällt der Server auf ein anderes Protokoll zurück oder schmeißt dich raus.
// MQTT over WebSocket
oClient.Specifications.Hixie76 := False;
oClient.Specifications.RFC6455 := True;
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: mqttv3.1');
// GraphQL over WS
oClient.HeadersRequest.Add('Sec-WebSocket-Protocol: graphql-ws');
Fehler 8: Puffergrößen zu klein (oder zu groß)
Symptom: Hohe CPU auf einem Server, der viele kleine Nachrichten pusht, oder Out-of-Memory auf einem, der große pusht.
Ursache: Standardmäßige Sende-/Empfangspuffer sind für den allgemeinen Gebrauch dimensioniert. Tune sie auf deine Traffic-Form.
// Small messages (chat, telemetry): smaller buffers reduce per-conn memory oServer.IOHandler.RecvBufferSize := 4096; oServer.IOHandler.SendBufferSize := 4096; // Big messages (file transfer, media): larger buffers reduce syscalls oServer.IOHandler.RecvBufferSize := 65536; oServer.IOHandler.SendBufferSize := 65536;
Fehler 9: Keine Origin-Prüfung auf öffentlichen Servern
Symptom: Das Sicherheits-Audit markiert: „Jede Website kann sich über den Browser eines Nutzers mit deinem WebSocket verbinden.“
Ursache: Das WebSocket-Protokoll erzwingt kein Same-Origin. Dein Server muss den Origin-Header validieren.
procedure TForm1.ServerBeforeConnect(Connection: TsgcWSConnection;
var Continue: Boolean);
var
vOrigin: string;
begin
vOrigin := Connection.Headers.Values['Origin'];
Continue := (vOrigin = 'https://app.example.com')
or (vOrigin = 'https://admin.example.com');
end;
Fehler 10: Sensible Daten in OnMessage loggen
Symptom: Der Auditor findet API-Keys, JWTs oder PII in Log-Dateien. Compliance-Problem.
Ursache: Das bequeme LogMemo.Lines.Add(Text) in OnMessage schreibt jeden Payload für immer auf die Platte.
procedure TForm1.ServerMessage(Connection: TsgcWSConnection; const Text: string);
begin
// Hash / redact before logging
LogMemo.Lines.Add(Format('[%s] %d bytes (sha=%s)',
[Connection.Guid, Length(Text), ShortHash(Text)]));
Process(Text);
end;
Bonus: history.txt nicht lesen
Jedes Release bringt eine history.txt mit, die jede Änderung, jeden Fix und jeden Breaking-Note seit 2013 listet. Fünf Minuten Durchsicht nach jedem Upgrade sparen später Stunden an „Warum funktioniert das nicht mehr?“-Debugging.
Bonus 2: Komponenten-Versionen über Projekte mischen
Delphi-Entwickler kopieren manchmal eine einzelne .pas aus einem neueren sgcWebSockets-Release in ein älteres Projekt — „nur diese eine Datei“. Das geht gut, bis es nicht mehr geht: Die Datei hängt von Typen ab, die sich zwei Releases zuvor geändert haben — der Linker scheitert mysteriös oder, schlimmer, linkt, crasht aber zur Laufzeit. Upgrade die ganze Bibliothek immer zusammen. Die 30 Sekunden, die du durch das Kopieren einer Datei sparst, sind die vier Stunden Debugging nicht wert, wenn etwas Nachgelagertes bricht.
Bonus 3: WebSocket als Fire-and-Forget behandeln
WebSocket ist keine Message-Queue. Es ist ein bidirektionaler Byte-Strom über TCP. Bricht das Netz mitten in einer Nachricht ein, ist der Frame weg und wird nicht automatisch nachgeliefert. Für geschäftskritische Nachrichten musst du dein eigenes Quittungsprotokoll obenauf bauen — meist eine UUID pro Nachricht, einen expliziten ACK vom Empfänger und einen Resend des Senders nach Timeout. Diese Schicht zu überspringen ist okay für „User tippt“-Benachrichtigungen und fatal für „User hat den Warenkorb bezahlt“.
Bonus 4: Memos Speicher fressen lassen
Symptom: Das Diagnose-Memo auf deinem Debug-Formular ist nach ein paar Stunden Traffic das Erste, das deinen Client in den OOM treibt. Du gibst sgcWebSockets die Schuld; sgcWebSockets ist unschuldig.
Ursache: Ein TMemo behält jede jemals hinzugefügte Zeile. Bei 100 Zeilen pro Sekunde sind das 360.000 Zeilen pro Stunde. Jede Zeile allokiert einen String. Die VCL rendert Tausende unsichtbare Zeilen bei jedem WM_PAINT. Dein Prozess kommt zum Stillstand, während die Bibliothek nichts falsch macht.
// Cap the diagnostic memo
procedure TForm1.LogLine(const aText: string);
const
cMaxLines = 500;
begin
TThread.Queue(nil, procedure
begin
while Memo1.Lines.Count > cMaxLines do
Memo1.Lines.Delete(0);
Memo1.Lines.Add(aText);
end);
end;
Noch besser: In eine rollende Datei via LoggerPro loggen und nur die letzten 200 Zeilen zur visuellen Diagnose ins Memo spiegeln. Produktionscode sollte nie aus einem Netzwerk-Thread in ein UI-Control schreiben.
Bonus 5: Server nicht vor Application.Terminate schließen
Symptom: Beim App-Shutdown hängt der Prozess 30 Sekunden, oder das OS meldet in Client-Logs unbehandelte Exceptions, weil Verbindungen unsauber abgerissen wurden.
Ursache: Der Server-Destruktor sendet jeder Verbindung einen Close-Frame und wartet darauf, dass das OS den Listening-Port freigibt. Rufst du Application.Terminate vor oServer.Active := False auf, sterben die Verbindungen mitten im Handshake und der OS-Port bleibt im TIME_WAIT — ein schneller Neustart ist dann blockiert.
procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if oServer.Active then
begin
oServer.Broadcast('{"event":"shutdown","reconnect_after":5}');
Sleep(200); // let frames flush
oServer.Active := False; // graceful close
end;
end;
Für Konsolen-Server hänge dich unter Windows in SetConsoleCtrlHandler oder unter Linux in SIGTERM und fahre dieselbe Shutdown-Sequenz. Paare das mit einer HUP-/Restart-Schleife in deinem Service-Manager und du hast Deploys ohne abgerissene Verbindungen.
Das Muster hinter den Mustern
Die meisten dieser Fehler haben eine gemeinsame Wurzel: anzunehmen, dass das Netz zuverlässig ist. Ist es nicht. Halboffene TCP-Verbindungen passieren. Mobilfunknetze brechen weg. Firmen-Proxys zerschießen TLS. WLAN wechselt. Server starten neu. Cloud-Loadbalancer beenden inaktive Verbindungen nach 60 Sekunden. Eine WebSocket-App, die diese Bedingungen nicht überlebt, ist nicht fertig — sie ist eine Happy-Path-Demo. Die gute Nachricht: Die Bibliothek bietet für jedes dieser Szenarien einen Schalter. Die schlechte Nachricht: Die meisten sind aus Rückwärtskompatibilität standardmäßig aus. Zwei Stunden, in denen du die Dokuseiten zu WatchDog, HeartBeat, Reconnect und TLS-Optionen liest, sind die billigste Versicherung, die du je kaufst.
Muster zweiter Ordnung: Respektiere den I/O-Thread. Alles, was länger als eine Millisekunde dauert — DB-Abfrage, File-I/O, externer HTTP-Aufruf, Regex auf einem langen String, JSON-Parse auf einem 100-KB-Payload — gehört auf einen Worker-Thread, nicht in OnMessage. Mach diese Regel absolut. Junior-Entwickler werden sie in drei Monaten verletzen; eine Code-Review-Checkliste mit „kein Blockieren in OnMessage“ fängt es vor dem Release ab.
Wie es weitergeht
Wenn du einen Hochlast-Server tunst, lies als Nächstes sgcWebSockets-Performance-Tuning. Neu bei der Bibliothek? Starte im Getting-Started-Hub und verbinde deinen ersten WebSocket in fünf Minuten.