10 Veelgemaakte fouten bij sgcWebSockets (en hoe ze op te lossen)

· Componenten

Na vele jaren van het beantwoorden van supporttickets, zijn dezelfde handvol problemen verantwoordelijk voor de overgrote meerderheid van “mijn WebSocket-verbinding doet raar”-meldingen. Geen ervan zijn bugs in de bibliotheek — allemaal zijn configuratiefouten die vijf minuten kosten om op te lossen zodra je weet dat ze bestaan. Dit bericht somt de tien meest voorkomende op, met het symptoom, de oorzaak en de one-liner die elk oplost.

Als je maar een fout uit deze lijst leest, maak het dan #2. Het blokkeren van de OnMessage-event is verantwoordelijk voor meer “de bibliotheek is traag”-tickets dan elke andere categorie samen, en het is onzichtbaar tot je voorbij een paar honderd gelijktijdige clients schaalt. Bespaar jezelf de late-night productie-debugsessie en fix het voordat je live gaat.

Fout 1: WatchDog-reconnect uitgeschakeld

Symptoom: de client verbindt bij opstart, draait uren prima, en stopt dan stilzwijgend met berichten ontvangen na een wifi-storing, een VPN-reconnect of een server-herstart.

Oorzaak: de WatchDog-property staat standaard uit. De bibliotheek neemt niet aan dat je automatische reconnects wilt — sommige apps willen fail-fast-gedrag. De meeste niet.

oClient.WatchDog.Enabled  := True;
oClient.WatchDog.Interval := 10;   // try every 10 seconds
oClient.WatchDog.Attempts := 0;    // 0 = unlimited

Fire-and-forget. Combineer het met OnDisconnect-logging zodat je in het veld kunt zien hoe vaak reconnects afgaan. Voor mobiele of laptop-deployments waar de gebruiker tussen netwerken roamt, verbind ook de OS-netwerkverandering-notificatie (SystemEvents.NetworkAvailabilityChanged op .NET, de WMI-route op Windows-Delphi) om een onmiddellijke reconnect te triggeren in plaats van te wachten op de volgende intervaltik.

Fout 2: Het OnMessage-event blokkeren

Symptoom: doorvoer stort in onder load. De server voelt “vastgelopen”. CPU is laag, maar berichten stapelen op.

Oorzaak: OnMessage draait op de I/O-worker-thread. Als je er een trage database, een externe HTTP-API of een langlopende parser binnen aanroept, uithonger je elke andere verbinding die die worker deelt.

// 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;

Fout 3: HeartBeat negeren

Symptoom: ghost-verbindingen stapelen op. Connections.Count blijft groeien hoewel je weet dat clients zijn gestorven.

Oorzaak: zonder applicatieniveau-heartbeats gaat de OS-TCP-keepalive-timer pas af na ~2 uur. Tegen die tijd heb je tienduizenden zombie-sockets die geheugen opeten.

oServer.HeartBeat.Enabled  := True;
oServer.HeartBeat.Interval := 30;
oServer.HeartBeat.Timeout  := 90;

Fout 4: Niet-passende TLS-versies

Symptoom: client krijgt “handshake failed” of “SSL_ERROR_SYSCALL”. Verbinding werkt op een dev-box maar faalt in productie achter een corporate proxy.

Oorzaak: de client SSL-opties staan standaard op TLS 1.0–1.2 voor backward compatibility; moderne servers vereisen alleen TLS 1.2/1.3. Of de OpenSSL-DLL’s zijn oud.

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.

Fout 5: Gefragmenteerde berichten niet afhandelen

Symptoom: de server ontvangt het eerste deel van een groot JSON-document, je parser faalt, en de verbinding wordt gesloten.

Oorzaak: standaard hermonteert sgcWebSockets fragmenten voor je en vuurt het pas OnMessage af met de volledige payload. Maar als je om geheugenredenen ReadOptions.FragmentMode := frgPartial hebt gezet, moet je hermonteren.

// 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;

Fout 6: Synchrone API gebruiken in de GUI-thread

Symptoom: de UI bevriest voor enkele seconden bij verbinden, of bij het aanroepen van WriteData op een trage verbinding.

Oorzaak: blokkerende calls op de main thread. Gebruik altijd het async-patroon in VCL/FMX-apps, of roep aan vanuit een worker thread.

// 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

Fout 7: Vergeten een subprotocol te selecteren

Symptoom: verbinding slaagt maar de peer weigert elk frame, of antwoordt met een ander formaat dan verwacht.

Oorzaak: veel WebSocket-servers (MQTT-over-WS, STOMP, GraphQL-WS, Phoenix) vereisen een specifiek subprotocol in de handshake. Zonder dat valt de server terug op een ander protocol of dropt hij je.

// 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');

Fout 8: Buffergroottes te klein (of te groot)

Symptoom: hoge CPU op een server die veel kleine berichten pusht, of out-of-memory op een server die grote berichten pusht.

Oorzaak: standaard send/receive-buffers zijn op algemeen gebruik gedimensioneerd. Tune ze naar je verkeervorm.

// 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;

Fout 9: Geen Origin-check op publieke servers

Symptoom: security audit signaleert “elke website kan via een browser van een gebruiker verbinden met je WebSocket”.

Oorzaak: het WebSocket-protocol dwingt geen same-origin af. Je server moet de Origin-header valideren.

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;

Fout 10: Gevoelige data loggen in OnMessage

Symptoom: auditor vindt API-keys, JWT’s of PII in logbestanden. Compliance-probleem.

Oorzaak: de eenvoudige LogMemo.Lines.Add(Text) in OnMessage schrijft elke payload voor altijd naar disk.

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 niet lezen

Elke release komt met een history.txt die elke wijziging, fix en breaking note sinds 2013 noemt. Vijf minuten doorbladeren na elke upgrade bespaart later uren van “waarom is dit gestopt met werken”-debuggen.

Bonus 2: Componentversies mixen tussen projecten

Delphi-ontwikkelaars kopieren soms een enkel .pas uit een nieuwere sgcWebSockets-release in een ouder project, “alleen dit ene bestand”. Dit werkt tot het niet werkt — het bestand hangt af van types die twee releases geleden zijn veranderd, en de linker faalt mysterieus of, erger, linkt maar crasht bij runtime. Upgrade altijd de hele bibliotheek samen. De 30 seconden bespaard door het kopieren van een bestand zijn de vier uur debuggen niet waard wanneer iets downstream breekt.

Bonus 3: WebSocket behandelen als fire-and-forget

WebSocket is geen message queue. Het is een bidirectionele byte-stream over TCP. Als het netwerk midden in een bericht uitvalt, is het frame verloren en wordt nooit automatisch opnieuw bezorgd. Voor business-kritische berichten moet je je eigen acknowledgement-protocol erbovenop bouwen — meestal een per-message UUID, een expliciete ACK van de ontvanger, en een resend op de zender na een timeout. Deze laag overslaan is prima voor “user is typing”-notificaties en fataal voor “user paid for cart”.

Bonus 4: Memo’s geheugen laten lekken

Symptoom: de diagnostische memo op je debug-formulier is het eerste dat je client OOM’t na enkele uren verkeer. Je geeft sgcWebSockets de schuld; sgcWebSockets is onschuldig.

Oorzaak: een TMemo behoudt elke regel die ooit is toegevoegd. Bij 100 regels per seconde is dat 360.000 regels per uur. Elke regel alloceert een string. De VCL rendert duizenden onzichtbare regels bij elke WM_PAINT. Je proces komt tot stilstand terwijl de bibliotheek niets verkeerds doet.

// 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;

Nog beter: log naar een rolling file via LoggerPro en spiegel alleen de laatste 200 regels in de memo voor visuele debugging. Productiecode zou nooit naar een UI-control vanuit een netwerk-thread mogen schrijven.

Bonus 5: De server niet sluiten voor Application.Terminate

Symptoom: bij app-afsluiten hangt het proces 30 seconden, of het OS rapporteert unhandled exceptions in clientlogs omdat verbindingen ungracefully werden afgebroken.

Oorzaak: de server-destructor stuurt een close-frame naar elke verbinding en wacht tot het OS de luister-poort vrijgeeft. Als je Application.Terminate aanroept voor oServer.Active := False, sterven de verbindingen midden in de handshake en blijft de OS-poort in TIME_WAIT, wat een snelle herstart blokkeert.

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;

Voor console-servers, hook SetConsoleCtrlHandler op Windows of SIGTERM op Linux en draai dezelfde shutdown-sequence. Combineer dit met een HUP/restart-loop in je service-manager en je hebt zero-dropped-connection-deploys.

Het patroon achter de patronen

De meeste van deze fouten hebben een gemeenschappelijke wortel: aannemen dat het netwerk betrouwbaar is. Dat is het niet. Half-open TCP-verbindingen gebeuren. Mobiele netwerken vallen weg. Corporate proxies breken TLS. Wifi roamt. Servers herstarten. Cloud load balancers termineren idle verbindingen na 60 seconden. Een WebSocket-app die deze omstandigheden niet overleeft, is niet af — het is een happy-path-demo. Het goede nieuws: de bibliotheek stelt een control bloot voor elk van deze scenario’s. Het slechte nieuws: de meeste staan standaard uit voor backward compatibility. Twee uur besteden aan het lezen van de WatchDog-, HeartBeat-, Reconnect- en TLS-optie-pagina’s van de documentatie is de goedkoopste verzekering die je ooit zult kopen.

Tweede-orde-patroon: respecteer de I/O-thread. Alles dat meer dan een milliseconde duurt — database-query, file-I/O, externe HTTP-call, regex op een lange string, JSON-parse op een 100 KB-payload — hoort op een worker thread, niet in OnMessage. Maak deze regel absoluut. Junior-ontwikkelaars zullen hem over drie maanden overtreden; een code-review-checklist die “geen blokkering in OnMessage” bevat, vangt dat voor het live gaat.

Waar nu naartoe

Als je een server met veel verkeer tunt, lees dan vervolgens sgcWebSockets Performance Tuning. Nieuw bij de bibliotheek? Begin bij de Aan de slag-hub en verbind je eerste WebSocket in vijf minuten.