Руководство по серверному вещанию с помощью ASP.NET SignalR 1.x

Патрик Флетчер, Том Дайкстра

Предупреждение

Эта документация не подходит для последней версии SignalR. Взгляните на ASP.NET Core SignalR.

В этом руководстве показано, как создать веб-приложение, использующее ASP.NET SignalR для предоставления функциональности трансляции сервера. Трансляция сервера означает, что обмен данными, отправленными клиентам, инициируется сервером. Для этого сценария требуется другой подход программирования, отличный от одноранговых сценариев, таких как приложения чата, в которых обмен данными, отправленные клиентам, инициируются одним или несколькими клиентами.

Приложение, которое вы создадите в этом руководстве, имитирует биржевой тикер, типичный сценарий для функциональности серверного вещания.

Комментарии к руководству приветствуются. Если у вас есть вопросы, которые не связаны напрямую с руководством, их можно опубликовать на форуме ASP.NET SignalR или StackOverflow.com.

Обзор

Пакет NuGet Microsoft.AspNet.SignalR.Sample устанавливает пример симулированного приложения биржевых котировок в проекте Visual Studio. В первой части этого руководства вы создадите упрощенную версию этого приложения с нуля. В оставшейся части руководства вы установите пакет NuGet и просмотрите дополнительные функции и код, которые он создает.

Приложение тикера акций является представителем своего рода приложения в режиме реального времени, в котором вы хотите периодически отправлять или транслировать уведомления с сервера на все подключенные клиенты.

Приложение, которое вы создадите в первой части этого пособия, отображает таблицу с биржевыми данными.

Начальная версия StockTicker

Периодически сервер случайным образом обновляет цены на акции и отправляет обновления всем подключенным клиентам. В браузере числа и символы в столбцах "Изменение " и % столбцы динамически изменяются в ответ на уведомления с сервера. Если открыть дополнительные браузеры с одинаковым URL-адресом, все они отображают одни и те же данные и одинаковые изменения данных одновременно.

В этом руководстве содержатся следующие разделы:

Замечание

Если вы не хотите работать с инструкциями по созданию приложения, вы можете установить пакет SignalR.Sample в новом проекте пустого веб-приложения ASP.NET и ознакомиться с этими инструкциями, чтобы получить объяснения кода. Первая часть руководства охватывает подмножество кода SignalR.Sample, а вторая часть объясняет ключевые функции дополнительных функций в пакете SignalR.Sample.

Необходимые условия

Перед началом работы убедитесь, что на компьютере установлен Visual Studio 2012 или 2010 с пакетом обновления 1 (SP1). Если у вас нет Visual Studio, ознакомьтесь со страницей загрузок ASP.NET, чтобы бесплатно получить Visual Studio 2012 Express для веб-разработки.

Если у вас есть Visual Studio 2010, убедитесь, что NuGet установлен.

Создание проекта

  1. В меню "Файл" выберите пункт "Создать проект".

  2. В диалоговом окне "Новый проект" разверните C# в разделе "Шаблоны " и выберите веб-сайт.

  3. Выберите шаблон пустого веб-приложения ASP.NET , назовите проект SignalR.StockTicker и нажмите кнопку "ОК".

    Диалоговое окно

Добавьте пакеты NuGet SignalR

Добавление пакетов SignalR и JQuery NuGet

Вы можете добавить функции SignalR в проект, установив пакет NuGet.

  1. Щелкните "Сервис" | Диспетчер пакетов NuGet | Консоль диспетчера пакетов.

  2. Введите следующую команду в диспетчере пакетов.

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
    

    Пакет SignalR устанавливает ряд других пакетов NuGet в качестве зависимостей. После завершения установки у вас будут все компоненты сервера и клиента, необходимые для использования SignalR в приложении ASP.NET.

Настройка кода сервера

В этом разделе вы настроили код, который выполняется на сервере.

Создание класса Stock

Начните с создания класса модели Stock, который будет использоваться для хранения и передачи информации о запасе.

  1. Создайте файл класса в папке проекта, назовите его Stock.cs, а затем замените код шаблона следующим кодом:

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

    Два свойства, заданные при создании акций, являются символом (например, MSFT для Майкрософт) и ценой. Другие свойства зависят от того, как и когда задано значение Price. При первом установлении Price значение распространяется на DayOpen. В последующие времена при установке цены значения свойств Change и PercentChange вычисляются на основе разницы между Price и DayOpen.

Создание классов StockTicker и StockTickerHub

Api SignalR Hub будет использоваться для обработки взаимодействия между серверами и клиентами. Класс StockTickerHub, производный от класса SignalR Hub, будет обрабатывать получение подключений и вызовов методов от клиентов. Кроме того, необходимо поддерживать данные акций и запускать объект Таймера для периодического активации обновлений цен независимо от клиентских подключений. Эти функции нельзя поместить в класс Концентратора, так как экземпляры концентратора являются временными. Экземпляр класса Концентратора создается для каждой операции в концентраторе, например подключений и вызовов от клиента к серверу. Таким образом, механизм, который сохраняет данные акций, обновляет цены и передает обновления цен, должен выполняться в отдельном классе, который будет называться StockTicker.

Вещание из StockTicker

Требуется только один экземпляр класса StockTicker для запуска на сервере, поэтому вам потребуется настроить ссылку из каждого экземпляра StockTickerHub на одиночный экземпляр StockTicker. Класс StockTicker должен уметь транслировать данные пользователям, поскольку он располагает фондовыми данными и инициирует обновления, но StockTicker не является классом Hub. Поэтому класс StockTicker должен получить ссылку на объект контекста подключения SignalR Hub. Затем он может использовать объект контекста подключения SignalR для трансляции клиентам.

  1. В обозревателе решений щелкните проект правой кнопкой мыши и нажмите кнопку "Добавить новый элемент".

  2. Если у вас есть Visual Studio 2012 с обновлением ASP.NET и Web Tools 2012.2, щелкните Веб в Visual C# и выберите шаблон элемента SignalR Hub Class. В противном случае выберите шаблон класса .

  3. Назовите новый класс StockTickerHub.cs и нажмите кнопку "Добавить".

    Добавьте StockTickerHub.cs

  4. Замените код шаблона следующим кодом:

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

    Класс Хаб используется для определения методов, которые клиенты могут вызывать на сервере. Вы определяете один метод: GetAllStocks() Когда клиент изначально подключается к серверу, он вызовет этот метод, чтобы получить список всех акций с текущими ценами. Метод может выполняться синхронно и возвращать IEnumerable<Stock> , так как он возвращает данные из памяти. Если метод должен был получить данные, выполняя то, что будет включать ожидание, например поиск базы данных или вызов веб-службы, вы указали Task<IEnumerable<Stock>> бы в качестве возвращаемого значения, чтобы включить асинхронную обработку. Дополнительные сведения см. в Руководстве по API Центров SignalR в ASP.NET — Сервер — Когда выполнять асинхронно.

    Атрибут HubName определяет, как на концентратор будут ссылаться в коде JavaScript на клиентской стороне. Имя клиента по умолчанию, если не используется этот атрибут, будет представлено в верблюжем регистре версии имени класса, которая в данном случае будет выглядеть как stockTickerHub.

    Как вы увидите позже при создании класса StockTicker, синглтон этого класса создается в статическом свойстве Instance. Этот одиночный экземпляр StockTicker остается в памяти, независимо от того, сколько клиентов подключается или отключается, и этот экземпляр используется методом GetAllStocks для возвращения текущей информации об акциях.

  5. Создайте файл класса в папке проекта, назовите его StockTicker.cs, а затем замените код шаблона следующим кодом:

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

    Так как несколько потоков будут выполнять один и тот же экземпляр кода StockTicker, класс StockTicker должен быть потокобезопасным.

    Хранение экземпляра синглтона в статическом поле

    Код инициализирует статическое _instance поле, которое поддерживает свойство Instance с экземпляром класса, и это единственный экземпляр класса, который можно создать, так как конструктор помечается как закрытый. Неактивная инициализация используется для поля _instance, а не по соображениям производительности, а для обеспечения того, чтобы создание экземпляра было потокобезопасным.

    private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));
    
    public static StockTicker Instance
    {
        get
        {
            return _instance.Value;
        }
    }
    

    Каждый раз, когда клиент подключается к серверу, новый экземпляр класса StockTickerHub, запущенного в отдельном потоке, получает одноэлементный экземпляр StockTicker из статического свойства StockTicker.Instance, как вы видели ранее в классе StockTickerHub.

    Хранение данных в ConcurrentDictionary

    Конструктор инициализирует коллекцию _stocks с некоторыми образцами данных акций, а GetAllStocks возвращает акции. Как вы видели ранее, эта коллекция акций в свою очередь возвращается методом StockTickerHub.GetAllStocks, который является методом сервера в классе Hub, который могут вызывать клиенты.

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

    Коллекция акций определяется как тип ConcurrentDictionary для безопасности потоков. В качестве альтернативы можно использовать объект Dictionary и явно заблокировать словарь при внесении изменений в него.

    Для этого примера приложения допустимо хранить данные приложения в памяти и терять их при удалении экземпляра StockTicker. В реальном приложении вы будете работать с серверным хранилищем данных, например базой данных.

    Периодическое обновление цен на акции

    Конструктор запускает объект Таймера, который периодически вызывает методы, которые обновляют цены на акции случайным образом.

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

    UpdateStockPrices вызывается таймером, который передает значение NULL в параметре состояния. Перед обновлением цен устанавливается блокировка на объект _updateStockPricesLock. Код проверяет, уже ли обновляется другой поток цен, а затем вызывает TryUpdateStockPrice для каждой акции в списке. Метод TryUpdateStockPrice решает, следует ли изменить цену акций и на сколько её изменить. Если цена акций изменяется, вызывается BroadcastStockPrice для трансляции изменения цены акций всем подключенным клиентам.

    Флаг _updatingStockPrices помечается как волатильный, чтобы обеспечить доступ к нему потокобезопасным.

    private volatile bool _updatingStockPrices = false;
    

    В реальном приложении метод TryUpdateStockPrice вызовет веб-службу для поиска цены; в этом коде используется генератор случайных чисел для случайных изменений.

    Получение контекста SignalR, чтобы класс StockTicker мог передавать данные клиентам.

    Так как изменения цен происходят здесь в объекте StockTicker, это объект, который должен вызывать метод updateStockPrice для всех подключенных клиентов. В классе Hub есть API для вызова клиентских методов, но StockTicker не является производным от класса Hub и не имеет ссылки на какой-либо объект Hub. Таким образом, чтобы транслировать подключенным клиентам, класс StockTicker должен получить экземпляр контекста SignalR для класса StockTickerHub и использовать его для вызова методов клиентам.

    Код получает ссылку на контекст SignalR после создания экземпляра класса синглтон, и затем передаёт эту ссылку в конструктор, который помещает её в свойство Clients.

    Существует две причины, по которым вы хотите получить контекст только один раз: получение контекста является дорогой операцией, и получение ее один раз гарантирует, что предполагаемый порядок сообщений, отправленных клиентам, сохраняется.

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

    Получение свойства Clients контекста и его размещение в свойстве StockTickerClient позволяет писать код для вызова клиентских методов, который будет выглядеть так же, как в классе хаба. Например, для трансляции для всех клиентов можно написать Client.All.updateStockPrice(stock).

    Метод updateStockPrice, который вы вызываете в BroadcastStockPrice, еще не существует; Вы добавите его позже при написании кода, который выполняется на клиенте. Здесь можно ссылаться на updateStockPrice, так как Client.All является динамическим, что означает, что выражение будет оцениваться во время выполнения. При выполнении этого вызова метода SignalR отправит имя метода и значение параметра клиенту, а если у клиента есть метод с именем updateStockPrice, этот метод будет вызван, и значение параметра будет передано в него.

    Clients.All означает отправку всем клиентам. SignalR предоставляет другие параметры, чтобы указать, в какие клиенты или группы клиентов следует отправлять. Дополнительные сведения см. в разделе HubConnectionContext.

Регистрация маршрута SignalR

Сервер должен знать, какой URL-адрес должен перехватывать и направлять в SignalR. Для этого вы добавите код в файл Global.asax .

  1. В обозревателе решений щелкните проект правой кнопкой мыши и нажмите кнопку "Добавить новый элемент".

  2. Выберите шаблон элемента класса глобального приложения и нажмите кнопку "Добавить".

    Добавить global.asax

  3. Добавьте код регистрации маршрута SignalR в метод Application_Start:

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

    По умолчанию базовый URL-адрес для всего трафика SignalR — "/signalr", а "/signalr/hubs" используется для получения динамически созданного файла JavaScript, который определяет прокси-серверы для всех центров, которые есть в приложении. Метод MapHubs включает перегрузки, позволяющие указать другой базовый URL-адрес и определенные параметры SignalR в экземпляре класса HubConfiguration .

  4. Добавьте инструкцию using в верхней части файла.

    using System.Web.Routing;
    
  5. Сохраните и закройте файл Global.asax и создайте проект.

Теперь вы завершили настройку кода сервера. В следующем разделе вы настроите клиент.

Настройка клиентского кода

  1. Создайте HTML-файл в папке проекта и назовите его StockTicker.html.

  2. Замените код шаблона следующим кодом:

    <!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>
    

    HTML создает таблицу с 5 столбцами, строкой заголовка и строкой данных с одной ячейкой, которая охватывает все 5 столбцов. Строка данных отображает "загрузка..." и будет отображаться только на короткое время при запуске приложения. Код JavaScript удаляет эту строку и заменяет её строками с данными о запасах, полученными с сервера.

    Теги скриптов указывают файл скрипта jQuery, основной файл скрипта SignalR, файл скрипта прокси-сервера SignalR и файл скрипта StockTicker, который вы создадите позже. Файл скрипта SignalR прокси, указывающий URL-адрес "/signalr/hubs", динамически создается и определяет методы прокси для методов класса Hub, в данном случае для StockTickerHub.GetAllStocks. Если вы предпочитаете, вы можете создать этот файл JavaScript вручную с помощью служебных программ SignalR и отключить динамическое создание файла в вызове метода MapHubs.

  3. Это важно

    Убедитесь, что ссылки на файлы JavaScript в StockTicker.html верны. То есть убедитесь, что версия jQuery в теге скрипта (1.8.2 в примере) совпадает с версией jQuery в папке "Скрипты " проекта и убедитесь, что версия SignalR в теге скрипта совпадает с версией SignalR в папке "Скрипты " проекта. При необходимости измените имена файлов в тегах скрипта.

  4. В обозревателе решений щелкните правой кнопкой мыши StockTicker.htmlи нажмите кнопку "Задать как начальную страницу".

  5. Создайте файл JavaScript в папке проекта и назовите его StockTicker.js..

  6. Замените код шаблона следующим кодом:

    // 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 ссылается на прокси-серверы SignalR. Код получает ссылку на прокси-сервер для класса StockTickerHub и помещает его в переменную тикера. Имя прокси-сервера — это имя, заданное атрибутом [HubName]:

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

    После определения всех переменных и функций последняя строка кода в файле инициализирует подключение SignalR путем вызова функции запуска SignalR. Начальная функция выполняется асинхронно и возвращает объект jQuery Deferred, что означает, что можно вызвать выполненную функцию, чтобы указать функцию для вызова при завершении асинхронной операции.

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

    Функция init вызывает функцию getAllStocks на сервере и использует сведения, возвращаемые сервером для обновления фондовой таблицы. Обратите внимание, что по умолчанию на клиенте вы должны использовать стиль camelCase, хотя на сервере имя метода записано в стиле PascalCase. Правило регистра верблюда применяется только к методам, а не к объектам. Например, вы ссылаетесь на stock.Symbol и stock.Price, а не stock.symbol или 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();
    }
    

    Если вы хотите использовать регистр pascal на клиенте или если вы хотите использовать совершенно другое имя метода, можно украсить метод Hub атрибутом HubMethodName так же, как вы украшали класс HubName атрибутом HubName.

    В методе init HTML для строки таблицы создается для каждого объекта акций, полученного от сервера, с помощью вызова formatStock для форматирования свойств объекта акций, а затем с помощью вызова функции supplant (которая определена в верхней части StockTicker.js) для замены заполнителей в переменной rowTemplate значениями свойств объекта акций. Затем полученный HTML-код добавляется в таблицу акций.

    Вы вызываете init, передавая его в качестве функции обратного вызова, которая выполняется после завершения асинхронной функции запуска. Если вы вызвали init в качестве отдельной инструкции JavaScript после вызова start, функция вызовет ошибку, так как она будет выполняться немедленно, не ожидая завершения установления соединения. В этом случае функция init попытается вызвать функцию getAllStocks до установки подключения к серверу.

    Когда сервер изменяет цену акций, он вызывает updateStockPrice на подключенных клиентах. Функция добавляется в клиентское свойство прокси-сервера stockTicker, чтобы сделать его доступным для вызовов с сервера.

    ticker.client.updateStockPrice = function (stock) {
        var displayStock = formatStock(stock),
            $row = $(rowTemplate.supplant(displayStock));
    
        $stockTableBody.find('tr[data-symbol=' + stock.Symbol + ']')
            .replaceWith($row);
        }
    

    Функция updateStockPrice форматирует объект акций, полученный от сервера, в строку таблицы так же, как в функции init. Однако вместо добавления строки в таблицу, он находит текущую строку акции в таблице и заменяет её новой.

Тестирование приложения

  1. Нажмите F5 , чтобы запустить приложение в режиме отладки.

    Таблица акций изначально отображает строку "загрузка...", затем после небольшой задержки отображаются исходные данные акций, а затем цены на акции начинают меняться.

    Загрузка

    Начальная таблица запасов

    Таблица запасов принимает изменения с сервера

  2. Скопируйте URL-адрес из адресной строки браузера и вставьте его в одно или несколько новых окон браузера.

    Начальное отображение акций совпадает с первым браузером и изменения происходят одновременно.

  3. Закройте все браузеры и откройте новый браузер, а затем перейдите по одному URL-адресу.

    Объект StockTicker singleton продолжал работать на сервере, поэтому в таблице акций показано, что акции продолжали меняться. (Начальная таблица с нулевыми цифрами изменений не отображается.)

  4. Закройте браузер.

Включение ведения журнала

SignalR имеет встроенную функцию ведения журнала, которую можно включить на клиенте, чтобы помочь в устранении неполадок. В этом разделе описано, как включить ведение журнала и просмотреть примеры того, как журналы указывают, какой из следующих методов транспорта SignalR использует:

Для любого заданного подключения SignalR выбирает лучший метод транспорта, который поддерживается как сервером, так и клиентом.

  1. Откройте StockTicker.js и добавьте строку кода, чтобы включить ведение журнала непосредственно перед кодом, который инициализирует подключение в конце файла:

    // Start the connection
    $.connection.hub.logging = true;
    $.connection.hub.start().done(init);
    
  2. Нажмите клавишу F5, чтобы запустить проект.

  3. Откройте окно средств разработчика браузера и выберите консоль, чтобы просмотреть журналы. Возможно, потребуется обновить страницу, чтобы просмотреть логи SignalR, в которых согласовывается метод транспорта для нового подключения.

    Если вы используете Internet Explorer 10 в Windows 8 (IIS 8), метод транспорта — WebSockets.

    Консоль IE 10 IIS 8

    Если вы используете Internet Explorer 10 в Windows 7 (IIS 7.5), метод транспорта является iframe.

    Консоль IE 10, IIS 7.5

    В Firefox установите надстройку Firebug, чтобы получить окно консоли. Если вы используете Firefox 19 в Windows 8 (IIS 8), метод транспорта — WebSockets.

    Firefox 19 IIS 8 Websockets

    Если вы используете Firefox 19 в Windows 7 (IIS 7.5), метод транспорта — это события, отправляемые сервером.

    Консоль Firefox 19 IIS 7.5

Установка и проверка полного примера StockTicker

Приложение StockTicker, установленное пакетом NuGet Microsoft.AspNet.SignalR.Sample , содержит больше функций, чем упрощенная версия, которую вы только что создали с нуля. В этом разделе руководства вы устанавливаете пакет NuGet и просматриваете новые функции и код, реализующий их.

Установка пакета NuGet SignalR.Sample

  1. В обозревателе решений щелкните проект правой кнопкой мыши и выберите пункт "Управление пакетами NuGet".

  2. В диалоговом окне "Управление пакетами NuGet" нажмите кнопку "Online", введите SignalR.Sample в поле "Поиск в Интернете ", а затем нажмите кнопку "Установить " в пакете SignalR.Sample .

    Установка пакета SignalR.Sample

  3. В файле Global.asax закомментируйте строку RouteTable.Routes.MapHubs();, которую вы добавили ранее в методе Application_Start.

    Код в Global.asax больше не нужен, так как пакет SignalR.Sample регистрирует маршрут SignalR в файле 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();
            }
        }
    }
    

    Класс WebActivator, на который ссылается атрибут сборки, включен в пакет NuGet WebActivatorEx, который устанавливается в качестве зависимости пакета SignalR.Sample.

  4. В обозревателе решений разверните папку SignalR.Sample , созданную путем установки пакета SignalR.Sample.

  5. В папке SignalR.Sample щелкните правой кнопкой мыши StockTicker.htmlи нажмите кнопку "Задать как начальную страницу".

    Замечание

    Установка пакета NuGet SignalR.Sample может изменить версию jQuery, которая есть в папке "Скрипты ". Новый файлStockTicker.html , который пакет устанавливает в папке SignalR.Sample , будет синхронизирован с версией jQuery, которую устанавливает пакет, но если вы хотите снова запустить исходный файлStockTicker.html , может потребоваться сначала обновить ссылку jQuery в теге скрипта.

Запуск приложения

  1. Нажмите клавишу F5, чтобы запустить приложение.

    Помимо сетки, которую вы видели ранее, полное приложение биржевых тикеров демонстрирует горизонтально прокручивающее окно, отображающее те же данные акций. При первом запуске приложения "рынок" является "закрытым", и вы видите статическую сетку и окно тикера, которое не прокручивается.

    Начало экрана StockTicker

    При нажатии кнопки Open Market поле Live Stock Ticker начинает прокручиваться по горизонтали, и сервер начинает периодически транслировать изменения цен на акции случайным образом. Каждый раз, когда меняется курс акций, обновляются как сетка Таблица Фондового Рынка, так и поле Тикер Фондового Рынка. Когда изменение цен на акции положительное, акции отображаются с зеленым фоном, и когда изменение отрицательное, акции отображаются красным фоном.

    Приложение StockTicker, открытый рынок

    Кнопка "Закрыть рынок" останавливает изменения и останавливает прокрутку тикера, а кнопка сброса сбрасывает все данные акций в начальное состояние до начала изменений цен. Если открыть больше окон браузера и перейти по одному URL-адресу, в каждом браузере будут динамически обновляться одни и те же данные. Щелкнув одну из кнопок, все браузеры реагируют одинаково одновременно.

Дисплей Live Stock Ticker

Отображение Live Stock Ticker — это неупорядоченный список в элементе div, который форматируется в одну строку по стилям CSS. Тикер инициализируется и обновляется так же, как и таблица: заменяя заполнители в строке шаблона <li> и динамически добавляя элементы <li> в элемент <ul>. Прокрутка выполняется с помощью анимируемой функции jQuery, чтобы изменить поле слева от неупорядоченного списка в div.

Html-код тикера акций:

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

Тикера акций CSS:

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

Код jQuery, который делает его прокручивающим

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

Дополнительные методы на сервере, который клиент может вызывать

Класс StockTickerHub определяет четыре дополнительных метода, которые клиент может вызывать:

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

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

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

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

OpenMarket, CloseMarket и Reset вызываются в ответ на кнопки в верхней части страницы. Они представляют шаблон, при котором один клиент инициирует изменение состояния, которое немедленно распространяется на всех клиентов. Каждый из этих методов вызывает метод в классе StockTicker, который влияет на изменение состояния рынка, а затем передает новое состояние.

В классе StockTicker состояние рынка поддерживается свойством MarketState, возвращающим значение перечисления MarketState:

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

public enum MarketState
{
    Closed,
    Open
}

Каждый из методов, изменяющих состояние рынка, делает это внутри блока блокировки, потому что класс StockTicker должен быть потокобезопасным:

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

Чтобы убедиться, что этот код является потокобезопасным, поле _marketState, которое поддерживает свойство MarketState, помечается как изменяющееся,

private volatile MarketState _marketState;

Методы BroadcastMarketStateChange и BroadcastMarketReset похожи на метод BroadcastStockPrice, за исключением того, что они вызывают различные методы, определенные на клиенте:

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

Дополнительные функции на клиенте, который сервер может вызывать

Функция updateStockPrice теперь обрабатывает как сетку, так и дисплей тикера, и использует jQuery.Color для вспышки красных и зеленых цветов.

Новые функции в SignalR.StockTicker.js включают и отключают кнопки в зависимости от состояния рынка, а также останавливают или запускают горизонтальную прокрутку окна тикера. Так как несколько функций добавляются в ticker.client, функция расширения jQuery используется для их добавления.

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

Дополнительная настройка клиента после установки подключения

После того как клиент устанавливает подключение, ему необходимо выполнить некоторую дополнительную работу: узнать, открыт ли рынок или закрыт, чтобы вызвать соответствующую функцию marketOpened или marketClosed, а также привязать вызовы метода сервера к кнопкам.

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

Методы сервера не связаны с кнопками до тех пор, пока не будет установлено соединение, чтобы код не смог вызвать методы сервера, прежде чем они станут доступны.

Дальнейшие действия

В этом руководстве вы узнали, как программировать приложение SignalR, которое передает сообщения с сервера на все подключенные клиенты, как периодически, так и в ответ на уведомления от любого клиента. Шаблон использования многопоточного однопоточного экземпляра для поддержания состояния сервера также можно использовать в сценариях с несколькими игроками в онлайн-играх. Пример см. в игре ShootR, основанной на SignalR.

Учебники, демонстрирующие сценарии однорангового взаимодействия, см. в статье "Начало работы с SignalR " и "Обновление в реальном времени с помощью SignalR".

Дополнительные сведения о концепциях разработки SignalR см. на следующих сайтах для исходного кода и ресурсов SignalR: