Ya lo he escrito en el post anterior: Node.js es un proyecto muy interesante, muy adecuado como backend en muchos entornos, y particularmente en aquellos que dependen en gran medida del rendimiento I/O. Como ejemplo, aquí va uno de los últimos casos de éxito.

En este post, además de volver a usar Node.js y Redis (otro proyecto cada vez más importante), voy a dar cuatro pinceladas sobre una de las nuevas tendencias que van apareciendo en la web: WebSockets.

Para completar el ejercicio, escribiré un mini-proyecto para graficar todo tipo de valores numéricos en un plasmoid de KDE. Bueno, en realidad en cualquier navegador con soporte WebSockets (la lista es reducida, como veremos más adelante). Así, pasaremos a un punto de vista más de administrador de sistemas que de desarrollador web.

¿Qué son los WebSockets?

Internet es cada vez más "web". Las aplicaciones son cada vez más dinámicas, complejas e interactivas. Esto no es malo, obviamente, pero supone que mientras que los usuarios tienen más y más medios para interactuar con las webs, los administradores de estos entornos han visto como lo que antes se podía gestionar sin mayores problemas, ahora necesita todo tipo de caches e "inventos" sólo porque el sistema de votación de videos (por ejemplo) triplica la cantidad de accesos al sitio y a la base de datos.

Estos problemas no son, ni mucho menos, nuevos. Durante los últimos años se ha ido desarrollando mucha tecnología, para mejorar la experiencia de usuario, por supuesto, pero también para que la comunicación cliente/servidor sea más controlable y eficaz. Lo último: WebSockets.

Simplificando mucho, los WS básicamente ofrecen un canal full duplex entre el cliente y el servidor. De esta manera, en lugar de abrir una conexión web tradicional vía ajax (o similar) cada vez que se necesite un dato, el navegador crea un único canal permanente con el servidor, y lo usa tanto para enviar como para recibir datos. Esto último es importante, ya que el servidor puede usar la vía de comunicación activamente, por ejemplo para enviar un broadcast a todos sus clientes, y no sólo como respuesta ante una petición.

WS orientado a la administración de sistemas

Entonces, tenemos una nueva tecnología, muy orientada a la web, y por lo tanto muy fácil de usar. Además, ofrece algo que, aún sin ser tiempo real, es lo suficientemente ágil como para usarse para cosas como la monitorización de sistemas, que es precisamente el ejemplo con el que voy a seguir en este post.

Mi planteamiento inicial era hacer una pequeña aplicación que mostrase los datos de una base de datos rrd. Sin embargo, esto centraba el ejercicio demasiado en torno a la programación del módulo para Node.js, así que he preferido reutilizar los conceptos de otros posts anteriores, limitando el servidor WebSockets al envío de los datos que encuentre en una base de datos Redis.

Para hacerlo más interesante, el cliente será un plasmoid de KDE, aunque no deja de ser una página web normal y corriente, teniendo en cuenta que voy a usar el motor WebKit para su implementación (se pueden programar plasmoids en C, python, javascript, ...)

Las limitaciones

El protocolo WebSockets todavía es un draft del IETF. Ya sólo teniendo esto en cuenta podemos decir que está "verde".

En cuanto a navegadores, se necesita lo último de lo último en versiones (Opera, Firefox, Chrome, Safari...). Además, en el caso de Firefox (y Opera), el soporte, aunque presente, está deshabilitado por algunas lagunas en torno a la seguridad del protocolo cuando interactúa con un proxy. Ahora bien, para activarlo noy hay más que cambiar una clave (network.websocket.override-security-block).

Como ejemplo, para escribir este post he usado Ubuntu 10.10 y Fedora 14, con sus versiones respectivas de KDE, y con soporte para ejecutar plasmoids WebKit (cada distribución tiene su nombre de paquete, así que lo buscáis vosotros :-D ).

En la práctica, si queréis usar WebSockets en una aplicación "de verdad", hoy en día os recomiendo usar algo como socket.io, que es capaz de detectar lo que el navegador soporta, y en función de eso usar WS o simularlo con flash.

Bien, empecemos con la aplicación.

El servidor

Para guardar los datos vamos a usar Redis, con un hash que vamos a llamar "datos", y en el que guardaremos los valores a graficar. Traducido a perl:

  my %datos;
  $datos{todos} = 40;
  $datos{server1} = 10;
  $datos{server2} = 20;
  $datos{server3} = 10;

Esto en Redis:

  redis-cli hset datos todos 40
  redis-cli hset datos server1 10
  redis-cli hset datos server1 20
  redis-cli hset datos server1 10

Para las pruebas he usado un bucle bash:

  while true; do redis-cli hset datos todos $((10 + $RANDOM % 20)); sleep 1; done;
  ...

Existen varias formas de escribir un servidor WebSockets, pero pocas tan fácilies como Node.js y su módulo websocket-server. Tambien vamos a usar el módulo de Redis, así que:

  npm install websocket-server
  npm install hiredis redis

Una vez instalados los componentes, es el momento de escribir las 70 líneas que hacen falta para tener un servidor 100% funcional y capaz de gestionar tantos gráficos como queramos:

  var net = require('net');
  var sys = require("sys");
  var ws = require("websocket-server");
  var redis = require("redis");
  var puertoredis = 6379;
  var hostredis = 'localhost';

  var server = ws.createServer({debug: true});
  var client = redis.createClient(puertoredis,hostredis);
  var conexion;
  var mensaje;

  client.on("error", function (err) {
      sys.log("Redis  error en la conexion a: " + client.host + " en el puerto: " + client.port + " - " + err);
  });

  server.addListener("connection", function(conn){
      var graficomostrado = "";
      var servidores = [];
      sys.log("Conexion abierta: " + conn.id);
      client.hkeys("datos", function (err, replies) {
          replies.forEach(function (reply, i) {
              sys.log("Respuesta: " + reply);
              servidores.push(reply);
          });
          graficomostrado = servidores[0];
          mensaje = {"titulo": graficomostrado, "todos": servidores};
          conn.send(JSON.stringify(mensaje));
      });
      conn.addListener("message", function(message){
          var dato = JSON.parse(message);
          if (dato) {
              if (dato.orden == "changeGraphic") {
                  graficomostrado = dato.grafico;
                  conn.send(JSON.stringify({changedGraphic: graficomostrado}));
                  client.hget("datos", graficomostrado, function (err, reply) {
                      mensaje = {"inicio":[(new Date()).getTime(), reply]};
                      conn.send(JSON.stringify(mensaje));
                  });
              } else if (dato.orden == "getData") {
                  client.hget("datos", graficomostrado, function (err, reply) {
                      mensaje = {"actualizacion":[(new Date()).getTime(), reply]};
                      conn.send(JSON.stringify(mensaje));
                  });
              }
          }
      });
  });
  server.addListener("error", function(){
      sys.log("Error de algun tipo en la conexion");
  });
  server.addListener("disconnected", function(conn){
      sys.log("Desconectada la sesion " + conn.id);
  });
  server.listen(80);

Así de fácil es. Cuando un cliente establezca una conexión se llamará a:

  server.addListener("connection", function(conn) {....

En este ejemplo, el trabajo fundamental de este bloque es enviar al cliente el listado de datos disponibles vía JSON:

  ....
  mensaje = {"titulo": graficomostrado, "todos": servidores};
  conn.send(JSON.stringify(mensaje));

A partir de aquí, la conexión está establecida. Cada vez que el cliente la use se llamará a:

  conn.addListener("message", function(message) {....

En este caso, el servidor atenderá las peticiones de cambio de gráfico y de actualización de gráfico:

  var dato = JSON.parse(message);
  if (dato.orden == "changeGraphic") {
      graficomostrado = dato.grafico;
      conn.send(JSON.stringify({changedGraphic: graficomostrado}));
      client.hget("datos", graficomostrado, function (err, reply) {
          mensaje = {"inicio":[(new Date()).getTime(), reply]};
          conn.send(JSON.stringify(mensaje));
      });
  } else if (dato.orden == "getData") {
      client.hget("datos", graficomostrado, function (err, reply) {
          mensaje = {"actualizacion":[(new Date()).getTime(), reply]};
          conn.send(JSON.stringify(mensaje));
      });
  }

Y con hacer que el servidor escuche en el puerto 80... ¡A correr!

  server.listen(80);

El cliente

El código del servidor y del cliente está disponible en Github, así que voy a limitar la explicación a lo fundamental, dejando a un lado el uso de Jquery, Jquery-ui o de la excelente librería Highcharts, que he usado a pesar de no ser completamente libre, porque me ha permitido terminar el ejemplo mucho más rápido.

Una aplicación que quiere usar WebSockets debe comprobar si el navegador lo permite:

  if (!("WebSocket" in window))   {
    // No tiene soporte websockets, deshabilitamos botones.
    $('#containergraficos').empty();
    $('#containergraficos').html('

Sin soporte WebSockets.

'); $("#conectar").button({ disabled: true }); } else { // Sí tiene soporte websockets socket = new WebSocket(host); }

A partir de aquí, sólo queda reaccionar ante cada evento:

  socket.onopen = function(){
     .......
  }
  socket.onmessage = function(msg){
     // código íntegro en github
     var resultado = JSON.parse(msg.data);
     if (resultado.inicio) {
        var datos = [];
        datos.push({x: parseInt(resultado.inicio[0]), y: parseInt(resultado.inicio[1])});
        options.series[0].data = datos;
        chart = new Highcharts.Chart(options);
     } else if (resultado.actualizacion) {
        chart.series[0].addPoint({x: parseInt(resultado.actualizacion[0]), y: parseInt(resultado.actualizacion[1])});
     } else if (resultado.changedGraphic) {
        $("#grafico").button("option", "label", resultado.changedGraphic);
     } else if (resultado.titulo) {
        $("#grafico").button("option", "label", resultado.titulo);
        var mensaje = {"orden": "changeGraphic", "grafico": resultado.titulo};
        socket.send(JSON.stringify(mensaje));
     }
  }
  socket.onclose = function(){
     ...
  }
  socket.onerror = function() {
     ...
  }

En definitiva, unos pocos mensajes JSON de un lado para otro del socket.

Además, cada 5 segundos vamos a hacer que Highcharts pida datos nuevos:

  events: {
     load: function() {
        var series = this.series[0];
        setInterval(function() {
           if (socket) {
              var mensaje = {"orden": "getData", "grafico": grafico};
              socket.send(JSON.stringify(mensaje));
           } else {
              var x = (new Date()).getTime();
              series.addPoint([x, 0], true, true);
           }
        }, 5000); //5 segundos
     }
  }

El plasmoid

Evidentemente, hasta ahora no hemos hecho nada que no se pueda ejecutar en un navegador. Ahora lo empaquetaremos en un plasmoid, aunque no deja de ser más que un zip con todo el contenido y un fichero metadata.desktop con la descripción del propio plasmoid, que entre otros inlcuye:

  [Desktop Entry]
  Name=Forondarenanet
  Comment=Pruebas para forondarena.net
  Icon=chronometer
  Type=Service
  X-KDE-ServiceTypes=Plasma/Applet
  X-Plasma-API=webkit
  X-Plasma-MainScript=code/index.html
  X-Plasma-DefaultSize=600,530

KDE tiene una utilidad para probar los plasmoids sin instalarlos, llamada "plasmoidviewer". Una vez probado, se puede instalar con:

  plasmapkg -i forondarenanet.plasmoid

Recordad que el fichero no es más que un zip. En este caso, la estructura es la siguiente:

  ./metadata.desktop
  ./contents
  ./contents/code
  ./contents/code/css
  ./contents/code/css/images
  ./contents/code/css/images/ui-bg_gloss-wave_16_121212_500x100.png
  ...
  ./contents/code/css/images/ui-icons_cd0a0a_256x240.png
  ./contents/code/css/jquery-ui-1.8.9.custom.css
  ./contents/code/css/forondarenanet.css
  ./contents/code/index.html
  ./contents/code/js
  ./contents/code/js/jquery-1.4.4.min.js
  ./contents/code/js/highcharts.js
  ./contents/code/js/forondarenanet.js
  ./contents/code/js/jquery-ui-1.8.9.custom.min.js
  ./contents/code/img

Y poco más. Para terminar con un ejemplo, cuatro plasmoids mostrando gráficos diferentes:

Pantallazo plasmoids

Que por supuesto, sólo generan 4 conexiones, a pesar de estar actualizándose cada 5 segundos:

  # netstat -nt
  Active Internet connections (w/o servers)
  Proto Recv-Q Send-Q Local Address               Foreign Address             State
  tcp        0      0 192.168.10.136:36903        192.168.10.131:80           ESTABLISHED
  tcp        0      0 192.168.10.136:36904        192.168.10.131:80           ESTABLISHED
  tcp        0      0 192.168.10.136:36905        192.168.10.131:80           ESTABLISHED
  tcp        0      0 192.168.10.136:36906        192.168.10.131:80           ESTABLISHED

Servidor pop-before-smtp en 67 líneas con nodejs y redis

mié, 01 dic 2010 by Foron

Llevaba tiempo, la verdad, queriendo escribir un post sobre una de esas nuevas herramientas que aparecen de vez en cuando, que prometen un montón de cosas, pero que en realidad nunca sabemos hasta dónde van a llegar. En este caso me estoy refiriendo a node.js.

Buscando algo que programar se me ha ocurrido escribir un sencillo servidor popbeforesmtp con node.js. La verdad es que resulta algo paradógico usar algo tan nuevo para esto, cuando pbs (vamos abreviando) es uno de los sistemas de autentificación más odiados del mundo del correo electrónico, propio de una época en la que el spam casi ni existía (buff!!). Como decía, no es más que una excusa para hacer algo "práctico" con node.js.

Los tres lectores de mi blog saben lo que es pbs, pero probablemente no hayan oido hablar sobre node.js. A ver si sacamos algo en claro de todo esto.

El problema

Digamos que tenemos un sistema con unos cuantos servidores SMTP que usamos para enviar correo. Aunque normalmente usamos algún tipo de validación ESMTP, tenemos que mantener la compatibilidad con algunos clientes de correo que han venido enviando correo sin ningún problema sólo a través de validación POP.

Pop before smtp permite que un cliente de correo pueda enviar un mensaje una vez se haya validado vía POP. El servidor SMTP, por lo tanto, debe ser capaz de guardar un histórico de conexiones y de permitir envíos, normalmente durante unos pocos minutos, sin pedir ningún otro tipo de credencial.

Ya tenemos todos los elementos que necesitamos sobre la mesa. Veamos:

  • Un servidor SMTP sobre el que implementar "el invento".
  • Un servidor POP desde el que obtener la información de validación.
  • Una aplicación que relacione ambos mundos, y que sea capaz de funcionar tanto en una infraestructura de una máquina como en clusters de varios servidores POP y SMTP (con todo lo que esto significa).

Un servidor SMTP

Actualmente tenemos en el mercado open source cuatro tres servidores SMTP que podemos usar:

  • Postfix
  • Exim
  • Sendmail
  • Qmail

Cada uno usa sus propios mecanismos de auntentificación. Para este ejemplo me voy a centrar en Postfix.

Postfix tiene muchos controles "nativos" para permitir o denegar envíos de correo. Se pueden establecer políticas de acceso en base a IPs origen, cuentas origen, destino, helo, contenido, .... De hecho, también se pueden escribir reglas compuestas que combinen las anteriores.

Sin embargo, para pbs necesitamos mantener un listado de IPs para las que se permiten envíos(fácil), pero que expiren cada x minutos.

Para estos casos más elaborados Postfix ofrece lo que se llama access policy delegation, y que no es más que un sencillo API para conectarse con una aplicación externa que se encargará de tomar las decisiones de validación.

Dicho de otra forma, Postfix enviará a un socket (UNIX o TCP) la información de la sesión SMTP que se quiere comprobar, y recibirá por ese mismo socket el "OK" (o no) correspondiente.

El software que vamos a escribir, por lo tanto, va a recibir esto por un socket:

  request=smtpd_access_policy
  protocol_state=RCPT
  protocol_name=ESMTP
  client_address=192.168.10.131
  client_name=fn131.example.org
  reverse_client_name=fn131.example.org
  helo_name=example.org
  sender=patata@example.org
  recipient=nospam@example.com
  ......

A lo que tiene que responder generalmente o "OK", o "reject", o "dunno". [1]

Un servidor POP

El que más nos guste (Dovecot, Courier, ...). El único requerimiento es que escriba en un log parseable los login de usuarios.

Una aplicación encargada de la política

Aquí está lo interesante de este post. Veamos lo que necesitamos:

  • Una aplicación, en nuestro caso un servidor TCP, que reciba las conexiones desde Postfix y entregue las respuestas.
  • Una base de datos de algún tipo que almacene la información. A ser posible, debe poder distribuirse entre muchos servidores SMTP de forma transparente.
  • Un procesador de logs POP capaz de insertar registros en la base de datos que usará la aplicación.

Empecemos por la base de datos. Estamos hablando básicamente de "algo" sencillo, en lo que guardar un listado de IPs, y que además auto-expire los registros pasados unos minutos. Además, debe ser lo más rápida posible y permitir un gran número de conexiones simultáneas.

Vaya, si estamos hablando de....Memcached.

Pros:

  • Estable. Muchos años en entornos muy importantes.
  • Perfecta para guardar claves (IPs) y valores (cualquier cosa en este caso. El rcpt nos podría venir bien).
  • Expira automáticamente los registros antiguos.
  • Tiene interfaces perl, python, ....
  • Rápida.

Cons:

  • ¿Cómo usamos una base de datos común en un cluster?
  • ¿Perdemos la información de logins ante un fallo o reinicio de la aplicación?

Por lo tanto, tenemos dos problemas. Memcache no está especialmente pensado para funcionar en "modo cluster", sincronizando su contenido en n servidores; y además pierde el contenido cuando se reinicia. O sea, si tenemos que reiniciar Memcached perdemos los datos de usuarios que ya se han validado (aunque sólo sea cosa de una media hora).

Obviamente, las ventajas superan a los inconvenientes. Sin embargo, en el "mercado" tenemos una aplicación que, además de tener los pros, supera los cons: Redis, que permite un modelo master/slave, y además permite volcar periódicamente la información de memoria a disco para su recuperación en caso de caida. Es más, permite tener una especie de redo log en disco.

Bien, ya tenemos el almacén de información. ¿Cómo añadimos los datos? Usaremos cualquier aplicación de las muchas que permiten ejecutar una acción en función de haber encontrado un patrón en un log. Un ejemplo en perl es mailwatch, pero se puede usar cualquier otra. En el fondo, lo único que hay que hacer es conectarse al nodo master de Redis, y después ejecutar el comando "SETEX <host_validado> <segundos> <rcpt>".

Bien, sólo nos queda la aplicación. Necesitamos que:

  • Escuche en un puerto TCP.
  • Esté muy orientado a eventos.
  • Tenga un muy buen rendimiento.
  • Sea sencillo de programar.

Y aquí es donde aparece node.js.

node.js

Hay algo indudable en la informática de hoy en día: Cada vez está más orientada a la web. En este entorno se usan muchos lenguajes, desde php a .net; pero si hay uno que ha usado "casi" todo el mundo, ese es javascript.

Node.js es una forma de llevar javascript fuera del contexto de los navegadores web. Básicamente es una especie de wrapper para V8, el compilador de javascript de Google. Y esto es algo muy importante, porque estamos hablando de uno de los compiladores más avanzados que hay hoy en día. Evidentemente, node.js no consigue el rendimiento de C, pero supera con cierta facilidad a perl o python, todo además sin perder la familiaridad y versatilidad de javascript.

Node.js, además de usar V8, implementa algunas de las funcionalidades que hacen falta para ejecutarse fuera de un navegador, como la manipulación de datos binarios.

La otra característica de node es que está completamente orientado a eventos, y por lo tanto necesita un cierto cambio de chip por parte del programador. En node.js casi nada es síncrono: Cuando queremos acceder a un fichero, por ejemplo, en lugar de esperar a que el disco se sitúe en la posición adecuada y lea los datos, seguimos con la ejecución del programa y con el resto de eventos, hasta que los datos estén disponibles. Según Yahoo, gracias a este modelo un único procesador Xeon a 2.5GHz ejecutando un proxy inverso puede servir cerca de 2100 peticiones por segundo [2].

Dejo la instalación de node.js (y de Redis, y de mailgraph, ...) para el lector. Veamos el código de nuestro servidor:

Empezamos por la configuración de Postfix, en /etc/postfix/main.cf:

  smtpd_recipient_restrictions = .... check_policy_service inet:127.0.0.1:10000 ....

Nuestro servidor va a escuchar en localhost y en el puerto 10000.

Y ahora vamos con el código, en sus 67 líneas, aunque para ser correctos, falta la mayoría de la gestión de errores:

 var net = require('net');
 var sys = require("sys");
 var redis = require("redis");

 var puertoplcy = 10000;
 var hostplcy = 'localhost';

 var puertoredis = 6379;
 var hostredis = 'localhost';

Hemos definido las variables y las librerías que vamos a usar, incluyendo una "third party" para acceso a Redis que hemos instalado con (npm install redis).

 var server = net.createServer(function (stream) {

         var client = redis.createClient(puertoredis,hostredis);

         client.on("error", function (err) {
                 sys.log("Redis  error en la conexion a: " + client.host + " en el puerto: " + client.port + " - " + err);
         });

         stream.setEncoding('utf8');
         stream.setNoDelay(true);

         stream.on('connect', function () {
                 sys.log("Conexion nueva.");
         });

         stream.on('data', function (data) {
                 sys.log("Correo entrante:\n" + data);
                 var clientaddr, tmpclientaddr;
                 var rcpt, tmprcpt;

                 var datos = data.split('\n');
                 for (var i = 0; i < datos.length; i++ ) {
                         if (datos[i] && datos[i].match(/client_address=/) ) {
                                 tmpclientaddr = datos[i].split("=");
                                 clientaddr = tmpclientaddr[1].trim();
                                 sys.log("Extraido el client address: " + clientaddr);
                         } else if (datos[i] && datos[i].match(/recipient=/) ) {
                                 tmprcpt = datos[i].split("=");
                                 rcpt = tmprcpt[1].trim();
                                 sys.log("Extraido el rcpt to: " + rcpt);
                         }
                 }

                 client.get(clientaddr, function (err, reply) {
                         if (err) {
                                 sys.log("Get error: " + err);
                         }

                         if (reply) {
                                 sys.log("Se ha encontrado el host: " + clientaddr + " que ha usado: " + reply);
                                 stream.write('action=ok\n\n');
                         } else {
                                 sys.log("El host: " + clientaddr + " con el usuario: " + rcpt + " no se ha validado");
                                 stream.write('action=reject 550 Relay denegado.\n\n');
                         }
                 });
         });

         stream.on('end', function () {
                 sys.log("Fin de la conexion");
                 stream.end();
         });

 });

Este es el servidor, con un poco de logueo por consola que en condiciones normales se debería quitar. Hemos definido eventos para conexiones nuevas, para cuando entran datos nuevos, ...; tenemos un callback para cuando leemos datos de Redis, .....

Al final, si tenemos el dato devolvemos un "action=ok\n\n", y si no lo tenemos pasamos un "action=reject 550 Relay denegado.\n\n", aunque esto puede variar en función de la configuración de Postfix.

Y por último, hacemos que el servidor escuche en el puerto 10000 de localhost:

  server.listen(puertoplcy, hostplcy);

Y ya está. Todo esto lo guardamos en un fichero (pbs_filter.js por ejemplo), y lo ejecutamos desde una shell:

  /usr/local/bin/node pbs_filter.js

Bueno, para terminar, una imagen del planteamiento:

Esquema ejemplo

En definitiva, node.js es otro más de esos proyectos que aparecen periódicamente y que cogen cierta fuerza. Esto no es nada nuevo; hay muchos proyectos abandonados que tuvieron "su momento", pero creo que tiene ciertas características que lo hacen interesante. El tiempo dirá si tiene futuro.

Notas

[1]postfix tiene muy buena documentación. RTFineM.
[2]Node.js, igual que Memcached y otros, no es multi-hilo. En el mismo enlace de Yahoo hay una interesante discusión sobre el uso de varios cores.
read more