Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 20 de marzo de 2012
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.

18 Comentarios:

Ferson dijo...

Impresionante!
Muchas gracias por el trabajo, gran post!

FerSon.

josé M. Aguilar dijo...

Gracias, Ferson!

Un saludo.

Fer dijo...

José tengo una duda que no tiene del todo relación con SignalR pero algo si por el tema de las conexiones abiertas y asincronia y creo que tu podrás ayudarme.

Tengo que paralizar durante unos segundos antes de tratar las peticiones a un action de un controlador concreto (MVC 3), tengo miedo de bloquear los hilos y que el IIS comience a no responder a otras peticiones. De momento he cambiado el controlador por uno asincrono.

Que harías dentro de la action para hacer un sleep que afecte lo mínimo a la disponibilidad del iis? Un task y dentro el sleep es una buena opción?

Gracias de ante mano. Un saludo.
Ferson.

josé M. Aguilar dijo...

Hola, Fer!

Hombre, lo más razonable para no agotar el thread pool sería no poner un Sleep ;-)

Pero bueno, una vez decidido que hay que hacerlo, el controlador asíncrono asegura al menos que IIS podrá seguir procesando peticiones. La implementación que comentas, incluir el Sleep en un hilo lanzado de forma asíncrona desde la acción me parece acertada.

Saludos!

Fer dijo...

Muchas gracias!

Ayuda mucho contar con tu opinión.

Una tontería más (creo que es una tontería) podría usar una llamada asincrona a un web service en otro servidor y hacer allí el sleep de está forma no agotaría el servidor de front-end. ¿Que opinas es una tonteria?

Muchas gracias.
Saludos!

josé M. Aguilar dijo...

Hola!

Probablemente la llamada al servicio web externo te consuma más recursos del front-end que invocar directamente al Sleep(). Desde el frontend tendrías que realizar la conexión al "servidor de sleep", mantenerla abierta hasta que éste responda, y cerrarla al finalizar, y esto tiene su coste.

De todas formas, insisto: poner un Sleep() en algún punto te va a limitar siempre la escalabilidad. No sé lo que estás implementando, pero probablemente haya alternativas más flexibles y menos dependientes de la potencia de tu hardware.

Saludos.

Fer dijo...

Hola,

pues trabajamos en una web en la que las cuotas a las que los clientes ordenar sus compras varia constantemente, por seguridad debemos retener cierta peticiones durante unos segundos antes de comenzar las verificaciones.

Nos ocurre que hay clientes que obtienen información privilegiada antes que nosotros o al mismo tiempo y pueden realizar las compras con cierta ventaja.

Cuando el cliente es de escritorio realizamos el retardo en el cliente pero en web no encuentro la solución optima ya que aunque use marcas de tiempo generadas en el servidor al añadir al carro y verificarla al confirmar la compra y asegurar que la petición ha sido retardada tampoco debo admitir peticiones muy antiguas por las fluctuaciones y suponiendo un retardo de 5 seg. las que tengan por ejemplo 8 o 10 puedes ser malintencionadas también pero también pueden proceder de conexiones lentas y no tengo claro como detectarlas. Me daría pena penalizar a los clientes de mobile o con malas conexiones.

este es nuestro link al producto para que puedas entenderme mejor:
kirolsoft.com

Puedo implementar la alternativa del lado cliente que es mucho mejor para el rendimiento y disponibilidad de los servidores, lo que no soy consciente es que supone el sleep utilizando asincronía.

Saludos y mil gracias!

josé M. Aguilar dijo...

Hola, Fer!

Bueno, pues parece que algo complejo sí que es tu escenario.

Meter el delay en cliente te puede solucionar el tema, pero ya sabes que en la web el lado cliente es frágil e inseguro por naturaleza. Alguien con conocimientos podría alterar tus scripts y saltarse los retardos alegremente.

¿Habéis evaluado la posibilidad de separar el almacenamiento de la apuesta de su proceso? Es decir, si las apuestas simplemente se graban cuando el usuario las envía y es otro proceso independiente (como un worker role de Azure) el que las procesa, probablemente tu solución podría escalar mejor y tendrías todo el control sobre los tiempos.

Saludos.

Fer dijo...

Hola pues no lo había pensado. Es un punto de vista interesante.

Las bases de datos de Azure no puedo usar por restricciones de reglamento.

Podría intentarlo simulando un worker internamente lo único estoy un poco verde en como desde el worker interno conecto otra vez con la petición para darle una respuesta (esto empieza a recordarme a SignalR :).

Hasta ahora no cabía en mi cabeza la magia de poder usar comet y la verdad es que aunque veo que es posible no acabo de interiorizar como lo hace y tampoco como conectar desde otro proceso con el del IIS. Creo que tengo que estudiar muuucho más :)

Muchas gracias.
Saludos.

Viure dijo...

Hola, Gracias por todos los esfuerzos que estas haciendo por difundir signalR, es una maravilla.

Usando estos ejemplos que has puesto pero pasandolos a vb, me he topado conque no se puede acceder a las propiedades del Caller creadas desde el script de cliente.

En vb nos regala un bonito error "Conversion from type 'Task(Of Object)' to type 'String' " . Parece un problema con los dynamicObject en vb, pero aunque en stackoverflow esta abierta esta incidencia, no parece que nadie la haya resuelto.

http://stackoverflow.com/questions/11032730/setting-and-getting-signalr-caller-properties-in-vb-net?answertab=votes#tab-top


ejemplo:

cliente
$(function () {

// Proxy created on the fly
var chat = $.connection.chat;
chat.idsesion = "loquesea";
[..]

servidor
Dim name as string = Caller.idsesion


Alguna idea.
Gracias. Un Saludo

josé M. Aguilar dijo...

Hola, Viure.

Efectivamente, parece que es un problema sin resolver de VB.NET con SignalR. He encontrado esa referencia que comentas en Stackoverflow, y un bug reportado en el repositorio de Signalr en Github, pero no se han aportado soluciones en ninguno de los casos.

Sin embargo, en una prueba rápida sí he comprobado que puedes "acceder" en modo escritura desde el servidor a esas variables, lo que no puedes es leerlas :-?

En fin, supongo que de momento tendrás que ir pasando los valores que te interesen como parámetros a los métodos del servidor, o bien implementar los hubs en C#.


Gracias por comentar y un saludo.

Viure dijo...

Hola,
Por si a alguno le ha pasado, y sigue dandose cabezazos con vb...

Después de unas cuantas pruebas tanto en c# como en vb, los datos seguro llegan en el Caller.
Simplemente era la forma de sacarlos en vb de este objeto dinámico.

Estaba intentanto hacer un metodo estático basansonme en el GetDynamicMember, pero he visto que había una librería instalable desde nuget llamada impromptu:

http://code.google.com/p/impromptu-interface/

Y utilizando esta librería, haciendo la llamada así parece que llega bien a las propiedades dinámicas:
Impromptu.InvokeGet(Caller, "name")

Pd:ya me iba a rendir y seguir tu sugerencia, que no era mala, pero soy un poco cabezudo.

josé M. Aguilar dijo...

Buenísimo!!

Muchas gracias por compartirlo!

Unknown dijo...

Hola Jose
Estoy iniciando con SignalR y me parece interesantisimo, pero no he podido hacer funcionar mi primer test con esta tecnología, siempre me muestra un error en el javascript al compilar.
Este código lo tengo en el html.
$(function () {
var hub = $.connection.HubLogTest;
hub.escribir = function (mensaje) {
$("#dvlog").html(mensaje);
};
$.connection.hub.start();
});
En este caso al compilar me dice No se puede establecer la propiedad 'escribir' de referencia nula o sin definir

¿Hay algo que deba hacer antes de compilar la aplicación?

Adicionalmente cuando inserto la linea
script src="signalr/hubs" type="text/javascript" el html me muestra una advertencia de que no existe la ruta signalr/hubs

Estoy usando la versión 1.1.3.

Que me recomiendas de todo esto y Gracias de antemano.

josé M. Aguilar dijo...

Hola!

El error javascript probablemente te lo está dando por eso, porque no encuentra /signalr/hubs.

Revisa la configuración del servidor, pues es posible que no estés activando Signalr.

Saludos.

Unknown dijo...

Que debería revisar?
Como activo signalR ?
Gracias!

josé M. Aguilar dijo...

Buenas!

Ahí hay un tutorial para la versión 1:

http://www.asp.net/signalr/overview/signalr-1x/getting-started/tutorial-getting-started-with-signalr

Saludos.

Unknown dijo...

Gracias Jose
has sido muy amable :)