martes, 22 de noviembre de 2011

Explorando WebSockets con SignalR.NET

Explorando WebSockets con SignalR.NET
Una de las decisiones que se deben tomar a la hora de diseñar una arquitectura en una aplicación Web corresponde a cómo el usuario va a recibir notificaciones de eventos que suceden en el ámbito de la aplicación que se esté desarrollando. Por ejemplo, a uno le puede interesar que cada vez que un usuario entra a la aplicación el resto de usuarios reciban una notificación conforme que dicho usuario acaba de entrar en la Web.
Este requisito funcional se puede resolver desde el punto de vista de arquitectura mínimo de dos maneras:
  • Pull. Desde el lado del cliente se realizan de manera periódica consultas al servidor para averiguar si hay nuevos usuarios que hayan entrado a la Web desde la última vez que se realizó esta averiguación. A esta manera de proceder se le llama realizar un "pulling" de consultas y tiene el inconveniente que puede cargar de mucho tráfico la red, impactando al rendimiento del servidor que debe atender estas peticiones, sobre todo si se quiere obtener esta información en tiempo real, ya que se debería aumentar la frecuencia en la que se realizan estas consultas.
  • Push. Es el servidor quien informa al navegador (y por ende, a los usuarios) que un nuevo usuario ha entrado a la Web, de manera que tan sólo se produce tráfico cuando es necesario, o sea, cuando se produce una nueva notificación que debe ser enviada a los usuarios, produciéndose además esta información en tiempo real.
Lo que pretendo explicar en esta publicación corresponde a cómo se puede implementar una arquitectura basada en un diseño "push" dentro de nuestras aplicaciones Web. 
La siguiente figura muestra aproximadamente lo que deseamos desarrollar:



Como se observa en la figura, hay tres usuarios: Juan, Ana y Luis, que entran a nuestra Web por ese orden. Cuando Juan entra a la Web, se enviará un mensaje a todos los usuarios conectados que Juan ha entrado. Como será el primero, nadie visualizará dicho mensaje. Luego entrará Ana, de manera que Juan, al ya estar conectado, sí verá el mensaje que enviará nuestra Web de que "Ana ha entrado!". Finalmente, al entrar Luis, tanto Juan como Ana recibirán el mensaje de que Luis ha entrado.

Para realizar esta implementación, vamos a emplear ( como ingredientes a esta receta :) ):
Veamos por pasos cómo realizar esta implementación.

Paso 1. Abrir Visual Studio 2010 y crear un nuevo proyecto tipo ASP.NET MVC 3 Web Application. Llamar al proyecto, por ejemplo, SignalR.Test.



Paso 2. Como plantilla de proyecto, seleccionar la plantilla vacía (empty) y como View engine, en mi caso prefiero Razor, aunque podéis usar si lo preferís ASPX.


Paso 3. Añadir mediante NuGet el paquete SignalR que podemos buscar "on-line". Para ello, pulsaremos botón derecho sobre la carpeta "References" y seleccionaremos la opción de menú "Manage NuGet Packages..."


Paso 4. Buscamos el package "SignalR" habiendo seleccionado la pestaña "Online" de la izquierda, y seleccionamos el package SignalR.


Ahora dispondremos de las librerías y ficheros en JavaScript necesarios para comenzar a trabajar.


Paso 5. Crear una nueva carpeta en la raíz del proyecto llamada "Services" y añadir una nueva clase llamada MessageHub. El explorador de soluciones debe quedar de la siguiente manera:



Paso 6. Añadimos el siguiente código en la clase "MessageHub":
using SignalR.Hubs;

namespace SignalR.Test.Services
{
    public class MessageHub : Hub
    {
        public void StartSession(string userName)
        {
            string message = string.Format("{0} ha entrado!", userName);
            //Call the addMessage method on all clients.
            Clients.addMessage(message);
        }
    }
}
Vemos que el método "StartSession" realiza una llamada a un método llamado "addMessage" mediante el objeto "Clients". Más adelante veremos que este método corresponde a una función implementada en JavaScript y que se ejecuta en el código de cliente, o sea, del navegador.

Paso 7. Creamos una nueva clase Controller llamada HomeController. Para ello pulsamos botón derecho sobre la carpeta "Controllers" y seleccionamos la opción de menú Add >> Controller...






Y luego indicamos en la nueva ventana que se nos muestra que nuestro controller se va ha llamar HomeController:




Paso 8. Ahora vamos a añadir la vista. Si abrimos el fichero HomeController.cs que se nos acaba de crear, veremos que tenemos un método llamado "Index()". Vamos a pulsar con el botón derecho del ratón sobre dicho método y vamos a seleccionar la opción de menú: "Add View...". Las opciones las vamos a dejar tal y como nos vienen por defecto.






Paso 9. Antes de modificar el fichero "Index.cshtml" que se ha generado en la carpeta "Views >> Home", vamos a añadirle las siguientes líneas de código al fichero "_Layout.cshtml" que se encuentra en la carpeta "Views >> Shared":
<script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")" type="text/javascript">script>
<script src="@Url.Content("~/Scripts/jquery.signalR.min.js")" type="text/javascript">script>
<script src="@Url.Content("~/signalr/hubs")" type="text/javascript">
 El fichero _Layout.cshtml debería quedar de la siguiente manera:
DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Titletitle>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
   
    <script src="@Url.Content("~/Scripts/jquery-1.6.4.min.js")" type="text/javascript">script>
    <script src="@Url.Content("~/Scripts/jquery.signalR.min.js")" type="text/javascript">script>
    <script src="@Url.Content("~/signalr/hubs")" type="text/javascript">

head>
<body>
    @RenderBody()
body>
html>
Paso 10. Ahora sí, con estas referencias en _Layout.cshtml podemos añadir el código HTML + JavaScript necesario en el fichero Index.cshtml. El código que vamos a añadir va a ser el siguiente:
<script type="text/javascript">
    $(function () {
        // Proxy created on the fly
        var messageHub = $.connection.messageHub;


        // Declare a function on the chat hub so the server can invoke it
        messageHub.addMessage = function (message) {
            $('#messages').append('
  • ' + message + '
  • ');
            };

            $("#startSession").click(function () {
                // Start the connection
                $.connection.hub.start(function () {
                    $("#notifications").show();
                    var userName = $('#userName').val();
                    messageHub.startSession(userName);
                });
            });
        });
    script>
    <div id="login" style="border: 1px solid #FF6600">
        <span>Tu nombre:span>
        <br />
        <input type="text" id="userName" />
        <br />
        <input type="button" id="startSession" value="Inicia sesión" />
    div>
    <br />
    <br />
    <div id="notifications" style="display:none; border: 1px dotted #808080">
        <ul id="messages">ul>
    div>
    Como notas interesantes, ver que mediante $.connection.messageHub se está accediendo a la clase MessageHub que hemos definido anteriormente. Además, en el código JavaScript hemos añadido una función que se ejecutará justo cuando la conexión al servidor se haya establecido:
    $.connection.hub.start(function () {
                    $("#notifications").show();
                    var userName = $('#userName').val();
                    messageHub.startSession(userName);
                });
    En este caso estamos llamado al método "StartSession" de la clase MessageHub. Como habíamos visto en dicho método, había una llamada a la función addMessage que debía implementar el cliente. Como vemos en este código, dicha función está aquí implementada, de manera que, en este caso, cuando desde cliente se llame al método StartSession, el servidor llamará a la función addMessage de todos los clientes conectados, de manera que se mostrará una nueva línea en la pantalla del navegador con el mensaje que se envíe desde servidor. En nuestro caso: "{0} ha entrado!" donde {0} corresponde al literal que el usuario introduzca en la caja de texto con identificador "userName".
    Para probar este código, lo ejecutamos y abrimos tres pestañas en el navegador con la misma URL que se genera al ejecutarlo. En mi caso, la URL que se genera es: http://localhost:11830/ de manera que copiaré esta URL y la pegaré en tres nuevas pestañas del navegador.
    En la primera pestaña introduciré mi nombre y pulsaré al botón "Inicia sesión". Podré observar que me muestra un mensaje diciéndome que he entrado a la aplicación.
    En la segunda pestaña introduciré otro nombre y pulsaré el botón "Inicia sesión". Si vuelvo a la pestaña anterior, veré que muestra un nuevo mensaje indicándome que un nuevo usuario acaba de entrar. La misma operación podríamos repetirla en la tercera pestaña, esta vez viendo cómo se nos refresca la vista tanto en la primera como en la segunda pestaña.
    Hasta aquí ya hemos visto cómo funciona el uso de SignalR en un proyecto ASP.NET MVC 3. Añadiré un par de temas más que creo que son interesantes, y quizás más adelante amplíe este tema con una nueva entrada.
    Los dos temas que voy a tratar son:
    • Gestionar usuarios: almacenarlos y poder enviar notificaciones tan solo a aquellos usuarios que yo desée.
    • Enviar notificaciones de proceso a cliente: Publicar un servicio que permita que un proceso pueda enviar notificaciones a una lista de usuarios.
    Gestión de usuarios
    Para gestionar usuarios, vamos a crear una nueva clase en nuestro modelo llamada HubUser dentro de la carpeta "Models". Vamos a añadir el siguiente código:

    namespace SignalR.Test.Models
    {
        public class HubUser
        {
            public string ClientId { get; set; }
            public string Name { get; set; }
        }
    }
    Vamos a añadir también una clase llamada HubRepository dentro de la carpeta Models con el siguiente código:

    using System.Collections.Generic;

    namespace SignalR.Test.Models
    {
        public class HubRepository
        {
            public HashSet<HubUser> Users { get; set; }

            public HubRepository()
            {
                Users = new HashSet();
            }
        }
    }
    Ahora vamos a añadir en la clase MessageHub una nueva propiedad llamada "_repository":

     private static readonly HubRepository _repository = new HubRepository();
    Esta propiedad va a ser la encargada de almacenar los nuevos usuarios que se vayan conectando, por lo que modificaremos el método "StartSession" para almacenar los usuarios que se vayan conectando a la aplicación. El código final de cómo debe quedar la clase MessageHub es el siguiente:

    using SignalR.Hubs;
    using SignalR.Test.Models;
    using System;
    using System.Linq;
    namespace SignalR.Test.Services
    {
        public class MessageHub Hub
        {
            private static readonly HubRepository _repository = new HubRepository();

            public void StartSession(string userName)
            {
                HubUser user = _repository.Users.FirstOrDefault(u => u.Name.Equals(userName, StringComparison.OrdinalIgnoreCase));

                if (user == null)
                    user = AddUser(userName);
                Caller.name = user.Name;
                string message = string.Format("{0} ha entrado!", userName);
                // Call the addMessage method on all clients.
                Clients.addMessage(message);
            }

            private HubUser AddUser(string userName)
            {
                var user = new HubUser
                {
                    Name = userName,
                    ClientId = Context.ClientId
                };

                _repository.Users.Add(user);

                return user;
            }
        }
    }

    Vemos que la propiedad ClientId del nuevo usuario se obtiene a partir del objeto Context implementado por HubContext. Este identificador se mantiene mientras el usuario mantiene la conexión (mantiene el navegador abierto) de manera que si cierra el navegador y lo vuelve a abrir este valor cambiará. Una manera de mantener la conectividad con el usuario a pesar que éste cierra el navegador sería incluyendo una nueva propiedad en la clase HubUser que identifique al usuario (por ejemplo un UserId) Al tratar dicho usuario de acceder de nuevo al servidor, se obtendría su configuración buscándolo en el repositorio.
    Enviar notificaciones de proceso a cliente
    Con esta implementación se consigue enviar mensajes únicamente a aquellos usuarios que uno desea. Y no necesariamente tiene que provocar dicho mensaje un usuario, podría ser por ejemplo, un proceso (cada vez que el servicio que da de alta un nuevo usuario se quiere enviar un mensaje a un grupo de usuarios administradores que un usuario se ha dado de alta. En este caso sería el proceso de alta de usuario quien deba generar la notificación). Para ello vamos a crear un servicio que reciba como parámetros de entrada una lista de usuarios y un mensaje, de manera que envíe el mensaje recibido únicamente a la lista de usuarios que recibe como parámetro.
    En la carpeta Services vamos a añadir un nuevo elemento del tipo WCF Service y lo vamos a llamar HubService:
    Vamos a modificar la interfaz IHubService de la siguiente manera:


    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.ServiceModel;
    using System.Text;

    namespace SignalR.Test.Services
    {
        // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IHubService" in both code and config file together.
        [ServiceContract]
        public interface IHubService
        {
            [OperationContract]
            void Send(string message, IEnumerable<string> usersList);

            [OperationContract]
            IEnumerable<string> GetConnectedUsers();
        }
    }
    Ahora vamos a implementar esta interfaz en la clase HubService:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Runtime.Serialization;
    using System.ServiceModel;
    using System.Text;
    using SignalR.Hubs;
    using SignalR.Test.Models;

    namespace SignalR.Test.Services
    {
        // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "HubService" in code, svc and config file together.
        public class HubService : IHubService
        {
            public void Send(string message, IEnumerable<string> usersList)
            {
                var clientHub = Hub.GetClients<MessageHub>();
                foreach (string userName in usersList)
                {
                    HubUser user = MessageHub.Repository.Users.FirstOrDefault(u => u.Name == userName);
                    if (user != null)
                        clientHub[user.ClientId].addMessage(message);
                }
            }
            public IEnumerable<string> GetConnectedUsers()
            {
                return MessageHub.Repository.Users.Select(p => p.Name);
            }
        }
    }
    Hay que hacer pública la propiedad _repository para que sea accesible desde la clase HubService. Para ello vamos a añadir la siguiente línea en la clase MessageHub:

    public static HubRepository Repository
    {
        get { return _repository; }
    }

    Y para probar que podemos enviar mensajes tan solo a una lista de usuarios, podemos crear una aplicación WinForms que consuma el servicio GetConnectedUsers, para obtener la lista de usuarios conectados, y Send, para enviarles mensajes.
    Si tenéis alguna duda, trataré de responder de la mejor manera posible.


    Un saludo!