Etiquetado: SignalR

Migrando el tres en raya: Azure SDK 2.4, SignalR 2.1.1, Windows 8.1

El año pasado escribí una serie de tres artículos donde explicaba cómo crear un juego de tres en raya para jugar online en tiempo real, desde el navegador y también desde una app Windows 8. Como la tecnología avanza muy rápido, al cabo de un mes de escribir el artículo el código ya no compilaba con los últimos SDK. Así que, ahora que estoy de vacaciones me he dedicado a procrastinar y a actualizar alguno de mis proyectos personales para no tener que hacer lo que me había propuesto :). Aquí os dejo los pasos a realizar para migrar, aunque podéis descargar directamente el nuevo paquete aquí.

Como primer paso, conviene tener Visual Studio 2013.3 instalado y actualizar el SDK de Windows Azure al último disponible (2.4 por ahora).
Sigue leyendo

Anuncios

Juegos sociales online con SignalR y Windows Azure (3 de 3)

Eight!
En los artículos anteriores de esta serie hemos visto cómo utilizar SignalR para proporcionar un canal de comunicaciones desde el servidor hacia el cliente. Además, modificamos el código para funcionar en Azure para mejorar la escalabilidad de nuestro servicio. Llegamos al final de la serie con un reto que me he puesto a mi mismo, utilizar lo que hemos hecho desde una aplicación de Windows 8 y que no se nos complique el artículo.

SignalR tiene librerías cliente tanto para .Net como JavaScript. Como os prometí un artículo sencillo, utilizaremos JavaScript, porque así aprovecharemos el código que ya tenemos de forma casi directa dentro de nuestra aplicación.

App de tres en raya

Como sabéis, Windows 8 te permite utilizar librerías estándar como jQuery, lo más probable es que lo que hicimos en la versión web nos funcione con muy pocos cambios. Así que… ¡Manos a la obra!
Empezamos creando una aplicación de la tienda Windows en HTML5/JavaScript:

3er.30.w8project

Para conectarnos a nuestro Hub necesitaremos las librerías cliente de SignalR. De nuevo Nuget nos ayudará a instalarlas:

3er.30.w8jsclient

HTML

El código HTML será casi el mismo que el que hicimos en nuestro default.html, sólo cambiaremos una cosa, pues no podemos utilizar la función prompt para pedir el nombre de usuario; mientras no tengamos un login, ponemos una caja de texto en pantalla. Recordad que estoy enfocando los ejemplos al uso básico de SignalR, conectarnos al sistema de autentificación ya lo haremos en otro momento:

<body>
    <label for="userName">Nombre del jugador: </label><input type="text" id="userName" />
    <div id="partida">        
...

Cliente del Hub

SignalR nos permite definir manualmente las llamadas al Hub directamente, pero también genera automáticamente un proxy con el código para nosotros en http://127.0.0.1/signalr/hubs. Si abrimos esta dirección con el Internet Explorer intentará descargarse el archivo JavaScript autogenerado:
descargarhub

Nos guardarmos el archivo en la carpeta js de nuestro proyecto para poder referenciarlo desde la página default.html. Cuando abramos el script veremos que en la línea donde se define el signalR.hub se utiliza un path relativo. Nos bastará cambiar el path por uno absoluto que apunte a nuestro servidor de Azure. Como por ahora seguimos usando el emulador, apuntamos a la dirección de bucle local 127.0.0.1:

signalR.hub = $.hubConnection("http://127.0.0.1/signalr", { useDefaultPath: false });

El script tresenraya.js también lo copiamos a la carpeta js y modificamos la llamada por la lectura en el campo username que hemos creado antes:

//prompt("Escribe tu nombre");
$username.change(null, function (e) {
    nombre = e.target.value;
});

Definimos la variable $username junto a las otras:

$username = $("#userName");

Y ya casi lo tenemos, sólo nos queda enlazar a los scripts y cambiar el tema a light para que se vea parecido a nuestra página. La etiqueta head de la página default.html de nuestra app queda como esta:

<head>
    <meta charset="utf-8" />
    <title>TresEnRapp</title>

    <!-- WinJS references -->
    <link href="//Microsoft.WinJS.1.0/css/ui-light.css" rel="stylesheet" />
    <script src="//Microsoft.WinJS.1.0/js/base.js"></script>
    <script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

    <!-- TresEnRapp references -->
    <link href="/css/default.css" rel="stylesheet" />

    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="Scripts/jquery.signalR-1.1.2.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="/js/hub.js"></script>
    <script src="js/tresenraya.js"></script>
    <script src="/js/default.js"></script>

</head>

Y aquí tenéis la aplicación en Windows 8 jugando contra otra en IE10:

3er.24.sidebysidew8

Forzar el uso de WebSockets

Nuestra aplicación Windows 8 ha funcionado bien, pero si miramos la salida de la consola de JavaScript veremos los siguientes mensajes:
3er.25.websockets
Tenemos dos mensajes de error. El primero proviene de una comprobación que hace jQuery para ajustarse a las capacidades de cada navegador. El segundo debe preocuparnos un poco más, nuestra aplicación no está utilizando WebSockets y podría hacerlo.
El mensaje nos lo dice claro, no hemos habilitado las llamadas cross-domain en SignalR, aunque ha sido lo suficientemente listo como para encontrar otra manera de funcionar sin WebSockets. Antes no necesitábamos habilitar las llamadas cross-domain pues el código se ejecutaba en la página proveniente del sitio, pero ahora hemos creado el front-end dentro de una app de la tienda Windows, así que la llamada es cross-domain. Para habilitarlo sólo tenemos que configurar SignalR antes de arrancarlo en el Global.asax.cs:

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableCrossDomain = true;
// Register the default hubs route: ~/signalr
RouteTable.Routes.MapHubs(hubConfiguration);

Al ejecutar de nuevo la aplicación ya habrá desaparecido el error y estaremos aprovechando todo el potencial que nos proporciona Windows 8.

¿Y ahora qué?

Tal como os prometí, hacer que nuestra app funcionara no ha sido complicado. Crear una aplicación válida para la tienda Windows y que además tenga éxito ya es otra historia.
Ahora es vuestro turno. Nos quedan, entre otras cosas, las siguientes tareas:

  • Autentificación: podemos utilizar los sistemas estándar de autentificación de usuarios y luego aplicar el atributo Authorize para permitir o denegar el acceso a métodos y servicios.
  • Control de conexión y desconexión de usuarios: SignalR nos avisa cuando ocurre una conexión/desconexión/reconexión de usuario, nosotros “sólo” tendremos que gestionar esos cambios de estado para saber si un usuario está online o no.
  • Windows 8: esta aplicación es sólo una prueba de concepto. Hacer que nuestra aplicación brille nos costará un poco más, para conseguirlo tenemos grandes consejos en el MSDN

Espero que os haya gustado. Podéis descargar el código completo del ejemplo en el siguiente enlace de Codeplex.

Juegos sociales online con SignalR y Windows Azure (2 de 3)

Clouds
En el artículo anterior vimos cómo utilizar SignalR para crear servicios con un canal de comunicaciones abierto con el cliente. Recordad que es siempre el cliente el que establece la comunicación y luego el servidor utiliza la técnica más adecuada en cada caso para mantener el canal abierto.

El ejemplo, aunque con bastante código, no deja de ser un caso básico y con poca escalabilidad. Si queremos dar servicio a miles o millones de usuarios simultáneos, necesitaremos que nuestro servicio pueda crecer a lo ancho y no a lo alto. En lugar de aumentar potencia de CPU y RAM a una sola máquina, nos permite ir añadiendo más máquinas a medida que las necesitemos, que trabajarán en paralelo y nos permitirán un número de usuarios simultáneos sin límite.
Con Windows Azure podemos hacer esto y mucho más. Para que nuestra aplicación funcione en un entorno Cloud realizaremos algunos cambios en la misma:

  • Hasta ahora estábamos almacenando las partidas en memoria, en una aplicación en la nube no podemos utilizar esta técnica porque trabajaremos con múltiples instancias y cada una tiene su propia memoria. Necesitamos un lugar de almacenamiento que puedan compartir las diferentes instancias. En Azure tenemos diferentes posibilidades: Windows Azure SQL para datos relacionales, Windows Azure Table Storage para datos no relacionales (NoSQL), o Windows Azure Caching si necesitamos algo pequeño y muy rápido. En nuestro ejemplo usaremos las tablas de Windows Azure Storage.
  • Añadiremos un enlace al ServiceBus, de manera que cada vez que uno de los servidores necesite enviar información a todos los clientes conectados al servicio pueda avisar al resto de servidores del cluster para que también envíen esa información a sus clientes.

Qué necesito

Os recuerdo que para este ejercicio necesitamos:

Convertir un proyecto web en proyecto Azure

El primer paso es añadir a nuestra solución un proyecto de Azure que nos configurará el paquete de despliegue en la nube. Pulsamos el botón derecho sobre el proyecto y nos aparecerá la opción Add Windows Azure Cloud Service Project:

3er.11.AddCloudService

Si ejecutamos nuestra aplicación ahora, el Visual Studio arrancará el emulador de Windows Azure y nuestra aplicación se ejecutará en el entorno simulado. Al principio nos parecerá que funciona todo, para comprobar que en realidad nos va a fallar todo nos basta con configurar el rol para que se ejecuten dos instancias.

En el proyecto TresEnRaya.Azure, abrimos la carpeta Roles y hacemos doble-click en nuestro rol TresEnRaya, en la configuración podemos cambiar el número de instancias:
3er.11.increasinstances

Al incrementar el número de instancias, haremos que cada nueva conexión vaya a una máquina distinta, es decir, se irán balanceando las conexiones. Como os he comentado antes, las instancias no comparten memoria ni cpu, son instancias completamente independientes, incluso en el simulador. Con nuestro diseño de aplicación con listas en memoria nos encontramos con un problema importante: cada instancia tiene su lista de usuarios, solicitudes y partidas y no se ven entre ellas. Como podemos ver en la siguiente imagen, el botón para jugar contra “Manolo” debería estar en dos de los navegadores y sólo aparece en uno:
3er.11.ymanolo

Windows Azure Storage

Para almacenar las partidas y las solicitudes y que todas las instancias de nuestro servicio tengan acceso vamos a utilizar el Azure Table Service del Windows Azure Storage.
Utilizaremos el Storage en lugar de SQL Azure porque la sencillez de los datos nos lo permite y es un almacenamiento mucho más económico que el SQL.

Una explicación rápida: las tablas de Windows Azure son listas organizadas por clave, pueden contener hasta 252 valores en cada registro y tienen un límite de 100TB. Los puntos clave de las tablas en Azure que vamos a encontrarnos durante el desarrollo de esta aplicación son:

  • Los elementos se identifican mediante una clave compuesta por dos elementos PartitionKey y RowKey.
  • La información está agrupada por PartitionKey, de tal manera que recuperar múltiples registros de una misma PartitionKey es muy rápido.
  • Realizar consultas que impliquen diferentes PartitionKey o buscar por otras propiedades que no formen parte de la clave penaliza el rendimiento
  • No existen las relaciones entre tablas, es decir, no podremos hacer consultas cruzadas, olvidad lo que sabéis de SQL y las reglas de normalización de tablas. Tendremos que trabajar de otra manera, seguramente repitiendo datos organizados de formas distintas en múltiples tablas.

Definición de las tablas

En C# las tablas se pueden definir directamente desde clases, podremos utilizar las mismas clases que ya teníamos, heredando de la clase TableEntity:

public class Jugador:TableEntity
{
    public Jugador()
    {
    }

    public Jugador(string pais, string nombre, string id)
    {
        PartitionKey = pais;
        RowKey = nombre;
        Id = id;
    }

    public string Pais { get { return PartitionKey; } }
    public string Nombre { get { return RowKey; } }
    public string Id { get; set; }
}

Al transformar la clase necesitamos añadir un constructor por defecto y convertimos los campos país y nombre en la PartitionKey y RowKey respectivamente.
Así podremos realizar consultas sobre todos los jugadores de un mismo país sin perder rendimiento.
El problema que nos encontraremos será encontrar el jugador por Id de conexión, algo que hacemos bastante dentro de la clase DatosPartida. Como una consulta por Id nos penalizará el rendimiento, lo que haremos será crear otra tabla para poder buscar por Id.

public class JugadorPorId : TableEntity
{
    public JugadorPorId() { 
    }

    public JugadorPorId(string pais, string nombre, string id)
    {
        PartitionKey = pais;
        RowKey = id;
        Nombre = nombre;
    }

    public string Pais { get { return PartitionKey; } }
    public string Nombre { get; set; }
    public string Id { get { return RowKey; } }
}

El caso de las partidas es más complicado, pues los jugadores vienen de otra tabla y no podemos guardar en las tablas árboles de objetos, tienen que ser objetos bastante planos. En nuestro caso vamos a modificar un poco la forma en que se guardan esas propiedades en los métodos WriteEntity y ReadEntity

public class Partida:TableEntity
{
    public const int Dimension = 3;
    public Partida()
    {
    }

    public Partida(string pais, string id)
    {
        PartitionKey = pais;
        RowKey = id;
        _tablero = new char[Dimension * Dimension];
        for (int i = 0; i < _tablero.Length; i++)
        {
            _tablero[i] = ' ';
        }
    }

    public string Pais { get { return PartitionKey; } }
    public string Id { get { return RowKey; } }
    public Jugador Jugador1 { get; set; }
    public Jugador Jugador2 { get; set; }
    public int Turno { get; set; }

    public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
    {
        var context= base.WriteEntity(operationContext);
        context.Remove("Jugador1");
        context.Add("Jugador1Nombre", EntityProperty.GeneratePropertyForString(Jugador1.Nombre));
        context.Add("Jugador1Id", EntityProperty.GeneratePropertyForString(Jugador1.Id));
        context.Remove("Jugador2");
        context.Add("Jugador2Nombre", EntityProperty.GeneratePropertyForString( Jugador2.Nombre));
        context.Add("Jugador2Id", EntityProperty.GeneratePropertyForString(Jugador2.Id));
        return context;
    }
    public override void ReadEntity(IDictionary<string, EntityProperty> properties, 
        OperationContext operationContext)
    {
        Jugador1 = new Jugador {
            PartitionKey= properties["Pais"].StringValue,
            RowKey = properties["Jugador1Nombre"].StringValue ,
            Id = properties["Jugador1Id"].StringValue
        };
        Jugador2 = new Jugador {
            PartitionKey= properties["Pais"].StringValue,
            RowKey = properties["Jugador2Nombre"].StringValue ,
            Id = properties["Jugador2Id"].StringValue
        };
        base.ReadEntity(properties, operationContext);
    }

(no pongo el resto de código pues es igual al código del capítulo anterior).

Creación de las tablas en Azure

Hemos creado las entidades por código y las tablas las vamos a crear igual. Modificaremos la clase DatosPartida que ya teníamos definida, para que al arrancar cree las tablas si es que no existen.

public class DatosPartida
{
    static CloudTable _solicitudes;
    static CloudTable _partidas;
    static CloudTable _jugadores;
    static CloudTable _jugadoresPorId;

    static DatosPartida()
    {
        // Retrieve the storage account from the connection string.
        CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
            CloudConfigurationManager.GetSetting("StorageConnectionString"));

        // Create the table client.
        CloudTableClient tableClient = storageAccount.CreateCloudTableClient();

        // Create the table if it doesn't exist.
        _solicitudes = tableClient.GetTableReference("Solicitudes");
        _solicitudes.CreateIfNotExists();

        _partidas = tableClient.GetTableReference("Partidas");
        _partidas.CreateIfNotExists();

        _jugadores = tableClient.GetTableReference("Jugadores");
        _jugadores.CreateIfNotExists();

        _jugadoresPorId = tableClient.GetTableReference("JugadoresPorId");
        _jugadoresPorId.CreateIfNotExists();
    }

En el constructor estático estamos conectando al servicio de tablas mediante una cadena de conexión que he puesto en la configuración del rol. Para definirla hacemos doble click sobre el rol:
3er.12.webrole
Y en la sección Settings podremos definir nuestro valor de configuración. Por ahora utilizaremos el emulador local del storage.
3er.13.storage
A partir de este punto, modificamos los métodos de acceso que utilizábamos antes sobre listas para que accedan a las tablas. Como veréis he procurado no consultar las tablas sin una PartitionKey:

     public DatosPartida()
     {
     }

     public Jugador NuevoJugador(string pais, string nombre, string id)
     {
         var jugador = new Jugador(pais, nombre, id);
         TableOperation insertJugador = TableOperation.InsertOrReplace(jugador);
         _jugadores.Execute(insertJugador);

         TableOperation insertJugadorId = TableOperation.InsertOrReplace(new JugadorPorId(id, pais, nombre));
         _jugadoresPorId.Execute(insertJugadorId);
         return jugador;
     }

     public Jugador NuevaSolicitud(Jugador jugador)
     {
         var solicitud = new Jugador(jugador.Pais, jugador.Nombre, jugador.Id);
         TableOperation operation = TableOperation.InsertOrReplace(solicitud);
         _solicitudes.Execute(operation);
         return solicitud;
     }

     public Jugador ObtenerSolicitud(string pais, string nombre)
     {
         var op = TableOperation.Retrieve<Jugador>(pais, nombre);
         var result = _solicitudes.Execute(op);
         var solicitud = result.Result as Jugador;
         return solicitud;
     }

     public bool BorrarSolicitud(Jugador solicitud)
     {
         var op = TableOperation.Delete(solicitud);
         var result = _solicitudes.Execute(op);
         return result.HttpStatusCode == 204;
     }

     public Partida EmpezarPartida(string pais, Jugador jugador1, Jugador jugador2)
     {
         var partida = new Partida(pais, Guid.NewGuid().ToString())
         {
             Jugador1 = jugador1,
             Jugador2 = jugador2
         };
         var empiezaPartidaOp = TableOperation.InsertOrReplace(partida);
         _partidas.Execute(empiezaPartidaOp);
         return partida;
     }

     public Jugador ObtenerJugador(string pais, string id)
     {
         var op = TableOperation.Retrieve<JugadorPorId>(pais, id);
         var jugadorxid = _jugadoresPorId.Execute(op).Result as JugadorPorId;
         if (jugadorxid != null)
         {
             op = TableOperation.Retrieve<Jugador>(pais, jugadorxid.Nombre);
             return _jugadores.Execute(op).Result as Jugador;
         }
         return null;
     }

     public Partida ObtenerPartida(string pais, string id)
     {
         var op = TableOperation.Retrieve<Partida>(pais, id);
         return _partidas.Execute(op).Result as Partida;
     }

     public void GuardarMovimiento(Partida partida)
     {
         var replaceOp = TableOperation.Replace(partida);
         _partidas.Execute(replaceOp);
     }

     public IEnumerable<Jugador> ListaDisponibles(string pais)
     {
         var solicitudQuery = new TableQuery<Jugador>().Where(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, pais));
         return _solicitudes.ExecuteQuery<Jugador>(solicitudQuery);
     }
 }

ServiceBus y SignalR

Si ejecutamos ahora la aplicación, parece que funciona™ pero tiene un gran fallo que podremos comprobar aumentando el número de instancias. Como vimos al principio del post, las diferentes instancias no se hablan entre sí, lo que provoca que si tenemos dos clientes de nuestra aplicación y cada uno está conectado a una instancia diferente, nuestra aplicación no funcionará, o sólo lo hará a medias.
Windows Azure tiene un mecanismo para resolver esto, el Service Bus, que nos permite crear suscripciones a la información agrupadas por “temas”. De esta manera, cuando algo cambie en una instancia podemos avisar a todas las otras.
Por suerte, SignalR implementa esta funcionalidad con el ServiceBus, así que sólo tendremos que crear una cuenta de ServiceBus y conectarla a nuestra aplicación, SignalR se encargará de gestionar los canales.

El ServiceBus no tiene emulador. En la versión anterior podíamos instalarlo en local, pero todavía no han publicado la nueva, así que para poder utilizarlo, incluso en local, tendremos que crear uno en una cuenta de Azure. El coste del ServiceBus es muy pequeño (en la fecha de publicación del artículo €0,0075 al mes por cada 10.000 mensajes) así que no nos vamos a arruinar por hacer unas pruebas.
Si tenéis alguna cuenta MSDN os entrará dentro de los recursos gratuitos que tenéis. Si no es así, podéis crear una cuenta de evaluación gratuita durante un mes: http://www.windowsazure.com/es-es/pricing/free-trial/

En nuestro portal de Azure creamos un espacio de nombres para nuestra aplicación, yo he sido muy original y lo he llamado tresenraya:
3er.20.servbus
Recordad ponerlo en una región que esté cerca de vuestros usuarios, pues es muy conveniente que todos los servicios que vamos a usar estén en la misma región, evita tráfico innecesario.
Una vez esté activo abrimos la información de la conexión y la copiamos:

3er.23.signalrsbconect
Esta información de conexión la guardaremos otra vez en las propiedades del rol:

3er.21.servbusconn

Para poder utilizar el ServiceBus necesitaremos importar con nuget el paquete de Microsoft.AspNet.SignalR.ServiceBus:
3er.22.signalrsb

Una vez instalada la librería para el ServiceBus, sólo nos queda avisar a SignalR que debe utilizarlo, así en el Application_Start de Global.asax.cs indicaremos a SignalR qué debe hacer:

protected void Application_Start(object sender, EventArgs e)
{
    // Register the default hubs route: ~/signalr
    RouteTable.Routes.MapHubs();

    var sbConnectionString = CloudConfigurationManager.GetSetting("ServiceBusConnectionString");
    GlobalHost.DependencyResolver.UseServiceBus(sbConnectionString, "TresEnRaya");
}

Resumen

En este capítulo hemos modificado la aplicación de juego para que funcionara bien dentro de un entorno cloud. El mayor trabajo ha sido cambiar el sistema de almacenamiento, pues el Hub de SignalR no lo hemos tocado y sólo hemos tenido que añadir dos líneas de código para que SignalR funcione correctamente con múltiples instancias.
En el próximo artículo crearemos la aplicación cliente en Windows 8. Como ya hemos hecho lo difícil, os prometo que la app de Windows 8 será coser y cantar.

Descarga el código del ejemplo de Codeplex

Juegos sociales online con SignalR y Windows Azure (1 de 3)

EOD technicians play tic-tac-toe with a children at a family day picnic

Hoy os propongo una mini-serie de 3 capítulos sobre SignalR y Windows Azure con un ejemplo de juego sencillo: el tres en raya, en el que podremos jugar contra otro oponente online.

La tarea presenta algunas complicaciones que resolveremos gracias a SignalR y Azure de forma muy sencilla:

  • Notificaciones: al realizar cada jugada debemos notificar al oponente. Hay muchas maneras de recibir esta notificación, dependiendo del navegador que utilicemos podemos hacer polling, que consiste en ir pidiendo novedades al servidor de forma cíclica, long polling o utilizar websockets para recibir la jugada. SignalR nos simplificará la tarea detectando automáticamente las capacidades de nuestro navegador y utilizando la técnica más adecuada en cada uno.
  • Escalabilidad: ¿qué pasa si nuestro juego tiene éxito y tenemos millones de usuarios simultáneos? Necesitaremos que el servicio escale y para eso tenemos Windows Azure, utilizaremos un Web Role para el servicio y Azure Storage Tables para almacenar la información.
  • Comunicaciones: al escalar a lo ancho nos encontraremos con otro problema, los clientes de un servidor no reciben información de los otros servidores, deberemos establecer un canal de comunicaciones entre los servidores para que reciban qué están haciendo los demás. Para ello utilizaremos el Service Bus de Azure, que nos proporciona un modelo de eventos y suscripciones http://www.asp.net/signalr/overview/performance-and-scaling/scaleout-in-signalr

Atacaremos estos puntos clave uno por uno y en este primer capítulo habrá bastante código. Montaremos todo el sistema base para el juego. Hoy empezamos creando la aplicación básica con SignalR, guardando la información en listas en memoria como en los ejemplos básicos, en el siguiente capítulo le añadiremos Azure y en el último añadiremos otro cliente al sistema, además del cliente web que hacemos hoy.

Antes de empezar os dejo un par de “disclaimers”:

Nota para puristas: voy a hacer bastantes simplificaciones para que este artículo sea didáctico, aunque me duela, no voy a utilizar MVVM, MVC, TDD ni nada que me aparte de lo que pretendo mostrar hoy, el uso de SignalR en Azure.

Nota para pragmáticos: no os esperéis poder hacer copy/paste del código para vuestra aplicación en producción. Aparte de lo comentado en la advertencia anterior, el ejemplo no será completo, voy a dejar algunas cosas como ejercicios para que hagáis vosotros 🙂

Y ahora que ya no tengo ninguna responsabilidad puedo empezar a hackear tranquilo.

¿Qué necesito?

Para poder compilar y probar el código vas a necesitar:

SignalR

Para crear cualquier juego multijugador online en tiempo real tenemos que establecer canales de comunicación entre todas las partes implicadas. Normalmente no es algo trivial, más si añadimos diferentes plataformas de cliente, tales como web, aplicaciones móviles y de tableta. Para que funcione bien tendremos que gestionar los posibles problemas de conexión, establecer puntos de conexión entre clientes diferentes, crear un canal de difusión para enviar notificaciones a todos los clientes, confirmar que los mensajes llegan, crear librerías de cliente que mantengan la conexión abierta con el servidor, autorizar e identificar a los usuarios y un largo etcétera de funcionalidades.

Cuando se trata de un cliente Web, añadimos otra vuelta de tuerca, pues debemos tener en cuenta las capacidades de cada navegador para decidir en el momento qué tecnología de comunicaciones funcionará mejor.

SignalR es una librería para ASP.NET que nos liberará de gestionar todos estos problemas y nos permitirá concentrarnos en la tarea que realmente queremos hacer. Nos proporciona una librería en servidor para gestionar todas las conexiones y suscripciones, además de generar dinámicamente un script que nos permitirá realizar la conexión desde el cliente.

Un tres en raya social

Partimos de una aplicación sencilla como el tres en raya para no complicar demasiado el ejemplo, algo intermedio entre  el ejemplo básico de chat y el juego online ShootR.

Empezaremos con una aplicación ASP.NET vacía:

tictactoe_01_newproject

Una vez tengamos nuestra solución, necesitaremos un archivo Global.asax y una página HTML donde mostrar nuestro tablero:

3er.01a.Globalasax

En el mismo menú seleccionamos HTML Page y la llamamos default:

3er.01b.defaulthtmlDentro del body de la página colocamos un panel de tres en raya:

<div id="partida">
    <button id="empezar">Empezar nueva partida</button>
    <style>
        #tablero {
            height: 160px;
            width: 160px;
            padding: 5px;
        }

        .row {
            height: 50px;
            width: 150px;
        }

        .col {
            height: 48px;
            width: 48px;
            border: 1px solid black;
            float: left;
            text-align: center;
            line-height: 48px;
        }

        .row0, .row2 {
            background-color: rgba(0,0,0,0.2);
        }

        .col1 {
            background-color: rgba(0,0,0,0.2);
        }
    </style>

    <div id="tablero">
        <div class="row row0">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
        <div class="row row1">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
        <div class="row row2">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
    </div>

    <div>
        <h2>Solicitud de partidas</h2>
        <div id="partidas">
            <div>Cargando...</div>
        </div>
    </div>
</div>
<div>
    <h2>Mensajes</h2>
    <div id="mensajes"></div>
</div>

En cuanto al código, empezaremos con la parte de servidor. Vamos a utilizar SignalR, necesitaremos añadir las referencias a las librerías en nuestro proyecto. Por suerte tenemos la herramienta Nuget que nos permitirá descargar e instalar en nuestro proyecto todo lo necesario con un click:
3er.02.alt.Nuget
Buscamos en la sección Online por SignalR:
3er.02.signalr
Nuget nos instalará todas las dependencias que necesita la librería y acto seguido nosotros cambiaremos el evento Application_Start en el Global.asax.cs, tal como nos indica el propio SignalR al instalarse:

protected void Application_Start(object sender, EventArgs e)
{
    // Register the default hubs route: ~/signalr
    RouteTable.Routes.MapHubs();
}

Esta pequeña línea realiza la magia que nos permitirá comunicarnos con los clientes desde el servidor. En nuestro proyecto ya podemos crear un Hub, el punto de conexión entre los clientes y el servidor, al que llamaremos PartidaHub:
3er.03.signalrhub

La plantilla nos creará a mínima expresión de un Hub:

public class PartidaHub : Hub
{
    public void Hello()
    {
        Clients.All.hello();
    }
}

Este código de servidor que se ha generado automáticamente, está llamando a una función definida en todos los clientes conectados a este Hub llamada “hello”; SignalR se encargará de que eso ocurra.
En el Hub vamos a crear métodos que podrán ser llamados por el cliente y el mismo hub, a su vez, podrá realizar llamadas a todos los clientes, al emisor del mensaje o a receptores concretos, mediante los siguientes métodos:

//como el anuncio aquel de refrescos...
//para todos
Clients.All.hello();
//para el que llama
Clients.Caller.hello();
//para todos los demás
Clients.Others.hello();
//para uno en concreto
Clients.Client(id).hello();

Si vamos a mirar la definición de estas propiedades y métodos veremos que son de tipo dynamic, lo que nos permite escribir nombres de funciones que no tenemos definidas en ningún sitio de nuestro código C#.

Datos de partida

Antes de ponernos con el Hub creamos unas cuantas estructuras que necesitamos para jugar: los jugadores, las partidas y las solicitudes de partida, para que un jugador pueda empezar una partida y así otro pueda apuntarse.
Creamos una carpeta Data y empezamos a crear unas cuantas clases, la primera una clase para almacenar la partida y la lógica de juego:

public class Partida
{
    public const int Dimension = 3;

    public Partida(string pais, string id)
    {
        Pais = pais;
        Id = id;
        _tablero = new char[Dimension * Dimension];
        for (int i = 0; i < _tablero.Length; i++)
        {
            _tablero[i] = ' ';
        }
    }

    public string Pais { get; set; }
    public string Id { get; set; }
    public Jugador Jugador1 { get; set; }
    public Jugador Jugador2 { get; set; }
    public int Turno { get; set; }

    char[] _tablero;
    public string Tablero
    {
        get
        {
            return new string(_tablero);
        }
        set
        {
            _tablero = value.ToCharArray();
        }
    }

    public bool Marcar(int x, int y, int jugador)
    {
       if (Estado == EstadoPartida.EnJuego)
       {
           var posicion = Dimension * x + y;
           if (jugador == Turno)
           {
               if (_tablero[posicion] == ' ')
               {
                   var valor = Turno == 0 ? 'O' : 'X';
                   _tablero[Dimension * x + y] = valor;
                   Turno = Turno == 0 ? 1 : 0;
                   return ComprobarFinal();
               }
               else
               {
                   throw new InvalidOperationException("La casilla está ocupada");
               }
           }
           else
           {
               throw new InvalidOperationException("No es tu turno");
           }
       }
       else
       {
           throw new InvalidCastException("El juego ha acabado");
       }
   }

   public char Valor(int x, int y)
   {
       return _tablero[Dimension * x + y];
   }

   bool ComprobarFinal()
   {
       int[] jugadas = new int[Partida.Dimension * 2 + 2];

       for (int i = 0; i < Partida.Dimension; i++)
       {
           for (int j = 0; j < Partida.Dimension; j++)
           {
               var v = Valor(i, j);
               int sum = 0;
               if (v == 'X')
               {
                   sum = -1;
               }
               else if (v == 'O')
               {
                   sum = 1;
               }
               jugadas[i] += sum;
               jugadas[Partida.Dimension + j] += sum;
               if (i == j)
               {
                   jugadas[Partida.Dimension * 2] += sum;
               }
               if (i + j == Partida.Dimension - 1)
               {
                   jugadas[Partida.Dimension * 2 + 1] += sum;
               }
           }
       }
       if (jugadas.Count((x) => x == 3) > 0)
       {
           Estado = EstadoPartida.Gana1;
       }
       else if (jugadas.Count((x) => x == -3) > 0)
       {
           Estado = EstadoPartida.Gana2;
       }
       else if (!Tablero.Contains(' '))
       {
           Estado = EstadoPartida.Empate;
       }

       return Estado != EstadoPartida.EnJuego;
   }
}

También necesitaremos una clase Jugador para identificarlos:

public class Jugador
{
    public Jugador(string pais, string nombre, string id)
    {
        Nombre = nombre;
        Pais = pais;
        Id = id;
    }

    public string Pais { get; set; }
    public string Nombre { get; set; }
    public string Id { get; set; }
}

Y una clase DatosPartida que nos permita ir añadiendo jugadores, solicitudes y partidas de forma ordenada. Esta clase nos vendrá bien a la hora de transformar nuestra aplicación a Azure:

public class DatosPartida
{
    List<Jugador> _jugadores = new List<Jugador>();
    List<Partida> _partidas = new List<Partida>();
    List<Jugador> _solicitudes = new List<Jugador>();

    public DatosPartida()
    {
    }

    public Jugador NuevoJugador(string pais, string nombre, string id)
    {
        var jugador=_jugadores.FirstOrDefault(j => j.Pais == pais && j.Nombre == nombre);
        if (jugador == null)
        {

            jugador = new Jugador(pais, nombre, id);
            _jugadores.Add(jugador);
        }
        else
        {
            //actualiza el id del jugador, imaginaremos, por ahora, que si se llaman igual es el mismo...
            jugador.Id = id;
        }
        return jugador;
    }

    public Jugador NuevaSolicitud(Jugador jugador)
    {
        _solicitudes.Add(jugador);
        return jugador;
    }

    public Jugador ObtenerSolicitud(string pais, string nombre)
    {
        return _solicitudes.FirstOrDefault(s => s.Pais == pais && s.Nombre == nombre);
    }

    public bool BorrarSolicitud(Jugador solicitud)
    {
        return _solicitudes.Remove(solicitud);
    }

    public Partida EmpezarPartida(string pais, Jugador jugador1, Jugador jugador2)
    {
        var partida = new Partida(pais, Guid.NewGuid().ToString())
        {
            Jugador1 = jugador1,
            Jugador2 = jugador2
        };
        _partidas.Add(partida);
        return partida;
    }

    public Jugador ObtenerJugador(string pais, string id)
    {
        return _jugadores.FirstOrDefault(jugador => jugador.Id == id);
    }

    public Partida ObtenerPartida(string pais, string id)
    {
        return _partidas.FirstOrDefault(p=>p.Pais==pais && p.Id==id);
    }

    public void GuardarMovimiento(Partida partida)
    {
        var old=ObtenerPartida(partida.Pais, partida.Id);
        _partidas.Remove(old);
        _partidas.Add(partida);
    }

    public IEnumerable<Jugador> ListaDisponibles(string pais)
    {
        return _solicitudes.Where((s)=>s.Pais==pais);
    }
}

Y finalmente escribiremos el Hub, donde cada vez que queramos notificar algo a un cliente o a muchos utilizaremos los métodos dinámicos de Clients.All , Clients.Caller, etc.:

public class PartidaHub : Hub
{
    private readonly static Lazy<DatosPartida> _datos = new Lazy<DatosPartida>(true);
    public DatosPartida Datos { get { return _datos.Value; } }

    public Jugador NuevaPartida(string pais, string nombre)
    {
        var jugador = Datos.NuevoJugador(pais, nombre, Context.ConnectionId);
        var solicitud=Datos.NuevaSolicitud(jugador);
        Clients.All.Nueva(solicitud);  //avisamos a todos que una nueva partida ha sido solicitada
        return jugador;
    }

    public IEnumerable<Jugador> PartidasDisponibles(string pais)
    {
        return Datos.ListaDisponibles(pais);
    }

    public bool Jugar(string pais, string jugador1, string jugador2)
    {
        var solicitud = Datos.ObtenerSolicitud(pais, jugador1);
        if (solicitud != null)
        {
            var jugadorB=Datos.ObtenerJugador(pais, Context.ConnectionId);
            if (jugadorB == null)
                jugadorB = Datos.NuevoJugador(pais, jugador2, Context.ConnectionId);
            Partida partida = Datos.EmpezarPartida(pais, solicitud, jugadorB);

            //eliminamos las solicitudes pendientes de los jugadores
            Datos.BorrarSolicitud(solicitud);
            var solicitud2 = Datos.ObtenerSolicitud(jugadorB.Pais, jugadorB.Nombre);
            if (solicitud2 != null)
            {
                Datos.BorrarSolicitud(solicitud2);
            }

            //notificacmos solicitudes eliminadas
            Clients.All.PartidaEliminada(solicitud);
            if (solicitud2 != null)
            {
                Clients.All.PartidaEliminada(solicitud2);
            }
            //notificamos al llamante sobre el comienzo de la partida
            Clients.Caller.Jugando(partida);
            //notificamos al jugador que solicitó una partida sobre el comienzo de la misma
            Clients.Client(solicitud.Id).Jugando(partida);
            return true;
        }
        else
        {
            error("Ya no existe la partida");
        }
        return false;
    }

    public void MarcaCasilla(string pais, string nombre, int x, int y)
    {
        var partida = Datos.ObtenerPartida(pais, nombre);

        if (partida != null)
        {
            int turno = 0;
            if (Context.ConnectionId == partida.Jugador2.Id)
                turno = 1;
            int turnoAnterior = partida.Turno;

            try
            {
                bool acabada = partida.Marcar(x, y, turno);
                Datos.GuardarMovimiento(partida);
                Clients.Client(partida.Jugador1.Id).Jugada(partida);
                Clients.Client(partida.Jugador2.Id).Jugada(partida);

                if (acabada)
                {
                    switch (partida.Estado)
                    {
                        case EstadoPartida.Empate:
                            Clients.Client(partida.Jugador1.Id).FinJuego("Esta vez hay empate");
                            Clients.Client(partida.Jugador2.Id).FinJuego("Esta vez hay empate");
                            break;
                        case EstadoPartida.Gana1:
                            Clients.Client(partida.Jugador1.Id).FinJuego("Ganaste la partida!!!");
                            Clients.Client(partida.Jugador2.Id).FinJuego("Esta vez te ganó " + partida.Jugador1.Nombre);
                            break;
                        case EstadoPartida.Gana2:
                            Clients.Client(partida.Jugador2.Id).FinJuego("Ganaste la partida!!!");
                            Clients.Client(partida.Jugador1.Id).FinJuego("Esta vez te ganó " + partida.Jugador2.Nombre);
                            break;
                    }
                }
            }
            catch (InvalidOperationException ex)
            {
                mensaje(ex.Message);
            }
        }
        else
        {
            error("no existe la partida");
        }
    }

    private void error(string error)
    {
        mensaje("Error: " + error);
    }

    private void mensaje(string msg)
    {
        Clients.Caller.Mensaje(msg);
    }
}

Código en cliente

En el lado del cliente vamos a definir unas funciones para comunicarnos con el servidor. Lo primero que necesitamos es obtener el Hub que hemos definido antes. SignalR crea para nosotros una librería JavaScript que tiene todo lo necesario para comunicarnos con este.
Creamos en la carpeta Scripts un script “tresenraya.js” y añadimos al final de la página html las referencias a los siguientes scripts, antes del cierre de la etiqueta body:

<script src="Scripts/jquery-2.0.1.min.js"></script>
<script src="Scripts/jquery.signalR-1.1.2.min.js"></script>
<!--Reference the autogenerated SignalR hub script. -->
<script src="/signalr/hubs"></script>
<script src="Scripts/tresenraya.js"></script>

El script /signalr/hubs es el que genera SignalR automáticamente con el código para acceder a nuestros hubs.
Una vez hemos añadido los scripts, podemos empezar a escribir nuestro tresenraya.js. En primer lugar necesitamos conectar al hub e inicializar, en nuestro versión sencilla pediremos el nombre al usuario nada más empezar, lo que vendría a ser el login de los pobres (y confiados). Para el código js usamos jQuery1.6.4 que es el que nos ha instalado SignalR:

$(function () {
    //obtenemos el hub
    var servicioPartidas = $.connection.partidaHub;
    //iniciamos la conexión y una vez iniciada (método done) llamamos a nuestra función de inicialización
    $.connection.hub.start().done(init);

    var pais = "Mallorca";  //para no pedir tantos datos al principio vamos a suponer 
                            //que estáis todos de vacaciones en mi isla ^^

    function init() {
        nombre = prompt("Escribe tu nombre");
        //llamamos a una función de servidor que nos devuelve una lista de partidas
        //que se realizan en Mallorca ^^
        servicioPartidas.server.partidasDisponibles(pais).done(function (list) {
            $partidas.empty();
            $.each(list, function () {
                crearEntrada(this);
            });

        });
        //botón nueva partida
        $btnEmpezar.bind("click", null, function (e) {
            servicioPartidas.server.nuevaPartida(pais, nombre);
            return false;
        });
        //inicializamos los recuadros del 3 en raya
        for (var row = 0; row < 3; row++) {
            for (var col = 0; col < 3; col++) {
                var $celda = $(".row" + row + " .col" + col);
                $celda.click(function () {
                    var x = row;
                    var y = col;
                    return function () {
                        if (partidaActual != null) {
                            servicioPartidas.server.marcaCasilla(partidaActual.Pais, partidaActual.Id, x, y);
                        }
                    }
                }());
            }
        }
    }

Fijaos que estamos llamando a los métodos que definimos en nuestro Hub: nuevaPartida, marcarCasilla, etc., y usamos los mismos parámetros que hemos definido en el código C#.

Para el código anterior necesitamos unas cuantas funciones más que nos permiten mostrar las solicitudes de partida que van llegando:

$btnEmpezar = $("#empezar");
$partidas = $("#partidas");
$mensajes = $("#mensajes");
var partidaActual;
var nombre;
var partidaListItem = "<div id='{Pais}_{Nombre}item'>Pais: {Pais} Usuario:{Nombre} <button id='{Pais}_{Nombre}'>Jugar</button></div>";

// A simple templating method for replacing placeholders enclosed in curly braces.
if (!String.prototype.supplant) {
    String.prototype.supplant = function (o) {
        return this.replace(/{([^{}]*)}/g,
            function (a, b) {
                var r = o[b];
                return typeof r === 'string' || typeof r === 'number' ? r : a;
            }
        );
    };
}

function crearId(entity) {
    return entity.Pais + "_" + entity.Nombre;
}

function crearEntrada(solicitud) {
    if (solicitud.Nombre != nombre) {
        $partidas.append(partidaListItem.supplant(solicitud));
        $("#" + crearId(solicitud)).click(function (e) {
            var valores = e.currentTarget.id.split("_");
            servicioPartidas.server.jugar(valores[0], valores[1],nombre);
            return false;
        });
    }
}

Recibir notificaciones del servidor

Hasta ahora hemos escrito código que envía información, pero también queremos recibir el push que realiza el servidor hacia los clientes. Para esto tenemos que definir unas funciones con los mismos nombres y parámetros que utilizamos en el lado del servidor.
Si recordáis el código anterior, al crear una nueva solicitud de partida llamábamos al método Clients.All.Nueva(solicitud);. Este es el método que tendremos que escribir en el cliente, por ejemplo, con el siguiente código recibimos el mensaje de partidas nuevas:

servicioPartidas.client.nueva = function (solicitud) {
    if (solicitud.Nombre == nombre && solicitud.Pais == pais) {
        //si es tu partida deshabilita el botón de empezar.. ya estás jugando
        $btnEmpezar.text("Esperando a que llegue un jugador");
        $btnEmpezar.prop("disabled", true);
        $partidas.prop("disabled", true);
    }
    else {
        //si no es tu partida saca la lista
        crearEntrada(solicitud);
    }

Aquí cuando se nos notifica el principio y final del juego:

servicioPartidas.client.jugando = function (partida) {
    partidaActual = partida;
    $btnEmpezar.text("Jugando partida " + partida.Jugador1.Nombre + " contra " + partida.Jugador2.Nombre);
    $btnEmpezar.prop("disabled", true);
    $partidas.prop("disabled", true);
    pintarTablero(partida.Tablero);
};

function pintarTablero(tablero) {
    for (var row = 0; row < 3; row++) {
        for (var col = 0; col < 3; col++) {
            var $celda = $(".row" + row + " .col" + col);
            $celda.text(tablero[3 * row + col]);
        }
    }
}

servicioPartidas.client.finJuego = function (mensaje) {
    $mensajes.append(mensaje + "<br/>");
    $btnEmpezar.text("Empezar nuevo juego");
    $btnEmpezar.prop("disabled", false);
    $partidas.prop("disabled", false);
}

Cada vez que hay una jugada desde el servidor se nos manda el tablero y lo volvemos a representar:

servicioPartidas.client.jugada = function (partida) {
    //cuando llega una jugada de nuestra partida en curso
    //pintamos el tablero
    pintarTablero(partida.Tablero);
}

Y dos funciones más que también necesitamos:

//los mensajes también irán apareciendo
servicioPartidas.client.mensaje = function (msg) {
    $mensajes.append(msg + "<br/>");
}

servicioPartidas.client.partidaEliminada = function (solicitud) {
    $("#" + crearId(solicitud) + "item").remove();
}

Tras escribir (o cortapegar) todo este código ya os debería funcionar la primera versión del tres en raya online. Mañana transformaremos el código para que funcione en un WebRole de Windows Azure.
3er.01.pacowins
Espero vuestros comentarios!!! Id pensando cómo haríais que esta aplicación funcione bien en Azure.

Si habéis llegado hasta aquí os merecéis este vídeo viejuno que os explicará por qué he elegido el tres en raya: