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, 13 de marzo de 2012
Qué divertidoLo divertido de escribir sobre productos que están todavía en fase de desarrollo es que cambian... y a veces, ¡de qué forma! Pues esto ha ocurrido con SignalR: recientemente se publicó la revisión 0.4 y bastantes cosas de las tratadas en el post anterior de la serie ha quedado en agua de borrajas. En fin, estaba avisado, así que mucho no puedo quejarme ;-)

Por tanto, esta tercera entrega de la serie vamos a dedicarla (otra vez ;-)) a las conexiones persistentes, veremos qué cosas han cambiado con la llegada de la revisión 0.4, y desarrollaremos un nuevo ejemplo que ilustre las novedades que podemos encontrar a la hora de trabajar a bajo nivel con SignalR.

1. Resumen rápido: persistent connections y la versión 0.4 de SignalR

Para los que ya dominabais SignalR, os resumo las novedades principales que he encontrado hasta el momento en esta nueva revisión:
  • han desaparecido todos los métodos síncronos (OnConnected, OnDisconnect, OnReceived…) que implementábamos en el ejemplo de la segunda entrega de la serie para tomar el control ante determinados eventos de la conexión.
  • por tanto, ahora es obligatorio utilizar las versiones asíncronas (OnConnectedAsync, OnDisconnectAsync, OnReceivedAsync…), que retornan un objeto Task para implementar la lógica de gestión de los eventos.
  • SignalR ha sido desacoplado de ASP.NET, lo que permite utilizarlo en otros entornos. Como consecuencia, la firma de algunos métodos ha sido modificada. Así, cuando somos notificados de una nueva conexión, ya no recibimos el contexto HTTP tradicional, sino abstracciones propias de SignalR y desvinculadas de ASP.NET, lo cual supone alguna pequeña limitación aunque fácilmente salvable.
  • El proceso de negociación utiliza el transporte más apropiado para cada navegador, teniendo en cuenta de los componentes disponibles en servidor. En Internet Explorer se utiliza el transporte “Forever frame”, mientras que Firefox y Chrome usan “Long Polling” cuando el servidor no corre sobre .NET 4.5 (en caso contrario usaría websockets de HTML5).
  • Curiosamente, lo que antes denominábamos “clientId”, que es ese GUID que identifica de forma única a cada cliente conectado, ha pasado a llamarse “connectionId”. No afecta a nada, sigue significando lo mismo, pero es un cambio conceptual destinado a que tengamos claro qué indica ese dato.
  • Ahora, los broadcasts enviados justo durante la conexión de un nuevo cliente incluyen al propio cliente conectado, lo que evita tener que hacer el hack al que nos veíamos obligados en el post anterior (el “ping”).
A continuación desarrollaremos un ejemplo completo para que podemos ver de nuevo en funcionamiento esta maravilla. Como siempre, al final del artículo encontraréis un enlace para descargar el proyecto de demostración, en el que también he modificado el ejemplo que vimos en el post anterior para adaptarlo a los cambios de la revisión de SignalR.

2. Creación de un chat simple con SignalR

En este artículo vamos a complicar ligeramente el ejemplo anterior para construir un pequeño chat al estilo del que encontramos en Facebook, y cuyo aspecto podéis ver en la captura de pantalla siguiente:

Captura de pantalla
De forma muy similar al ejemplo que vimos en el post anterior, necesitaremos:
  • Crear nuestro servicio, como ya sabemos, implementando una clase que herede de PersistentConnection. Llamaremos a esta clase SimpleChatService.
  • Tomar el control cuando un cliente envía un mensaje al servidor, con objeto de hacer un broadcast al resto de usuarios. Esto lo conseguiremos sobrescribiendo el método OnReceivedAsync.
  • También tendremos que registrar la ruta hacia el endpoint, de forma que las peticiones puedan llegar a él.
  • Y, por último, implementaremos el lado cliente, que consistirá en el UI y los scripts que se conectan al servidor, envían los mensajes cuando son tecleados por el usuario, y muestran en pantalla a su vez los textos enviados por otros usuarios.
Vamos con ello.

3. El lado servidor. Asincronía al 100%.

Un sistema basado en SignalR no podría funcionar correctamente si no utilizara las capacidades de asincronía del framework, debido a la gran cantidad de mensajes que pueden llegar a intercambiarse entre cliente y servidor. Por ello, el equipo del producto ha puesto especial énfasis en utilizar métodos asíncronos siempre que sea posible, evitando así bloqueos innecesarios y aumentando la capacidad de respuesta del servicio.

En el post anterior veíamos que podemos tomar el control del sistema cuando los usuarios se conectan, desconectan, o envían información al servidor sobrescribiendo los métodos OnConnected(), OnDisconnect(), y OnReceived() respectivamente. Sin embargo, el uso de esos métodos síncronos podían hacer ver que este aspecto no era importante.

La versión 0.4 ha dado la vuelta a esto, y obliga a utilizar sus equivalentes asíncronos OnConnectedAsync(), OnDisconnectAsync() y OnReceivedAsync(); en estos casos siempre se retornarán objetos de tipo Task, lo que abre la puerta a la paralelización y mejor aprovechamiento de la infraestructura sobre la que estamos corriendo.

Por tanto, comenzaremos la implementación de nuevo servicio heredando de PersistentConnection y sobrescribiendo, aunque de momento con un cuerpo vacío, el método que necesitamos para nuestro chat:
public class SimpleChatService : PersistentConnection
{

    protected override Task OnReceivedAsync(string connectionId, string data)
    {
        // ...
    }
}
Observad el retorno del objeto Task, que representa la tarea asíncrona que iniciamos desde el método. Si el trabajo a realizar en su interior es asíncrono podemos utilizar cualquiera de las vías disponibles para crear un Task, lo retornamos, y listo, por ejemplo así:
    protected override Task OnReceivedAsync(string connectionId, string data)
    {
        return Task.Factory.StartNew(() =>
            // Async task here...
        );
    }
Pero, ¿qué ocurre si la lógica que queremos implementar en ellos es puramente síncrona? Pues no pasa nada, la ejecutaremos y retornaremos una llamada a la implementación por defecto del método, que ya se encarga de devolvernos un Task vacío :-)
    protected override Task OnReceivedAsync(string connectionId, string data)
    {
        // Do some sync work and 
        // then return an empty Task
        return base.OnReceivedAsync(connectionId, data);
    }
Bueno, continuemos con la implementación de nuestro chat. Cuando un cliente nos envíe un texto, lo recibiremos en el método OnReceivedAsync, y lo que queremos hacer es enviarlo al resto de usuarios conectados, ¿no? Pues simplemente usaremos la propiedad Connection disponible en la clase base y llamaremos a su método BroadCast(). Y dado que éste retorna un Task, directamente podremos retornarlo como resultado del método. La implementación completa de la clase queda como sigue:
public class SimpleChatService : PersistentConnection
{
    protected override Task OnReceivedAsync(string connectionId, string data)
    {
        string clientDescription = getClientDescription();
        return Connection.Broadcast(new { user = clientDescription, message = data });
    }
 
    private static string getClientDescription()
    {
        var context = HttpContext.Current;
        var name = context.Request.IsAuthenticated
                        ? context.User.Identity.Name
                        : context.Request.UserHostAddress;
        return name;
    }
}

Chat en funcionamientoMediante la invocación a BroadCast() estamos enviando a todos los clientes conectados un objeto anónimo serializado en JSON. En este caso, dicho objeto contará únicamente con dos propiedades, user y message, que recuperaremos desde el lado cliente para mostrar el mensaje en pantalla junto con el nombre del usuario emisor.

El otro método que aparece en el código, getClientDescription(), es simplemente una ayuda para obtener el nombre del usuario que se mostrará en pantalla, que será el User.Identity.Name si está autenticado, o la IP en caso contrario.

No sé si os estáis dando cuenta, pero, ¡estamos implementando el lado servidor de un mini-chat en menos de diez líneas de código!

4. Rutado de peticiones hacia el endpoint

Como también vimos en el artículo anterior de la serie, para que las peticiones lleguen al endpoint es necesario registrarlo en el sistema de routing. El lugar para hacerlo, como es habitual, será el global.asax:
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        RegisterGlobalFilters(GlobalFilters.Filters);
        RegisterSignalrConnections(RouteTable.Routes);
        RegisterRoutes(RouteTable.Routes);
    }
 
    public static void RegisterSignalrConnections(RouteCollection routes)
    {
        routes.MapConnection<SimpleChatService>("SimpleChat", "SimpleChatService/{*operation}");
    }
Esto es todo lo que necesitamos para dejar configuradas las rutas. El parámetro genérico de la llamada a MapConnection() indica la clase que implementa el servicio; el primer parámetro del método es simplemente el nombre de la ruta (no tiene mayor importancia), y el segundo especifica la URL a través de la cual será posible acceder al endpoint.

Y así, podemos pasar ahora a implementar el cliente.

5. El lado cliente

En el proyecto de demostración que podéis descargar al final del post está un poco más trabajado a nivel de interfaz de usuario, aquí vamos a comentar únicamente los principales aspectos a tener en cuenta.

En lo relativo a la presentación, nuestro chat sólo necesita la inclusión del siguiente código en la página:
<div id="chat">
    <div id="chat-messages">
        <!-- Placeholder for messages -->
    </div>
    <form method="POST" action="#" id="chat-form">
        <input type="text" id="chat-message" />
        <input type="submit" value="Send" />
    </form>
</div>
Como podéis intuir, el bloque con identificador “chat-messages” será utilizado, obviamente, para introducir los mensajes que vayamos recibiendo desde el servidor, es decir, los escritos por todos los usuarios que estén participando en el chat. El formulario, por otra parte, simplemente es el mecanismo mediante el cual los usuarios podrán enviar sus mensajes.

Además de esto necesitaremos algunos scripts que le den vidilla al asunto:
    $(function () {
        $("#chat-form").submit(function () {
            var message = $.trim($("#chat-message").val());
            if (message != "") {
                conn.send(message);
            }
            $("#chat-message").focus().val("");
            return false;
        });
 
        var conn = $.connection("SimpleChatService");
        conn.received(function (data) {
            var user = data.user;
            var message = data.message;
            $('#chat-messages')
                .append("<div><strong>" + user + "</strong>: " +
                         message + "</div>"
                );
          
            var sh = $('#chat-messages')[0].scrollHeight;
            $("#chat-messages").animate({ scrollTop: sh }, 3000);
        });
        conn.start();
    });

A grandes rasgos, lo que estamos haciendo es lo siguiente:
  • En primer lugar, creamos y asignamos la función de tratamiento del evento submit del formulario. En su interior, lo único que hacemos es enviar el contenido de la caja de texto “chat-message” al servidor, limpiar dicho control y volver a posicionar sobre él el foco de edición. El texto enviado desde este punto será el que recibimos en el método OnReceivedAsync() que hemos visto anteriormente implementado en el servidor.
  • A continuación, se obtiene una conexión al servicio, almacenándola en la variable conn.
  • Implementamos el callback received sobre el objeto conn, que será invocado cuando se reciban datos enviados por el servidor. El parámetro data será la instancia del objeto anónimo que enviamos desde el servidor mediante el broadcast, por eso podemos usar directamente sus propiedades name y message para añadirlo a la ventana del chat. Finalmente, desplazamos la ventana hasta el final del scroll para que se puedan ver directamente los últimos mensajes recibidos.
  • Por último, iniciamos la conexión llamando a método start() del objeto correspondiente.
Y con esto hemos acabado la implementación de nuestro simplísimo chat. Como os comentaba, en el proyecto de demostración está un poco más trabajado a nivel de interfaz, pero básicamente es lo que hemos visto aquí.


SimplechatSimpleChat

6. Punto extra: agrupación de clientes en SignalR

Hasta ahora, hemos visto que al hacer un broadcast el mensaje es enviado a todos y cada uno de los clientes que se encuentran en ese momento conectados al servicio. Esto es válido en muchos escenarios, pero hay otros en los que necesitamos algún mecanismo para segmentar o agrupar los clientes según determinados criterios.

Un ejemplo clásico serían los chats reales, donde es habitual encontrar “salas”. Los mensajes enviados por un usuario concreto son vistos únicamente por los compañeros de la sala en la que se encuentra, por lo que podríamos considerar que se trata simplemente de un mecanismo de agrupación de usuarios.

SignalR incluye de serie mecanismos para añadir o eliminar usuarios (clientes) a grupos y enviar mensajes a éstos de forma bastante sencilla, mediante los siguientes métodos de la clase PersistentConnection (que, por cierto, también retornan un objeto de tipo Task):
  • AddToGroup(string connectionId, string groupName): agrega el cliente con el identificador clientId al grupo denominado groupName.
  • RemoveFromGroup(string connectionId, string groupName): elimina el cliente clientId del grupo identificado como groupName.
  • SendToGroup(string groupName, object value): envía el objeto value, convenientemente serializado en JSON, a todos los clientes conectados al grupo groupName.
Fijaos que, de momento, el API es bastante simple en lo relativo a los grupos. Por ejemplo, no hay forma de vaciar un grupo, o de consultar los clientes pertenecientes a cada uno de ellos, aunque parece que está previsto ampliar las funcionalidades disponibles en futuras versiones del producto.

De nuevo os animo a que descarguéis el proyecto de demostración y veáis SignalR en ejecución. Un framework que, sin duda, vale la pena conocer por la cantidad de escenarios en los que podemos utilizarlo y aportar ese factor “¡uau!” que comentaba al principio de esta serie.

Publicado en: Variable not found.

6 Comentarios:

Sergio León dijo...

Tiene una pintaza increible!
Gracias por la info.

josé M. Aguilar dijo...

Gracias a tí, Sergio, por comentar :-)

Camilo Cabrales dijo...

Buenas tardes, excelente la explicación.
Estoy creando una aplicación en la que es necesario leer un webservice este web service retorna algunos datos despues de recibirlos deseo enviar los datos a las personas que estan conectadas al servicio de SignalR. es posible hacer esto con signalR. alguna pista por donde buscar
Gracias

josé M. Aguilar dijo...

Hola, Camilo!

Por supuesto, con Signalr puedes hacer envíos a todos los usuarios conectados, por lo que podría cubrir el escenario que comentas.

En cuanto a por dónde buscar, difícilmente encontrarás ejemplos exactos con tu caso; lo mejor es que aprendas Signalr mirando la aún escasa documentación del producto (http://www.asp.net/signalr), leyendo blogs o haciéndote con un libro.

Y aprovecho para hacer un poco de publicidad ;) --> http://www.variablenotfound.com/2013/03/mi-libro-introduccion-aspnet-signalr.html.

Saludos!

Mario dijo...

Estimado, muy buena e interesante explicación. Con respecto a las actualizaciones de versiones ¿actualmente a cambiado SignalR hasta hoy? en cuanto a sus métodos y sus características. La pregunta es porque estoy indagando y buscando información de distintas fuentes, pero al desarrollar los ejemplos aveces no me funcionan. Utilizo VS2012 e instalo los paquetes de SignalR desde Nuget los cuales supongo son las ultimas versiones. Saludos

José María Aguilar dijo...

Hola, Mario!

Los conceptos son idénticos, pero la verdad es que año y medio da para bastantes cambios, es normal que no te funcionen los ejemplos, aunque normalmente son por pequeños detalles.

Puedes encontrar ejemplos más actualizados en http://www.asp.net/signalr.

Saludos!