Tutorial: Difusión de servidores con SignalR 2

Advertencia

Esta documentación no se aplica a la última versión de SignalR. Eche un vistazo a SignalR de ASP.NET Core.

Este tutorial muestra cómo crear una aplicación web que use SignalR 2 de ASP.NET para ofrecer la función de difusión de servidores. La difusión de servidores significa que es el servidor el que inicia las comunicaciones que se envían a los clientes.

La aplicación que va a crear con este tutorial simula un teletipo de bolsa (StockTicker), una situación propia de la función de difusión de servidores. Periódicamente, el servidor actualiza aleatoriamente los precios de las acciones y envía las actualizaciones a todos los clientes conectados. En el explorador, los números y símbolos de las columnas Change (Cambio) y % cambian dinámicamente en respuesta a las notificaciones del servidor. Si abre otros exploradores en la misma URL, todos mostrarán simultáneamente los mismos datos y los mismos cambios en los datos.

Screenshot showing how multiple web browsers show the same updated data simultaneously.

En este tutorial, hizo lo siguiente:

  • Creación del proyecto
  • Configuración del código de servidor
  • Examen del código del servidor
  • Configuración del código de cliente
  • Examen del código de cliente
  • Prueba de la aplicación
  • Habilitar registro

Importante

Si no quiere seguir los pasos para compilar la aplicación, puede instalar el paquete SignalR.Sample en un nuevo proyecto de Aplicación web de ASP.NET vacía. Si instala el paquete NuGet sin pasar por los pasos de este tutorial, deberá seguir las instrucciones del archivo readme.txt. Para ejecutar el paquete, debe agregar una clase de startup de OWIN que llame al método ConfigureSignalR en el paquete instalado. Se emitirá un error si no agrega la clase de startup de OWIN. Consulte la sección Instalación del ejemplo StockTicker en este artículo.

Requisitos previos

Creación del proyecto

En esta sección se muestra cómo usar Visual Studio 2017 para crear una aplicación web ASP.NET vacía.

  1. En Visual Studio, cree una aplicación web de ASP.NET.

    Screenshot showing how to create an ASP.NET Web Application.

  2. En la ventana Nueva aplicación web de ASP.NET Web - SignalR.StockTicker, deje seleccionada la opción Vacía y seleccione Aceptar.

Configuración del código de servidor

En esta sección, configurará el código que se ejecuta en el servidor.

Creación de la clase Stock (acción)

Comenzará creando la clase de modelo Stock, que usará para almacenar y transmitir información sobre una acción.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>clase.

  2. Asigne al proyecto el nombre Stock y agréguelo al proyecto.

  3. Sustituya el código en el archivo Stock.cs por el código siguiente:

    using System;
    
    namespace SignalR.StockTicker
    {
        public class Stock
        {
            private decimal _price;
    
            public string Symbol { get; set; }
    
            public decimal Price
            {
                get
                {
                    return _price;
                }
                set
                {
                    if (_price == value)
                    {
                        return;
                    }
    
                    _price = value;
    
                    if (DayOpen == 0)
                    {
                        DayOpen = _price;
                    }
                }
            }
    
            public decimal DayOpen { get; private set; }
    
            public decimal Change
            {
                get
                {
                    return Price - DayOpen;
                }
            }
    
            public double PercentChange
            {
                get
                {
                    return (double)Math.Round(Change / Price, 4);
                }
            }
        }
    }
    

    Las dos propiedades que establecerá al crear acciones son Symbol (por ejemplo, MSFT para Microsoft) y Price. Las demás propiedades dependen de cómo y cuándo establezca Price. La primera vez que establezca Price, el valor se propagará a DayOpen. Posteriormente, al configurar Price, la aplicación calcula los valores de propiedad Change y PercentChange en función de la diferencia entre Price y DayOpen.

Creación de las clases StockTicker y StockTickerHub

Va a utilizar la API de SignalR Hub para controlar la interacción entre servidor y cliente. Una clase StockTickerHub que derive de la clase Hub de SignalR controlará la recepción de conexiones y llamadas a métodos de los clientes. También debe mantener los datos de las acciones y ejecutar un objeto Timer. El objeto Timer desencadenará periódicamente actualizaciones de precios, independientemente de las conexiones de cliente. No es posible colocar estas funciones en una clase Hub, ya que los concentradores son transitorios. La aplicación crea una instancia de clase Hub para cada tarea del concentrador, como las conexiones y las llamadas desde el cliente al servidor. Por ello, el mecanismo que mantiene los datos de acciones, actualiza los precios y difunde las actualizaciones de precios tiene que ejecutarse en una clase separada. Esta clase se denominará como StockTicker.

Broadcasting from StockTicker

Solo quiere que se ejecute en el servidor una instancia de la clase StockTicker, por lo que deberá configurar una referencia de cada instancia StockTickerHub a la instancia singleton StockTicker. La clase StockTicker tiene que retransmitirse a los clientes porque tiene los datos de las acciones y activa actualizaciones, pero StockTicker no es una clase Hub. La clase StockTicker tiene que obtener una referencia al objeto de contexto de conexión de concentrador SignalR. Después, puede usar el objeto de contexto de conexión de SignalR para retransmitir a los clientes.

Creación de StockTickerHub.cs

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>nuevo elemento.

  2. En Agregar nuevo elemento - SignalR.StockTicker, seleccione Instalado>Visual C#>Web>SignalR y, a continuación, seleccione Clase de concentrador de SignalR (v2).

  3. Asigne a la clase el nombre StockTickerHub y añádala al proyecto.

    Este paso crea el archivo de clase StockTickerHub.cs. Al mismo tiempo, agrega al proyecto un conjunto de archivos de script y referencias de ensamblado compatible con SignalR.

  4. Sustituya el código en el archivo StockTickerHub.cs por el código siguiente:

    using System.Collections.Generic;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        [HubName("stockTickerMini")]
        public class StockTickerHub : Hub
        {
            private readonly StockTicker _stockTicker;
    
            public StockTickerHub() : this(StockTicker.Instance) { }
    
            public StockTickerHub(StockTicker stockTicker)
            {
                _stockTicker = stockTicker;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stockTicker.GetAllStocks();
            }
        }
    }
    
  5. Guarde el archivo.

La clase Hub (concentrador) se usa para definir los métodos a los que los clientes pueden llamar en el servidor. Está definiendo un método: GetAllStocks(). Cuando un cliente se conecta inicialmente al servidor, llamará a este método para obtener una lista de todas las acciones con sus precios actuales. El método puede ejecutarse de forma sincrónica y devolver como resultado IEnumerable<Stock>, ya que devuelve datos de la memoria.

Si el método tuviera que obtener los datos haciendo algo que implicara una espera, como una búsqueda en la base de datos o una llamada a un servicio web, se especificaría Task<IEnumerable<Stock>> como valor de retorno para habilitar el procesamiento asincrónico. Para más información, consulte Guía de la API de ASP.NET SignalR Hubs - Servidor: Cuándo ejecutar asincrónicamente.

El atributo HubName especifica cómo hará referencia la aplicación al centro en código JavaScript en el cliente. Si no se usa este atributo, el nombre predeterminado en el cliente es una versión con formato camelCase del nombre de la clase, que, en este caso, sería stockTickerHub.

Como verá más adelante al crear la clase StockTicker, la aplicación crea una instancia singleton de esa clase en su propiedad Instance estática. Esa instancia singleton de StockTicker está en la memoria independientemente del número de clientes que se conecten o desconecten. Esa instancia es lo que utiliza el método GetAllStocks() para devolver como resultado la información actualizada de las acciones.

Creación de StockTicker.cs

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>clase.

  2. Asigne a la clase el nombre StockTicker y añádala al proyecto.

  3. Sustituya el código en el archivo StockTicker.cs por el código siguiente:

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Threading;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    
    namespace SignalR.StockTicker
    {
        public class StockTicker
        {
            // Singleton instance
            private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
            private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
            private readonly object _updateStockPricesLock = new object();
    
            //stock can go up or down by a percentage of this factor on each change
            private readonly double _rangePercent = .002;
    
            private readonly TimeSpan _updateInterval = TimeSpan.FromMilliseconds(250);
            private readonly Random _updateOrNotRandom = new Random();
    
            private readonly Timer _timer;
            private volatile bool _updatingStockPrices = false;
    
            private StockTicker(IHubConnectionContext<dynamic> clients)
            {
                Clients = clients;
    
                _stocks.Clear();
                var stocks = new List<Stock>
                {
                    new Stock { Symbol = "MSFT", Price = 30.31m },
                    new Stock { Symbol = "APPL", Price = 578.18m },
                    new Stock { Symbol = "GOOG", Price = 570.30m }
                };
                stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));
    
                _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
    
            }
    
            public static StockTicker Instance
            {
                get
                {
                    return _instance.Value;
                }
            }
    
            private IHubConnectionContext<dynamic> Clients
            {
                get;
                set;
            }
    
            public IEnumerable<Stock> GetAllStocks()
            {
                return _stocks.Values;
            }
    
            private void UpdateStockPrices(object state)
            {
                lock (_updateStockPricesLock)
                {
                    if (!_updatingStockPrices)
                    {
                        _updatingStockPrices = true;
    
                        foreach (var stock in _stocks.Values)
                        {
                            if (TryUpdateStockPrice(stock))
                            {
                                BroadcastStockPrice(stock);
                            }
                        }
    
                        _updatingStockPrices = false;
                    }
                }
            }
    
            private bool TryUpdateStockPrice(Stock stock)
            {
                // Randomly choose whether to update this stock or not
                var r = _updateOrNotRandom.NextDouble();
                if (r > .1)
                {
                    return false;
                }
    
                // Update the stock price by a random factor of the range percent
                var random = new Random((int)Math.Floor(stock.Price));
                var percentChange = random.NextDouble() * _rangePercent;
                var pos = random.NextDouble() > .51;
                var change = Math.Round(stock.Price * (decimal)percentChange, 2);
                change = pos ? change : -change;
    
                stock.Price += change;
                return true;
            }
    
            private void BroadcastStockPrice(Stock stock)
            {
                Clients.All.updateStockPrice(stock);
            }
    
        }
    }
    

Dado que todos los subprocesos ejecutarán la misma instancia del código de StockTicker, la clase StockTicker tiene que ser segura para subprocesos.

Examen del código del servidor

Si examina el código del servidor, comprenderá enseguida cómo funciona la aplicación.

Almacenamiento de la instancia singleton en un campo estático

El código inicia el campo estático _instance que respalda la propiedad Instance con una instancia de la clase. Dado que el constructor es privado, es la única instancia de la clase que puede crear la aplicación. La aplicación recurre a la inicialización diferida para el campo _instance. Esto no se debe a motivos de rendimiento; se trata de asegurarse de que la creación de la instancia sea segura para subprocesos.

private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

public static StockTicker Instance
{
    get
    {
        return _instance.Value;
    }
}

Cada vez que un cliente se conecta al servidor, una nueva instancia de la clase StockTickerHub que se ejecuta en un subproceso separado obtiene la instancia singleton StockTicker de la propiedad estática StockTicker.Instance, como ha visto anteriormente en la clase StockTickerHub.

Almacenamiento de datos de acciones en un ConcurrentDictionary

El constructor inicializa la colección _stocks con algunos datos de acciones de ejemplo, y GetAllStocks devuelve las acciones. Como hemos visto anteriormente, esta colección de acciones se devuelve mediante StockTickerHub.GetAllStocks, que es un método de servidor en la clase Hub al que pueden llamar los clientes.

private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    _stocks.Clear();
    var stocks = new List<Stock>
    {
        new Stock { Symbol = "MSFT", Price = 30.31m },
        new Stock { Symbol = "APPL", Price = 578.18m },
        new Stock { Symbol = "GOOG", Price = 570.30m }
    };
    stocks.ForEach(stock => _stocks.TryAdd(stock.Symbol, stock));

    _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
}

public IEnumerable<Stock> GetAllStocks()
{
    return _stocks.Values;
}

La colección de acciones se define como un tipo ConcurrentDictionary en aras de la seguridad para subprocesos. Como alternativa, puede usar un objeto Dictionary y bloquear explícitamente el diccionario cuando realice cambios en él.

Para esta aplicación de ejemplo, no pasa nada por almacenar los datos de la aplicación en memoria y perderlos cuando la aplicación elimina la instancia StockTicker. En una aplicación real, trabajaría con un almacén de datos de back-end, como si fuera una base de datos.

Actualización periódica de los precios de las acciones

El constructor pone en marcha un objeto Timer que llama periódicamente a métodos que actualizan los precios de las acciones de forma aleatoria.

_timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);

private void UpdateStockPrices(object state)
{
    lock (_updateStockPricesLock)
    {
        if (!_updatingStockPrices)
        {
            _updatingStockPrices = true;

            foreach (var stock in _stocks.Values)
            {
                if (TryUpdateStockPrice(stock))
                {
                    BroadcastStockPrice(stock);
                }
            }

            _updatingStockPrices = false;
        }
    }
}

private bool TryUpdateStockPrice(Stock stock)
{
    // Randomly choose whether to update this stock or not
    var r = _updateOrNotRandom.NextDouble();
    if (r > .1)
    {
        return false;
    }

    // Update the stock price by a random factor of the range percent
    var random = new Random((int)Math.Floor(stock.Price));
    var percentChange = random.NextDouble() * _rangePercent;
    var pos = random.NextDouble() > .51;
    var change = Math.Round(stock.Price * (decimal)percentChange, 2);
    change = pos ? change : -change;

    stock.Price += change;
    return true;
}

Timer llama a UpdateStockPrices, que pasa null en el parámetro de estado. Antes de actualizar los precios, la aplicación toma un bloqueo en el objeto _updateStockPricesLock. El código comprueba si otro subproceso ya está actualizando los precios, y, entonces, llama a TryUpdateStockPrice en cada acción de la lista. El método TryUpdateStockPrice determina si se cambia el precio de las acciones y por cuánto. Si cambia el precio de las acciones, la aplicación llama BroadcastStockPrice para difundir el cambio de precio de la acción a todos los clientes conectados.

La marca _updatingStockPrices está designada como volátil para asegurarse de que sea segura para subprocesos.

private volatile bool _updatingStockPrices = false;

En una aplicación real, el método TryUpdateStockPrice llamaría a un servicio web para consultar el precio. En este código, la aplicación usa un generador de números aleatorios para aplicar cambios aleatoriamente.

Obtención del contexto de SignalR para que la clase StockTicker pueda transmitir a los clientes

Dado que los cambios de precio se originan, en este caso, en el objeto StockTicker, es el objeto el que necesita llamar a un método updateStockPrice en todos los clientes conectados. En una clase Hub, dispone de una API para llamar a métodos de cliente, pero StockTicker no se deriva de la clase Hub y carece de referencia a ningún objeto Hub. Para difundir a los clientes conectados, la clase StockTicker tiene que obtener la instancia de contexto de SignalR para la clase StockTickerHub y usarla para llamar a métodos en los clientes.

El código obtiene una referencia al contexto de SignalR cuando crea la instancia de clase singleton, se pasa esa referencia al constructor y este la coloca en la propiedad Clients.

Hay dos motivos que justifican la adquisición el contexto solo una vez: obtener el contexto es una operación costosa, y obtenerlo una sola vez garantiza que la aplicación mantiene el orden previsto de los mensajes enviados a los clientes.

private readonly static Lazy<StockTicker> _instance =
    new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

private StockTicker(IHubConnectionContext<dynamic> clients)
{
    Clients = clients;

    // Remainder of constructor ...
}

private IHubConnectionContext<dynamic> Clients
{
    get;
    set;
}

private void BroadcastStockPrice(Stock stock)
{
    Clients.All.updateStockPrice(stock);
}

Obtener la propiedad Clients del contexto y colocarla en la propiedad StockTickerClient le permite escribir código para llamar a los métodos del cliente con el mismo aspecto que tendría en una clase Hub. Por ejemplo, puede escribir Clients.All.updateStockPrice(stock) para difundir a todos los clientes.

El método updateStockPrice al que llama en BroadcastStockPrice no existe aún; lo agregará más adelante al escribir código que se ejecute en el cliente. Puede referirse a updateStockPrice en este punto porque Clients.All es dinámico, lo que significa que la aplicación evaluará la expresión en tiempo de ejecución. Cuando se ejecute esta llamada a método, SignalR enviará el nombre del método y el valor del parámetro al cliente; si este dispone de un método llamado updateStockPrice, se llamará a ese método y se le transmitirá el valor del parámetro.

Clients.All significa enviar a todos los clientes. SignalR le ofrece otras opciones para especificar a qué clientes o grupos de clientes enviar. Para más información, véase HubConnectionContext.

Registro de la ruta de SignalR

El servidor necesita saber qué URL interceptar y dirigir a SignalR. Para proceder, agregue una clase de startup de OWIN.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>nuevo elemento.

  2. En Agregar nuevo elemento - SignalR.StockTicker seleccione Instalado>Visual C#>Web y, a continuación, seleccione Clase de startup de OWIN.

  3. Asigne a la clase el nombre Startup y seleccione Aceptar.

  4. Sustituya el código predeterminado del archivo Startup.cs por este código:

    using System;
    using System.Threading.Tasks;
    using Microsoft.Owin;
    using Owin;
    
    [assembly: OwinStartup(typeof(SignalR.StockTicker.Startup))]
    
    namespace SignalR.StockTicker
    {
        public class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                // Any connection or hub wire up and configuration should go here
                app.MapSignalR();
            }
    
        }
    }
    

Ya ha finalizado la configuración del código del servidor. En la siguiente sección, establecerá la configuración del cliente.

Configuración del código de cliente

En esta sección, configurará el código que se ejecuta en el cliente.

Creación de la página HTML y el archivo JavaScript

La página HTML mostrará los datos y el archivo JavaScript los organizará.

Creación de StockTicker.html

En primer lugar, agregará el cliente HTML.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>página HTML.

  2. Asigne al archivo el nombre StockTicker y seleccione Aceptar.

  3. Sustituya el código predeterminado del archivo StockTicker.html por este código:

    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>ASP.NET SignalR Stock Ticker</title>
        <style>
            body {
                font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
                font-size: 16px;
            }
            #stockTable table {
                border-collapse: collapse;
            }
                #stockTable table th, #stockTable table td {
                    padding: 2px 6px;
                }
                #stockTable table td {
                    text-align: right;
                }
            #stockTable .loading td {
                text-align: left;
            }
        </style>
    </head>
    <body>
        <h1>ASP.NET SignalR Stock Ticker Sample</h1>
    
        <h2>Live Stock Table</h2>
        <div id="stockTable">
            <table border="1">
                <thead>
                    <tr><th>Symbol</th><th>Price</th><th>Open</th><th>Change</th><th>%</th></tr>
                </thead>
                <tbody>
                    <tr class="loading"><td colspan="5">loading...</td></tr>
                </tbody>
            </table>
        </div>
    
        <!--Script references. -->
        <!--Reference the jQuery library. -->
        <script src="/Scripts/jquery-1.10.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-2.1.0.js"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="/signalr/hubs"></script>
        <!--Reference the StockTicker script. -->
        <script src="StockTicker.js"></script>
    </body>
    </html>
    

    El HTML crea una tabla con cinco columnas, una fila de encabezado y una fila de datos con una única celda que abarca las cinco columnas. La fila de datos muestra brevemente «Cargando...» al iniciar la aplicación. El código JavaScript eliminará esa fila y agregará, en su lugar, filas con datos de acciones recuperados del servidor.

    Las etiquetas de script especifican:

    • el archivo de script jQuery;

    • el archivo principal de script de SignalR;

    • el archivo de script de proxies de SignalR;

    • un archivo de script StockTicker que creará más adelante.

    La aplicación genera dinámicamente el archivo de script de proxies de SignalR. Especifica la dirección URL "/signalr/hubs" y define los métodos de proxy para los métodos de la clase Hub, en este caso, para StockTickerHub.GetAllStocks. Si lo prefiere, puede generar este archivo JavaScript manualmente mediante las utilidades de SignalR. No olvide deshabilitar la creación de archivos dinámicos en la llamada al método MapHubs.

  4. En el Explorador de soluciones, expanda Scripts.

    Las bibliotecas de scripts para jQuery y SignalR están visibles en el proyecto.

    Importante

    El administrador de paquetes instalará una versión posterior de los scripts de SignalR.

  5. Actualice las referencias de script en el bloque de código para que se correspondan con las versiones de los archivos de script del proyecto.

  6. En Explorador de soluciones, haga clic con el botón derecho del ratón en StockTicker.html, y después haga clic en Establecer como página de inicio.

Creación de StockTicker.js

Ahora, cree el archivo de JavaScript.

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Agregar>archivo de JavaScript.

  2. Asigne al archivo el nombre StockTicker y seleccione Aceptar.

  3. Agregue este código al archivo StockTicker.js:

    // 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 () {
    
        var ticker = $.connection.stockTickerMini, // the generated client-side hub proxy
            up = '▲',
            down = '▼',
            $stockTable = $('#stockTable'),
            $stockTableBody = $stockTable.find('tbody'),
            rowTemplate = '<tr data-symbol="{Symbol}"><td>{Symbol}</td><td>{Price}</td><td>{DayOpen}</td><td>{Direction} {Change}</td><td>{PercentChange}</td></tr>';
    
        function formatStock(stock) {
            return $.extend(stock, {
                Price: stock.Price.toFixed(2),
                PercentChange: (stock.PercentChange * 100).toFixed(2) + '%',
                Direction: stock.Change === 0 ? '' : stock.Change >= 0 ? up : down
            });
        }
    
        function init() {
            ticker.server.getAllStocks().done(function (stocks) {
                $stockTableBody.empty();
                $.each(stocks, function () {
                    var stock = formatStock(this);
                    $stockTableBody.append(rowTemplate.supplant(stock));
                });
            });
        }
    
        // Add a client-side hub method that the server will call
        ticker.client.updateStockPrice = function (stock) {
            var displayStock = formatStock(stock),
                $row = $(rowTemplate.supplant(displayStock));
    
            $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
                .replaceWith($row);
            }
    
        // Start the connection
        $.connection.hub.start().done(init);
    
    });
    

Examen del código de cliente

Si examina el código de cliente, entenderá enseguida cómo interactúa el código de cliente con el código de servidor para que la aplicación funcione.

Inicio de la conexión

$.connection hace referencia a los servidores proxy de SignalR. El código obtiene una referencia al proxy de la clase StockTickerHub y la coloca en la variable ticker. El nombre del proxy es el que se estableció con el atributo HubName:

var ticker = $.connection.stockTickerMini
[HubName("stockTickerMini")]
public class StockTickerHub : Hub

Una vez definidas todas las variables y funciones, la última línea de código del archivo inicializa la conexión de SignalR llamando a la función start de SignalR. La función start se ejecuta de forma asincrónica y devuelve un objeto jQuery Deferred. Puede llamar a la función done para especificar la función a la que se llamará cuando la aplicación finalice la acción asincrónica.

$.connection.hub.start().done(init);

Obtención de todas las acciones

La función init llama a la función getAllStocks del servidor y usa la información que este devuelve para actualizar la tabla de acciones. Tenga en cuenta que, de forma predeterminada, debe usar camelCasing en el cliente, aunque el nombre del método en el servidor esté en Pascal. La regla de camelCasing solo se aplica a los métodos, no a los objetos; por ejemplo, hace referencia a stock.Symbol y stock.Price, no a stock.symbol o stock.price.

function init() {
    ticker.server.getAllStocks().done(function (stocks) {
        $stockTableBody.empty();
        $.each(stocks, function () {
            var stock = formatStock(this);
            $stockTableBody.append(rowTemplate.supplant(stock));
        });
    });
}
public IEnumerable<Stock> GetAllStocks()
{
    return _stockTicker.GetAllStocks();
}

En el método init, la aplicación crea HTML para una fila de tabla para cada objeto de acción recibido del servidor; a tal efecto, llama a formatStock para dar formato a las propiedades del objeto stock y, a continuación, llama a supplant para reemplazar los marcadores de posición de la variable rowTemplate por los valores de propiedad del objeto stock. El HTML resultante se añade después a la tabla de acciones.

Nota:

Para llamar a init, este se transmite como una función callback que se ejecuta una vez finalizada la función asincrónica start. Si llamara a init como una instrucción JavaScript independiente después de llamar a start, la función fallaría porque se ejecutaría inmediatamente sin esperar a que la función start terminara de establecer la conexión. En ese caso, la función init intentaría llamar a la función getAllStocks antes de que la aplicación establezca una conexión de servidor.

Obtención de precios de acciones actualizados

Cuando el servidor cambia el precio de una acción, llama a updateStockPrice en los clientes conectados. La aplicación agrega la función a la propiedad de cliente del proxy stockTicker para que esté disponible para las llamadas desde el servidor.

ticker.client.updateStockPrice = function (stock) {
    var displayStock = formatStock(stock),
        $row = $(rowTemplate.supplant(displayStock));

    $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
        .replaceWith($row);
    }

La función updateStockPrice da formato a un objeto de acción recibido del servidor en una fila de la tabla, del mismo modo que ocurre con la función init. En lugar de añadir la fila a la tabla, busca la fila actual de la acción en la tabla y sustituye esa fila por la nueva.

Prueba de la aplicación

Puede probar la aplicación para asegurarse de que funciona. Verá que todas las ventanas del explorador muestran la tabla de acciones activas con los precios de las acciones en fluctuación.

  1. En la barra de herramientas, active Depuración de scripts y, a continuación, seleccione el botón de reproducción para ejecutar la aplicación en modo de depuración.

    Screenshot of user turning on debugging mode and selecting play.

    Se abrirá una ventana del explorador que muestra la tabla de acciones en tiempo real. La tabla de acciones muestra inicialmente la línea «Cargando...»; al poco tiempo, se muestran los datos iniciales de las acciones, y después comienzan a cambiar los precios.

  2. Copie la dirección URL del explorador, abra otros dos exploradores y pegue las direcciones URL en las barras de dirección.

    La visualización inicial de acciones es la misma que la del primer explorador y los cambios se producen simultáneamente.

  3. Cierre todos los exploradores, abra uno nuevo y acceda a la misma URL.

    El objeto singleton StockTicker ha seguido ejecutándose en el servidor. La tabla de acciones en tiempo real muestra cómo han seguido cambiando las acciones. No verá la tabla inicial con sin cambios en las cifras.

  4. Cierre el explorador.

Habilitar registro

SignalR lleva integrada una función de registro que se puede habilitar en el cliente para ayudar a solucionar problemas. En esta sección, se habilita el registro y se muestran ejemplos que ilustran cómo los registros le indican cuál de los siguientes métodos de transporte está usando SignalR:

Para cualquier conexión, SignalR elige el mejor método de transporte que admitan tanto el servidor como el cliente.

  1. Abra StockTicker.js.

  2. Agregue la línea de código resaltada para habilitar el registro inmediatamente antes del código que inicializa la conexión al final del archivo:

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  3. Presione F5 para ejecutar el proyecto.

  4. Abra la ventana de herramientas de desarrollo de su explorador y seleccione la consola para ver los registros. Puede que tenga que actualizar la página para ver los registros de SignalR negociando el método de transporte para una nueva conexión.

    • Si está ejecutando Internet Explorer 10 en Windows 8 (IIS 8), el método de transporte es WebSockets.

    • Si está ejecutando Internet Explorer 10 en Windows 7 (IIS 7.5), el método de transporte es iframe.

    • Si está ejecutando Firefox 19 en Windows 8 (IIS 8), el método de transporte es WebSockets.

      Sugerencia

      En Firefox, instale el complemento Firebug para obtener una ventana de consola.

    • Si está ejecutando Firefox 19 en Windows 7 (IIS 7.5), el método de transporte es el de eventos enviados por el servidor.

Instalación del ejemplo de StockTicker

Microsoft.AspNet.SignalR.Sample instala la aplicación StockTicker. El paquete NuGet incluye más características que la versión simplificada que ha creado desde cero. En esta sección del tutorial, instalará el paquete NuGet y revisará las nuevas características y el código que las ejecuta.

Importante

Si instala el paquete sin llevar a cabo los pasos anteriores de este tutorial, debe agregar al proyecto una clase de startup de OWIN. Este paso se explica en este archivo readme.txt del paquete NuGet.

Instalación del paquete NuGet SignalR.Sample

  1. En el Explorador de soluciones, haga clic con el botón derecho en el proyecto y seleccione Administrar paquetes NuGet.

  2. En Administrador de paquetes NuGet: SignalR.StockTicker, seleccione Examinar.

  3. En Origen del paquete, seleccione nuget.org.

  4. Introduzca SignalR.Sample en el cuadro de búsqueda y seleccione Microsoft.AspNet.SignalR.Sample>Instalar.

  5. En el Explorador de soluciones, expanda la carpeta SignalR.Sample.

    La instalación del paquete SignalR.Sample conlleva la creación de la carpeta y su contenido.

  6. En la carpeta SignalR.Sample, haga clic con el botón derecho del ratón en StockTicker.html, y después seleccione Establecer como página de inicio.

    Nota:

    La instalación del paquete NuGet SignalR.Sample podría cambiar la versión de jQuery que tiene en su carpeta Scripts. El nuevo archivo StockTicker.html que el paquete instala en la carpeta SignalR.Sample estará sincronizado con la versión de jQuery que el paquete instale, pero si quiere volver a ejecutar su archivo original StockTicker.html, es posible que primero tenga que actualizar la referencia a jQuery en la etiqueta de script.

Ejecución de la aplicación

La tabla que vio en la primera aplicación tenía características útiles. La aplicación de teletipo de bolsa completa muestra nuevas características: una ventana de desplazamiento horizontal que muestra los datos de las acciones y cambios de color a medida que las acciones suben y bajan.

  1. Presione F5 para ejecutar la aplicación.

    Cuando ejecuta la aplicación por primera vez, el "mercado" está "cerrado", y se ve una cuadrícula estática y una ventana de teletipo que no se desplaza.

  2. Seleccione Abrir mercado.

    Screenshot of the live ticker.

    • El cuadro del teletipo de bolsa en directo comienza a desplazarse horizontalmente y el servidor empieza a emitir periódicamente los cambios de precio de las acciones de forma aleatoria.

    • Cada vez que cambia el precio de una acción, la aplicación actualiza tanto la tabla de bolsa en directo como el teletipo de bolsa en directo.

    • Cuando el cambio de precio de una acción es positivo, la aplicación muestra las acciones con un fondo verde.

    • Cuando el cambio es negativo, la aplicación muestra las acciones con un fondo rojo

  3. Seleccione Cerrar mercado.

    • La tabla deja de actualizarse.

    • El teletipo deja de desplazarse.

  4. Seleccione Restablecer.

    • Se restablecen todos los datos de las acciones.

    • La aplicación restaura el estado inicial antes de que empezaran a cambiar los precios.

  5. Copie la dirección URL del explorador, abra otros dos exploradores y pegue las direcciones URL en las barras de dirección.

  6. Verá los mismos datos actualizados dinámicamente al mismo tiempo en cada explorador.

  7. Al seleccionar cualquiera de los controles, todos los exploradores responden de la misma manera al mismo tiempo.

Visualización del teletipo de bolsa en directo

La visualización del teletipo de bolsa en directo es una lista desordenada en un <div> elemento al que se aplica formato en una sola línea mediante estilos CSS. El teletipo se inicializa y actualiza del mismo modo que la tabla: se sustituyen los marcadores de posición en una cadena de plantilla <li> y se añaden dinámicamente los elementos <li> al elemento <ul>. La aplicación incluye el desplazamiento mediante la función animate jQuery para variar el margen izquierdo de la lista desordenada dentro de <div>.

SignalR.Sample StockTicker.html

El código HTML del teletipo de bolsa:

<h2>Live Stock Ticker</h2>
<div id="stockTicker">
    <div class="inner">
        <ul>
            <li class="loading">loading...</li>
        </ul>
    </div>
</div>

SignalR.Sample StockTicker.css

El código CSS del teletipo de bolsa:

#stockTicker {
    overflow: hidden;
    width: 450px;
    height: 24px;
    border: 1px solid #999;
    }

    #stockTicker .inner {
        width: 9999px;
    }

    #stockTicker ul {
        display: inline-block;
        list-style-type: none;
        margin: 0;
        padding: 0;
    }

    #stockTicker li {
        display: inline-block;
        margin-right: 8px;   
    }

    /*<li data-symbol="{Symbol}"><span class="symbol">{Symbol}</span><span class="price">{Price}</span><span class="change">{PercentChange}</span></li>*/
    #stockTicker .symbol {
        font-weight: bold;
    }

    #stockTicker .change {
        font-style: italic;
    }

SignalR.Sample SignalR.StockTicker.js

El código jQuery que hace que se desplace:

function scrollTicker() {
    var w = $stockTickerUl.width();
    $stockTickerUl.css({ marginLeft: w });
    $stockTickerUl.animate({ marginLeft: -w }, 15000, 'linear', scrollTicker);
}

Métodos adicionales en el servidor a los que el cliente puede llamar

Para aportar flexibilidad a la aplicación, hay métodos adicionales a los que esta puede llamar.

SignalR.Sample StockTickerHub.cs

La clase StockTickerHub define cuatro métodos adicionales a los que el cliente puede llamar:

public string GetMarketState()
{
    return _stockTicker.MarketState.ToString();
}

public void OpenMarket()
{
    _stockTicker.OpenMarket();
}

public void CloseMarket()
{
    _stockTicker.CloseMarket();
}

public void Reset()
{
    _stockTicker.Reset();
}

La aplicación llama a OpenMarket, CloseMarkety Reset como respuesta a los botones de la parte superior de la página. Muestran el patrón de un cliente que desencadena un cambio de estado que se propaga inmediatamente a todos los clientes. Cada uno de estos métodos llama a un método de la clase StockTicker que efectúa el cambio de estado del mercado y, a continuación, transmite el nuevo estado.

SignalR.Sample StockTicker.cs

En la clase StockTicker, la aplicación mantiene el estado del mercado con una propiedad MarketState que devuelve un valor de enumeración MarketState:

public MarketState MarketState
{
    get { return _marketState; }
    private set { _marketState = value; }
}

public enum MarketState
{
    Closed,
    Open
}

Cada uno de los métodos que cambian el estado del mercado lo hace dentro de un bloque de bloqueo, ya que la clase StockTicker tiene que ser segura para subprocesos:

public void OpenMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Open)
        {
            _timer = new Timer(UpdateStockPrices, null, _updateInterval, _updateInterval);
            MarketState = MarketState.Open;
            BroadcastMarketStateChange(MarketState.Open);
        }
    }
}

public void CloseMarket()
{
    lock (_marketStateLock)
    {
        if (MarketState == MarketState.Open)
        {
            if (_timer != null)
            {
                _timer.Dispose();
            }
            MarketState = MarketState.Closed;
            BroadcastMarketStateChange(MarketState.Closed);
        }
    }
}

public void Reset()
{
    lock (_marketStateLock)
    {
        if (MarketState != MarketState.Closed)
        {
            throw new InvalidOperationException("Market must be closed before it can be reset.");
        }
        LoadDefaultStocks();
        BroadcastMarketReset();
    }
}

Para asegurarse de que este código sea seguro para subprocesos, el campo _marketState que respalda la propiedad MarketState designa volatile:

private volatile MarketState _marketState;

Los métodos BroadcastMarketStateChange y BroadcastMarketReset son similares al método BroadcastStockPrice que ya ha repasado, salvo que llaman a métodos diferentes definidos en el cliente:

private void BroadcastMarketStateChange(MarketState marketState)
{
    switch (marketState)
    {
        case MarketState.Open:
            Clients.All.marketOpened();
            break;
        case MarketState.Closed:
            Clients.All.marketClosed();
            break;
        default:
            break;
    }
}

private void BroadcastMarketReset()
{
    Clients.All.marketReset();
}

Funciones adicionales en el cliente a las que puede llamar el servidor

La función updateStockPrice controla ahora tanto la tabla como la pantalla del teletipo, y usa jQuery.Color para mostrar colores rojos y verdes.

Las nuevas funciones de SignalR.StockTicker.js habilitan y deshabilitan los botones en función del estado del mercado. También detienen o inician el desplazamiento horizontal del teletipo de bolsa en directo. Dado que se agregan muchas funciones a ticker.client, la aplicación usa la función de extensión jQuery para agregarlas.

$.extend(ticker.client, {
    updateStockPrice: function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock)),
            $li = $(liTemplate.supplant(displayStock)),
            bg = stock.LastChange === 0
                ? '255,216,0' // yellow
                : stock.LastChange > 0
                    ? '154,240,117' // green
                    : '255,148,148'; // red

        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        $stockTickerUl.find('li[data-symbol=' + stock.Symbol + ']')
            .replaceWith($li);

        $row.flash(bg, 1000);
        $li.flash(bg, 1000);
    },

    marketOpened: function () {
        $("#open").prop("disabled", true);
        $("#close").prop("disabled", false);
        $("#reset").prop("disabled", true);
        scrollTicker();
    },

    marketClosed: function () {
        $("#open").prop("disabled", false);
        $("#close").prop("disabled", true);
        $("#reset").prop("disabled", false);
        stopTicker();
    },

    marketReset: function () {
        return init();
    }
});

Configuración adicional del cliente tras establecer la conexión

Una vez que el cliente establece la conexión, aún quedan cosas por hacer:

  • averiguar si el mercado está abierto o cerrado para llamar a la función marketOpened o marketClosed adecuada;

  • adjuntar las llamadas de método de servidor a los botones.

$.connection.hub.start()
    .pipe(init)
    .pipe(function () {
        return ticker.server.getMarketState();
    })
    .done(function (state) {
        if (state === 'Open') {
            ticker.client.marketOpened();
        } else {
            ticker.client.marketClosed();
        }

        // Wire up the buttons
        $("#open").click(function () {
            ticker.server.openMarket();
        });

        $("#close").click(function () {
            ticker.server.closeMarket();
        });

        $("#reset").click(function () {
            ticker.server.reset();
        });
    });

Los métodos de servidor no se conectan a los botones hasta que la aplicación establece la conexión. Esto se hace así para que el código no pueda llamar a los métodos de servidor antes de que estén disponibles.

Recursos adicionales

En este tutorial, ha aprendido a programar una aplicación SignalR que transmite mensajes desde el servidor a todos los clientes conectados. Ahora puede difundir mensajes periódicamente y en respuesta a las notificaciones de cualquier cliente. Puedes usar el concepto de instancia singleton multiproceso para mantener el estado del servidor en situaciones de juegos en línea con varios jugadores. Para ver un ejemplo, consulte el juego ShootR basado en SignalR.

Para ver tutoriales que muestran escenarios de comunicación entre nodos del mismo nivel, consulte Introducción a SignalR y Actualización en tiempo real con SignalR.

Para más información sobre SignalR, consulte los siguientes recursos.

Pasos siguientes

En este tutorial ha:

  • Proyecto creado
  • Configuración del código de servidor
  • Código de servidor examinado
  • Configuración del código de cliente
  • Código de cliente examinado
  • Probar la aplicación
  • Registro habilitado

Pase al siguiente artículo para aprender a crear una aplicación web en tiempo real que use SignalR 2 de ASP.NET.