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.