Dodanie Socket.io do wielowątkowego Node.js

Zdjęcie Vidar Nordli-Mathisen na Unsplash

Jedną z wad węzła jest to, że jest jednowątkowy. Oczywiście jest na to sposób - mianowicie moduł o nazwie klaster. Klaster pozwala nam rozprzestrzeniać naszą aplikację na wiele wątków.

Teraz jednak pojawia się nowy problem. Widzisz, nasz kod uruchamiany w wielu instancjach ma naprawdę poważne wady. Jeden z nich nie ma państw globalnych.

Zwykle w przypadku jednowątkowego nie byłoby to wielkim zmartwieniem. Dla nas teraz wszystko zmienia.

Zobaczmy dlaczego.

Więc jaki jest problem?

Nasza aplikacja to prosty czat online działający na czterech wątkach. Umożliwia to jednoczesne zalogowanie się użytkownika na telefonie i komputerze.

Wyobraź sobie, że mamy gniazda skonfigurowane dokładnie tak, jak ustawiamy je dla jednego wątku. Innymi słowy, mamy teraz jedno duże państwo globalne z gniazdami.

Gdy użytkownik loguje się na swoim komputerze, strona internetowa otwiera połączenie z instancją Socket.io na naszym serwerze. Gniazdo jest przechowywane w stanie wątku # 3.

Teraz wyobraź sobie, że użytkownik idzie do kuchni po przekąskę i zabiera ze sobą swój telefon - oczywiście chcąc nadal pisać SMS-y ze znajomymi w Internecie.

Ich telefon łączy się z wątkiem nr 4, a gniazdo jest zapisywane w stanie wątku.

Wysłanie wiadomości z telefonu nie przyniesie mu żadnego pożytku. Tylko osoby z wątku nr 3 będą mogły zobaczyć wiadomość. To dlatego, że gniazda zapisane w wątku nr 3 nie są w jakiś sposób magicznie przechowywane w wątkach nr 1, 2 i 4.

Zabawne, nawet sam użytkownik nie zobaczy swoich wiadomości na swoim komputerze, gdy wrócą z kuchni.

Oczywiście, kiedy odświeżają witrynę, możemy wysłać żądanie GET i pobrać 50 ostatnich wiadomości, ale nie możemy powiedzieć, że jest to sposób „dynamiczny”, prawda?

Dlaczego to się dzieje?

Rozłożenie naszego serwera na wiele wątków jest w pewnym sensie równoznaczne z posiadaniem kilku oddzielnych serwerów. Nie znają się nawzajem i na pewno nie dzielą żadnej pamięci. Oznacza to, że obiekt w jednej instancji nie istnieje w drugiej.

Gniazda zapisane w wątku # 3 niekoniecznie są wszystkimi gniazdami, z których korzysta obecnie użytkownik. Jeśli znajomi użytkownika mają różne wątki, nie zobaczą wiadomości użytkownika, dopóki nie odświeży strony.

Idealnie chcielibyśmy powiadomić inne instancje o zdarzeniu dla użytkownika. W ten sposób możemy być pewni, że każde podłączone urządzenie otrzymuje aktualizacje na żywo.

Rozwiązanie

Możemy powiadomić inne wątki, korzystając z paradygmatu publikowania / subskrybowania wiadomości Redis (pubsub).

Redis to magazyn struktur danych w pamięci typu open source (na licencji BSD). Może być używany jako baza danych, pamięć podręczna i broker komunikatów.

Oznacza to, że możemy użyć Redis do dystrybucji zdarzeń między naszymi instancjami.

Zauważ, że zwykle przechowalibyśmy całą naszą strukturę w Redis. Ponieważ jednak struktura nie jest serializowalna i musi być utrzymywana „przy życiu” w pamięci, będziemy przechowywać jej część w każdym przypadku.

Przepływ

Zastanówmy się teraz, w jaki sposób zamierzamy obsłużyć nadchodzące wydarzenie.

  1. Wiadomość o nazwie event dociera do jednego z naszych gniazd - w ten sposób nie musimy słuchać każdego możliwego zdarzenia.
  2. Wewnątrz obiektu przekazanego do funkcji obsługi tego zdarzenia jako argumentu możemy znaleźć nazwę zdarzenia. Na przykład sendMessage - .on ('message', ({event}) => {}).
  3. Jeśli dla tej nazwy istnieje moduł obsługi, wykonamy go.
  4. Przewodnik może wykonać wysyłkę z odpowiedzią.
  5. Wysłanie wysyła zdarzenie odpowiedzi do naszego pubu Redis. Stamtąd jest emitowany do każdego z naszych wystąpień.
  6. Każda instancja emituje ją do swojego gniazda, zapewniając, że każdy podłączony klient otrzyma zdarzenie.

Wydaje się skomplikowane, wiem, ale trzymaj się mnie.

Realizacja

Oto repozytorium z gotowym środowiskiem, dzięki czemu nie musimy samodzielnie instalować i konfigurować wszystkiego.

Najpierw skonfigurujemy serwer za pomocą Express.

Tworzymy aplikację Express, serwer HTTP i gniazda inicjujące.

Teraz możemy skupić się na dodawaniu gniazd.

Przekazujemy instancję serwera Socket.io do naszej funkcji, w której ustawiamy oprogramowanie pośrednie.

onAuth

Funkcja onAuth po prostu imituje fałszywą autoryzację. W naszym przypadku jest on oparty na tokenach.

Osobiście prawdopodobnie w przyszłości zastąpiłbym go JWT, ale nie jest w żaden sposób egzekwowany.

Przejdźmy teraz do oprogramowania pośredniego onConnection.

onConnection

Widzimy tutaj, że pobieramy identyfikator użytkownika, który został ustawiony w poprzednim oprogramowaniu pośrednim, i zapisujemy go w naszym socketsState, gdzie kluczem jest identyfikator, a wartość jest tablicą gniazd.

Następnie nasłuchujemy zdarzenia wiadomości. Nasza cała logika opiera się na tym - każde zdarzenie, które wysyła nam frontend, będzie się nazywać: wiadomość.

Nazwa zdarzenia zostanie wysłana do obiektu argumentów - jak podano powyżej.

Handlery

Jak widać w onConnection, a konkretnie w detektorze zdarzenia wiadomości, szukamy procedury obsługi na podstawie nazwy zdarzenia.

Nasze procedury obsługi to po prostu obiekt, w którym kluczem jest nazwa zdarzenia, a wartością jest funkcja. Użyjemy go do nasłuchiwania zdarzeń i odpowiedniego reagowania.

Później dodamy również funkcję wysyłania i wykorzystamy ją do wysłania zdarzenia między instancjami.

SocketsState

Znamy interfejs naszego stanu, ale musimy go jeszcze wdrożyć.

Dodajemy metody dodawania i usuwania gniazda, a także emitowania zdarzenia.

Funkcja dodawania sprawdza, czy stan ma właściwość równą identyfikatorowi użytkownika. W takim przypadku po prostu dodajemy go do naszej już istniejącej tablicy. W przeciwnym razie najpierw tworzymy nową tablicę.

Funkcja usuwania sprawdza również, czy stan ma identyfikator użytkownika we właściwościach. Jeśli nie - nic nie robi. W przeciwnym razie filtruje tablicę, aby usunąć gniazdo z tablicy. Następnie, jeśli tablica jest pusta, usuwa ją ze stanu, ustawiając właściwość na niezdefiniowaną.

Publikacja Redis

Do stworzenia naszego pubsub użyjemy pakietu o nazwie node-redis-pubsub.

Dodawanie wysyłki

Ok, teraz pozostaje tylko dodać funkcję wysyłki…

… I dodaj detektor dla wiadomości wychodzącej_socket. W ten sposób każda instancja odbiera zdarzenie i wysyła je do gniazd użytkownika.

Dzięki temu wszystko jest wielowątkowe

Na koniec dodajmy kod potrzebny do tego, aby nasz serwer był wielowątkowy.

Uwaga: Musimy zabić port, ponieważ po wyjściu z procesu Nodemon za pomocą Ctrl + c po prostu się tam zawiesza.

Po drobnych poprawkach mamy teraz działające gniazda we wszystkich instancjach. W rezultacie: znacznie bardziej wydajny serwer.

Dziękuję bardzo za przeczytanie!

Rozumiem, że z początku wszystko to może wydawać się przytłaczające i męczące, aby wziąć to wszystko na raz. Mając to na uwadze, gorąco zachęcam do ponownego przeczytania całego kodu i rozważenia go jako całości.

Jeśli masz jakieś pytania lub komentarze, umieść je w sekcji komentarzy poniżej lub wyślij mi wiadomość.

Sprawdź moje media społecznościowe!

Dołącz do mojego newslettera!

Pierwotnie opublikowany na stronie www.mcieslar.com 10 września 2018 r.