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, 24 de enero de 2012
Hace poco estuvimos viendo por aquí conceptos básicos sobre SignalR, el componente que nos permite crear espectaculares aplicaciones en las que múltiples usuarios pueden estar colaborando de forma simultánea, asíncrona, y en tiempo real.

Entre otras cosas, comentábamos que SignalR  crea una capa de abstracciones sobre una conexión virtual permanente entre cliente y servidor, sobre la que podemos trabajar de diferentes formas:
  • mediante conexiones persistentes, la opción de menor nivel, que proporciona mecanismos de notificación de conexión y desconexión de clientes, así como para recibir y enviar mensajes asíncronos a clientes conectados, tanto de forma individual como colectiva.
  • mediante el uso de “hubs”, que ofrece una interfaz de desarrollo mucho más sencilla, con una integración entre cliente y servidor que parece pura magia, y que seguro será la opción más utilizada por su potencia y facilidad de uso.
En este post estudiaremos la primera opción, conexiones persistentes. Los hubs los veremos en un artículo posterior de la serie, aunque si sois impacientes ya podéis ir leyendo el fantástico post del amigo Marc Rubiño sobre el tema, “Push con SignalR”.

Demo de conexiones persistentesBueno, pues vamos al tema: emplearemos esta vía para implementar una funcionalidad bastante simple, pero nada trivial utilizando las herramientas habituales de ASP.NET: mostrar en una página, en tiempo real, información sobre los usuarios que están llegando a ella, los que la abandonan y el número de usuarios que hay conectados justo en ese momento, en tiempo real.

Para ello haremos lo siguiente:
  1. En el lado servidor, implementaremos un servicio (endpoint) SignalR, que es el que procesará las conexiones y desconexiones de clientes, y enviará información actualizada por las conexiones abiertas.
  2. Registraremos este endpoint durante la inicialización de la aplicación, asociándole una URL de acceso a las funcionalidades del servicio.
  3. En el lado cliente implementaremos la conexión con el servicio, capturaremos la información que nos vaya enviando y la mostraremos en la página en forma de log.
El resultado lucirá tal y como se muestra en la captura de pantalla adjunta. Aunque si lo preferís, podéis verlo en vivo y en directo descargando y ejecutando el proyecto de demostración que encontraréis al final de este artículo.

Ya en el post anterior de la serie vimos cómo descargar e instalar SignalR en un proyecto, así que vamos a suponer que ese paso ya lo hemos realizado previamente.

1. Implementación del endpoint

El endpoint, o servicio SignalR, que vamos a implementar utilizando el enfoque de conexión persistente es simplemente una clase que hereda de SignalR.PersistentConnection, en la que podemos sobrescribir los métodos que necesitemos para implementar nuestras funcionalidades. En ella encontramos métodos como OnConnected(), OnDisconnect(), OnReceived(), y bastantes más, que nos permiten tomar el control cuando se producen determinados eventos de interés en la conexión:
    public class VisitorsService : PersistentConnection
    {
        protected override void  OnConnected(HttpContextBase context, string clientId) { ... }
        protected override void  OnDisconnect(string clientId) { ... }
        protected override void  OnReceived(string clientId, string data) { ... }
        // [...]
    }
Observad que el interfaz es bastante similar a la que encontramos al trabajar directamente con sockets: podemos introducir lógica cuando un nuevo cliente se conecte sobrescribiendo el método OnConnected(),  cuando se desconecte, haciendo lo propio con OnDisconnect(), o cuando el cliente envíe algún tipo de mensaje al servidor, que ejecutará la funcionalidad implementada en OnReceived().

De la misma forma, la clase base PersistentConnection ofrece mecanismos para enviar mensajes directos a un cliente, a grupos de ellos, o a todos los clientes conectados.

Volviendo al sistema que estamos desarrollando, básicamente para alcanzar nuestros objetivos necesitamos:
  • tomar el control en el momento en que se produce una nueva conexión (método OnConnected), momento en que enviaremos al resto de clientes un mensaje con información sobre el cliente conectado y el total de conexiones activas.
  • tomar el control en el momento en que se produce la desconexión de un cliente (método OnDisconnect()), para notificar al resto y actualizarles el número de clientes conectados.

1.1. Notificando a los clientes las nuevas conexiones

Cuando se realiza una nueva conexión al servicio, es decir, la llegada de un nuevo cliente, SignalR invocará al método OnConnected() del endpoint suministrándole el contexto de la petición HTTP actual, y un “ClientId”. El primero nos puede ser muy interesante para acceder a información de la petición (como el navegador, IP, cookies, información de autenticación, etc.), y el segundo es un identificador único generado por SignalR para realizar el seguimiento de la conexión.

Implementamos nuestro método y lo comentamos justo a continuación:
    protected override void OnConnected(HttpContextBase context, string clientId)
    {
        var clientDescription = getClientDescription(context);
        _clients.TryAdd(clientId, clientDescription);
 
        string text = clientDescription + " arrived.";
        var msg = new NotificationMessage(text, _clients.Count);
        Connection.Broadcast(msg);
    }
Lo primero que hacemos en la implementación del método es obtener una descripción textual del cliente (que puede ser el nombre del usuario autenticado, o su IP), utilizando el método getClientDescription(), que veremos más adelante. Esta descripción, asociada al ClientId, es almacenada en el diccionario estático _clients, lo que nos permitirá conocer en todo momento los clientes conectados.

Justo después componemos el mensaje y realizamos el envío a todos los usuarios conectados invocando el método Broadcast() de la propiedad de instancia Connection, que nos da acceso al canal virtual abierto entre clientes y servidor. El parámetro que recibe este método es de tipo object, y viajará serializado en formato JSON hasta cada uno de los clientes conectados; en este caso, hemos creado una clase llamada NotificationMessage que contiene toda la información que necesitamos suministrarles:
public class NotificationMessage
{
    public NotificationMessage(string message, int onlineUsers)
    {
        OnlineUsers = onlineUsers;
        Message = message;
    }
 
    public string Date
    {
        get { return System.DateTime.Now.ToLongTimeString(); }
    }
    public string Message { get; set; }
    public int OnlineUsers { get; set; }
}
Es conveniente tener en cuenta, sin embargo, que es posible enviar cualquier tipo de objeto: tipos propios (como en el ejemplo anterior), objetos anónimos, primitivos, o lo que se nos ocurra. Simplemente será serializado como JSON y llegará al cliente de forma directa (más adelante veremos cómo).

Los miembros auxiliares utilizados en el código anterior son los siguientes:
    private static ConcurrentDictionary<string, string> _clients =
        new ConcurrentDictionary<string, string>();
 
    private static string getClientDescription(HttpContextBase context)
    {
        var browser = context.Request.Browser.Browser + " " +
                        context.Request.Browser.Version;
        var name = context.Request.IsAuthenticated ?
                    "User " + context.User.Identity.Name :
                    "IP " + context.Request.UserHostAddress;
        return name + " (" + browser + ")";
    }
Observad que el diccionario donde almacenamos la información sobre las conexiones ha sido definido como ConcurrentDictionary para evitar problemas de concurrencia durante las actualizaciones, y es estático para que su información sea compartida entre todas las instancias del servicio.

1.2. Notificando a los clientes las desconexiones

Cuando SignalR detecta que un cliente se ha desconectado, invocará al método virtual OnDisconnect() del endpoint, lo cual nos permite introducir lógica de gestión del evento. En nuestro caso, simplemente necesitamos eliminar al cliente del diccionario donde los estamos almacenando,

De la misma forma, debemos controlar las desconexiones para notificar este hecho a los clientes aún conectados, para lo que sobrescribimos el método OnDisconnect():
    protected override void OnDisconnect(string clientId)
    {
        string text, clientDescription;
 
        if (_clients.TryRemove(clientId, out clientDescription))
            text = clientDescription + " is leaving.";
        else
            text = "Unknown user leaving.";
 
        var msg = new NotificationMessage(text, _clients.Count);
        Connection.Broadcast(msg);
    }
En este método recibimos el ClientId que SignalR asignó al cliente en el momento de iniciar la conexión; lo único que hacemos es buscarlo en el diccionario de clientes donde los estamos almacenando, eliminarlo, y enviar un mensaje broadcast al resto de usuarios indicando la desconexión que se ha producido.

Cuando implementéis funcionalidades en la desconexión, tened en cuenta que SignalR tarda unos segundos en darse cuenta de las desconexiones (recordad que con el transporte utilizado por defecto se trata de una conexión persistente virtual) por lo que puede aparecer un leve retraso en las notificaciones. Estos tiempos, en cualquier caso, pueden ser configurados (en el proyecto de demostración podéis ver cómo hacerlo).

[Actualización]: como bien indica Arturo en un comentario del post, para que las desconexiones sean notificadas correctamente es necesario utilizar IIS o IIS Express. Con Cassini (el servidor web integrado en VS) no funcionará bien este mecanismo.

1.3. Algunas observaciones adicionales

Al principio de comenzar a jugar con conexiones persistentes de SignalR, una de las cosas que pueden llamar la atención es que si en la implementación del método OnConnected() enviamos un broadcast a todos los usuarios conectados,  el usuario actual (el que ha provocado la llamada a OnConnected) no recibirá el mensaje; o en otras palabras, el broadcast llegará a todos los clientes excepto al que acaba de realizar la conexión.

Desconozco si se trata de un comportamiento por diseño, si es algo que se modificará en posteriores revisiones de SignalR (recordemos que en estos momentos es todavía una versión preliminar), o si simplemente se trata de un nombre para el método poco afortunado, pues en mi opinión da a entender que la conexión ya ha sido realizada y, por tanto, el broadcast debería llegarle también.

Pero en cualquier caso, en la implementación del proyecto de pruebas que podéis descargar al final de este post veréis cómo lo he solucionado incluyendo una llamada explícita (“ping”) desde el cliente al servidor para forzar el envío de un mensaje de actualización justo después de completarse la conexión. Conceptualmente, lo que se hace es:
  • desde el cliente, una vez se ha realizado la conexión, realizar un envío de datos al servidor, algo similar a un “ping”,
  • en el método OnReceived() del servidor, capturar el mensaje enviado desde el cliente y responderle de forma directa con la información que nos interese hacerle llegar, que podría ser un mensaje de bienvenida y, como en otras ocasiones, el número de usuarios conectados: 
        protected override void OnReceived(string clientId, string data)
        {
            var msg = new NotificationMessage("Hi!", _clients.Count);
            Send(clientId, msg);
        }
Más adelante, cuando tratemos la parte cliente del servicio, veremos cómo está implementado el envío desde el cliente de este “ping”.

2. Registro de ruta

Una vez tenemos el servicio implementado, debemos registrar en el sistema de routing de ASP.NET una URL a través de la cual será posible acceder al mismo. El lugar idóneo para hacerlo, como siempre que se trata de cargar la tabla de rutas, es en el global.asax, para que se ejecute durante la inicialización de la aplicación.

Por ejemplo, en una aplicación ASP.NET MVC podría ser algo así:
    public static void RegisterSignalrConnections(RouteCollection routes)
    {
        routes.MapConnection<VisitorsService>("Visitors", "VisitorsService/{*operation}");
    }
 
    protected void Application_Start()
    {
        RegisterSignalrConnections(RouteTable.Routes);
        [...]
    }
Observad que lo único que estamos haciendo es añadir a la tabla de rutas una entrada en la que asociamos el servicio, en este caso nuestra clase VisitorsService, a la dirección “VisitorsService/{*operation}”, que será la URL de acceso al mismo.

El primer parámetro que enviamos al método MapConnection() es simplemente el nombre de la entrada en la tabla de rutas, no tiene demasiada importancia.

3. Implementación del cliente web

La implementación de clientes web para las conexiones persistentes desarrolladas con SignalR es bastante simple, y comienza incluyendo en la página o vista una referencia hacia la biblioteca cliente de este componente:
<script src="@Url.Content("~/Scripts/jquery.signalR.js")" type="text/javascript"></script>
Como siempre, esta inclusión puede realizarse a nivel de página, o bien en la Master o Layout si queremos aplicarlo a todas las vistas del sistema.
Nota: si queremos dar soporte a clientes antiguos que no soportan deserialización JSON de forma nativa (por ejemplo, IE7), será necesario descargar desde Nuget la biblioteca de scripts json2.js y referenciarla en la página antes de la carga de SignalR.js. En caso contrario, se lanzará una excepción con el error:

“SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8” 
Centrándonos en nuestra aplicación, el marcado HTML será tan simple como el que se muestra a continuación, lo único que hacemos es dejar un “hueco” en el que introduciremos los mensajes que se vayan recibiendo del servidor:
<h2>Log</h2>
<div id="log"></div>
A continuación, necesitamos implementar el código de script que realice las siguientes tareas:
  • iniciar la conexión con el endpoint,
  • tras ello, enviar un “ping” para recibir el mensaje de bienvenida (recordad lo que os comentaba previamente de que el broadcast no se recibe por el cliente que inicia la conexión),
  • mostrar en el log la información recibida del servidor.
Y el código de script tampoco puede ser más sencillo:
<script type="text/javascript">
    $(function () {
        var conn = $.connection("VisitorsService");
        conn.received(function (data) {
            var text = data.Date + " - " + data.Message + " " +
                data.OnlineUsers + " users online.";
            
            $('#log').prepend("<div>" + text + "</div>");
        });
 
        conn.start(function () {
            conn.send("ping");
        });
    });
</script>
Lo comentamos muy rápidamente:
  • en la variable conn obtenemos una referencia hacia el endpoint, identificado por el nombre de la conexión persistente, en este caso, VisitorsService.
  • sobre ella, definimos la función received(), que será invocada cuando el servidor envíe información. El parámetro que recibe la función anónima es la información enviada desde el servidor, que, recordaréis, en este caso se trataba en objetos de tipo NotificationMessage. Dado que la serialización y deserialización se realizan de forma automática, podemos acceder directamente a sus miembros, como podéis ver en el código para montar el mensaje e introducirlo en el log.
  • por último, iniciamos la conexión invocando al método start() de la conexión. Observad que este método admite un callback que será llamado cuando la conexión se haya establecido, momento que aprovechamos para enviar el “ping” al servidor que nos permitirá recibir el mensaje de bienvenida.
Y ¡esto es todo!

Si tenéis un ratillo, no dejéis de descargar el proyecto de prueba y jugar un rato con él. Y sobre todo, observad las pocas líneas de código que hemos tenido que emplear para resolver esta funcionalidad y comparadlo con lo que supondría implementarla de forma artesana, con las técnicas tradicionales.
Descargar proyecto de prueba.

En el próximo post de la serie veremos cómo utilizar los Hubs de SignalR, un planteamiento con una abstracción aún mayor sobre la conexión y una integración entre cliente y servidor simplemente espectacular.

Publicado en Variable not found.

9 Comentarios:

Arturo Null dijo...

Excelente artículo. SignalR es magia!

Tuve un problema con OnDisconnect usando Casini. Es necesario usar IISExpress para que OnDisconnect sea lanzado.

Saludos

Arturo Null dijo...

Hola José,

Cuando actualizo la página, me considera como un nuevo usuario conectado. Segundos después informa que un usuario desconocido se ha desconectado y el conteo de usuarios conectados se soluciona.

Como puedo hacer para que al actualizar la página reconozca que es el mismo usuario y no lo contabilice como un nuevo usuario. Me he fijado en el clientId y cambia por cada actualización de página que hago.

Saludos

josé M. Aguilar dijo...

Hola, Arturo.

Gracias por comentar y por la puntualización!

Un saludo.

josé M. Aguilar dijo...

Hola de nuevo, Arturo.

Efectivamente, el ClientID cambia cada vez que se inicia una conexión, y ésta ocurre al cargar la página. Por tanto, una actualización (vía F5 o similar) provocaría una nueva conexión y, como consecuencia, es un nuevo cliente.

A todos los efectos, una recarga implica una desconexión y una nueva conexión.

Es posible que pueda modificarse ese comportamiento tocando en el mecanismo de creación de conexiones, o en el de generación de ClientID, aunque todavía no he profundizado tanto como para poder darte una solución más elaborada ;-)

Saludos!

Arturo Null dijo...

Vale, gracias.

Mas que una necesidad, era un capricho. :)

Saludos

josé M. Aguilar dijo...

Ok :-DDD

A mí también me ha despertado la curiosidad. A ver si un día saco un rato y lo investigo ;-)

Saludos & gracias.

Sergio León dijo...

Genial, tiene una pinta estupenda!

ang dijo...

increible tutorial, es dificil conseguir un tutorial mejor explicado de SignalR y más aun, escrito en español, solo un detalle que no entiendo mucho es al momento de hacer el enrutamient, normalmente yo simplemente defino mi ruta usando MapRoute,cual es la diferencia con usar MapConnection??...gracias...

josé M. Aguilar dijo...

Hola, @ang.

Muchas gracias por tu comentario.

Usar MapConnection es simplemente por comodidad. Es un extensor que define SignalR sobre la colección de rutas para poder crear directamente asignaciones de rutas hacia el handler encargado de gestionar las conexiones persistentes, añadiendo algún constraint.

Es decir, al final lo que vas a tener es una entrada más en la tabla de rutas, simplemente lo consigues más fácilmente si usas MapConnection.

Saludos!