Tutorial: Difusión de servidores con ASP.NET SignalR 1.x

por Patrick Fletcher, Tom Dykstra

Advertencia

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

Este tutorial muestra cómo crear una aplicación web que use ASP.NET SignalR para proporcionar la funcionalidad de difusión de servidores. La difusión de servidores significa que las comunicaciones enviadas a los clientes son iniciadas por el servidor. Este escenario requiere un enfoque de programación diferente al de los escenarios de punto a punto, como las aplicaciones de chat, en las que las comunicaciones enviadas a los clientes son iniciadas por uno o más de los clientes.

La aplicación que creará en este tutorial simula un teletipo de bolsa, un escenario típico para la funcionalidad de difusión de servidores.

Los comentarios sobre el tutorial son bienvenidos. Si tiene preguntas que no estén directamente relacionadas con el tutorial, puede publicarlas en el foro de ASP.NET SignalR o en StackOverflow.com.

Información general

El paquete NuGet Microsoft.AspNet.SignalR.Sample instala una aplicación de teletipo de bolsa simulada de ejemplo en un proyecto de Visual Studio. En la primera parte de este tutorial, creará una versión simplificada de esa aplicación desde cero. En el resto del tutorial, instalará el paquete NuGet y revisará las características adicionales y el código que crea.

La aplicación del teletipo de bolsa es representativa de un tipo de aplicación en tiempo real en la que se quieren "insertar" periódicamente, o difundir, notificaciones desde el servidor a todos los clientes conectados.

La aplicación que va a compilar en la primera parte de este tutorial muestra una cuadrícula con datos de acciones.

StockTicker initial version

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.

Este tutorial contiene las siguientes secciones:

Nota:

Si no quiere seguir los pasos para desarrollar la aplicación, puede instalar el paquete SignalR.Sample en un nuevo proyecto Aplicación web de ASP.NET vacía y leer estos pasos para obtener explicaciones sobre el código. La primera parte del tutorial cubre un subconjunto del código de SignalR.Sample y la segunda explica las características clave de la funcionalidad adicional del paquete SignalR.Sample.

Requisitos previos

Antes de empezar, asegúrese de que tiene Visual Studio 2012 o 2010 SP1 instalado en su equipo. Si no tiene Visual Studio, consulte Descargas de ASP.NET para obtener la versión gratuita de Visual Studio 2012 Express para la Web.

Si tiene Visual Studio 2010, asegúrese de que NuGet está instalado.

Creación del proyecto

  1. En el menú Archivo, haga clic en Nuevo proyecto.

  2. En el cuadro de diálogo Nuevo proyecto, expanda C# en Plantillas y seleccione Web.

  3. Seleccione la plantilla Aplicación web vacía de ASP.NET, nombre el proyecto SignalR.StockTicker y haga clic en Aceptar.

    New Project dialog box

Agregar los paquetes NuGet de SignalR

Agregue los paquetes NuGet de SignalR y JQuery

Puede agregar la funcionalidad de SignalR a un proyecto instalando un paquete NuGet.

  1. Haga clic en Herramientas | Administrador de paquetes NuGet | Consola del administrador de paquetes.

  2. Escriba el siguiente comando en el administrador de paquetes.

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    El paquete de SignalR instala una serie de otros paquetes NuGet como dependencias. Una vez finalizada la instalación, tendrá todos los componentes de servidor y cliente necesarios para usar SignalR en una aplicación de ASP.NET.

Configuración del código de servidor

En esta sección se establece la configuración del código que se ejecuta en el servidor.

Crear la clase Stock (Acción)

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

  1. Cree un nuevo archivo de clase en la carpeta del proyecto, llámelo Stock.cs, y después reemplace el código de la plantilla 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 (Símbolo, por ejemplo, MSFT para Microsoft) y Price (Precio). 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 (Apertura). Las siguientes veces que establezca Price, los valores de las propiedades Change y PercentChange (PorcentajeCambio) se calcularán en función de la diferencia entre Price y DayOpen.

Creación de las clases StockTicker y StockTickerHub

Usará la API de SignalR Hub para controlar la interacción entre servidor y cliente. Una clase StockTickerHub que derive de la clase SignalR Hub controlará la recepción de conexiones y llamadas a métodos de los clientes. También necesita mantener los datos de acciones y ejecutar un objeto Temporizador para desencadenar periódicamente actualizaciones de precios, independientemente de las conexiones de los clientes. No puede colocar estas funciones en una clase Hub, ya que las instancias de Hub son transitorias. Se crea una instancia de la clase Hub para cada operación en el hub, como las conexiones y llamadas del cliente al servidor. Así que el mecanismo que mantiene los datos de acciones, actualiza los precios y difunde las actualizaciones de precios tiene que ejecutarse en una clase separada, a la que llamará StockTicker.

Broadcasting from StockTicker

Solo quiere que se ejecute una instancia de la clase StockTicker en el servidor, por lo que tendrá que establecer una referencia desde cada instancia de StockTickerHub a la instancia de singleton de StockTicker. La clase StockTicker tiene que poder transmitir a los clientes porque tiene los datos de acciones y desencadena las actualizaciones, pero StockTicker no es una clase Hub. Por lo tanto, la clase StockTicker tiene que obtener una referencia al objeto de contexto de conexión de SignalR Hub. Después puede usar el objeto de contexto de conexión de SignalR para transmitir a los clientes.

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

  2. Si tiene Visual Studio 2012 con la actualización ASP.NET and Web Tools 2012.2, haga clic en Web en Visual C# y seleccione la plantilla de elementos Clase SignalR Hub. En caso contrario, seleccione la plantilla Clase.

  3. Nombre la nueva clase StockTickerHub.cs y haga clic en Agregar.

    Add StockTickerHub.cs

  4. Reemplace el código de plantilla por el código siguiente:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    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();
            }
        }
    }
    

    La clase Hub 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 IEnumerable<Stock> porque está devolviendo 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 se hará referencia al centro de conectividad en el código JavaScript del cliente. El nombre predeterminado en el cliente si no usa este atributo es una versión con formato camelCase del nombre de la clase, que en este caso sería stockTickerHub.

    Como verá más adelante cuando cree la clase StockTicker, se crea una instancia singleton de esa clase en su propiedad estática Instance. Esa instancia singleton de StockTicker permanece en memoria sin importar cuántos clientes se conecten o desconecten, y esa instancia es la que usa el método GetAllStocks para devolver la información actual de las acciones.

  5. Cree un nuevo archivo de clase en la carpeta del proyecto, llámelo StockTicker.cs, y después reemplace el código de la plantilla 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 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 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 varios subprocesos ejecutarán la misma instancia del código de StockTicker, la clase StockTicker tiene que ser segura para subprocesos.

    Almacenar la instancia singleton en un campo estático

    El código inicializa el campo estático _instance que devuelve la propiedad Instance con una instancia de la clase, y esta es la única instancia de la clase que se puede crear, porque el constructor está marcado como privado. La inicialización diferida se usa para el campo _instance, no por razones de rendimiento, sino para asegurar que la creación de la instancia es 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 ha visto antes, esta colección de acciones es devuelta a su vez por StockTickerHub.GetAllStocks que es un método del servidor en la clase Hub al que pueden llamar los clientes.

    private readonly ConcurrentDictionary<string, Stock> _stocks = new ConcurrentDictionary<string, Stock>();
    
    private StockTicker(IHubConnectionContext 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 para la seguridad para subprocesos. Como alternativa, podría 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 se elimine la instancia de StockTicker. En una aplicación real trabajaría con un almacén de datos de back-end, como 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;
    }
    

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

    La marca _updatingStockPrices está marcada como volátil para asegurar que el acceso a ella es seguro para subprocesos.

    private volatile bool _updatingStockPrices = false;
    

    En una aplicación real, el método TryUpdateStockPrice llamaría a un servicio web para buscar el precio; en este código se usa un generador de números aleatorios para hacer cambios al azar.

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

    Dado que los cambios de precio se originan aquí, en el objeto StockTicker, este es el objeto que necesita llamar a un método updateStockPrice en todos los clientes conectados. En una clase Hub tiene una API para llamar a los métodos del cliente, pero StockTicker no deriva de la clase Hub y no tiene una referencia a ningún objeto Hub. Por lo tanto, para transmitir 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 los métodos de 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 pone en la propiedad Clients.

    Hay dos razones por las que quiere obtener el contexto solo una vez: obtener el contexto es una operación cara, y obtenerlo una vez asegura que se preserva 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 clients)
    {
        Clients = clients;
    
        // Remainder of constructor ...
    }
    
    private IHubConnectionContext Clients
    {
        get;
        set;
    }
    
    private void BroadcastStockPrice(Stock stock)
    {
        Clients.All.updateStockPrice(stock);
    }
    

    Obtener la propiedad Clients del contexto y ponerla 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, para transmitir a todos los clientes puede escribir Clients.All.updateStockPrice(stock).

    El método updateStockPrice al que está llamando en BroadcastStockPrice aún no existe; lo agregará más adelante cuando escriba el código que se ejecuta en el cliente. Puede hacer referencia a updateStockPrice aquí porque Clients.All es dinámico, lo que significa que la expresión se evaluará 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, y si el cliente tiene un método llamado updateStockPrice, se llamará a ese método y se le pasará 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, consulte HubConnectionContext.

Registro de la ruta de SignalR

El servidor necesita saber qué URL interceptar y dirigir a SignalR. Para ello, agregará algo de código al archivo Global.asax.

  1. En Explorador de soluciones, haga clic con el botón derecho en el proyecto y después haga clic en Añadir nuevo elemento.

  2. Seleccione la plantilla del elemento Clase de aplicación global y, después, haga clic en Añadir.

    Add global.asax

  3. Agregue el código de registro de ruta de SignalR al método Application_Start:

    protected void Application_Start(object sender, EventArgs e)
    {
        RouteTable.Routes.MapHubs();
    }
    

    De manera predeterminada, la URL base para todo el tráfico de SignalR es "/signalr", y "/signalr/hubs" se usa para recuperar un archivo JavaScript generado dinámicamente que define proxies para todos los Hubs que tenga en su aplicación. El método MapHubs incluye sobrecargas que le permiten especificar una URL base diferente y determinadas opciones de SignalR en una instancia de la clase HubConfiguration.

  4. Agregue una instrucción using en la parte superior del archivo:

    using System.Web.Routing;
    
  5. Guarde y cierre el archivo Global.asax y compile el proyecto.

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

Configurar el código de cliente

  1. Cree un nuevo archivo HTML en la carpeta del proyecto y nómbrelo StockTicker.html.

  2. Reemplace el código de plantilla por el código siguiente:

    <!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.8.2.min.js" ></script>
        <!--Reference the SignalR library. -->
        <script src="/Scripts/jquery.signalR-1.0.1.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 5 columnas, una fila de encabezado y una fila de datos con una única celda que abarca las 5 columnas. La fila de datos muestra "cargando..." y solo se mostrará momentáneamente cuando se inicie 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 de jQuery, el archivo de script de SignalR Core, el archivo de script de los proxies de SignalR y un archivo de script de StockTicker que creará más adelante. El archivo de script de proxies de SignalR, que especifica la URL "/signalr/hubs", se genera dinámicamente y define 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 usando SignalR Utilities y deshabilitando la creación dinámica de archivos en la llamada al método MapHubs.

  3. Importante

    Asegúrese de que las referencias del archivo JavaScript en StockTicker.html son correctas. Es decir, asegúrese de que la versión de jQuery en su etiqueta de scripts (1.8.2 en el ejemplo) es la misma que la versión de jQuery en la carpeta Scripts de su proyecto, y asegúrese de que la versión de SignalR en su etiqueta de scripts es la misma que la versión de SignalR en la carpeta Scripts de su proyecto. Cambie los nombres de los archivos en las etiquetas de los scripts si es necesario.

  4. 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.

  5. Cree un nuevo archivo JavaScript en la carpeta del proyecto y nómbrelo StockTicker.js.

  6. Reemplace el código de plantilla por el código siguiente:

    // 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);
    
    });
    

    $.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 teletipo. El nombre del proxy es el que se estableció mediante 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, lo que significa que puede llamar a la función done para especificar la función a la que llamar cuando finalice la operación asincrónica.

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

    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 "camelCase" en el cliente, aunque el nombre del método está en "PascalCase" en el servidor. La regla de camelCase 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();
    }
    

    Si quisiera usar PascalCase en el cliente, o si quisiera usar un nombre de método completamente diferente, podría decorar el método Hub con el atributo HubMethodName de la misma forma que decoró la propia clase Hub con el atributo HubName.

    En el método init, se crea HTML para una fila de tabla para cada objeto stock recibido del servidor llamando a formatStock para dar formato a las propiedades del objeto stock, y después llamando a supplant (que se define en la parte superior de StockTicker.js) para reemplazar los marcadores de posición de la variable rowTemplate por los valores de las propiedades del objeto stock. El HTML resultante se añade después a la tabla de existencias.

    Se llama a init pasándola como una función de devolución de llamada a que se ejecuta una vez finalizada la función de inicio asincrónico. 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 se establezca la conexión con el servidor.

    Cuando el servidor cambia el precio de una acción, llama a updateStockPrice en los clientes conectados. La función se agrega a la propiedad cliente del proxy de 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 stock recibido del servidor en una fila de la tabla del mismo modo que en la función init. Sin embargo, 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

  1. Presione F5 para ejecutar la aplicación en modo de depuración.

    La tabla de acciones muestra inicialmente la línea "cargando...", después de un corto retraso se muestran los datos iniciales de las acciones, y después los precios de las acciones empiezan a cambiar.

    Loading

    Initial stock table

    Stock table receiving changes from server

  2. Copie la dirección URL de la barra de direcciones del explorador y péguela en una o más ventanas nuevas del explorador.

    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 y abra uno nuevo, después vaya a la misma URL.

    El objeto singleton StockTicker ha seguido ejecutándose en el servidor, por lo que la visualización de la tabla de acciones muestra que las acciones han seguido cambiando. (No verá la tabla inicial con las cifras de cambio cero).

  4. Cierre el explorador.

Habilitar registro

SignalR tiene una funcionalidad de registro integrada que puede habilitar en el cliente para ayudar a solucionar problemas. En esta sección se habilita el registro y se ven ejemplos que muestran 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 y agregue una línea de código 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);
    
  2. Presione F5 para ejecutar el proyecto.

  3. 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.

    IE 10 IIS 8 Console

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

    IE 10 Console, IIS 7.5

    En Firefox, instale el complemento Firebug para obtener una ventana de consola. Si está ejecutando Firefox 19 en Windows 8 (IIS 8), el método de transporte es WebSockets.

    Firefox 19 IIS 8 Websockets

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

    Firefox 19 IIS 7.5 Console

Instalar y revisar la muestra completa de StockTicker

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

Instalación del paquete NuGet SignalR.Sample

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

  2. En el cuadro de diálogo Administrar paquetes NuGet, haga clic en En línea, escriba SignalR.Sample en el cuadro Buscar en línea y, después, haga clic en Instalar en el paquete SignalR.Sample.

    Install SignalR.Sample package

  3. En el archivo Global.asax, marque como comentario la línea RouteTable.Routes.MapHubs(); que agregó anteriormente en el método Application_Start.

    El código en Global.asax ya no es necesario porque el paquete SignalR.Sample registra la ruta de SignalR en el archivo App_Start/RegisterHubs.cs:

    [assembly: WebActivator.PreApplicationStartMethod(typeof(SignalR.StockTicker.RegisterHubs), "Start")]
    
    namespace SignalR.StockTicker
    {
        public static class RegisterHubs
        {
            public static void Start()
            {
                // Register the default hubs route: ~/signalr/hubs
                RouteTable.Routes.MapHubs();
            }
        }
    }
    

    La clase WebActivator a la que hace referencia el atributo de ensamblado está incluida en el paquete NuGet WebActivatorEx, que se instala como dependencia del paquete SignalR.Sample.

  4. En Explorador de soluciones, expanda la carpeta SignalR.Sample que se creó al instalar el paquete SignalR.Sample.

  5. En la carpeta SignalR.Sample, 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.

    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

  1. Presione F5 para ejecutar la aplicación.

    Además de la cuadrícula que ha visto antes, la aplicación completa del teletipo de bolsa muestra una ventana de desplazamiento horizontal que muestra los mismos datos de acciones. 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.

    StockTicker screen start

    Al hacer clic en Abrir mercado, el cuadro 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, se actualizan tanto la cuadrícula Tabla de bolsa en directo como el cuadro Teletipo de bolsa en directo. Cuando el cambio de precio de una acción es positivo, la acción se muestra con fondo verde, y cuando el cambio es negativo, la acción se muestra con fondo rojo.

    StockTicker app, market open

    El botón Cerrar mercado detiene los cambios y detiene el desplazamiento del teletipo, y el botón Restablecer restablece todos los datos de acciones al estado inicial antes de que comenzaran los cambios de precios. Si abre más ventanas del explorador y va a la misma URL, verá los mismos datos actualizados dinámicamente al mismo tiempo en cada explorador. Al hacer clic en uno de los botones, todos los exploradores responden de la misma manera al mismo tiempo.

Visualización del teletipo de stock en directo

La visualización del teletipo de stock en directo es una lista desordenada en un elemento de división 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: reemplazando los marcadores de posición en una cadena de plantilla <li> y añadiendo dinámicamente los elementos <li> al elemento <ul>. El desplazamiento se realiza usando la función de animación de jQuery para variar el margen izquierdo de la lista desordenada dentro de la división.

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>

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;
    }

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

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();
}

Se llama a OpenMarket, CloseMarket y Reset en respuesta a los botones de la parte superior de la página. Demuestran 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 después transmite el nuevo estado.

En la clase StockTicker, el estado del mercado se mantiene mediante 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 hacen dentro de un bloque de bloqueo porque 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 asegurar que este código es seguro para subprocesos, el campo _marketState que vuelve a la propiedad MarketState se marca como volátil,

private volatile MarketState _marketState;

Los métodos BroadcastMarketStateChange y BroadcastMarketReset son similares al método BroadcastStockPrice que ya ha visto, 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 el servidor puede llamar

La función updateStockPrice ahora controla tanto la cuadrícula como la visualización del teletipo, y usa jQuery.Color para instalar de forma intermitente los colores rojo y verde.

Las nuevas funciones en SignalR.StockTicker.js habilitan y deshabilitan los botones en función del estado del mercado, y detienen o inician el desplazamiento horizontal de la ventana del teletipo. Dado que se están agregando varias funciones a ticker.client, se usa la función jQuery extend 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

Después de que el cliente establezca la conexión, tiene que realizar un trabajo adicional: averiguar si el mercado está abierto o cerrado para llamar a la función marketOpened o marketClosed adecuada, y adjuntar las llamadas a los métodos del 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 del servidor no se conectan a los botones hasta que se establece la conexión, para que el código no pueda intentar llamar a los métodos del servidor antes de que estén disponibles.

Pasos siguientes

En este tutorial ha aprendido a programar una aplicación de SignalR que emite mensajes desde el servidor a todos los clientes conectados, tanto de forma periódica como en respuesta a las notificaciones de cualquier cliente. El patrón de usar una instancia singleton multiproceso para mantener el estado del servidor también puede usarse en escenarios de juegos en línea multijugador. Para ver un ejemplo, consulte el juego ShootR que se basa en SignalR.

Para ver tutoriales que muestran escenarios de comunicación punto a punto, consulte Introducción a SignalR y Actualización en tiempo real con SignalR.

Para aprender conceptos de desarrollo de SignalR más avanzados, visite los siguientes sitios para obtener el código fuente y los recursos de SignalR: