lunes, 30 de diciembre de 2013

WebSockets en MVC - Ejemplo práctico Horse Race

WebSockets es un protocolo de nueva generación que permite establecer comunicaciones bidireccionales sin la necesidad de abrir múltiples conexiones http en entornos web entre cliente y servidor. El objetivo del post no es explicar a fondo en que consiste este tipo de comunicación sino ver un ejemplo relativamente sencillo sobre como implementarlo en un entorno de desarrollo actual para hacernos una idea en que consiste.

Microsoft soporta este protocolo a nivel de servidor tanto en entornos ASP.NET WebForms como MVC. En nuestro caso nos centraremos en un entorno MVC, desarrollaremos paso a paso un ejemplo práctico de una carrera de caballos que se desplazarán horizontalmente sobre un canvas en una vista HTML5. Al ser algo relativamente reciente necesitaremos como mínimo una versión 2010 de Visual Studio y framework 4.0.

Veamos el ejemplo paso a paso:

Abrimos nuevos proyecto desde Visual Studio, creamos un proyecto MVC vacío desde Visual Studio (aquí estoy usando 2013 express)

Añadimos una carpeta "controllers", pulsamos botón derecho sobre la carpeta y en el menú contextual que se despliega seleccionamos "Agregar Controlador". En este punto veremos que el bueno de Visual Studio ha creado el resto de ficheros y carpetas necesarias para el site MVC automáticamente. Aprovechando la carpeta Content, crearía un subdirectorio Images donde ubicaré la imagen de los 2 caballos que compiten (negro y blanco).

Aquí tenemos nuestro controlador por defecto con el método Index.
namespace HorseRace.Controllers
{
    public class HorseRaceController : Controller
    {
        //
        // GET: /HorseRace/
        public ActionResult Index()
        {
            return View();
        }
    }
}
Si pulsamos botón derecho en el método Index el menú contextual nos permite añadir una vista para el método Index. Procedemos a crearla, en el menú asistente de creación de la vista dejamos los datos por defecto (no asociamos ningún modelo).

Si vamos bien hasta aquí tendremos una carpeta Views en el proyecto con una subcarpeta HorseRace y allí dentro se debería haber creado la vista Index.shtml.

En la vista añadiremos un elemento canvas, 2 imágenes ocultas y un apartado script. El canvas será el circuito de carreras donde veremos como avanzan los caballos. Se trataría de copiar este código en la vista HTML.

Index.shtml
<canvas height="100" id="myCanvas" style="border:1px solid #000000;background-color:#8BA870" width="560"></canvas>
<img src="~/Content/Images/caballonegro.png" style="visibility: hidden" id="caballo1" />
<img src="~/Content/Images/caballoblanco.png" style="visibility:hidden" id="caballo2" />
<script>
    var c = document.getElementById('myCanvas');
    var ctx = c.getContext('2d');
    var caballo2 = document.getElementById('caballo1');
    var caballo1 = document.getElementById('caballo2');
    var xC1 = 1;
    var yC1 = 4;
    var xC2 = 1;
    var yC2 = 54;
    initField();
    caballo1.addEventListener("load", drawHorseBlanco(xC1,yC1), false);
    caballo2.addEventListener("load", drawHorseNegro(xC2,yC2), false);
    

    function initField()
    {
        ctx.clearRect(0,0,c.width,c.height);
        ctx.strokeStyle = '#FFFFFF';
        ctx.moveTo(0, 50);
        ctx.lineTo(c.width, 50);
        ctx.stroke();
    }

    function drawHorseBlanco(x, y)
    {
        ctx.drawImage(caballo1, x, y);
    }

    function drawHorseNegro(x, y) {
        ctx.drawImage(caballo2, x, y);
    }
</script>
Hasta el momento tenemos el campo de carreras y los 2 caballos listos para competir, pero no hemos hecho prácticamente nada respecto a lo lógica de server ni hemos implementado todavía ningún aspecto de la comunicación WebSockets.

Llegado este punto ejecutaría el proyecto y veremos si todo está ok hasta aquí. En principio si indicamos la url http://[Host]/HorseRace/Index deberíamos visualizar algo como esto de la imagen.

WebSockets en MVC - Ejemplo práctico Horse Race


Dejamos por un rato el HTML-javascript de la parte cliente y vamos a definir la lógica de servidor en este caso tampoco me rompo los cuernos. Se trata de avanzar al azar la posición de los 2 caballos hasta que uno llega a la meta. Creamos una carpeta Business en el mismo proyecto y añadimos una clase Race.cs que se encargará de desplazar la coordenada X de ambos caballos de manera aleatoria hasta llegar a una posición limite. Es importante que el método InitHorseRace esté implementado de forma que puede ejecutarse de manera asíncrona.
namespace HorseRace.Business
{
    public class Race
    {
        public const int maxpositionX = 550;
        public List<int> mPosicionesCaballos = new List<int>() { 1, 1 };

        public Task InitHorseRace() 
        {
            return Task.Run(() =>
            {
                while (mPosicionesCaballos[0] < maxpositionX && mPosicionesCaballos[1] < maxpositionX)
                {
                    Thread.Sleep(50);
                    Movement();
                }
            }, new CancellationToken());

        }

        public void Movement() 
        {
            Random r = new Random();
            mPosicionesCaballos[0] += r.Next(1, 5);
            mPosicionesCaballos[1] += r.Next(1, 5);
        }

        public List<int> GetMovement() 
        {
            return mPosicionesCaballos;
        }
    }
}
Ahora ya sí vamos a crear la comunicación WebSockets entre cliente y servidor. Pulsamos botón derecho sobre la carpeta Controllers y añadimos un controlador en blanco. Hacemos que el controlador herede de ApiController, si partimos de un proyecto MVC vacío tendremos que añadir a global.asax el mappeo de la ruta.

namespace HorseRace.Controllers
{
  public class WsHorseRaceController : ApiController
  {
    //
    // Get: api/WsHorseRace/
    public HttpResponseMessage Get()
    {
      if (System.Web.HttpContext.Current.IsWebSocketRequest)
      {
        System.Web.HttpContext.Current.AcceptWebSocketRequest(ProcessWsRace);
      }
      return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
    }

    private async Task ProcessWsRace(AspNetWebSocketContext context)
    {  
      Task tRace;
      try
      {
        WebSocket socket = context.WebSocket;
        while (true)
        {
          ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]);
          WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None);
          if (socket.State == WebSocketState.Open)
          {
            string userMessage = Encoding.UTF8.GetString(buffer.Array, 0, result.Count);
            switch (userMessage)
            {
              case "init":
                Race BusinessHorseRace = new Race();
                tRace = BusinessHorseRace.InitHorseRace();
                while (!tRace.IsCompleted)
                {
                  Thread.Sleep(50);
                  List<int> positions = BusinessHorseRace.GetMovement();

                  buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(System.Web.Helpers.Json.Encode(positions)));
                  await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
                }
               break;
            }
          }
          else
          {
            break;
          }
        }
      }
      catch (Exception ex) 
      {
        throw ex;
      }
  }
}
Mappeo de la ruta para ApiController.
GlobalConfiguration.Configuration.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
En cliente (vista) añadiremos un botón que iniciará la conexión con el servidor... una vez iniciada el servidor irá enviando "comandos" para que cliente pueda mostrar como se van moviendo los caballos a medida que avance la carrera en servidor.
//añadir este botón debajo del canvas en index.shtml
<input type="button" id="btnConnect" value="Iniciar Carrera" />
//añadir este código js en la sección "script" de la vista index.shtml.
    var ws;
    $().ready(function () {
        $('#btnConnect').click(function () {
            ws = new WebSocket('ws://' + window.location.hostname + '/api/WsHorseRace');
            ws.onopen = function () {
                ws.send('init');
            };
            ws.onmessage = function (evt) {
                var positions = $.parseJSON(evt.data);
                initField();
                drawHorseBlanco(positions[0], yC1);
                drawHorseNegro(positions[1], yC2);
            };
            ws.onerror = function (evt) {
                alert(evt.message);
                ws.close();
            };
            ws.onclose = function () {
            };
        });
Ahora si pulsamos F5 ya deberíamos ver como los caballos al pulsar "Iniciar Carrera" se empiezan a mover por el eje X cada 50 milisegundos aleatoriamente entre 1 y 5 coordenadas. Tal y como está montada la lógica en servidor el que tenga más suerte con el random ganará la carrera.

WebSockets en MVC - Ejemplo práctico Horse Race

WebSockets en MVC - Ejemplo práctico Horse Race

En el ejemplo faltaría controlar cuando la carrera acaba, cerrar la conexión ws y mostrar una etiqueta en cliente mostrando el caballo ganador....

Consideraciones:
  • Este tipo de protocolo en servidor no está soportado por todas las versiones de IIS. Tened en cuenta que con la versión IIS Express de Visual Studio no funciona, tendréis que crearos el site en el servidor web local de la máquina.
  • La parte cliente también requiere una versión reciente del navegador, si alguien está planteándose implementar este tipo de comunicación en algún proyecto aseguraos a partir de que versión de cada navegador está soportado. Recomiendo echarle un ojo a SignalR.
  • En parte servidor para implementar el controlar que activa la conexión WebSocket usamos clases que pertenecen al ensamblado System.Net, System.Net.Http y System.Net.WebSockets. Depende que paquetes tengáis instalados o no en vuestro entorno es posible que tengáis que tirar de NuGet para obtener estos ensamblados.
  • La parte cliente necesita una referencia Jquery.
  • En el ejemplo hemos optado por implementar WebSockets en un proyecto MVC pero podríamos haberlo implementado también en ASP .NET tradicional.
  • Hemos optado en cliente por conectar al servicio usando código javacript pero también se podría crear un cliente con código .NET usando el mismo ensamblado System.Net.WebSockets.

Hasta aquí el artículo de hoy, el siguiente veremos el mismo caso práctico pero usando SignalR 2.0. Si alguien está interesado en que le envíe el código contactad conmigo vía linkedin o google. Como siempre cualquier comentario o debate es bienvenido y recordaros que podéis seguir areaTIC en las redes sociales!

No hay comentarios:

Publicar un comentario en la entrada