WebSocket w Node.js: serwer, klient, heartbeat i ponowne łączenie
WebSocket w Node.js: serwer ws, wbudowany klient, heartbeat | We.Inc
Zbuduj serwer WebSocket w Node.js z pakietem ws, połącz się wbudowanym klientem WebSocket z Node, wykrywaj martwe sockety przez ping i pong i łącz się ponownie z backoffem.
Node.js nie ma wbudowanego serwera WebSocket, więc większość projektów instaluje pakiet ws i tworzy WebSocketServer. Po stronie klienta Node dostarcza globalny WebSocket zgodny z przeglądarką, dodany w v21.0.0 i v20.10.0, dostępny bez flagi od v22.0.0 i oznaczony jako stabilny w v22.4.0. Przed startem produkcyjnym dodaj heartbeat ping i pong oraz ponowne łączenie z backoffem.
Który element za co odpowiada
Zadanie
Użyj
Skąd to pochodzi
Przyjmowanie połączeń
new WebSocketServer({ port })
pakiet ws (npm install ws)
Łączenie z Node
new WebSocket(url)
Globalny w Node 22 i nowszych
Łączenie z przeglądarki
new WebSocket(url)
Wbudowany w każdą nowoczesną przeglądarkę
Wykrywanie martwych socketów
ws.ping() plus listener pong
pakiet ws
Przetrwanie restartów
Ponowne łączenie z wykładniczym backoffem
Twój własny kod klienta
Ten poradnik buduje po kolei jeden mały serwer w stylu czatu i klientów, którzy z nim rozmawiają: serwer, który odsyła echo i rozgłasza wiadomości, skrypt Node, który łączy się bez żadnych zależności, heartbeat, który odrzuca połączenia po cichu martwe, oraz klienta, który wraca po restarcie serwera. Poniższy kalkulator pokazuje, jak rosną opóźnienia ponownych prób, zanim wpiszesz je w kod.
Uruchom npm install ws w projekcie, którego package.json zawiera "type": "module", aby składnia import z przykładów działała.
Uruchom plik serwera poleceniem node server.js. Nasłuchuje na porcie 8080 i loguje każdą wiadomość.
Uruchom plik klienta w drugim terminalu na Node 22 lub nowszym albo otwórz konsolę przeglądarki i wklej ten sam kod.
Zatrzymaj serwer przez Ctrl+C i uruchom go ponownie, aby zobaczyć, jak klient czeka, ponawia próbę i odzyskuje połączenie.
Zawsze podpinaj listener error do każdego socketu po stronie serwera. ws emituje zdarzenia error, a nieobsłużone może wyłożyć proces.
Sprawdzaj req.headers.origin w handlerze połączenia i zamykaj sockety z originów, których nie obsługujesz. Przeglądarki go wysyłają, także na stronach atakujących.
Reverse proxy ucinają bezczynne sockety. nginx domyślnie zamyka proxowany WebSocket, jeśli backend nic nie wysyła przez 60 sekund, więc trzymaj interwał ping poniżej tej wartości.
Dodawaj losowy jitter do opóźnień ponownego łączenia. Bez niego po restarcie każdy klient ponawia próbę w tej samej chwili i wszyscy razem uderzają w serwer.
Pętle rozgłaszające powinny pomijać klientów, których readyState nie jest OPEN, bo wysyłanie do zamykającego się socketu nic nie daje.
Najczęściej zadawane pytania
Czy Node.js ma wbudowany WebSocket?
Tylko klienta. Globalna klasa WebSocket pojawiła się w v21.0.0 i v20.10.0 za flagą --experimental-websocket, straciła flagę w v22.0.0 i przestała być eksperymentalna w v22.4.0. Wbudowanego serwera nadal nie ma, dlatego ws pozostaje typowym wyborem.
Używać ws czy Socket.IO?
ws mówi czystym protokołem WebSocket, więc połączyć się może każdy standardowy klient, w tym przeglądarka i globalny WebSocket z Node. Socket.IO dokłada na wierzch własny protokół z pokojami i automatycznym ponownym łączeniem, ale po drugiej stronie wymaga własnej biblioteki klienta.
Dlaczego połączenia umierają bez zdarzenia close?
Gdy ścieżka sieciowa znika, na przykład po zamknięciu klapy laptopa, żadna ramka close nie zostaje wysłana, a serwer trzyma półotwarty socket. Heartbeat je wyłapuje: socket, który nie odpowiedział na ostatni ping, zostaje zakończony w kolejnej rundzie.
Czy klient w przeglądarce może wysyłać pingi?
Nie. API przeglądarki ma tylko send() i close(). Przeglądarki same odpowiadają na pingi serwera pongami, dlatego heartbeat po stronie serwera działa także z klientami w przeglądarce.
Jak współdzielić port z serwerem HTTP?
Utwórz serwer HTTP samodzielnie, na przykład z Express lub node:http, a potem przekaż go jako new WebSocketServer({ server }). Żądania upgrade trafiają do ws, a zwykłe żądania do Twojej aplikacji.
Jak zaczyna się połączenie
WebSocket zaczyna życie jako żądanie HTTP GET z Upgrade: websocket. Serwer ws je sprawdza, odpowiada 101 Switching Protocols i wywołuje zdarzenie connection z nowym socketem i oryginalnym żądaniem. Wszystko, czego potrzebujesz z handshake'u, na przykład token w query stringu lub nagłówek Origin, odczytujesz z tego żądania.
Heartbeat prostymi słowami
Protokół mówi, że punkt końcowy, który otrzyma ping, musi odpowiedzieć pongiem z tymi samymi danymi. Serwer oznacza każdy socket jako żywy, gdy przychodzi pong, a potem na timerze kasuje oznaczenie i znów wysyła ping. Każdy socket, który przy następnym tyknięciu nadal nie ma oznaczenia, nigdy nie odpowiedział, więc serwer go kończy.
README pakietu ws używa do tego timera 30 sekund. Wybierz wartość wyraźnie niższą niż timeout bezczynności każdego proxy przed Tobą.
Ponowne łączenie bez zasypywania serwera
Dobry klient po pierwszym zerwaniu czeka chwilę, po każdej kolejnej porażce dłużej, aż do pułapu, i zeruje licznik, gdy połączenie się otworzy. Losowe rozłożenie oczekiwania sprawia, że tłum klientów nie wraca w tym samym rytmie.
Krok 1: serwer z ws
server.js: echo i rozgłaszanie
Każda wiadomość jest odsyłana do wszystkich otwartych klientów. Listener error sprawia, że jeden wadliwy socket nie wyłoży procesu.
Krok 2: klient bez zależności
client.js: globalny WebSocket z Node
Działa bez zmian na Node 22 lub nowszym i w konsoli przeglądarki, bo oba udostępniają tę samą klasę WebSocket.
Krok 3: odrzucanie martwych połączeń przez ping i pong
Nasza wersja wzorca z README pakietu ws: oznacz jako żywy przy pongu, zakończ wszystko, co przegapiło ostatnią rundę.
Krok 4: klient, który łączy się ponownie
reconnect.js: wykładniczy backoff z jitterem
Opóźnienie podwaja się po każdej nieudanej próbie aż do pułapu, dostaje losowy rozrzut i zeruje się, gdy połączenie się otworzy.
Za nginx
nginx przepuszcza upgrade tylko wtedy, gdy sam przekażesz nagłówki Upgrade i Connection. Przed nginx 1.29.7 potrzebujesz też proxy_http_version 1.1.
location nginx dla socketu
proxy_read_timeout podnosi limit bezczynności 60 sekund, a heartbeat powyżej i tak utrzymuje ruch.
Kalkulator opóźnienia ponownego łączenia
Ustaw pierwsze opóźnienie, mnożnik i pułap, aby zobaczyć, jak długo klient czeka przed każdą ponowną próbą, oraz najkrótsze i najdłuższe oczekiwanie po dodaniu jittera.
Pierwsze opóźnienie (ms)
Mnożnik
Pułap (ms)
Liczba pokazanych prób
Dodaj jitter (czekaj od połowy do pełnego opóźnienia)
Oczekiwanie przed każdą próbą ponownego połączenia