SignalR (IV): Hubs

Como vengo comentando desde hace un tiempo, SignalR es un framework realmente impresionante y aporta unas posibilidades enormes en prácticamente cualquier tipo de aplicación. Ya hemos visto qué es y las bases en las que se sustenta, y también hemos visto algunos ejemplos de uso utilizando conexiones persistentes (aquí y aquí), que es el enfoque de menor nivel disponible a la hora de desarrollar servicios basados en esta plataforma.

En este post ascenderemos a un nivel de abstracción mucho mayor que el proporcionado por las conexiones persistentes y veremos cómo utilizar los Hubs, otro mecanismo proporcionado por SignalR que nos permitirá lograr una integración increíble entre el código cliente y de servidor, y hará aún más sencilla la implementación de este tipo de servicios.

En este momento ya deberíais tener claro qué cosas se pueden hacer con las conexiones persistentes de SignalR, e incluso cómo implementarlas:

  • En el cliente:
    • Conectar con un endpoint o servicio SignalR, manteniendo la conexión virtualmente abierta.
    • Recibir asíncronamente (a través de un callback) los mensajes enviados desde el servidor mediante broadcasts o mensajes directos.
    • Enviar mensajes al servicio usando el método send().
  • En el servidor:
    • Detectar la conexión de nuevos clientes.
    • Detectar la desconexión de clientes.
    • Recibir los mensajes enviados por los clientes.
    • Enviar objetos a todos los clientes conectados, a grupos de ellos, o a un cliente concreto.
Podemos hacer algunas cosillas más, como capturar los errores tanto en cliente como en servidor; aunque no lo hemos visto en los anteriores posts, son aspectos bastante triviales.

Pues bien, con Hubs vamos a poder hacer exactamente las mismas cosas, pero usaremos una sintaxis mucho más fluida y directa tanto en cliente como en servidor, gracias a la mágica flexibilidad que aportan tecnologías como javascript y los tipos dinámicos de .NET.

Y para demostrarlo, implementaremos una sencilla hoja de cálculo multiusuario en tiempo real. Todos los usuarios conectados a la misma podrán editar celdas directamente, y las actualizaciones se irán propagando al resto de clientes de forma automática. Podéis descargar el proyecto para Visual Studio 2010 en el enlace que encontraréis al final del post, aunque en el siguiente vídeo se muestra la locura de resultado en ejecución con algunos clientes conectados editando celdas de forma aleatoria:

Hoja de cálculo multiusuario en ejecución
Veremos que teniendo las herramientas apropiadas es algo bastante sencillo. Conceptualmente, sólo tenemos que hacer lo siguiente:
  • en el lado cliente, cuando un usuario modifique el valor de una celda, enviar al servidor el nuevo valor V que ha introducido en la celda X, Y.
  • en el lado servidor, ante la recepción del mensaje anterior, notificar a todos los clientes conectados que el usuario U ha establecido el nuevo valor V en la celda X, Y.
  • de nuevo en el lado cliente, y ante la recepción del mensaje anterior, modificar la celda X, Y para que aparezca el nuevo valor V que introdujo el usuario U.
Obviamente, esto irá acompañado por algún script más para conseguir que se puedan editar las celdas de forma similar a una hoja de cálculo, o para mantener actualizados los sumatorios de la última fila, pero no nos centraremos en ello. En cualquier caso, siempre podéis verlo descargando el proyecto de prueba.

1. El lado servidor: creación de un Hub

En SignalR, un Hub es la clase donde se implementa el servicio, es decir, el punto desde el cual se gestionarán las peticiones enviadas por todos los clientes conectados al mismo, y desde donde se emitirán los mensajes destinados a actualizar su estado.

En la práctica, se trata simplemente de una clase que hereda de la clase Hub, definida en SignalR.Hubs, en cuyo interior encontraremos los métodos que van a ser invocados directamente desde el cliente. En tiempo de ejecución, SignalR creará un proxy o representante del hub en el lado cliente, con métodos idénticos a los definidos en la clase, en cuyo cuerpo introducirá las llamadas Ajax necesarias para que se produzca la invocación de forma automática. Por tanto, si nuestra clase Hub tiene un método llamado “hacerAlgo()” y el proxy que hemos creado en cliente se llama “hub”, podemos invocarlo desde script haciendo un simple hub.hacerAlgo().

En nuestro ejemplo en el servidor lo único que tenemos que hacer es recibir las notificaciones de cambios de celda y enviárselas al resto de usuarios, lo que podemos conseguir con estas líneas:

public class MultiuserExcel: Hub
{
    public void Update(int y, int x, string value)
    {
        this.Clients.updateCell(this.Caller.userName, y, x, value);
    }
}
Voy a destacar varios aspectos de este código, comenzando por su extrema simplicidad. Casi sin haber visto antes nada de SignalR se puede intuir lo que hace. Observad también que hemos llegado a un punto de abstracción tal que no vemos nada relativo a conexiones ni desconexiones, simplemente implementamos lógica de nuestro servicio.

Es interesante también la propiedad Clients de la clase Hub. Ésta representa al representa al conjunto de clientes conectados, y nos permite llamar directamente a funciones que creemos en el lado cliente. O sea, que si desde el servidor hacemos una llamada a Clients.hacerAlgo(), se ejecutará la función de script hacerAlgo() en todos y cada uno de los clientes conectados. Ya ahí cada uno podrá procesar el mensaje y parámetros que enviemos.

Por último llamar la atención también sobre la propiedad Caller. Ésta, también heredada de la clase base Hub, nos permite acceder a propiedades definidas a nivel de script en el cliente que ha realizado la llamada al método de servidor Update(). En nuestro caso, será una propiedad en la que almacenaremos el nombre del usuario conectado, como veremos algo más adelante.

Fijaos que las clásicas fronteras entre cliente y servidor parecen haberse disuelto, es como si ambas capas se ejecutaran en el mismo proceso, aunque obviamente no es así: es SignalR el que se está encargando de mapear las llamadas entre ellas de forma transparente. Y aunque pueda parecer pura brujería, se trata simplemente de un uso ingeniosísimo de los tipos dinámicos de .NET 4 y de la flexibilidad de javascript.

2. El lado cliente

Como comentaba anteriormente, voy a saltarme el código destinado a hacer que funcionen los mecanismos básicos de edición de la hoja de cálculo, y nos centraremos en lo que nos interesa en este momento, la implementación de la comunicación con el servidor.

Lo primero que debemos hacer en el lado cliente es referenciar desde la vista o página dos archivos de script. El primero de ellos es el mismo que utilizábamos con las conexiones persistentes, /scripts/jquery.signalR.js, mientras que el segundo es un script dinámico generado por SignalR en la dirección /signalr/hubs. Para generarlo, SignalR localizará todas las clases descendientes de Hub disponibles en el proyecto e incluirá en el script un proxy para cada una de ellas; en nuestro ejemplo, generará un objeto llamado $.connection.multiUserExcel (el nombre del hub), que es el que podremos utilizar como proxy, aunque normalmente lo asignaremos a una variable para hacer más cómodo su uso.

Por tanto, un primer acercamiento al código de la vista específico para conectarse a un servicio SignalR podría ser algo así:

<script src="@Url.Content("~/scripts/jquery.signalR.min.js")" type="text/javascript"></script>
<script src="signalr/hubs" type="text/javascript"></script>
<script type="text/javascript">
    $(function() {
        var hub = $.connection.multiuserExcel; // en “hub” tenemos el proxy
        // .. resto del código
    });
</script>
Una consecuencia derivada de la inclusión del script dinámico generado por SignalR es que, a diferencia de lo que ocurría con las conexiones persistentes, no será necesario modificar la tabla de rutas de nuestra aplicación, puesto que los proxies contienen toda la información necesaria para que los servicios puedan ser utilizados de forma directa.

2.1. Envío de mensajes al servidor

Si analizamos los objetos generados de forma dinámica por SignalR para representar al hub en el lado cliente (proxies) podremos ver que éstos incluyen métodos exactamente con el mismo nombre y parámetros que los que hemos implementado en el lado servidor. Es decir, si en nuestra clase Hub tenemos métodos X() e Y(), el proxy en cliente dispondrá de estos dos mismos métodos, aunque en su implementación únicamente se realizarán las llamadas vía Ajax a sus respectivos equivalentes en servidor.

Por tanto, volviendo a nuestro ejemplo, dado que hemos creado un método Update() en el Hub (lado servidor), desde el cliente tendremos disponible el método update() en el hub, que podemos utilizar directamente para enviar mensajes al servidor. Así, en nuestra hoja de cálculo podemos capturar el evento de cambio de cada celda y enviar la actualización al servidor para que la distribuya al resto de clientes conectados:

        $("table.excel input").change(function() {
            var newValue = $(this).val(); // Get current cell’s value
            var x = $(this).data("x");    // Get current cell’s coords
            var y = $(this).data("y");
            hub.update(y, x, newValue);  // Broadcast this change
            updateTotal(x);               // Update column total
        });
Un detalle importante a tener en cuenta es que para adaptarse a las convenciones de nombrado de Javascript, aunque el método en servidor comience por una mayúscula, su correspondencia en cliente comienza en minúscula. Esta conversión sólo se realiza en el primer carácter, el resto debe escribirse exactamente igual en ambos extremos.

2.2. ¿Variables de script accesibles desde el servidor?

Recordad que decíamos que era posible acceder desde el servidor a propiedades existentes a nivel de script simplemente referenciándolas mediante la propiedad Caller del Hub. Pues bien, para que esto sea así, las propiedades deben estar definidas sobre el proxy:
    $(function() {
        var hub = $.connection.multiuserExcel;
        hub.userName = prompt("Username:");
        
        // .. resto del código
A partir de ese momento podremos hacer uso de Caller.userName desde el servidor para acceder al valor que se haya introducido en la misma desde el cliente que realice la llamada al hub. Ojo, que el acceso a las variables es sensible al uso de mayúsculas y minúsculas, deben escribirse igual en ambos extremos.

Otro comportamiento curioso de SignalR a este respecto es que también es capaz de propagar los cambios realizados en el servidor sobre estas variables, de forma que su nuevo valor pasará al lado cliente de forma automática:

    this.Caller.message = "Current time: " + DateTime.Now.ToShortTimeString();
Obviamente, podemos crear tantas propiedades como necesitemos sobre el proxy, y todas ellas las tendremos disponibles en el servidor de la misma forma, tanto para consultar su valor como para modificarlo.

2.3. Recepción en cliente de mensajes enviados por el servidor

A diferencia de lo que habíamos visto usando conexiones persistentes, utilizando hubs no es necesario implementar evento, es mucho más simple. Lo único que debemos hacer es definir sobre el objeto que representa al hub en cliente el método o función a la que estamos llamando desde el servidor utilizando el objeto dinámico Clients.

En nuestro ejemplo, si recordáis, en el servidor estamos enviando el mensaje a todos los clientes conectados al servicio de la siguiente forma:

    this.Clients.updateCell(this.Caller.userName, y, x, value);
Por lo tanto, en cliente debemos implementar un método exactamente con el mismo nombre, y que reciba justo los parámetros que se envían desde el servidor (el usuario que realiza el cambio, la celda modificada y el nuevo valor de ésta):
    hub.updateCell = function(username, y, x, value) {
        if (username != hub.userName) {
            var elem = $("#cell" + y + "-" + x);
            elem.val(value);
            updateTotal(x); // Update column total
        }
    };

¡Y eso es todo!

En el proyecto de demostración encontraréis bastante más código que el que hemos visto en este post, pero principalmente va destinado a conseguir un look&feel similar a las hojas de cálculo tradicionales (bueno, salvando las distancias, claro!). En lo que respecta a la comunicación asíncrona desde y hacia el servidor, estas pocas líneas que hemos visto aquí son prácticamente todo lo que necesitamos para el sistema funcione y el resultado sea espectacular.

Como os he recomendado otras veces, no dejéis de descargar el proyecto y probar el sistema con varias instancias del navegador abiertas, o desde varios equipos. Veréis lo sorprendente y espectacular que resulta y lo sencillo que es de implementar.

Publicado en: Variable not found.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *