Учебник. Передача сообщений с сервера с помощью 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

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

Вы можете добавить функциональные возможности 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);
                }
            }
        }
    }
    

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

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

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

Вещание от StockTicker

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

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

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

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

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

    Атрибут 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 должен быть threadsafe.

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

    Код инициализирует статическое поле _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 в параметре state. Перед обновлением цен на объект _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 позволяет написать код для вызова клиентских методов, которые выглядят так же, как и в классе Hub. Например, для трансляции на все клиенты можно написать Clients.All.updateStockPrice(stock).

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

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

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

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

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

  2. Выберите шаблон элемента Global Application Class (Глобальный класс приложения ) и нажмите кнопку Добавить.

    Добавление 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 в папке Scripts проекта, и убедитесь, что версия SignalR в теге скрипта совпадает с версией SignalR в папке Scripts проекта. При необходимости измените имена файлов в тегах скрипта.

  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 и помещает его в переменную ticker. Имя прокси-сервера — это имя, заданное атрибутом [Имя_концентратора]:

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

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

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

    Функция init вызывает функцию getAllStocks на сервере и использует сведения, возвращаемые сервером, для обновления таблицы акций. Обратите внимание, что по умолчанию необходимо использовать верблюдий регистр на клиенте, хотя имя метода на сервере имеет pascal-регистр. Правило верблюдьего регистра применяется только к методам, а не к объектам. Например, вы ссылаетесь на акции. Символ и акции. Цена, а не 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 так же, как сам класс Hub с помощью атрибута HubName.

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

    Вызов init выполняется путем передачи его в качестве функции обратного вызова, которая выполняется после завершения асинхронной функции запуска. Если вы вызвали init как отдельный оператор JavaScript после вызова start, функция завершится ошибкой, так как она будет выполняться немедленно, не дожидаясь завершения соединения с функцией start. В этом случае функция инициализации попытается вызвать функцию 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 продолжал работать на сервере, поэтому на дисплее таблицы акций показано, что запасы продолжают меняться. (Начальная таблица с нулевыми измененными цифрами не отображается.)

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

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

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

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

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

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

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

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

    Консоль IE 10 IIS 8

    Если вы используете Internet Обозреватель 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 щелкните Интернет, введите 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 в папке Scripts . Новый файлStockTicker.html , который пакет устанавливает в папке SignalR.Sample , будет синхронизирован с версией jQuery, устанавливаемой пакетом, но если вы хотите снова запустить исходный файлStockTicker.html , может потребоваться сначала обновить ссылку jQuery в теге скрипта.

Выполнение приложения

  1. Нажмите клавишу F5 для запуска приложения.

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

    Запуск экрана StockTicker

    При нажатии кнопки Открыть рынок поле 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 должен быть threadsafe:

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

Чтобы убедиться, что этот код является threadsafe, поле _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: