10 najczęstszych błędów przy używaniu sgcWebSockets (i jak je naprawić)

· Komponenty

Po wielu latach odpowiadania na zgłoszenia wsparcia te same garstki problemów stanowią zdecydowaną większość raportów „moje połączenie WebSocket zachowuje się dziwnie”. Żaden z nich nie jest błędem w bibliotece — wszystkie to błędy konfiguracji, które trwają pięć minut, by je naprawić, gdy już wiesz, że istnieją. Ten wpis wymienia dziesięć najczęstszych, z objawem, przyczyną i jednolinijkową poprawką każdego z nich.

Jeśli przeczytasz tylko jeden błąd z tej listy, niech to będzie #2. Blokowanie zdarzenia OnMessage odpowiada za więcej zgłoszeń „biblioteka jest wolna” niż wszystkie inne kategorie razem wzięte i jest niewidoczne, dopóki nie skalujesz powyżej kilkuset jednoczesnych klientów. Oszczędź sobie nocnej sesji debugowania w produkcji i napraw to, zanim wdrożysz.

Błąd 1: Wyłączone ponowne łączenie WatchDog

Objaw: klient łączy się przy starcie, działa dobrze przez godziny, potem cicho przestaje odbierać wiadomości po zaniku Wi-Fi, ponownym połączeniu VPN lub restarcie serwera.

Przyczyna: właściwość WatchDog jest domyślnie wyłączona. Biblioteka nie zakłada, że chcesz automatycznych ponownych łączeń — niektóre aplikacje chcą zachowania fail-fast. Większość nie.

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

Ustaw i zapomnij. Połącz z logowaniem OnDisconnect, byś mógł widzieć w polu, jak często włączają się ponowne łączenia. Dla wdrożeń mobilnych lub laptopów, gdzie użytkownik wędruje między sieciami, podepnij również powiadomienie OS o zmianie sieci (SystemEvents.NetworkAvailabilityChanged na .NET, droga WMI na Windows-Delphi), by uruchomić natychmiastowe ponowne łączenie zamiast czekać na następne tyknięcie interwału.

Błąd 2: Blokowanie zdarzenia OnMessage

Objaw: przepustowość załamuje się pod obciążeniem. Serwer wydaje się „zacięty”. CPU jest niskie, ale wiadomości się piętrzą.

Przyczyna: OnMessage działa na wątku pracownika I/O. Jeśli wywołujesz wewnątrz niego wolną bazę danych, zewnętrzne API HTTP lub długo działający parser, głodzisz każde inne połączenie dzielące tego pracownika.

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

Błąd 3: Ignorowanie HeartBeat

Objaw: akumulują się połączenia widmo. Connections.Count ciągle rośnie, mimo że wiesz, że klienci umarli.

Przyczyna: bez heartbeatów na poziomie aplikacji timer keepalive TCP OS uruchamia się po ~2 godzinach. Wtedy masz dziesiątki tysięcy zombi gniazd zjadających pamięć.

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

Błąd 4: Niedopasowane wersje TLS

Objaw: klient dostaje „handshake failed” lub „SSL_ERROR_SYSCALL”. Połączenie działa na maszynie deweloperskiej, ale zawodzi w produkcji za firmowym proxy.

Przyczyna: opcje SSL klienta domyślnie mają TLS 1.0–1.2 dla wstecznej kompatybilności; nowoczesne serwery wymagają tylko TLS 1.2/1.3. Lub DLL OpenSSL są stare.

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.

Błąd 5: Nieobsługiwanie pofragmentowanych wiadomości

Objaw: serwer odbiera pierwszą część dużego dokumentu JSON, Twój parser zawodzi, a połączenie jest zamykane.

Przyczyna: domyślnie sgcWebSockets ponownie składa fragmenty za Ciebie i uruchamia OnMessage tylko z kompletnym ładunkiem. Ale jeśli ustawiłeś ReadOptions.FragmentMode := frgPartial ze względów pamięciowych, musisz ponownie złożyć.

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

Błąd 6: Używanie synchronicznego API w wątku GUI

Objaw: UI zamarza na kilka sekund podczas łączenia lub przy wywoływaniu WriteData na wolnym łączu.

Przyczyna: wywołania blokujące w głównym wątku. Zawsze używaj wzorca asynchronicznego w aplikacjach VCL/FMX lub wywołuj z wątku pracownika.

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

Błąd 7: Zapominanie o wyborze podprotokołu

Objaw: połączenie się udaje, ale peer odrzuca każdą ramkę lub odpowiada w innym formacie niż oczekiwano.

Przyczyna: wiele serwerów WebSocket (MQTT-over-WS, STOMP, GraphQL-WS, Phoenix) wymaga konkretnego podprotokołu w handshake. Bez niego serwer domyślnie używa innego protokołu lub Cię odrzuca.

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

Błąd 8: Rozmiary buforów zbyt małe (lub zbyt duże)

Objaw: wysokie CPU na serwerze pchającym wiele małych wiadomości lub brak pamięci na serwerze pchającym duże.

Przyczyna: domyślne bufory wysyłania/odbioru są wymiarowane do ogólnego użycia. Dostosuj je do kształtu Twojego ruchu.

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

Błąd 9: Brak sprawdzania Origin na publicznych serwerach

Objaw: audyt bezpieczeństwa zaznacza „dowolna strona internetowa może łączyć się z Twoim WebSocket przez przeglądarkę użytkownika”.

Przyczyna: protokół WebSocket nie wymusza same-origin. Twój serwer musi walidować nagłówek Origin.

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;

Błąd 10: Logowanie wrażliwych danych w OnMessage

Objaw: audytor znajduje klucze API, JWT lub PII w plikach logów. Problem zgodności.

Przyczyna: łatwe LogMemo.Lines.Add(Text) w OnMessage zapisuje każdy ładunek na dysk na zawsze.

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: Nieczytanie History.txt

Każde wydanie jest dostarczane z history.txt wymieniającym każdą zmianę, poprawkę i breaking note od 2013 roku. Pięć minut spędzonych na przeglądaniu go po każdym upgrade'ie oszczędza godziny debugowania „dlaczego to przestało działać” później.

Bonus 2: Mieszanie wersji komponentów między projektami

Deweloperzy Delphi czasem kopiują pojedynczy .pas z nowszego wydania sgcWebSockets do starszego projektu, „tylko ten jeden plik”. To działa, dopóki nie zadziała — plik zależy od typów, które zmieniły się dwa wydania temu, a linker tajemniczo zawodzi lub, gorzej, linkuje, ale crashuje w czasie wykonania. Zawsze upgraduj całą bibliotekę razem. 30 sekund zaoszczędzonych przez skopiowanie jednego pliku nie jest warte czterech godzin debugowania, gdy coś poniżej się zepsuje.

Bonus 3: Traktowanie WebSocket jako fire-and-forget

WebSocket to nie kolejka komunikatów. To dwukierunkowy strumień bajtów nad TCP. Jeśli sieć zniknie w środku wiadomości, ramka jest tracona i nigdy nie jest automatycznie ponownie dostarczana. Dla wiadomości krytycznych dla biznesu musisz dodać własny protokół potwierdzeń na wierzchu — zwykle UUID per-wiadomość, jawny ACK od odbiorcy i ponowne wysłanie po stronie nadawcy po timeoucie. Pominięcie tej warstwy jest OK dla powiadomień „użytkownik pisze” i śmiertelne dla „użytkownik zapłacił za koszyk”.

Bonus 4: Pozwalanie memo wyciekać pamięcią

Objaw: diagnostyczne memo na Twoim formularzu debugowym jest pierwszą rzeczą, która OOM-uje Twojego klienta po kilku godzinach ruchu. Obwiniasz sgcWebSockets; sgcWebSockets jest niewinne.

Przyczyna: TMemo zachowuje każdą linię kiedykolwiek dodaną. Przy 100 liniach na sekundę to 360 000 linii na godzinę. Każda linia alokuje string. VCL renderuje tysiące niewidocznych linii przy każdym WM_PAINT. Twój proces zwalnia do zatrzymania, podczas gdy biblioteka nie robi nic złego.

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

Jeszcze lepiej: loguj do pliku rolującego przez LoggerPro i tylko odbijaj ostatnie 200 linii do memo dla wizualnego debugowania. Kod produkcyjny nigdy nie powinien pisać do kontrolki UI z wątku sieciowego.

Bonus 5: Niezamykanie serwera przed Application.Terminate

Objaw: przy zamykaniu aplikacji proces zawiesza się na 30 sekund lub OS raportuje nieobsłużone wyjątki w logach klienta, ponieważ połączenia zostały zerwane niegracefulnie.

Przyczyna: destruktor serwera wysyła ramkę close do każdego połączenia i czeka, aż OS zwolni nasłuchujący port. Jeśli wywołasz Application.Terminate przed oServer.Active := False, połączenia umierają w środku handshake, a port OS pozostaje w TIME_WAIT, blokując szybki restart.

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;

Dla serwerów konsolowych podepnij SetConsoleCtrlHandler na Windows lub SIGTERM na Linux i uruchom tę samą sekwencję zamykania. Sparuj to z pętlą HUP/restart w Twoim menedżerze usług, a masz wdrożenia bez utraconych połączeń.

Wzorzec za wzorcami

Większość tych błędów ma wspólny korzeń: zakładanie, że sieć jest niezawodna. Nie jest. Półotwarte połączenia TCP się zdarzają. Sieci mobilne giną. Firmowe proxy łamią TLS. Wi-Fi wędruje. Serwery się restartują. Load balancery w chmurze terminują bezczynne połączenia po 60 sekundach. Aplikacja WebSocket, która nie przeżywa tych warunków, nie jest skończona — to dema happy-path. Dobra wiadomość: biblioteka udostępnia kontrolę dla każdego z tych scenariuszy. Zła wiadomość: większość z nich jest domyślnie wyłączona dla wstecznej kompatybilności. Dwie godziny spędzone na czytaniu stron dokumentacji o opcjach WatchDog, HeartBeat, Reconnect i TLS to najtańsze ubezpieczenie, jakie kiedykolwiek kupisz.

Wzorzec drugiego rzędu: szanuj wątek I/O. Wszystko, co zajmuje więcej niż milisekundę — zapytanie do bazy, I/O pliku, zewnętrzne wywołanie HTTP, regex na długim stringu, parsowanie JSON na ładunku 100 KB — należy do wątku pracownika, nie do OnMessage. Uczyń tę regułę absolutną. Junior deweloperzy złamią ją za trzy miesiące; lista kontrolna code-review zawierająca „bez blokowania w OnMessage” wyłapuje to przed wdrożeniem.

Dokąd dalej

Jeśli stroisz serwer o dużym ruchu, przeczytaj następnie Strojenie wydajności sgcWebSockets. Nowy w bibliotece? Zacznij od huba Pierwsze kroki i połącz swój pierwszy WebSocket w pięć minut.