Blog Tinux Net

node-socket.io

Todas las publicaciones etiquetadas node-socket.io por Blog Tinux Net
  • Publicado en

    Inicialización del proyecto

    El primer objetivo es configurar una página web HTML sencilla que proporcione un formulario y una lista de mensajes. Vamos a utilizar el marco web Node.JS express con este fin. Asegurarse tener Node.JS esta instalado.

    Primero creemos un package.json archivo de manifiesto que describe nuestro proyecto. Te recomiendo colocarlo en un directorio vacío dedicado (llamaré al mío) socket.io). package.json

    {
    

    "name": "socket.app",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "description": "",
    "dependencies": {
    "socket.io": "^4.7.5"
    }
    }

    Ahora, para poder completar fácilmente la propiedad de dependencias con las cosas que necesitamos, usaremos npm install:

    npm install express@4
    

    Una vez instalado, podemos crear un archivo index.js que configurará nuestra aplicación.

    index.js

        const express = require('express');
        const { createServer } = require('node:http');
    
        const app = express();
        const server = createServer(app);
    
        app.get('/', (req, res) => {
          res.send('<h1>Hello world</h1>');
        });
    
    server.listen(8080, () => {
      console.log('server running at http://localhost:8080');
    });
    

    This means that: Esto significa que:

    • Express inicializa la app para que sea un controlador de funciones que puede proporcionar a un servidor HTTP (como se ve en la línea 5).
    • Definimos un controlador de ruta / al que se llama cuando llegamos a nuestro sitio web.
    • Hacemos que el servidor http escuche en el puerto 8080.

    Si ejecuta node index.js, debería ver lo siguiente: enter image description here Obteniendo en el navegador...localhost:8080 y no el 3000

    enter image description here

    Sirviendo HTML

    Hasta ahora en index.js llamamos a res.send y le pasamos una cadena de HTML. Nuestro código se vería muy confuso si simplemente colocáramos el HTML de toda nuestra aplicación allí, por lo que crearemos un fichero index.html y lo serviremos en su lugar.

    Refactoricemos nuestro controlador de rutas para usar sendFile en su lugar.

    const express = require('express');
    const { createServer } = require('node:http');
    const { join } = require('node:path');
    
    const app = express();
    const server = createServer(app);
    
    app.get('/', (req, res) => {
      res.sendFile(join(__dirname, 'index.html'));
    });
    
    server.listen(3000, () => {
      console.log('server running at http://localhost:3000');
    });
    

    Poniendo lo siguiente en el archivo index.html:

    <ul id="messages"></ul>
    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
    </form>
    

    Si se reinicia el proceso (al presionar Control+C y ejecutar node index.js nuevamente) y actualiza la página, debería verse así:

    enter image description here

    Integración de Socket.IO

    Socket.IO se compone de dos partes:

    -Un servidor que se integra (o se monta en) el servidor HTTP Node.JS (el paquete socket.io) - Una biblioteca cliente que se carga en el lado del navegador (el paquete socket.io-client)

    Durante el desarrollo, socket.io nos sirve el cliente automáticamente, como veremos, por lo que por ahora sólo tenemos que instalar un módulo:

    npm install socket.io
    

    Eso instalará el módulo y agregará la dependencia al paquete.json. Ahora editemos index.js para agregarlo:

    const express = require('express');
    const { createServer } = require('node:http');
    const { join } = require('node:path');
    const { Server } = require('socket.io');
    
    const app = express();
    const server = createServer(app);
    const io = new Server(server);
    
    app.get('/', (req, res) => {
      res.sendFile(join(__dirname, 'index.html'));
    });
    
    io.on('connection', (socket) => {
      console.log('a user connected');
    });
    
    server.listen(8080, () => {
      console.log('server running at http://localhost:8080');
    });
    

    Observe que inicializo una nueva instancia de socket.io pasando el objeto servidor (el servidor HTTP). Luego escucho el evento de conexión de los connection entrantes y lo registro en la consola.

    Ahora en index.html agregue el siguiente fragmento antes de (etiqueta final del cuerpo): enter image description here

    Eso es todo lo que se necesita para cargar el cliente socket.io, que expone un global io (y el punto final (endpoint) GET /socket.io/socket.io.js), y luego conectarse.

    Si desea utilizar la versión local del archivo JS del lado del cliente, puede encontrarla en node_modules/socket.io/client-dist/socket.io.js.

    Truco: También puede utilizar un CDN en lugar de los archivos locales (e.g. ).

    Note que no se esta especificando ninguna URL cuando se llamo a io(), ya que de forma predeterminada intenta conectarme al host que sirve a la página.

    Si ahora reinicia el proceso (presionando Control+C y ejecutando node index.js nuevamente) y luego actualiza la página web, debería ver la consola imprimir “a user connected”.

    Intenta abrir varias pestañas y verás varios mensajes. enter image description here

    Cada socket también activa un evento de desconexión especial:

    io.on('connection', (socket) => {
      console.log('a user connected');
      socket.on('disconnect', () => {
        console.log('user disconnected');
      });
    });
    

    Luego, si actualiza una pestaña varias veces, podrá verla en acción. enter image description here

    Emitir eventos

    La idea principal detrás de Socket.IO es que puedes enviar y recibir cualquier evento que desees, con cualquier dato que desees. Cualquier objeto que pueda codificarse como lo hará JSON y también se admiten datos binarios.

    Hagámoslo de modo que cuando el usuario escribe un mensaje, el servidor lo recibe como un evento de mensaje de chat. La sección de script en index.html ahora debería verse de la siguiente manera: Y en index.js imprimimos el evento de mensaje de chat:

    io.on('connection', (socket) => {
       ,socket.on('chat message', (msg) => {
    enter code here
    console.log('message: ' + msg); });
    });
    

    Broadcasting

    El próximo objetivo es que emitamos el evento desde el servidor al resto de usuarios.

    Para enviar un evento a todos, Socket.IO nos brinda el método io.emit().

    // this will emit the event to all connected sockets
    io.emit('hello', 'world'); 
    

    Si desea enviar un mensaje a todos excepto a un determinado socket emisor, tenemos el indicador de transmisión para emitir desde ese socket:

    io.on('connection', (socket) => 
    {socket.broadcast.emit('hi');});
    

    En este caso, por simplicidad, enviaremos el mensaje a todos, incluido el remitente.

    io.on('connection', (socket) => {
      socket.on('chat message', (msg) => io.emit('chat message', msg);
      });
    });
    

    Y en el lado del cliente, cuando captamos un mensaje de chat, lo incluimos en la página.

    io.on('connection', (socket) => {
      socket.on('chat message', (msg) => io.emit('chat message', msg);
      });
    });
    

    Y en el lado del cliente, cuando captamos un mensaje de chat, lo incluimos en la página.

    const messages = document.getElementById('messages');
    
      socket.on('chat message', (msg) => {
    const item = document.createElement('li');
    item.textContent = msg;
    messages.appendChild(item);
    window.scrollTo(0, document.body.scrollHeight);
    

    });

    enter image description here

    Descripción general de la API

    Antes de continuar, hagamos un recorrido rápido por la API proporcionada por Socket.IO:

    Common API

    Los siguientes métodos están disponibles tanto para el cliente como para el servidor.

    Basic emit

    Como hemos visto en el paso 4, puedes enviar cualquier dato al otro lado con socket.emit():

    Desde el Cliente al Servidor:

    lado Cliente -> socket.emit('hello', 'world'); lado Servidor -> io.on('connection', (socket) => { socket.on('hello', (arg) => { console.log(arg); // 'world' }); }); y a la inversa Desde el Servidor al Cliente: lado Servidor: io.on('connection', (socket) => { socket.emit('hello', 'world'); }); lado Cliente: socket.on('hello', (arg) => { console.log(arg); // 'world' });

    Puede enviar cualquier cantidad de argumentos y se admiten todas las estructuras de datos serializables, incluidos objetos binarios como ArrayBuffer, TypedArray o Buffer (solo Node.js):

    From client to server: Client: socket.emit('hello', 1, '2', { 3: '4', 5: Uint8Array.from([6]) }); Server: io.on('connection', (socket) => { socket.on('hello', (arg1, arg2, arg3) => { console.log(arg1); // 1 console.log(arg2); // '2' console.log(arg3); // { 3: '4', 5: <Buffer 06> } }); }); From server to client Server: io.on('connection', (socket) => { socket.emit('hello', 1, '2', { 3: '4', 5: Buffer.from([6]) }); }); Client: `socket.on('hello', (arg1, arg2, arg3) => {

    console.log(arg1); // 1
    console.log(arg2); // '2'
    console.log(arg3); // { 3: '4', 5: ArrayBuffer (1) [ 6 ] }

    }); `

    No es necesario llamar a JSON.stringify() a objetos:

    Agradecimientos

    Los eventos son geniales, pero en algunos casos es posible que desees una API de solicitud y respuesta más clásica. En Socket.IO, esta función se denomina "reconocimientos".

    Viene en dos sabores:

    Con función de devolución de llamada

    Puede agregar una devolución de llamada como último argumento de la emisión (), y esta devolución de llamada se llamará una vez que la otra parte haya reconocido el evento:

    From client to server Client socket.timeout(5000).emit('request', { foo: 'bar' }, 'baz', (err, response) => { if (err) { // the server did not acknowledge the event in the given delay } else { console.log(response.status); // 'ok' } }); Server:

    io.on('connection', (socket) => {
      socket.on('request', (arg1, arg2, callback) => {
        console.log(arg1); // { foo: 'bar' }
        console.log(arg2); // 'baz'
        callback({
          status: 'ok'
        });
      });
    });
    

    From server to client Server:

    io.on('connection', (socket) => {
      socket.timeout(5000).emit('request', { foo: 'bar' }, 'baz', (err, response) => {
        if (err) {
          // the client did not acknowledge the event in the given delay
        } else {
          console.log(response.status); // 'ok'
        }
      });
    });
    

    Client:

    socket.on('request', (arg1, arg2, callback) => {
      console.log(arg1); // { foo: 'bar' }
      console.log(arg2); // 'baz'
      callback({
        status: 'ok'
      });
    });
    
    Con una promesa

    El método emitWithAck() proporciona la misma funcionalidad, pero devuelve una promesa que se resolverá una vez que la otra parte reconozca el evento:

    From client to server Client:

    try {
      const response = await socket.timeout(5000).emitWithAck('request', { foo: 'bar' }, 'baz');
      console.log(response.status); // 'ok'
    } catch (e) {
      // the server did not acknowledge the event in the given delay
    }
    

    Server:

    io.on('connection', (socket) => {
      socket.on('request', (arg1, arg2, callback) => {
        console.log(arg1); // { foo: 'bar' }
        console.log(arg2); // 'baz'
        callback({
          status: 'ok'
        });
      });
    });
    

    From server to client Server:

    io.on('connection', async (socket) => {
      try {
        const response = await socket.timeout(5000).emitWithAck('request', { foo: 'bar' }, 'baz');
        console.log(response.status); // 'ok'
      } catch (e) {
        // the client did not acknowledge the event in the given delay
      }
    });
    

    Client:

    socket.on('request', (arg1, arg2, callback) => {
      console.log(arg1); // { foo: 'bar' }
      console.log(arg2); // 'baz'
      callback({
        status: 'ok'
      });
    });
    

    Catch-all listeners (Escuchadores Generales)

    Un oyente general es un oyente al que se llamará para cualquier evento entrante. Esto es útil para depurar su aplicación: Sender (Emisor):

    socket.emit('hello', 1, '2', { 3: '4', 5: Uint8Array.from([6]) });
    

    Receiver (Receptor):

    socket.onAny((eventName, ...args) => {
      console.log(eventName); // 'hello'
      console.log(args); // [ 1, '2', { 3: '4', 5: ArrayBuffer (1) [ 6 ] } ]
    });
    

    De manera similar, para paquetes salientes:

    socket.onAnyOutgoing((eventName, ...args) => {
      console.log(eventName); // 'hello'
      console.log(args); // [ 1, '2', { 3: '4', 5: ArrayBuffer (1) [ 6 ] } ]
    });
    

    Server API

    Broadcasting

    Como hemos visto en el paso 5, puedes transmitir un evento a todos los clientes conectados con io.emit():

    io.emit('hello', 'world');
    

    enter image description here

    Rooms

    En la jerga de Socket.IO, una habitación (room) es un canal arbitrario al que los sockets pueden unirse y salir. Se puede utilizar para transmitir eventos a un subconjunto de clientes conectados:

    enter image description here

    ¡Eso es básicamente todo! Para referencia futura, toda la API se puede encontrar aquí (servidor) y aquí (cliente).

    Handling disconnections

    Ahora, resaltemos dos propiedades realmente importantes de Socket.IO:

    1. un cliente Socket.IO no siempre está conectado
    2. un servidor Socket.IO no almacena ningún evento

    Lo que significa que su aplicación debe poder sincronizar el estado local del cliente con el estado global del servidor después de una desconexión temporal.

    Nota: Lo que significa que su aplicación debe poder sincronizar el estado local del cliente con el estado global del servidor después de una desconexión temporal.

    En el contexto de nuestra aplicación de chat, esto implica que un cliente desconectado podría perderse algunos mensajes:

    enter image description here Veremos en los próximos pasos cómo podemos mejorar esto.

    Recuperación del estado de conexión

    Primero, manejemos las desconexiones fingiendo que no hubo desconexión: esta característica se llama "recuperación del estado de conexión" -> "Connection state recovery".

    Esta función almacenará temporalmente todos los eventos que envía el servidor e intentará restaurar el estado de un cliente cuando se vuelva a conectar:

    • restaurar sus habitaciones
    • envía cualquier evento perdido

    Debe estar habilitado en el lado del servidor: Incorporar en el index.js:

    const io = new Server(server, {
      connectionStateRecovery: {}
    });
    

    Video de Desconexion

    Como puede ver en el vídeo de arriba, el mensaje "en tiempo real" finalmente se entrega cuando se restablece la conexión.

    Nota: El botón "Desconectar" se agregó con fines de demostración.

    <form id="form" action="">
      <input id="input" autocomplete="off" /><button>Send</button>
      <button id="toggle-btn">Disconnect</button>
    </form>
    
    <script>
      const toggleButton = document.getElementById('toggle-btn');
    
      toggleButton.addEventListener('click', (e) => {
        e.preventDefault();
        if (socket.connected) {
          toggleButton.innerText = 'Connect';
          socket.disconnect();
        } else {
          toggleButton.innerText = 'Disconnect';
          socket.connect();
        }
      });
    </script>
    

    ¡Excelente! Ahora puedes preguntar:

    Pero esta es una característica increíble, ¿por qué no está habilitada de forma predeterminada?

    Hay varias razones para esto:

    no siempre funciona; por ejemplo, si el servidor falla abruptamente o se reinicia, es posible que el estado del cliente no se guarde no siempre es posible habilitar esta función al ampliarla

    Truco: Dicho esto, es realmente una gran característica ya que no es necesario sincronizar el estado del cliente después de una desconexión temporal (por ejemplo, cuando el usuario cambia de WiFi a 4G).

    Exploraremos una solución más general en el siguiente paso.

    Entrega del servidor

    Hay dos formas comunes de sincronizar el estado del cliente tras la reconexión:

    • o el servidor envía el estado completo
    • o el cliente realiza un seguimiento del último evento que ha procesado y el servidor envía las piezas faltantes

    Ambas son soluciones totalmente válidas y elegir una dependerá de su caso de uso. En este tutorial iremos con este último.

    Primero, persistamos los mensajes de nuestra aplicación de chat. Hoy en día hay muchas opciones excelentes, usaremos SQLite aquí. Instalemos los paquetes necesarios:

    npm install sqlite sqlite3
    

    Simplemente almacenaremos cada mensaje en una tabla SQL: index.js:

    const express = require('express');
    const { createServer } = require('node:http');
    const { join } = require('node:path');
    const { Server } = require('socket.io');
    const sqlite3 = require('sqlite3');
    const { open } = require('sqlite');
    
    async function main() {
      // open the database file
      const db = await open({
        filename: 'chat.db',
        driver: sqlite3.Database
      });
    
      // create our 'messages' table (you can ignore the 'client_offset' column for now)
      await db.exec(`
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            client_offset TEXT UNIQUE,
            content TEXT
        );
      `);
    
      const app = express();
      const server = createServer(app);
      const io = new Server(server, {
        connectionStateRecovery: {}
      });
    
      app.get('/', (req, res) => {
        res.sendFile(join(__dirname, 'index.html'));
      });
    
      io.on('connection', (socket) => {
        socket.on('chat message', async (msg) => {
          let result;
          try {
            // store the message in the database
            result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
          } catch (e) {
            // TODO handle the failure
            return;
          }
          // include the offset with the message
          io.emit('chat message', msg, result.lastID);
        });
      });
    
      server.listen(3000, () => {
        console.log('server running at http://localhost:3000');
      });
    }
    
    main();
    

    Luego, el cliente realizará un seguimiento del desplazamiento:

    index.html:

    <script>
      const socket = io({
        auth: {
          serverOffset: 0
        }
      });
    
      const form = document.getElementById('form');
      const input = document.getElementById('input');
      const messages = document.getElementById('messages');
    
      form.addEventListener('submit', (e) => {
        e.preventDefault();
        if (input.value) {
          socket.emit('chat message', input.value);
          input.value = '';
        }
      });
    
      socket.on('chat message', (msg, serverOffset) => {
        const item = document.createElement('li');
        item.textContent = msg;
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
        socket.auth.serverOffset = serverOffset;
      });
    </script>
    

    Y finalmente el servidor enviará los mensajes faltantes al (re)conexión:

    index.js

    // [...]
    
    io.on('connection', async (socket) => {
      socket.on('chat message', async (msg) => {
        let result;
        try {
          result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
        } catch (e) {
          // TODO handle the failure
          return;
        }
        io.emit('chat message', msg, result.lastID);
      });
    
      if (!socket.recovered) {
        // if the connection state recovery was not successful
        try {
          await db.each('SELECT id, content FROM messages WHERE id > ?',
            [socket.handshake.auth.serverOffset || 0],
            (_err, row) => {
              socket.emit('chat message', row.content, row.id);
            }
          )
        } catch (e) {
          // something went wrong
        }
      }
    });
    
    // [...]
    

    Video de recuperacion desde una base de datos

    Como puede ver en el video de arriba, funciona tanto después de una desconexión temporal como de una actualización de página completa. Truco: La diferencia con la función "Recuperación del estado de conexión" es que es posible que una recuperación exitosa no necesite llegar a su base de datos principal (puede recuperar los mensajes de una secuencia Redis, por ejemplo).

    Bien, ahora hablemos de la entrega al cliente (client delivery).

    Client delivery

    Veamos cómo podemos asegurarnos de que el servidor siempre reciba los mensajes enviados por los clientes. INFO: De forma predeterminada, Socket.IO proporciona una garantía de entrega "como máximo una vez" (también conocida como "disparar y olvidar"), lo que significa que no habrá reintento en caso de que el mensaje no llegue al servidor.

    Buffered events

    Cuando se desconecta un cliente, cualquier llamada a socket.emit() se almacena en búfer hasta la reconexión: Buffered events En el vídeo de arriba, el mensaje "en tiempo real" se almacena en el búfer hasta que se restablece la conexión.

    Este comportamiento podría ser totalmente suficiente para su aplicación. Sin embargo, hay algunos casos en los que se podría perder un mensaje:

    • la conexión se corta mientras se envía el evento
    • el servidor falla o se reinicia mientras se procesa - el evento la base de datos no está disponible temporalmente

    At least once

    Podemos implementar una garantía de "al menos una vez:

    • manualmente con acuse de recibo:

      function emit(socket, event, arg) { socket.timeout(5000).emit(event, arg, (err) => { if (err) { // no ack from the server, let's retry emit(socket, event, arg); } }); }

      emit(socket, 'hello', 'world');

    • o con la opción retries reintentos:

      const socket = io({
        ackTimeout: 10000,
        retries: 3
      });
      

      socket.emit('hello', 'world');

    En ambos casos, el cliente volverá a intentar enviar el mensaje hasta obtener un acuse de recibo del servidor:

    io.on('connection', (socket) => {
      socket.on('hello', (value, callback) => {
        // once the event is successfully handled
        callback();
      });
    })
    

    Truco: Con la opción de reintentos (retries) se garantiza el orden de los mensajes, ya que los mensajes se ponen en cola y se envían uno por uno. Este no es el caso de la primera opción.

    Exactly once

    El problema con los reintentos es que el servidor ahora podría recibir el mismo mensaje varias veces, por lo que necesita una forma de identificar de forma única cada mensaje y almacenarlo solo una vez en la base de datos.

    Veamos cómo podemos implementar una garantía "exactamente una vez" en nuestra aplicación de chat.

    Comenzaremos asignando un identificador único a cada mensaje del lado del cliente:

    index.html:

    <script>
      let counter = 0;
    
      const socket = io({
        auth: {
          serverOffset: 0
        },
        // enable retries
        ackTimeout: 10000,
        retries: 3,
      });
    
      const form = document.getElementById('form');
      const input = document.getElementById('input');
      const messages = document.getElementById('messages');
    
      form.addEventListener('submit', (e) => {
        e.preventDefault();
        if (input.value) {
          // compute a unique offset
          const clientOffset = `${socket.id}-${counter++}`;
          socket.emit('chat message', input.value, clientOffset);
          input.value = '';
        }
      });
    
      socket.on('chat message', (msg, serverOffset) => {
        const item = document.createElement('li');
        item.textContent = msg;
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
        socket.auth.serverOffset = serverOffset;
      });
    </script>
    

    Nota: El atributo socket.id es un identificador aleatorio de 20 caracteres que se asigna a cada conexión.

    También podríamos haber usado getRandomValues() para generar un desplazamiento único.

    Y luego almacenamos este desplazamiento junto con el mensaje en el lado del servidor:

    index.js

    // [...]
    
    io.on('connection', async (socket) => {
      socket.on('chat message', async (msg, clientOffset, callback) => {
        let result;
        try {
          result = await db.run('INSERT INTO messages (content, client_offset) VALUES (?, ?)', msg, clientOffset);
        } catch (e) {
          if (e.errno === 19 /* SQLITE_CONSTRAINT */ ) {
            // the message was already inserted, so we notify the client
            callback();
          } else {
            // nothing to do, just let the client retry
          }
          return;
        }
        io.emit('chat message', msg, result.lastID);
        // acknowledge the event
        callback();
      });
    
      if (!socket.recovered) {
        try {
          await db.each('SELECT id, content FROM messages WHERE id > ?',
            [socket.handshake.auth.serverOffset || 0],
            (_err, row) => {
              socket.emit('chat message', row.content, row.id);
            }
          )
        } catch (e) {
          // something went wrong
        }
      }
    });
    
    // [...]
    

    De esta manera, la restricción UNIQUE en la columna cliente_offset evita la duplicación del mensaje. Precaucion: No olvides reconocer el evento, de lo contrario el cliente seguirá intentándolo (hasta tiempos de reintento).

    socket.on('chat message', async (msg, clientOffset, callback) => {
      // ... and finally
      callback();
    });
    

    Info:

    Nuevamente, la garantía predeterminada ("como máximo una vez") -> ("at most once") puede ser suficiente para su aplicación, pero ahora sabe cómo hacerla más confiable.

    Scaling horizontally (Escalado horizontalmente)

    Ahora que nuestra aplicación es resistente a interrupciones temporales de la red, veamos cómo podemos escalarla horizontalmente para poder admitir miles de clientes simultáneos. Nota: - El escalado horizontal (también conocido como "escalado") significa agregar nuevos servidores a su infraestructura para hacer frente a nuevas demandas - El escalado vertical (también conocido como "escalado") significa agregar más recursos (potencia de procesamiento, memoria, almacenamiento, ...) a su infraestructura existente

    Primer paso: usemos todos los núcleos disponibles del host. De forma predeterminada, Node.js ejecuta su código Javascript en un solo subproceso, lo que significa que incluso con una CPU de 32 núcleos, solo se utilizará un núcleo. Afortunadamente, el módulo de clúster Node.js proporciona una manera conveniente de crear un subproceso de trabajador por núcleo.

    También necesitaremos una forma de reenviar eventos entre los servidores Socket.IO. A este componente lo llamamos "Adaptador".

    enter image description here Entonces, instalemos el adaptador del clúster:

    npm install @socket.io/cluster-adapter
    

    Ahora lo conectamos: index.js

    const express = require('express');
    const { createServer } = require('node:http');
    const { join } = require('node:path');
    const { Server } = require('socket.io');
    const sqlite3 = require('sqlite3');
    const { open } = require('sqlite');
    const { availableParallelism } = require('node:os');
    const cluster = require('node:cluster');
    const { createAdapter, setupPrimary } = require('@socket.io/cluster-adapter');
    
    if (cluster.isPrimary) {
      const numCPUs = availableParallelism();
      // create one worker per available core
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork({
          PORT: 3000 + i
        });
      }
    
      // set up the adapter on the primary thread
      return setupPrimary();
    }
    
    async function main() {
      const app = express();
      const server = createServer(app);
      const io = new Server(server, {
        connectionStateRecovery: {},
        // set up the adapter on each worker thread
        adapter: createAdapter()
      });
    
      // [...]
    
      // each worker will listen on a distinct port
      const port = process.env.PORT;
    
      server.listen(port, () => {
        console.log(`server running at http://localhost:${port}`);
      });
    }
    
    main();
    

    ¡Eso es todo! Esto generará un subproceso de trabajador por CPU disponible en su máquina. Veámoslo en acción:

    Escalado del Cluster

    Como puede ver en la barra de direcciones, cada pestaña del navegador está conectada a un servidor Socket.IO diferente y el adaptador simplemente reenvía los eventos del mensaje de chat entre ellos. Truco: Actualmente existen 5 implementaciones oficiales de adaptadores:

    • el adaptador Redis
    • el adaptador Redis Streams
    • el adaptador MongoDB
    • el adaptador Postgres
    • el adaptador de clúster

    Para que puedas elegir el que mejor se adapta a tus necesidades. Sin embargo, tenga en cuenta que algunas implementaciones no admiten la función de recuperación del estado de conexión; puede encontrar la matriz de compatibilidad aquí.

    Nota: en la mayoría de los casos, también deberá asegurarse de que todas las solicitudes HTTP de una sesión de Socket.IO lleguen al mismo servidor (también conocido como "sesión adhesiva"). Sin embargo, esto no es necesario aquí, ya que cada servidor Socket.IO tiene su propio puerto.

    ¡Y eso finalmente completa nuestra aplicación de chat! En este tutorial hemos visto cómo:

    • enviar un evento entre el cliente y el servidor
    • transmitir un evento a todos o a un subconjunto de clientes conectados
    • manejar desconexiones temporales
    • ampliar

    Ahora debería tener una mejor descripción general de las funciones proporcionadas por Socket.IO. ¡Ahora es su momento de crear su propia aplicación en tiempo real!

    Notas finales

    Código final del servidor

    index.js

    const express = require('express');
    const { createServer } = require('node:http');
    const { join } = require('node:path');
    const { Server } = require('socket.io');
    const sqlite3 = require('sqlite3');
    const { open } = require('sqlite');
    const { availableParallelism } = require('node:os');
    const cluster = require('node:cluster');
    const { createAdapter, setupPrimary } = require('@socket.io/cluster-adapter');
    
    if (cluster.isPrimary) {
      const numCPUs = availableParallelism();
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork({
          PORT: 3000 + i
        });
      }
    
      return setupPrimary();
    }
    
    async function main() {
      const db = await open({
        filename: 'chat.db',
        driver: sqlite3.Database
      });
    
      await db.exec(`
        CREATE TABLE IF NOT EXISTS messages (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          client_offset TEXT UNIQUE,
          content TEXT
        );
      `);
    
      const app = express();
      const server = createServer(app);
      const io = new Server(server, {
        connectionStateRecovery: {},
        adapter: createAdapter()
      });
    
      app.get('/', (req, res) => {
        res.sendFile(join(__dirname, 'index.html'));
      });
    
      io.on('connection', async (socket) => {
        socket.on('chat message', async (msg, clientOffset, callback) => {
          let result;
          try {
            result = await db.run('INSERT INTO messages (content, client_offset) VALUES (?, ?)', msg, clientOffset);
          } catch (e) {
            if (e.errno === 19 /* SQLITE_CONSTRAINT */ ) {
              callback();
            } else {
              // nothing to do, just let the client retry
            }
            return;
          }
          io.emit('chat message', msg, result.lastID);
          callback();
        });
    
        if (!socket.recovered) {
          try {
            await db.each('SELECT id, content FROM messages WHERE id > ?',
              [socket.handshake.auth.serverOffset || 0],
              (_err, row) => {
                socket.emit('chat message', row.content, row.id);
              }
            )
          } catch (e) {
            // something went wrong
          }
        }
      });
    
      const port = process.env.PORT;
    
      server.listen(port, () => {
        console.log(`server running at http://localhost:${port}`);
      });
    }
    
    main();
    

    Código de cliente final

    index.html

        <!DOCTYPE html>
    <html>
      <head>
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>Socket.IO chat</title>
        <style>
          body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
    
          #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
          #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
          #input:focus { outline: none; }
          #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }
    
          #messages { list-style-type: none; margin: 0; padding: 0; }
          #messages > li { padding: 0.5rem 1rem; }
          #messages > li:nth-child(odd) { background: #efefef; }
        </style>
      </head>
      <body>
        <ul id="messages"></ul>
        <form id="form" action="">
          <input id="input" autocomplete="off" /><button>Send</button>
        </form>
        <script src="/socket.io/socket.io.js"></script>
        <script>
          let counter = 0;
    
          const socket = io({
            ackTimeout: 10000,
            retries: 3,
            auth: {
              serverOffset: 0
            }
          });
    
          const form = document.getElementById('form');
          const input = document.getElementById('input');
          const messages = document.getElementById('messages');
    
          form.addEventListener('submit', (e) => {
            e.preventDefault();
            if (input.value) {
              const clientOffset = `${socket.id}-${counter++}`;
              socket.emit('chat message', input.value, clientOffset);
              input.value = '';
            }
          });
    
          socket.on('chat message', (msg, serverOffset) => {
            const item = document.createElement('li');
            item.textContent = msg;
            messages.appendChild(item);
            window.scrollTo(0, document.body.scrollHeight);
            socket.auth.serverOffset = serverOffset;
          });
        </script>
      </body>
    </html>
    

    Tarea

    Aquí tienes algunas ideas para mejorar la aplicación:

    • Transmita un mensaje a los usuarios conectados cuando alguien se conecta o desconecta.
    • Agregue soporte para apodos.
    • No envíes el mismo mensaje al usuario que lo envió. En su lugar, agregue el mensaje directamente tan pronto como presione enter.
    • Agregar “{user} es escribir” funcionalidad.
    • Muestra quién está en línea.
    • Agregue mensajería privada.
    • Comparte tus mejoras

    Obteniendo este ejemplo

    Puedes encontrarlo en GitHub aquí.

    git clone https://github.com/socketio/chat-example.git