Ajout de Socket.io à Node.js multi-thread

Photo de Vidar Nordli-Mathisen sur Unsplash

L'un des inconvénients de Node est qu'il est mono-thread. Bien sûr, il existe un moyen de le contourner - à savoir un module appelé cluster. Le cluster nous permet d'étendre notre application sur plusieurs threads.

Maintenant, cependant, un nouveau problème se présente. Vous voyez, notre code exécuté sur plusieurs instances présente des inconvénients importants. L'un d'eux n'a pas d'états globaux.

Normalement, dans une instance mono-threadée, cela ne serait pas très inquiétant. Pour nous maintenant ça change tout.

Voyons pourquoi.

Alors quel est le problème?

Notre application est un simple chat en ligne fonctionnant sur quatre threads. Cela permet à un utilisateur d'être connecté en même temps sur son téléphone et son ordinateur.

Imaginez que nous ayons des sockets configurés exactement comme nous les aurions définis pour un seul thread. En d'autres termes, nous avons maintenant un grand État mondial avec des sockets.

Lorsque l'utilisateur se connecte sur son ordinateur, le site Web ouvre la connexion avec une instance de Socket.io sur notre serveur. Le socket est stocké dans l'état du thread # 3.

Maintenant, imaginez que l'utilisateur se rende à la cuisine pour prendre une collation et emmène son téléphone avec lui - souhaitant naturellement rester en contact avec ses amis en ligne.

Leur téléphone se connecte au fil 4 et le socket est enregistré dans l’état du fil.

L'envoi d'un message à partir de leur téléphone ne fera aucun bien à l'utilisateur. Seules les personnes du fil 3 vont pouvoir voir le message. En effet, les sockets sauvegardées sur le thread n ° 3 ne sont pas non plus stockées de manière magique sur les threads n ° 1, n ° 2 et n ° 4.

Assez drôle, même l’utilisateur lui-même ne verra pas ses messages sur son ordinateur une fois de retour de la cuisine.

Bien sûr, lorsqu’ils actualisent le site Web, nous pourrions envoyer une demande GET et récupérer les 50 derniers messages, mais nous ne pouvons pas vraiment dire que c’est la méthode «dynamique», n'est-ce pas?

Pourquoi cela arrive-t-il?

Répartir notre serveur sur plusieurs threads équivaut en quelque sorte à avoir plusieurs serveurs distincts. Ils ne se connaissent pas et ne partagent aucun souvenir. Cela signifie qu'un objet sur une instance n'existe pas sur l'autre.

Les sockets sauvegardés dans le thread # 3 ne sont pas nécessairement tous les sockets que l'utilisateur utilise actuellement. Si les amis de l'utilisateur sont sur des threads différents, ils ne verront pas les messages de l'utilisateur à moins qu'ils actualisent le site Web.

Idéalement, nous aimerions informer les autres instances d'un événement pour l'utilisateur. De cette façon, nous pouvons être sûrs que chaque appareil connecté reçoit des mises à jour en direct.

Une solution

Nous pouvons notifier d’autres threads en utilisant le paradigme de messagerie de publication / abonnement (pubsub) de Redis.

Redis est un magasin de structures de données en mémoire open source (sous licence BSD). Il peut être utilisé comme base de données, cache et courtier de messages.

Cela signifie que nous pouvons utiliser Redis pour répartir les événements entre nos instances.

Notez que normalement, nous stockerions probablement toute notre structure dans Redis. Cependant, comme la structure n'est pas sérialisable et doit être maintenue «en vie» dans la mémoire, nous allons en stocker une partie sur chaque instance.

Le flux

Pensons maintenant aux étapes dans lesquelles nous allons gérer un événement entrant.

  1. L'événement appelé message parvient à l'une de nos prises - de cette façon, nous n'avons pas à écouter tous les événements possibles.
  2. À l'intérieur de l'objet transmis au gestionnaire de cet événement sous forme d'argument, nous pouvons trouver le nom de l'événement. Par exemple, sendMessage - .on ('message', ({event}) => {}).
  3. S'il existe un gestionnaire pour ce nom, nous allons l'exécuter.
  4. Le gestionnaire peut exécuter la distribution avec une réponse.
  5. La dépêche envoie l'événement de réponse à notre pub Redis. De là, il est émis à chacune de nos instances.
  6. Chaque instance l'envoie à son état socketsState, garantissant que chaque client connecté recevra l'événement.

Cela semble compliqué, je sais, mais supporte-moi.

la mise en oeuvre

Voici le référentiel avec l'environnement prêt, afin que nous n'ayons pas à installer et tout configurer nous-mêmes.

Premièrement, nous allons configurer un serveur avec Express.

Nous créons une application Express, un serveur HTTP et des sockets init.

Nous pouvons maintenant nous concentrer sur l’ajout de sockets.

Nous transmettons l’instance de serveur de Socket.io à notre fonction dans laquelle nous configurons les middlewares.

onAuth

La fonction onAuth imite simplement une autorisation fictive. Dans notre cas, il est basé sur des jetons.

Personnellement, je le remplacerais probablement par JWT à l’avenir, mais cela n’est aucunement appliqué.

Passons maintenant au middleware onConnection.

onConnection

Nous voyons ici que nous récupérons l’id de l’utilisateur, défini dans le middleware précédent, et l’enregistrons dans notre socketsState, la clé étant l’id et la valeur un tableau de sockets.

Ensuite, nous écoutons l'événement de message. Toute notre logique est basée sur cela - chaque événement que l'interface nous envoie sera appelé: message.

Le nom de l'événement sera envoyé à l'intérieur de l'objet arguments - comme indiqué ci-dessus.

Manutentionnaires

Comme vous pouvez le voir dans onConnection, en particulier dans le programme d'écoute de l'événement de message, nous recherchons un gestionnaire basé sur le nom de l'événement.

Nos gestionnaires sont simplement un objet dans lequel la clé est le nom de l'événement et la valeur est la fonction. Nous allons l'utiliser pour écouter les événements et réagir en conséquence.

De plus, nous ajouterons plus tard la fonction de répartition et nous en servirons pour envoyer l'événement à travers les instances.

SocketsState

Nous connaissons l'interface de notre État, mais nous ne l'avons pas encore implémentée.

Nous ajoutons des méthodes pour ajouter et supprimer un socket, ainsi que pour émettre un événement.

La fonction add vérifie si l’état a une propriété égale à l’identifiant de l’utilisateur. Si tel est le cas, nous l'ajoutons simplement à notre tableau déjà existant. Sinon, nous créons d'abord un nouveau tableau.

La fonction remove vérifie également si l'état a l'identifiant de l'utilisateur dans ses propriétés. Sinon, cela ne fait rien. Sinon, il filtre le tableau pour supprimer le socket du tableau. Ensuite, si le tableau est vide, il le supprime de l'état, en définissant la propriété sur indéfinie.

Pubsub de Redis

Pour créer notre pub, nous allons utiliser le paquet nommé node-redis-pubsub.

Ajout d'envoi

Ok, il ne reste plus qu’à ajouter la fonction de répartition…

… Et ajoutez un écouteur pour outgoing_socket_message. Ainsi, chaque instance reçoit l’événement et l’envoie aux sockets de l’utilisateur.

Rendre le tout multi-threadé

Enfin, ajoutons le code nécessaire à la multi-thread de notre serveur.

Remarque: nous devons tuer le port, car après avoir quitté notre processus Nodemon avec Ctrl + c, il s'y bloque.

Avec quelques ajustements, nous avons maintenant des sockets qui fonctionnent dans toutes les instances. Résultat: un serveur beaucoup plus efficace.

Merci beaucoup pour la lecture!

Je comprends que tout cela peut sembler accablant au début et difficile de tout prendre en même temps. Dans cet esprit, je vous encourage vivement à relire le code dans son intégralité et à le réfléchir dans son ensemble.

Si vous avez des questions ou des commentaires, n'hésitez pas à les mettre dans la section commentaires ci-dessous ou à m'envoyer un message.

Découvrez mes médias sociaux!

Rejoignez ma newsletter!

Publié à l'origine sur www.mcieslar.com le 10 septembre 2018.