23º septiembre 2020

Sondeo largo

Sondeo largo es la forma más sencilla de tener una conexión persistente con el servidor, que no utiliza ningún protocolo específico como WebSocket o Eventos enviados por el servidor.

Al ser muy fácil de implementar, también es suficientemente bueno en muchos casos.

Sondeo regular

La forma más sencilla de obtener nueva información del servidor es un sondeo periódico. Es decir, solicitudes regulares al servidor: “Hola, estoy aquí, ¿tienes información para mí?”. Por ejemplo, una vez cada 10 segundos.

En respuesta, el servidor primero se da cuenta de que el cliente está en línea, y segundo, envía un paquete de mensajes que recibió hasta ese momento.

Eso funciona, pero hay desventajas:

  1. Los mensajes se transmiten con un retraso de hasta 10 segundos (entre solicitudes).
  2. Incluso si no hay mensajes, el servidor se bombardea con solicitudes cada 10 segundos, aunque el usuario haya cambiado a otro lugar o esté dormido. Eso es bastante difícil de manejar, hablando en términos de rendimiento.

Entonces, si hablamos de un servicio muy pequeño, el enfoque puede ser viable, pero en general, necesita una mejora.

Sondeo largo

El llamado “sondeo largo” es una forma mucho mejor de sondear el servidor.

También es muy fácil de implementar y envía mensajes sin demoras.

El flujo:

  1. Se envía una solicitud al servidor.
  2. El servidor no cierra la conexión hasta que tiene un mensaje para enviar.
  3. Cuando aparece un mensaje, el servidor responde a la solicitud con él.
  4. El navegador realiza una nueva solicitud de inmediato.

La situación en la que el navegador envió una solicitud y tiene una conexión pendiente con el servidor, es estándar para este método. Solo cuando se entrega un mensaje, se restablece la conexión.

Si se pierde la conexión, por ejemplo, debido a un error de red, el navegador envía inmediatamente una nueva solicitud.

Un esquema de la función de suscripción del lado del cliente que realiza solicitudes largas:

async function subscribe() {
  let response = await fetch("/subscribe");

  if (response.status == 502) {
    // El estado 502 es un error de "tiempo de espera agotado" en la conexión,
    // puede suceder cuando la conexión estuvo pendiente durante demasiado tiempo,
    // y el servidor remoto o un proxy la cerró
    // vamos a reconectarnos
    await subscribe();
  } else if (response.status != 200) {
    // Un error : vamos a mostrarlo
    showMessage(response.statusText);
    // Vuelve a conectar en un segundo
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subscribe();
  } else {
    // Recibe y muestra el mensaje
    let message = await response.text();
    showMessage(message);
    // Llama a subscribe () nuevamente para obtener el siguiente mensaje
    await subscribe();
  }
}

subscribe();

Como puedes ver, la función subscribe realiza una búsqueda, luego espera la respuesta, la maneja y se llama a sí misma nuevamente.

El servidor debería estar bien aún con muchas conexiones pendientes

La arquitectura del servidor debe poder funcionar con muchas conexiones pendientes.

Algunas arquitecturas de servidor ejecutan un proceso por conexión, resultando en que habrá tantos procesos como conexiones y cada proceso requiere bastante memoria. Demasiadas conexiones la consumirán toda.

Este suele ser el caso de los backends escritos en lenguajes como PHP y Ruby.

Los servidores escritos con Node.js generalmente no tienen estos problemas.

Dicho esto, no es un problema del lenguaje sino de implementación. La mayoría de los lenguajes modernos, incluyendo PHP y Ruby permiten la implementación de un backend adecuado. Por favor asegúrate de que la arquitectura del servidor funcione bien con múltiples conexiones simultáneas.

Demostración: un chat

Aquí hay un chat de demostración, también puedes descargarlo y ejecutarlo localmente (si estás familiarizado con Node.js y puedes instalar módulos):

Resultado
browser.js
server.js
index.html
// Envío de mensajes, un simple POST
function PublishForm(form, url) {

  function sendMessage(message) {
    fetch(url, {
      method: 'POST',
      body: message
    });
  }

  form.onsubmit = function() {
    let message = form.message.value;
    if (message) {
      form.message.value = '';
      sendMessage(message);
    }
    return false;
  };
}

// Recibir mensajes con sondeo largo
function SubscribePane(elem, url) {

  function showMessage(message) {
    let messageElem = document.createElement('div');
    messageElem.append(message);
    elem.append(messageElem);
  }

  async function subscribe() {
    let response = await fetch(url);

    if (response.status == 502) {
      // El tiempo de conexión expiró
      // sucede cuando la conexión estuvo pendiente durante demasiado tiempo
      // vamos a reconectarnos
      await subscribe();
    } else if (response.status != 200) {
      // Mostrar Error
      showMessage(response.statusText);
      // Volver a conectar en un segundo
      await new Promise(resolve => setTimeout(resolve, 1000));
      await subscribe();
    } else {
      // Tengo un mensaje
      let message = await response.text();
      showMessage(message);
      await subscribe();
    }
  }

  subscribe();

}
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');

let fileServer = new static.Server('.');

let subscribers = Object.create(null);

function onSubscribe(req, res) {
  let id = Math.random();

  res.setHeader('Content-Type', 'text/plain;charset=utf-8');
  res.setHeader("Cache-Control", "no-cache, must-revalidate");

  subscribers[id] = res;

  req.on('close', function() {
    delete subscribers[id];
  });

}

function publish(message) {

  for (let id in subscribers) {
    let res = subscribers[id];
    res.end(message);
  }

  subscribers = Object.create(null);
}

function accept(req, res) {
  let urlParsed = url.parse(req.url, true);

  // El nuevo cliente quiere mensajes
  if (urlParsed.pathname == '/subscribe') {
    onSubscribe(req, res);
    return;
  }

  // enviando un mensaje
  if (urlParsed.pathname == '/publish' && req.method == 'POST') {
    // aceptar POST
    req.setEncoding('utf8');
    let message = '';
    req.on('data', function(chunk) {
      message += chunk;
    }).on('end', function() {
      publish(message); // publicarlo para todos
      res.end("ok");
    });

    return;
  }

  // el resto es estático
  fileServer.serve(req, res);

}

function close() {
  for (let id in subscribers) {
    let res = subscribers[id];
    res.end();
  }
}

// -----------------------------------

if (!module.parent) {
  http.createServer(accept).listen(8080);
  console.log('Servidor que se ejecuta en el puerto 8080');
} else {
  exports.accept = accept;

  if (process.send) {
     process.on('message', (msg) => {
       if (msg === 'shutdown') {
         close();
       }
     });
  }

  process.on('SIGINT', close);
}
<!DOCTYPE html>
<script src="browser.js"></script>

Todos los visitantes de esta página verán mensajes entre ellos.

<form name="publish">
  <input type="text" name="message" />
  <input type="submit" value="Enviar" />
</form>

<div id="subscribe">
</div>

<script>
  new PublishForm(document.forms.publish, 'publish');
  // parámetro de url aleatorio para evitar problemas de almacenamiento en caché
  new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
</script>

El código del navegador está en browser.js.

Área de uso

El sondeo largo funciona muy bien en situaciones en las que es raro recibir mensajes.

Si los mensajes llegan con mucha frecuencia, entonces el gráfico de mensajes solicitados vs recibidos, pintado arriba, se vuelve en forma de sierra.

Cada mensaje es una solicitud separada, provista de encabezados, sobrecarga de autenticación, etc.

Entonces, en este caso, se prefiere otro método, como Websocket o Eventos enviados por el servidor.

Mapa del Tutorial

Comentarios

lea esto antes de comentar…
  • Si tiene sugerencias sobre qué mejorar, por favor enviar una propuesta de GitHub o una solicitud de extracción en lugar de comentar.
  • Si no puede entender algo en el artículo, por favor explique.
  • Para insertar algunas palabras de código, use la etiqueta <code>, para varias líneas – envolverlas en la etiqueta <pre>, para más de 10 líneas – utilice una entorno controlado (sandbox) (plnkr, jsbin, codepen…)