Блог

Полезные статьи и новости о жизни WaveAccess

Использование SignalR в приложениях реального времени

За последние несколько месяцев WaveAccess успешно завершил разработку серии крупных проектов с использованием технологии SignalR. В статье мы опишем основные преимущества данной технологии.

ASP.NET SignalR (на момент написания статьи, версия 2.0.2) – библиотека, созданная для взаимодействия клиента и сервера в реальном времени. При этом участвовать могут как web, так и desktop приложения, но в данной статье речь пойдет о первых. С помощью SignalR можно без особых усилий абстрагироваться от технологий обмена данными и уделить время более важным вещам.

Основным принципом функционала является возможность вызывать клиентские JavaScript методы из серверного кода и наоборот.  Простой доступ к методам обеспечивается, отчасти, работой с динамическими объектами на сервере. Передача данных на практике осуществляется путём выбора оптимальной технологии общения клиента и сервера и её последующей инициализации для отдельно взятых клиентов. Нет необходимости заботиться о конкретных реализациях, подбирать оптимальную для различных клиентов – всё уже готово.

Относительно возможностей: идеальным случаем является использование WebSocket, и она будет обязательно выбрана при подходящих условиях.  Кроме этого, используются ещё три транспорта, однако они менее предпочтительны (больше расход памяти клиента и сервера, больше задержки  и т.д.). Не будем подробно останавливаться на поддержке и алгоритме их выбора, отметим только, что в IE старых версий используется так называемый foreverFrame (его принцип – создание iframe, в который загружаются данные с сервера). Этот транспорт упомянут по той причине, что на практике мы столкнулся с некоторыми сложностями, они рассмотрены далее. На сайте asp.net  (http://www.asp.net/signalr) размещено подробное описание всех аспектов работы с библиотекой и руководство пользователя с реальными примерами.

Решаемые задачи

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

Пример работы

На сайте разработчика указано большое количество всевозможных примеров и весь спектр настроек. Можно просто скопировать куски кода и через несколько минут увидеть результат. Все статьи обязательно нужно прочесть, чтобы понимать возможности и вероятные проблемы. В любом случае, создание приложения с нуля будет основой, а всё остальное понимание приходит с практикой.

Пошаговое создание «для чайников»:

1) Создаем новый пустой (Empty project template) проект .NET MVC 4 Application.

2) Добавляем библиотеку с помощью Nuget. Для этого к консоли «Package Manage Console» необходимо прописать Install-PackageMicrosoft.AspNet.SignalR.

3) В студии по умолчанию открывается readme файл, в котором есть несколько полезных ссылок, и пример основных настроек.

4) Теперь, когда все зависимости установлены, инициализируем настройки.

Добавляем в папку App_Start новый файл SignalRConfig.cs (название и местоположение не существенно), и добавляем следующий код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Owin;
 
namespace TestSignalRHub
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

Название класса должно быть именно «Startup», если просто его изменить – при запуске приложения увидим исключение. Если всё-таки есть необходимость изменить название, это можно сделать, добавив атрибут, указывающий, какой класс должен быть использован для инициализации.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Owin;
using Owin;
 [assembly: OwinStartup(typeof(TestSignalRHub.InitSignalR))]

namespace TestSignalRHub
{
    public class InitSignalR
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

Перегруженные методы MapSignalR принимают некоторые параметры, но для простого использования. Можно ничего не передавать, хотя мы добавили экземпляр класса настроек – HubConfiguration, где указали EnableDetailedErrors= true. После этого все исключения, произошедшие на сервере (разумеется, связанные с SignalR) возвращались в консоль браузера. Это возникало редко, но иногда давало понимание о том, что пошло не так.

5) Определение серверных методов.

Необходимо подготовить класс, в котором будут находиться серверные методы – реализация так называемого «хаба» (hub). Создадим папку Hubs в корне проекта и добавим класс TestHub, наследуем его от абстрактного класса Hub. Далее создадим метод (который затем будет вызываться с клиента),  и обнаружим,  что после наследования, из класса Hub доступно несколько свойств, а именно: Clients, Groups и Context.  Через Clients открыт доступ ко всем доступным пользователям, и это означает возможность вызывать клиентские методы у любого из пользователей. Через Groups можно управлять группами пользователей, и затем взаимодействовать с группой пользователей. И, наконец, Context – текущий контекст соединения, который включает в себя различную информацию (например, идентификатор соединения, cookies,..).  На рисунке показан вызов клиентских методов:

using System.Web;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Collections.Generic;
 
namespace TestSignalRHub.Hubs
{
    public class TestHub : Hub
    {
        public void UserConnecting()
        {
            var id = Context.ConnectionId;
           
            Clients.Others.userConnected(string.Format("New User, connectionId: {0}", id));
            Clients.Caller.userConnected(string.Format("Connected with connectionId: {0}", id));
        }
    }
}

Свойства Others и Caller (как, впрочем, и все остальные варианты обращения к клиенту) – динамические объекты, и название метода не доступно на этапе компиляции, оно появляется на этапе выполнения. Это делает доступ более простым и интуитивно понятным.

После соединения с сервером, на клиенте необходимо реализовать вызов серверного метода UserConnected. Здесь происходит получение идентификатора текущего соединения (у каждого клиента он уникальный и генерируется при подключении). Затем, обращаясь к списку клиентов, получаем ряд полезных свойств. Первое вызывает методы всех клиентов, кроме текущего, второе, наоборот – вызов методов текущего клиента. Есть еще несколько свойств и методов для обращения к клиентам, и наиболее примечательные: All – обращение ко всем, Client(*string*) – обращение к клиенту по идентификатору соединения, Clients(*listofstrings*)  – аналогично но для нескольких пользователей, и User – по имени пользователя. Имя пользователя – это название, с которым сопоставляется конкретный идентификатор соединения. По умолчанию, в SignalR 2.0,  используется IPrincipal.Identity.Name как имя пользователя, но эту логику можно переопределить.

6) Реализация клиентской части.

Для этого создадим пустое представление (можно использовать и просто html страницу, это не принципиально). Для простоты весь код будет находиться вместе с разметкой в пустом представлении Index.cshtml. При запуске приложения получаем пустую страницу, ну или какой-либо статический контент, например, заголовок. 

Index.cshtml, jquery, signalR 

Необходимо подключить jquery и jquery.signalR, а также некий путь «/signalr/hubs», перейдя по которому, мы ничего не обнаружим – контент по этому пути генерируется автоматически на этапе выполнения.

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

@{
ViewBag.Title = "Index";     
Layout = "~/Views/Shared/_Layout.cshtml"; }  
<h2>Index</h2>
<script type="text/javascript" src="/Scripts/jquery-2.1.0.js"></script>
<script type="text/javascript" src="/Scripts/jquery.signalR-2.0.3.js"></script>
<script type="text/javascript" src="/signalr/hubs"></script>
 
<script type="text/javascript">
    $(function() {
        var test = $.connection.testHub;
       
        test.client.userConnected = function (message) {
            $("#container").append("<div>" + message + "</div>");
        };
       
        $.connection.hub.start().done(function() {
            test.server.userConnecting();
        });
    });
</script>
 
<div id="container"></div>

 В общем случае, на странице должен быть инициализирован список методов «клиентской» части, затем произведено соединение, после чего можно вызывать серверные методы. Строка 19 – инициализирует клиентский метод, который можно вызывать с сервера. На строке 28 показано, как вызываются серверные методы. Важно, чтобы серверные методы вызывались уже после инициализации соединения (вызов метода start), функция done(callback) существует как раз для этого. Случается так, что на странице где-либо в коде (функции сторонних Фреймворков, обработчики каких-либо событий) может находиться вызов серверного метода. В таком случае в консоли браузера будет появляться ошибка при загрузке или выгрузке страницы, когда метод вызывается до инициализации соединения. Для проверки существования соединения перед вызовом метода можно определять статус соединения, как показано на рисунке ниже. Например, при переходе по внешней ссылке на странице необходимо вызвать серверный метод, и обновить какие-либо данные сервера.

Помимо индикатора успешного соединения (done),  существует возможность обработать ошибки через реализацию метода fail – он принимает на вход функцию, которая вызовется  в случае проблемы соединения. Например, нужно отобразить сообщение пользователю, либо попробовать подключиться снова. 

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

 

<h2>Index</h2>

 

 
<script type="text/javascript" src="/Scripts/jquery-2.1.0.js"></script>
<script type="text/javascript" src="/Scripts/jquery.signalR-2.0.3.js"></script>
<script type="text/javascript" src="/signalr/hubs"></script>
 
<script type="text/javascript">
    $(function() {
        var test = $.connection.testHub;
       
        var i = 0, timer = setInterval(function() {
            i++;
            if (test.connection.state == $.signalR.connectionState.connected) {
                clearInterval(timer);
                alert('connected!' + i);
                // test.server.loadData...
            }
        });
       
        test.client.userConnected = function (message) {
            $("#container").append("<div>" + message + "</div>");
        };
       
        $.connection.hub.start()
            .done(function() {
                test.server.userConnecting();
            })
            .fail(function() {
                alert('error')
            });
    });
</script>
 
<div id="container"></div> 

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

Теперь после перехода на страницу отображается информация «Connected with connectionId: …», отрабатывается серверный вызов метода Clients.Caller.userConnected. Что будет, если открыть страницу в другом окне или браузере? Для первого окна – выводится ещё одно сообщение «New User, connectionId: …». Таким образом, пользователи могут взаимодействовать с сервером, который, в свою очередь, работает сразу со всеми клиентами.

Рассмотренный выше пример – простейшая реализация, подобные примеры приведены на официальном сайте. На практике приходится использовать ещё несколько полезных возможностей и столкнуться с некоторыми проблемами. В начале разработки пригодится добавление строки: $.connection.hub.logging = true, после которой SignalR будет автоматически логировать все действия пользователя в консоль браузера. Также будет полезным отслеживание состояний путём добавления обработчика:

$.connection.hub.stateChanged(function (change) {
if (change.newState === $.signalR.connectionState.disconnected) {
    …
	}
		});

Здесь можно добавить какую-либо логику для разных статусов соединения (лучше не добавлять серверную логику на событие reconnect, так как оно возникает при перезагрузке сервера, или при ребилде проекта).

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

IHubContext context = GlobalHost.ConnectionManager.GetHubContext<MainHub>();

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

Логирование серверных методов для работы с клиентами лучше вынести отдельно. Это реализуется путем создания собственной реализации класса (_ClassName_), наследуемого от HubPipelineModule. Это абстрактный класс, содержащий ряд виртуальных методов, отвечающих за различные события обработки запросов и ответов. Настройка своей реализации осуществляется при конфигурировании, перед вызовом метода MapSignalR нужно вызвать GlobalHost.HubPipelineModule(new _ClassName_);

Проблемы и сложности, ввыявленные в процессе использования

При слишком большом объеме данных (например, массив из 500 000+ элементов), передаваемых между клиентом и сервером, могут сложиться следующие ситуации: OverflowException на сервере, или «отпадание» клиента по таймауту. Второе не случается при использовании транспортов WebSocket и ForeverFrame. Выход из данной ситуации – разделение данных на более мелкие части. Мы не видим в этом большой проблемы, так как всё равно необходимо обрабатывать эти данные на клиенте (что тоже может быть проблематичным из понятных соображений). Технически, эта проблема возникает из-за «пинга» клиента со стороны сервера. Клиент буквально «захлебывается» обработкой и не отвечает на запросы сервера. Различные таймауты могут быть изменены в настройках SignalR, но это нужно делать в последнюю очередь, или же для этого должны быть веские причины.

Ещё одна ситуация, которая может ненадолго поставить в тупик – получение HttpContext при обработке запросов через SignalR. На собственном опыте пришлось столкнуться со следующей ситуацией: необходимо было хранить в cookies некоторые идентификаторы для работы с пользовательскими данными. При этом получение их происходило путем чтения из HttpContext. Все работало как нужно, пока не началось тестирование с WebSockets. Оказалось, что данный подход не работает, и нужно использовать специальный HubCallerContext – это свойство Context, доступно внутри серверной реализации хаба. Стоит также указать, что сам принцип использования SignalR противоречит использованию сессии на сервере, а значит необходимо реализовывать свою логику хранения пользовательских состояний на сервере (если это нужно).

И третья, достаточно существенная, проблема связана с транспортом foreverFrame (для IE). В данном случае передача данных осуществляется без конвертации в json, а вставкой javascript кода непосредственно в iframe. Как известно, в IE существует проблема передачи некоторых типов данных при использовании фреймов. Замечены были проблемы с массивами и датами. После получения данных на клиенте попытки проверки массива через конструкцию «instanceof» языка javascript не приводят к желаемому результату. Дальнейшая обработка данных (возможно использование сторонних фреймворков) приводит к неожиданным проблемам. В недавнем времени эта особенность была в процессе устранения со стороны разработчиков, возможно, она уже устранена. В противном случае необходимо вручную обновлять набор получаемых данных, или же обращаться с ними с учётом возможных проблем (не использовать instanceof).

Итог

Использование библиотеки SignalR может значительно облегчить работу, если правильно соотносить поставленные задачи с возможностями. Перед началом рекомендуем прочесть руководство на официальном сайте, хотя бы потому, что постоянно добавляются новые возможности и советы по правильной реализации архитектуры. Тем более, в начале разработки важно правильно настроить логирование и правильно выбрать архитектурное решение. Если всё устаивает – реализация сэкономит много времени и усилий. При более длительной работе выгода от использования становится весьма очевидной. Многие аспекты не были рассмотрены, но стоит вспомнить о возможностях SelfHosting  и настройке безопасности с помощью атрибутов по типу .NET MVC. Постоянная поддержка и обновление функционала библиотеки обеспечивает гарантию работоспособности, невзирая на изменение стандартов или принципов технологий.

Заказать звонок

Удобное время:

Отменить

Пишите!

Присоединить
Файл не больше 30 Мб.
Отменить