[SIGNALR] Conexiones Persistentes

Con este articulo, vamos a comenzar una serie dedicada a SignalR, el cual nos permite establecer un canal de comunicación bidireccional en el servidor web y el cliente. Para este articulo vamos a usar un cliente web Javascript y un servidor web bajo ASP.NET MVC.

Antes de comenzar vamos a explicar algunos de los esquemas de comunicación más usados, para comprender mejor el sentido de signalr.

  • PULL: es el esquema básico de comunicación HTTP, en la que el cliente realiza una petición (GET, POST…) a un servidor web y queda a la espera de la respuesta. Se trata de un proceso sincrono, es decir, el cliente espera sin hacer nada hasta recibir la respuesta del servidor.
  • AJAX: En un esquema de comunicación PULL, AJAX permite añadir capacidades asincroninas. Inicialmente se realiza una petición http (pull), tras la cual, la web es mostrada en el cliente. Posteriormente, el cliente puede realizar peticiones AJAX sin abandonar la pagina actual. Estas peticiones se realizan en segundo plano de forma que podemos actualizar porciones de nuestra página o enviar datos al servidor.
  • POOLLING: este esquema surge por la necesidad de obtener datos en tiempo real. Por ejemplo: un chat, juegos online … Es este caso, el cliente realiza peticiones periódicas al servidor, para comprobar si hay datos pendientes. Como este modelo de comunicación, podemos simular la comunicación en tiempo real. Esta solución es fácil de implementar y es compatible con todos lo navegadores, pero puede llegar a crear problemas de rendimiento cuando el numero de clientes es alto o la frecuencia de las peticiones se reduce para dar mayor sensación de inmediatez.
    Sobre este esquema de comunicación, existen numerosas mejoras. Una de ellas es que las peticiones sean realizadas usando un periodo adaptativo, de forma que el intervalo de las peticiones varié en función de la carga del sistema, hora de día…
  • PUSH: Hasta ahora las peticiones de comunicación siempre habían sido iniciadas por el cliente, pero en muchos casos es necesario que sea el servidor, el que tome la iniciativa en el envió de datos. Sin embargo, por cuestiones de seguridad, esto es prácticamente imposible ya que resulta imposible establecer una conexión directa debido a la existencia de routers , firewalls o proxies. Por normal general este esquema requiere que el cliente inicie la comunicación y mantenga el canal abierto a la espera de recibir las actualizaciones.
    Algunas de las tecnologías que permiten este tipo de comunicación son:
    • WEBSOCKETS: es un estandar definido por el W3C que permite crear un canal bidireccional ente cliente y servidor. Esta es una de las mejores soluciones para la implemetación de servicios PUSH en tiempo real, pero solo es soportada por las últimas versiones de los navegadores web. Además, la implementación del servidor WebSocket usando tecnologías no fue posible (de una forma sencilla) hasta la llegada de IIS8, ASP.NET 4.5, WCF.
    • SERVER-SENT EVENTS: es un API basado en Javascript, el cual crea un canal de comunicación unidireccional desde el servidor al cliente, pero el canal es iniciado por el cliente. En este esquema, el cliente se registra sobre un origen de eventos del servidor y queda a la espera de recibir notificaciones. Esta implementación es mucho más compatible que WebSockets, ya que se basa en HTTP y Javascript.
      Nota: si el cliente necesita enviar datos al servidor, es necesario usar otro canal de comunicación diferente.
    • LONG POOLING: es un esquema similar al Pooling, pero con algunas diferencias. Al hacer una petición, si no hay datos, el canal queda abierto hasta que el servidor tenga que enviar algún dato o se produzca un error de timeout.
    • FOREVER FRAME: en este caso se una un tag HTML “IFRAME” para conseguir una conexión permanente con el servidor. En la Url del IFRAME hay que establecer la URL donde el servidor esta escuchando. Es un proceso muy similar a “Server-Sent Events”. Esta técnica tiene un amplio espectro de compatibilidad, ya que se basa en HTML, HTTP y Javascript.

SignalR se presenta como una técnologia que permite, de forma rápida y sencilla, añadir funcionalidad “realtime” a las aplicaciones web. En su origen, fue desarrollada por dos miembros del equipo de ASP.NET (David Fowler y Damian Edwards). En la actualidad no esta acoplado al framework de Microsoft, se puede instalar usando NuGet y permite usarlo en multitud de tipos de clientes.

SignalR es una librería que permite crear un canal de comunicación bidireccional entre el cliente y servidor. Este se encarga, de forma automática, de determinar cual es el esquema de comunicación más adecuado en base a las características del cliente y del servidor (Long Poolling, Forever Frame, WebSockets).

La ventaja de signalr, es que nos abstrae del protocolo a usar, control de las conexiones, control de latencia, errores … de forma que nos ofrece 2 apis muy sencillas para el intercambio de mensajes entre el cliente y el servidor:

  • Persistent Connections: ofrece un API similiar a la programación con sockets.
  • Hubs: Ofrece un API de programación al estilo RPC. Invocando a un método javascript, realizamos la invocación del método de un Hub del servidor.

Este articulo, lo vamos a dedicar al uso de las conexiones persistentes. Antes de nada vamos a preparar nuestro proyecto para trabajar con ASP.NET MVC, SignalR y AngularJS

  1. Creamos una nuevo proyecto ASP.NET MVC
  2. Agregamos el paquete Nuget para AngularJS
    install-package AngularJS
    
  3. Agregamos el paquete Nuget para SignalR
    install-package Microsoft.AspNet.SignalR
    

    Nota: Mientras escribimos este artículo, la versión disponible es la 2.2.0
    Nota: Al final del proceso de instalación, se crea un fichero “readme.txt” con instrucciones sobre como incluir signalr en nuestra proyecto. Esto es debido a que signalr se basa en OWIN.

  4. Modificamos el fichero “/App_Start/BundleConfig.cs” para incluir bundles para Angular y SignalR.
    using System.Web;
    using System.Web.Optimization;
    
    namespace ConexionesPersistentes
    {
        public class BundleConfig
        {
            // Para obtener más información sobre Bundles, visite http://go.microsoft.com/fwlink/?LinkId=301862
            public static void RegisterBundles(BundleCollection bundles)
            {
                bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                            "~/Scripts/jquery-{version}.js"));
    
                bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                            "~/Scripts/jquery.validate*"));
    
                // Utilice la versión de desarrollo de Modernizr para desarrollar y obtener información. De este modo, estará
                // preparado para la producción y podrá utilizar la herramienta de compilación disponible en http://modernizr.com para seleccionar solo las pruebas que necesite.
                bundles.Add(new ScriptBundle("~/bundles/modernizr").Include(
                            "~/Scripts/modernizr-*"));
    
                bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include(
                          "~/Scripts/bootstrap.js",
                          "~/Scripts/respond.js"));
    
                bundles.Add(new StyleBundle("~/Content/css").Include(
                          "~/Content/bootstrap.css",
                          "~/Content/site.css"));
    
                //angular
                bundles.Add(new ScriptBundle("~/bundles/angular").Include(
                          "~/Scripts/angular.min.js"));
    
                //signalr
                bundles.Add(new ScriptBundle("~/bundles/signalr").Include(
                          "~/Scripts/jquery.signalR-2.2.0.min.js"));
            }
        }
    }
    


    Observa como la librería js para
    SignalR es en realidad un plugin de jQuery, por lo que se necesita la libreria jQuery 1.6.4 o posterior.

  5. Creamos el controlador “HomeController” y agregamos una vista para el método “Index”. Este proceso creará el fichero de layout “_Layout.cshtml”
    Nota: Dependiendo de la plantilla que haya usado para crear el proyecto, puede que el controlador y la vista ya existan.
  6. Modificamos el fichero “_Layout.cshtml” para inclur los bundles de Angular y SignalR.
  7. Creamos la clase de inicialización para OWIN. Por convención, se debe llamar Startup.cs, debe estar en espacio de nombres raiz y debe tener un método llamado “Configuration” con un parámetro del tipo “IAppBuilder”.

    Si queremos personalizar la clase, hay 2 opciones:

    • Usar el atributo OwinStartup especificando el nombre de la clase y el metodo
            [assembly: OwinStartup(typeof(ConexionesPersistentes.InicioOwin.Configuracion))]
      
    • Modificar la sección “AppSettings” del fichero “web.config”, para añadir la key “owin:appStartup”
      <appSettings>  
        <add key="owin:appStartup" value="ConexionesPersistentes.InicioOwin.Configuracion" />
      </appSettings>
      

    En nuestro caso, vamos a crear una clase siguiendo las convenciones de OWIN. En el siguiente fragmento de código se muestra la clase “/App_Start/Startup.cs”

    using Owin;
    using ConexionesPersistentes.SignalR;
    namespace ConexionesPersistentes
    {
        public static class Startup
        {
            public static void Configuration(IAppBuilder app)
            {
                app.MapSignalR<Chat>("/chat");
            }
        }
    }
    

    En el fragmento anterior, hemos usado el método “MapSignalR” para establecer la url de escucha y la clase encarga de su gestión. En el ejemplo anteior podemos ver que nuestro servidor signalr escucha la url “/chat” y que la clase del servidor se llama “Chat”

  8. Creamos una carpeta llamada “SignalR” y dentro vamos a crear un nuevo elemento de tipo “Web\SignalR\Clase de Conexion Persistente de SignalR”
    AgregarConexionPersistente
    Esta plantilla genera un clase que hereda de PersistentConnection, cuyo contenido por defecto es:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    
    namespace ConexionesPersistentes.SignalR
    {
        public class Chat : PersistentConnection
        {
            protected override Task OnConnected(IRequest request, string connectionId)
            {
                return Connection.Send(connectionId, "Welcome!");
            }
    
            protected override Task OnReceived(IRequest request, string connectionId, string data)
            {
                return Connection.Broadcast(data);
            }
        }
    }
    

    De momento, no hay que preocuparse por esta clase. Más adelante veremos en detalle su implementación.

  9. Preparamos la vista trabajar con Angular. Para ello, vamos a crear un modulo y un controlador de Angular. En el siguiente fragmento de código podemos ver el contenido de la vista “Index.cshtml”
    @{
        ViewBag.Title = "Index";
    }
    <div ng-app="appChat" ng-controller="ctrlChat">
    
    </div>
    
    <script>
        var app = angular.module("appChat", []);
    
        app.value('$', $);
    
        app.controller("ctrlChat", function ($scope, $http, $) {
        });
    
    </script>
    

    Nota: el trabajar con Angular, es opcional.

En este punto, ya estamos preparados para crear nuestra aplicación con comunicación en tiempo real. Vamos a comenzar creando un sencillo ejemplo de un chat.

Lo primero es crear la conexión persistente usando “$.connection”.

@{
    ViewBag.Title = "Index";
}
<div ng-app="appChat" ng-controller="ctrlChat">

</div>

<script>
    var app = angular.module("appChat", []);

    app.value('$', $);

    app.controller("ctrlChat", function ($scope, $http, $) {
        $scope.conexion = null;

        $scope.iniciar = function () {
            $scope.conexion = $.connection("/chat");
            $scope.conexion.start();
        };

        $scope.iniciar();
    });

</script>

En el código anterior, hemos realizado las siguientes tareas:

  • Añadimos el objeto “conexion” al $scope de angular.
  • Creamos la función “iniciar”, la cual crea la conexión signalr sobre la url “/chat” y la iniciamos invocando al método “start”.

El inicio de la conexión signalr comienza con un proceso de negociación para determinar el protocolo de transporte más optimo. Si usamos un navegador actual y un servidor con Windoows Server 2012 o Windows 8, entonces se usará WebSockets.

Para ver un registro de las tareas efectuadas por signalr, debemos establecer a true la propiedad “logging”

        $scope.iniciar = function () {
            $scope.conexion = $.connection("/chat");
            $scope.conexion.logging = true;
            $scope.conexion.start();
        };

Si examinamos la consola del navegador y volvemos a ejecutar el ejemplo, podemos ver el protocolo de comunicaciones usado por signalr.

Navegador_Logging

En la imagen anterior, podemos ver como el proceso de negociación ha determinado usar “Websockets”.

Nota: Hay que tener en cuenta que el método “start” es asincrono, por lo que el código javascript continuará aunque no se haya completado la conexión con el servidor. Este punto es muy importante, ya que si justo después intentamos enviar un mensaje, lo más probable es que se produzca un error de conexión.

A continuación, vamos a ampliar el ejemplo añadiendo las siguientes modificaciones:

  1. Cuando un cliente se conecta al servidor, este debe
    • Enviar un mensaje de bienvenida al cliente conectado
    • Enviar un mensaje a informativo sobre la nueva conexión a todos los clientes excepto al cliente conectado
  2. Cuando un cliente se desconecta al servidor, este debe
    • Enviar un mensaje de despedida al cliente conectado
    • Enviar un mensaje a informativo sobre la desconexión a todos los clientes excepto al cliente conectado
  3. El cliente debe mostrar todos los mensajes recibidos del servidor
  4. El cliente debe mostrar los errores.

En el servidor, vamos a modificar la clase “SignalR\Chat.cs”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.SignalR;

namespace ConexionesPersistentes.SignalR
{
    public class Chat : PersistentConnection
    {
        async protected override Task OnConnected(IRequest request, string connectionId)
        {
            await Connection.Send(connectionId, "Bienvenido " + connectionId);

            await Connection.Broadcast("Nueva conexion con Id=" + connectionId, connectionId);
        }

        async protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
        {
            await Connection.Send(connectionId, "Bienvenido " + connectionId);

            await Connection.Broadcast("Cliente desconectado con Id=" + connectionId, connectionId);
        }
        async protected override Task OnReceived(IRequest request, string connectionId, string data)
        {

        }
    }
}

El método “Send”, permite enviar un mensaje desde el servidor al cliente. Este método tiene varias sobrecargas:

Send(string connectionId, object value)

Envía un mensaje (el parámetro object permite enviar una cadena de texto o un objeto el cual sera serializado a JSON).

Send(object value)

Envía un mensaje a todos los clientes conectados

El método “Broadcast”, permite enviar un mensaje desde el servidor a todos los clientes. La firma de este método es:

Broadcast(object value, params string[] excludeConnectionIds)

Este método permite enviar un mensaje a todos los clientes conectados. Si queremos excluir clientes, estos debe ser especificados como parametros adicionales o como un array.

En la parte cliente vamos a modificar la vista “/Views/Home/Index.cshtml”

@{
    ViewBag.Title = "Index";
}
<div ng-app="appChat" ng-controller="ctrlChat">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Signalr</h3>
        </div>
        <div class="panel-body">
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>Mensajes</th>
                    </tr>
                </thead>
                <tbody>
                    <tr ng-repeat="msg in mensajes">
                        <td>{{msg}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>

<script>
    var app = angular.module("appChat", []);

    app.value('$', $);

    app.controller("ctrlChat", function ($scope, $http, $) {
        $scope.conexion = null;
        $scope.mensajes = [];

        $scope.iniciar = function () {
            $scope.conexion = $.connection("/chat");
            $scope.conexion.logging = true;
            $scope.conexion.start();

            $scope.conexion.received(function (data) {
                $scope.$apply(function () {
                    $scope.mensajes.push(data);
                });
            });
            $scope.conexion.error(function (err) {
                $scope.$apply(function () {
                    $scope.mensajes.push("Error:" + err.message);
                });
            });
        };

        $scope.iniciar();
    });

</script>

En la vista hemos usado el evento “received”, para gestionar los mensajes recibidos del servidor. En nuestro ejemplo, nos limitados a añadir los mensajes recibidos a un array. Angular por si parte usa la directiva “ng-repeat” para pintar los mensajes en una tabla.

Por último, el evento “error” no permite mostrar los errores producidos durante la comunicación.

En la siguiente imagen, se puede ver un ejemplo donde hay varios clientes conectados y desconectados.

ConexionesPersistentes_Connect_Disconnect

A continuación vamos añadir un botón para enviar un mensaje al resto de clientes conectados. Inicialmente, vamos a cambiar la vista para añadir un una caja de texto junto con botón para enviar.

@{
    ViewBag.Title = "Index";
}
<div ng-app="appChat" ng-controller="ctrlChat">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Signalr</h3>
        </div>
        <div class="panel-body">
            <div class="form-group">
                <label for="txtMensaje">Mensaje</label>
                <input type="text" class="form-control" name="txtMensaje" ng-model="textoEnviar" />
            </div>
            <div class="btn-group">
                <input type="button" name="btnEnviar" class="btn btn-primary" value="Enviar" ng-click="enviar()" />
            </div>
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>Mensajes</th>
                    </tr>
                </thead>
                <tbody>
                    <tr ng-repeat="msg in mensajes">
                        <td>{{msg}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>

<script>
    var app = angular.module("appChat", []);

    app.value('$', $);

    app.controller("ctrlChat", function ($scope, $http, $) {
        $scope.conexion = null;
        $scope.mensajes = [];
        $scope.textoEnviar = "";

        $scope.iniciar = function () {
            $scope.conexion = $.connection("/chat");
            $scope.conexion.logging = true;
            $scope.conexion.start();

            $scope.conexion.received(function (data) {
                $scope.$apply(function () {
                    $scope.mensajes.push(data);
                });
            });
            $scope.conexion.error(function (err) {
                $scope.$apply(function () {
                    $scope.mensajes.push("Error:" + err.message);
                });
            });
        };

        $scope.enviar = function () {
            $scope.conexion.send($scope.textoEnviar);
        }

        $scope.iniciar();
    });

</script>

El ejemplo anterior, usa el método “send” de la conexión persistente.

En el servidor, vamos a completar el método “OnReceived”

        async protected override Task OnReceived(IRequest request, string connectionId, string data)
        {
            string mensaje = connectionId + ":" + data;
            await Connection.Broadcast(mensaje, connectionId);
        }

usando el método “Broadcast”, para enviar un mensaje todos los clientes excepto a si mismo. En la siguiente imagen se puede ver una captura de pantalla

ConexionesPersistentes_envio

Hasta ahora hemos visto como intercambiar mensajes de texto, pero también podemos enviar mensajes como un objeto. A continuación, mostramos el código del servidor

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNet.SignalR;
using Newtonsoft.Json;

namespace ConexionesPersistentes.SignalR
{
    public class MensajeChat
    {
        public string Tipo { get; set; }
        public string Texto { get; set; }
    }
    public class ChatObject : PersistentConnection
    {
        async protected override Task OnConnected(IRequest request, string connectionId)
        {
            await Connection.Send(connectionId, new MensajeChat() { Tipo = "Mensaje", Texto = "Bienvenido " + connectionId });
            await Connection.Broadcast(new MensajeChat() { Tipo = "Mensaje", Texto = "Nueva conexion con Id=" + connectionId }, connectionId);
        }

        async protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
        {
            await Connection.Send(connectionId, new MensajeChat() { Tipo = "Mensaje", Texto = "Adios " + connectionId });
            await Connection.Broadcast(new MensajeChat() { Tipo = "Mensaje", Texto = "Cliente desconectado con Id=" + connectionId }, connectionId);
        }

        async protected override Task OnReceived(IRequest request, string connectionId, string data)
        {
            try
            {
                MensajeChat mensaje = JsonConvert.DeserializeObject<MensajeChat>(data);
                await Connection.Broadcast(mensaje, connectionId);
            }
            catch (Exception ex)
            {

            }
        }
    }
}

Como vemos, el código es muy similar al anterior. Lo único a tener en cuenta es que para recuperar el mensaje tenemos que deserializarlo desde JSON. Para nuestra implementación hemos usado la libreria “NewtonSoft.JSON”.

Ahora vamos a mostrar el código de la vista.

@{
    ViewBag.Title = "Index";
}
<div ng-app="appChatObject" ng-controller="ctrlChatObject">
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Signalr - Mensajeria con Objetos</h3>
        </div>
        <div class="panel-body">
            <div class="form-group">
                <label for="txtMensaje">Mensaje</label>
                <input type="text" class="form-control" name="txtMensaje" ng-model="textoEnviar" />
            </div>
            <div class="btn-group">
                <input type="button" name="btnEnviar" class="btn btn-primary" value="Enviar" ng-click="enviar()" />
            </div>
            <table class="table table-striped">
                <thead>
                    <tr>
                        <th>Mensaje</th>
                    </tr>
                </thead>
                <tbody>
                    <tr ng-repeat="msgObj in mensajes">
                        <td>{{msgObj.Texto}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>

<script>
    var app = angular.module("appChatObject", []);

    app.value('$', $);

    app.controller("ctrlChatObject", function ($scope, $http, $) {
        $scope.mensajes = [];
        $scope.conexion = null;
        $scope.textoEnviar = "";

        $scope.iniciar = function () {
            $scope.conexion = $.connection("/chatobject");
            $scope.conexion.logging = true;
            $scope.conexion.start();

            $scope.conexion.received(function (data) {
                $scope.$apply(function () {
                    $scope.mensajes.push(data);
                });
            });
            $scope.conexion.error(function (err) {
                $scope.$apply(function () {
                    $scope.mensajes.push("Error:" + err.message);
                });
            });
        };

        $scope.enviar = function () {
            var mensaje = {};
            mensaje.Tipo = "Mensaje";
            mensaje.Texto = $scope.textoEnviar;
            $scope.conexion.send(mensaje);
        }

        $scope.iniciar();
    });

</script>

Con los métodos actuales, los mensajes son enviados a todos los clientes registrados (existe una sobrecarga para excluir ids de conexion), pero no podemos enviar datos a un grupo de clientes. Para solucionarlo, signalR incluye el control de grupos, permitiendonos agrupar conexiones en base a un nombre de grupo. A continuación vamos a implementar un ejemplo basado en grupos con la siguiente funcionlidad:

  • Cada cliente tendrá la opción de registrarse, dejar o enviar mensajes a un grupo concreto
  • El servidor debe enviar los mensajes solo a los clientes registrados en el grupo en cuestión

    El siguiente fragmento de código muestra el código del servidor

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    using Newtonsoft.Json;
    
    namespace ConexionesPersistentes.SignalR
    {
        public enum eTipoMensaje
        {
            Mensaje = 0,
            UnirseGrupo = 1,
            DejarGrupo = 2
        }
        public class MensajeChatGrupo
        {
            public eTipoMensaje Tipo { get; set; }
            public string Texto { get; set; }
            public string Grupo { get; set; }
        }
    
        public class ChatGroups : PersistentConnection
        {
            async protected override Task OnConnected(IRequest request, string connectionId)
            {
                await Connection.Send(connectionId, new MensajeChatGrupo() { Tipo = eTipoMensaje.Mensaje, Texto = "Bienvenido " + connectionId });
                await Connection.Broadcast(new MensajeChatGrupo() { Tipo = eTipoMensaje.Mensaje, Texto = "Nueva conexion con Id=" + connectionId }, connectionId);
            }
    
            async protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
            {
                await Connection.Send(connectionId, new MensajeChatGrupo() { Tipo = eTipoMensaje.Mensaje, Texto = "Adios " + connectionId });
                await Connection.Broadcast(new MensajeChatGrupo() { Tipo = eTipoMensaje.Mensaje, Texto = "Cliente desconectado con Id=" + connectionId }, connectionId);
            }
    
            async protected override Task OnReceived(IRequest request, string connectionId, string data)
            {
                MensajeChatGrupo mensaje = null;
                try
                {
                    mensaje = JsonConvert.DeserializeObject<MensajeChatGrupo>(data);
                }
                catch (Exception)
                {
                    throw;
                }
                if (mensaje != null)
                {
                    switch (mensaje.Tipo)
                    {
                        case eTipoMensaje.UnirseGrupo:
                            await this.Groups.Add(connectionId, mensaje.Grupo);
                            await this.Groups.Send(mensaje.Grupo, new MensajeChatGrupo() { Tipo = eTipoMensaje.Mensaje, Texto = "El cliente con Id=" + connectionId + " se ha unido al grupo " + mensaje.Grupo });
                            break;
                        case eTipoMensaje.DejarGrupo:
                            await this.Groups.Remove(connectionId, mensaje.Grupo);
                            await this.Groups.Send(mensaje.Grupo, new MensajeChatGrupo() { Tipo = eTipoMensaje.Mensaje, Texto = "El cliente con Id=" + connectionId + " ha dejado el grupo " + mensaje.Grupo });
                            break;
                        case eTipoMensaje.Mensaje:
                            await this.Groups.Send(mensaje.Grupo, mensaje);
                            break;
                    }
    
                }
    
    
            }
        }
    }
    

    Como podemos observar, la lógica para controlar la pertenencia a grupos la hemos incluido en el evento “OnReceived“, mediante el uso de la propiedad “Tipo” de nuestra clase “MensajeChatGrupo”. Las tareas realizadas son:

    1. Deserializamos el mensaje recibido del cliente para crear una instancia de la clase “ChatMensajeGrupo”.
    2. Para añadir un cliente a un grupo, usamos “this.Groups.Add” indicando el id del cliente y el nombre de grupo.
    3. Para borrar un cliente a un grupo, usamos “this.Groups.Remove” indicando el id del cliente y el nombre de grupo.
    4. Para enviar mensajes a un grupo, debemos usar la función “this.Groups.Send”. Nota: Anteriormente usábamos “Connection.Send”.

    Nota: El código para los métodos “OnConnected” y “OnDisconnected” son muy similar a los ejemplos anteriores.

    Por último, el código para la parte cliente es:

    @{
        ViewBag.Title = "Index";
    }
    <div ng-app="appChatGroups" ng-controller="ctrlChatGroups">
        <div class="panel panel-primary">
            <div class="panel-heading">
                <h3 class="panel-title">Signalr - Grupos</h3>
            </div>
            <div class="panel-body">
                <div class="form-group">
                    <label for="txtMensaje">Mensaje</label>
                    <input type="text" class="form-control" name="txtMensaje" ng-model="textoEnviar" />
                </div>
                <div class="form-group">
                    <label for="txtGrupo">Grupo</label>
                    <input type="text" class="form-control" name="txtGrupo" ng-model="grupo" />
                </div>
                <div class="btn-group">
                    <input type="button" name="btnEnviar" class="btn btn-primary" value="Enviar" ng-click="enviar()" />
                    <input type="button" name="btnUnirseGrupo" class="btn btn-success" value="Unirse a Grupo" ng-click="unirseGrupo()" />
                    <input type="button" name="btnDejarGrupo" class="btn btn-danger" value="Dejar Grupo" ng-click="dejarGrupo()" />
                </div>
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>Mensaje</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr ng-repeat="msgGrupo in mensajes">
                            <td>{{msgGrupo.Texto}}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    
    <script>
        var app = angular.module("appChatGroups", []);
    
        app.value('$', $);
    
        app.controller("ctrlChatGroups", function ($scope, $http, $) {
            $scope.mensajes = [];
            $scope.conexion = null;
            $scope.textoEnviar = "";
            $scope.grupo = "";
    
            $scope.iniciar = function () {
                $scope.conexion = $.connection("/chatgroups");
                $scope.conexion.logging = true;
                $scope.conexion.start();
    
                $scope.conexion.received(function (data) {
                    $scope.$apply(function () {
                        $scope.mensajes.push(data);
                    });
                });
                $scope.conexion.error(function (err) {
                    $scope.$apply(function () {
                        $scope.mensajes.push("Error:" + err.message);
                    });
                });
            };
    
            $scope.enviar = function () {
                var mensaje = {};
                mensaje.Tipo = 0;
                mensaje.Grupo = $scope.grupo;
                mensaje.Texto = $scope.textoEnviar;
                $scope.conexion.send(mensaje);
            }
    
            $scope.unirseGrupo = function () {
                var mensaje = {};
                mensaje.Tipo = 1;
                mensaje.Grupo = $scope.grupo;
                mensaje.Texto = "";
                $scope.conexion.send(mensaje);
            }
    
            $scope.dejarGrupo = function () {
                var mensaje = {};
                mensaje.Tipo = 2;
                mensaje.Grupo = $scope.grupo;
                mensaje.Texto = "";
                $scope.conexion.send(mensaje);
            }
    
            $scope.iniciar();
        });
    
    </script>
    

    El código mostrado es muy simple, pues incluye la lógica necesaria para registrarse en un grupo, dejarlo o enviar mensajes. En la siguiente captura de pantalla, se puede ver un ejemplo de ejecución.

    ConexionesPersistentes_Grupos

    En la gestión de grupos hay que tener en cuenta:

    1. Un mismo cliente se puede registrar en múltiples grupos
    2. SignalR no ofrece mecanismos para conocer los Ids de los clientes registrados en cada grupo, por lo que si lo necesitamos, es necesario montarlo por nuestra cuenta (usando dicccionarios, colecciones, bases de datos, etc..). El motivo de esta carencia es para mejorar la escalabilidad estructural.

    En resumen, las “conexiones persistentes” de signalr nos permiten trabajar sobre una conexión “virtual” que nos abstrae de la red y sus protocolos, y ofrecen una API para el intercambio de mensajes. Pero en la mayoría de los casos tendremos que implementar nuestra lógica para dar servicio a nuestra aplicación (Para el ejemplo de los grupos, hemos usado una propiedad Tipo para indicar el tipo de mensaje y la propiedad “Grupo” para indicar el nombre del grupo).

    En el siguiente enlace se puede descargar el proyecto de ejemplo.