Agregar Socket.io a Node.js multiproceso

Foto de Vidar Nordli-Mathisen en Unsplash

Una de las desventajas de Node es que es de un solo subproceso. Por supuesto, hay una forma de evitarlo: un módulo llamado clúster. Cluster nos permite distribuir nuestra aplicación en múltiples hilos.

Ahora, sin embargo, se presenta un nuevo problema. Vea, nuestro código que se ejecuta en varias instancias en realidad tiene algunas desventajas significativas. Uno de ellos es no tener estados globales.

Normalmente, en una instancia de subproceso único, esto no sería una gran preocupación. Para nosotros ahora lo cambia todo.

Veamos por qué.

¿Entonces, cuál es el problema?

Nuestra aplicación es un simple chat en línea que se ejecuta en cuatro hilos. Esto permite que un usuario inicie sesión al mismo tiempo en su teléfono y computadora.

Imagine que tenemos los zócalos configurados exactamente como los habríamos configurado para un hilo. En otras palabras, ahora tenemos un gran estado global con enchufes.

Cuando el usuario inicia sesión en su computadora, el sitio web abre la conexión con una instancia de Socket.io en nuestro servidor. El socket se almacena en el estado del hilo # 3.

Ahora, imagine que el usuario va a la cocina a tomar un refrigerio y lleva su teléfono consigo, naturalmente queriendo seguir enviando mensajes de texto con sus amigos en línea.

Su teléfono se conecta al hilo # 4, y el zócalo se guarda en el estado del hilo.

Enviar un mensaje desde su teléfono no le hará ningún bien al usuario. Solo las personas del hilo # 3 podrán ver el mensaje. Esto se debe a que los sockets guardados en el subproceso # 3 no están almacenados mágicamente de alguna manera en los subprocesos # 1, # 2 y # 4 también.

Curiosamente, incluso el propio usuario no verá sus mensajes en su computadora una vez que regrese de la cocina.

Por supuesto, cuando actualizan el sitio web, podríamos enviar una solicitud GET y obtener los últimos 50 mensajes, pero realmente no podemos decir que es la forma "dinámica", ¿verdad?

¿Por qué está pasando esto?

Extender nuestro servidor a través de múltiples hilos es, de alguna manera, equivalente a tener varios servidores separados. No saben de la existencia del otro y ciertamente no comparten ningún recuerdo. Esto significa que un objeto en una instancia no existe en la otra.

Los sockets guardados en el hilo # 3 no son necesariamente todos los sockets que el usuario está usando en este momento. Si los amigos del usuario están en diferentes hilos, no verán los mensajes del usuario a menos que actualicen el sitio web.

Idealmente, nos gustaría notificar a otras instancias sobre un evento para el usuario. De esta manera, podemos estar seguros de que todos los dispositivos conectados reciben actualizaciones en vivo.

Una solución

Podemos notificar a otros hilos utilizando el paradigma de mensajes de publicación / suscripción de Redis (pubsub).

Redis es un almacén de estructura de datos en memoria de código abierto (con licencia BSD). Se puede usar como base de datos, caché y agente de mensajes.

Esto significa que podemos usar Redis para distribuir eventos entre nuestras instancias.

Tenga en cuenta que normalmente probablemente almacenaríamos toda nuestra estructura dentro de Redis. Sin embargo, dado que la estructura no es serializable y necesita mantenerse "viva" dentro de la memoria, vamos a almacenar parte de ella en cada instancia.

El flujo

Ahora pensemos en los pasos en los que vamos a manejar un evento entrante.

  1. El evento llamado mensaje llega a uno de nuestros sockets; de esta manera, no tenemos que escuchar cada evento posible.
  2. Dentro del objeto pasado al controlador de este evento como argumento, podemos encontrar el nombre del evento. Por ejemplo, sendMessage - .on ('mensaje', ({event}) => {}).
  3. Si hay un controlador para este nombre, lo ejecutaremos.
  4. El controlador puede ejecutar el despacho con una respuesta.
  5. El despacho envía el evento de respuesta a nuestro pub de Redis. Desde allí se emite a cada una de nuestras instancias.
  6. Cada instancia lo emite a sus socketsState, asegurando que cada cliente conectado va a recibir el evento.

Parece complicado, lo sé, pero tengan paciencia conmigo.

Implementación

Aquí está el repositorio con el entorno listo, para que no tengamos que instalar y configurar todo nosotros mismos.

Primero, vamos a configurar un servidor con Express.

Creamos una aplicación Express, un servidor HTTP y sockets init.

Ahora podemos centrarnos en agregar sockets.

Pasamos la instancia del servidor Socket.io a nuestra función en la que configuramos los middlewares.

onAuth

La función onAuth simplemente imita una autorización simulada. En nuestro caso está basado en token.

Personalmente, probablemente lo reemplazaría con JWT en el futuro, pero no se aplica de ninguna manera.

Ahora, pasemos al middleware onConnection.

onConnection

Aquí vemos que recuperamos la identificación del usuario, que se configuró en el middleware anterior, y la guardamos en nuestros socketsState, siendo la clave la identificación y el valor una matriz de sockets.

A continuación, escuchamos el mensaje de evento. Toda nuestra lógica se basa en eso: cada evento que nos envíe la interfaz se llamará: mensaje.

El nombre del evento se enviará dentro del objeto de argumentos, como se indicó anteriormente.

Manipuladores

Como puede ver en onConnection, específicamente en el oyente del evento del mensaje, estamos buscando un controlador basado en el nombre del evento.

Nuestros controladores son simplemente un objeto en el que la clave es el nombre del evento y el valor es la función. Lo usaremos para escuchar eventos y responder en consecuencia.

Además, más adelante, agregaremos la función de envío y la usaremos para enviar el evento a través de las instancias.

SocketsState

Conocemos la interfaz de nuestro estado, pero aún tenemos que implementarla.

Agregamos métodos para agregar y eliminar un socket, así como para emitir un evento.

La función de agregar comprueba si el estado tiene una propiedad que es igual a la identificación del usuario. Si ese es el caso, simplemente lo agregamos a nuestra matriz ya existente. De lo contrario, primero creamos una nueva matriz.

La función remove también verifica si el estado tiene la identificación del usuario en sus propiedades. Si no, no hace nada. De lo contrario, filtra la matriz para eliminar el socket de la matriz. Luego, si la matriz está vacía, la elimina del estado y establece la propiedad como indefinida.

Pub pub de Redis

Para crear nuestro pubsub vamos a utilizar el paquete llamado node-redis-pubsub.

Agregar despacho

Ok, ahora todo lo que queda por hacer es agregar la función de despacho ...

... y agregue un oyente para outgoing_socket_message. De esta manera, cada instancia recibe el evento y lo envía a los sockets del usuario.

Haciéndolo todo multihilo

Finalmente, agreguemos el código necesario para que nuestro servidor sea multiproceso.

Nota: Tenemos que matar el puerto, porque después de salir de nuestro proceso Nodemon con Ctrl + c, simplemente se cuelga allí.

Con un pequeño ajuste, ahora tenemos enchufes de trabajo en todas las instancias. Como resultado: un servidor mucho más eficiente.

Muchas gracias por leer!

Aprecio que todo parezca abrumador al principio y extenuante asimilarlo todo de una vez. Con eso en mente, le recomiendo que lea el código nuevamente en su totalidad y lo considere en su conjunto.

Si tiene alguna pregunta o comentario, no dude en ponerlos en la sección de comentarios a continuación o enviarme un mensaje.

¡Mira mis redes sociales!

¡Únete a mi boletín!

Publicado originalmente en www.mcieslar.com el 10 de septiembre de 2018.