SignalR (II): Conexiones persistentes

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.

2 comentarios en “SignalR (II): Conexiones persistentes”

Deja un comentario

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