Websockets para administradores de sistemas

sáb, 26 feb 2011 by Foron

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

Comments