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.
Bueno, 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:
- 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.
- Registraremos este endpoint durante la inicialización de la aplicación, asociándole una URL de acceso a las funcionalidades del servicio.
- 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.
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
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) { ... }
// [...]
}
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
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);
}
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; }
}
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 + ")";
}
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
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);
}
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
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); }
2. Registro de ruta
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);
[...]
}
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
<script src="@Url.Content("~/Scripts/jquery.signalR.js")" type="text/javascript"></script>
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”
<h2>Log</h2> <div id="log"></div>
- 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.
<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>
- 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 tipoNotificationMessage
. 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.
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.
Publicado en Variable not found.
Muy buen artículo.
Muchas gracias, Lucas!