Sunday, November 18, 2012

Разработка real-time ASP.NET приложений с помощью SignalR

SignalR, на мой взгляд – одна из самых впечатляющих библиотек в ASP.NET, появившихся за последние пару лет. Я бы сказал, настоящее событие, своеобразная веха. И пока команда корпит над готовящимся релизом первой версии, самое время сделать ее небольшое описание. Однако прежде чем начать, стоит немного остановиться на том, зачем вообще кто-то придумывал эту библиотеку и какую проблему она решает.

Real-time веб приложения

Наверняка многие пользовались Facebook, Twitter или другими социальными сервисами и замечали, что когда приходят обновления новостей или статусов, страница обновляется в реальном времени и вы постоянно видите последнюю актуальную информацию. В Google Docs можно совместно редактировать документы, и все обновления синхронизированы и появляются у вас сразу же, как только их сделает другой пользователь. Это примеры сервисов, которые можно назвать real-time веб-приложениями. Основной их отличительной способностью является то, что они “ломают” привычную нам всем модель работы в вебе – запрос-ответ, благодаря чему пользователи видят обновление данных сразу же, как только они появляются на сервере.

Если не знать, насколько далеко вперед шагнули технологии за последние несколько лет, то можно предположить, что все подобные приложения реализованы при помощи периодического опрашивания сервера обычными Ajax-запросами, т.н. polling. Выглядит похоже, но это не так, в чем можно легко убедиться, просмотрев логи запросов из браузера на сервер в Firebug, Fiddler или Chrome developer tools.

Способы реализации

Итак, какие же есть способы реализации подобной функциональности, их достоинства и недостатки.

Техника Описание Преимущества Недостатки
Polling Постоянный опрос сервера Ajax-запросами + простота реализации
+ поддержка во всех современных браузерах
- задержка в результатах
- при уменьшении задержки существенно увеличивается нагрузка на сервер
Long Polling Ajax-запросы, идущие один за другим, но каждый запрос держится открытым в течение нескольких минут + сниженная нагрузка на сервер по сравнению с обычным Polling
+ уменьшенный трафик
+ поддержка во всех современных браузерах
- больше одновременно открытых соединений, т.к. каждый запрос живет дольше
Server-Sent Events Новый стандарт HTML5, работающий поверх HTTP. Позволяет создавать долгоживущее соединение с сервером, чтобы сервер мог отправлять данные на клиент + нет необходимости постоянно пересоединяться с сервером
+ нет изменений на стороне сервера, поэтому работает на всех современных веб-серверах
- не поддерживается в IE (даже в IE10)
- работает только в направлении сервер –> клиент (на сервер можно отправлять обычные Ajax запросы)
WebSockets Новый протокол (ws:// и wss://), работающий поверх TCP на одном уровне с HTTP. Позволяет создавать двустороннее долгоживущее соединение с клиентом + нет необходимости постоянно пересоединяться с сервером
+ работает в двустороннем режиме
- поддерживается не во всех веб-серверах (IIS8)
- поддерживается не во всех браузерах (в IE7-9, Android)

Для большей информации о Server-Sent Events и WebSockets советую посмотреть статью на HTML5 Rocks: http://www.html5rocks.com/en/tutorials/eventsource/basics/

Если посмотреть на достоинства и недостатки, то можно увидеть, что самый эффективный вариант – это WebSockets, но он не поддерживается во всех браузерах и будет поддерживаться лишь на IIS8 и выше. Server-Sent Events работает на более старых версиях IIS, но не поддерживается в IE, поэтому тоже подойдет далеко не всегда. Long Polling работает везде, но при этом далеко не так эффективен.

Идеальным решением было бы совмещение этих техник в разных случаях, но это сложно реализовать и поддерживать. Однако есть хорошая новость – это решение уже реализовано в библиотеке SignalR.

Введение в SignalR

SignalR – это библиотека для создания многопользовательских real-time ASP.NET (и не только) приложений. Она состоит из набора серверных и клиентских библиотек, и представляет собой абстракцию над целым набором транспортов. Все это добро – open source, лежащий на GitHub: https://github.com/SignalR, поэтому вы всегда можете пойти и посмотреть, что там внутри, какие есть баги и т.д. Кроме того, в разделе Wiki есть много информации о SignalR и примеров: https://github.com/SignalR/SignalR/wiki

SignalR был придуман и реализован двумя разработчиками Microsoft: Damian Edwards и David Fowler. В своих твиттерах они часто пишут полезную информацию о SignalR и сообщают о новостях. Также David ведет блог, в котором описывает все изменения в новых версиях. Если вы решите использовать библиотеку, то советую подписаться.

На момент написания этой статьи SignalR находится в предрелизном состоянии (версия 1.0 alpha2). Еще планируются один или несколько RC, после чего продукт будет выпущен в RTM.

Транспорты

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

image

То есть если сервер и клиент поддерживают WebSockets, то будет установлено WebSockets-соединение и все будут счастливы. Если же нет – то далее будет проверка, поддерживает ли клиент SSE, и если да – будет установлено это соединение. В случае IE сразу же будет испробован подход Forever Frame (невидимо висящий iframe устанавливает соединение и получает JS-инструкции с сервера) – это IE-хак, т.к. даже 10-я версия IE не поддерживает SSE. Молодцы, нечего сказать.

И в конце-концов, если клиент не поддерживает ни один из этих способов, или произошла ошибка, то SignalR откатится до самого надежного способа – Long Polling, который работает практически везде.

Архитектура

Архитектура SignalR очень проста. На сервере реализованы 2 вида API: низкоуровневый (PersistentConnection API) и высокоуровневый (Hub API), причем Hub опирается на PersistentConnection. Вы можете использовать любой из них, но в большинстве случаев вам будет достаточно возможностей, предоставляемых Hub API.

image

В альфе официально поддерживаются JavaScript (браузер), .NET 4 и WinRT клиенты. Windows Phone и Silverlight были в предыдущей версии, но в альфу не попали. Их обещают допилить ближе к релизу.

Также в альфу не попала библиотека для self hosting. Вместо нее обещают поддержку http://owin.org/.

Простое SignalR приложение

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

1) Открываем VS и создаем ASP.NET MVC приложение

2) Инсталлируем пакет Microsoft.AspNet.SignalR через NuGet:

Install-Package Microsoft.AspNet.SignalR

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

После инсталляции вы увидите несколько изменений в проекте:

  1. В References добавятся библиотеки SignalR
  2. В Scripts добавятся библиотеки jQuery.SignalR
  3. В папке App_Start появится класс RegisterHubs, который занимается регистрацией рута ~/signalr/hubs

3) Создаем класс MoveHub в папке Controllers (может быть и другая папка, например, Hubs, но хабы – тоже своеобразные контроллеры):

using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR_test.Controllers
{
	public class MoveHub : Hub
	{
		public void MoveShape(int x, int y)
		{
			Clients.All.shapeMoved(Context.ConnectionId, x, y);
		}
	}
}

Обращение Clients.All обозначает, что мы хотим отправить сообщение всем клиентам, которые подписались на события, происходящие в хабе. Метод MoveShape будет вызываться с клиента и вызывать метод shapeMoved во всех браузерах, открывших страницу с параметрами x, y (положение квадрата) и Context.ConnectionId (уникальный идентификатор клиента, отправившего запрос на сервер). Объект Clients.All (и другие) – dynamic, поэтому мы можем вызывать любые методы с любыми параметрами без опасения ошибок компиляции. В то же время это обозначает и то, что нам нужно быть предельно внимательными с именами методов и параметров, включая регистр – никакой проверки компиляции здесь нет.

4) Добавляем метод контроллера, который будет отображать нашу страничку Shape.cshtml:

public class HomeController : Controller
{
	...
	
	public ActionResult Shape()
	{
		return View();
	}
}

Здесь все просто, комментировать нечего.

5) Добавляем Shape.cshtml view, в котором ссылаемся на несколько JS файлов.






Сначала подключаем jQuery и jQuery.UI (для реализации draggable), затем библиотеку jQuery.SignalR, и в конце – обращение к JS-файлу с хабами. Это то место, где происходит вся магия SignalR. Файл генерируется на лету инфраструктурой библиотеки на основании всех хабов, которые есть в вашем коде. Если заглянуть в этот файл из браузера, то можно увидеть, как в нем регистрируются объекты и методы, использующиеся впоследствии из JavaScript-кода.

6) Кладем во view кусок JS-кода. В настоящем приложении JS-код лучше вынести в отдельный файл, но для наших целей и такого кода достаточно.


В JavaScript мы делаем следующее:

  1. Регистрируем метод shapeMoved, который будет “вызываться” из серверного кода. В методе двигаем фигуру, если мы сами не являемся источником запроса
  2. Стартуем соединение с сервером
  3. По успешному подключению регистрируем обработчик события draggable у фигуры, который будет вызывать серверный метод moveShape, находящийся в нашем хабе

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

Группы

Очень полезной возможностью SignalR является поддержка групп. Группы используются для объединения подключений и последующего их группового использования. Например, когда у вас есть разные объекты (комнаты чатов, аукционы и т.д.) и необходимо, чтобы оповещения получали лишь те пользователи, которые просматривают именно этот объект.

С группами очень просто работать:

// Add connection to group "foo"
Groups.Add(Context.ConnectionId, "foo");

// Call send on everyone in group "foo"
Clients.Group("foo").send(message);

// Call send on everyone else but the caller in group "foo"
Clients.OthersInGroup("foo").send(message);

// Call send on everyone in "foo" excluding the specified connection ids
Clients.Group("foo", Context.ConnectionId).send(message);

Другие возможности SignalR

В целом, SignalR реализует следующие сценарии:

  1. клиент вызывает метод на сервере
  2. сервер вызывает метод на клиенте/клиентах
  3. передача состояния с клиента на сервер и обратно
  4. поддержка передачи сложных объектов (JSON сериализация)
  5. определение соединения, отсоединения и пересоединения клиентов
  6. обращение к клиентам извне хаба при помощи специального интерфейса (то есть любой код на сервере может оповестить клиенты о событии)
  7. асинхронные сценарии

В официальной документации можно посмотреть более подробное описание всех API и их возможности, как на сервере, так и на клиентах:

Веб-ферма

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

Разработчики позаботились об этом и на данном этапе предлагают 3 решения:

  1. Поддержка Windows Azure Service Bus (идеально подходит для развертывания в Azure)
  2. Поддержка Service Bus for Windows Server
  3. Поддержка Redis

Если у вас не Azure, то остается два последних варианта. Из них лично я бы посоветовал Redis, т.к. он очень прост в установке и конфигурировании (его почти нет), в то время как Service Bus for Windows Server потребует от вас недюженных усилий.

Redis – это key-value storage, изначально разработанный для Linux, но под Windows существует несколько портов. Официально поддерживающийся порт и его настройка описаны в документации SignalR. Неофициальный, но по всей видимости более взрослый порт можно найти здесь: https://github.com/dmajkic/redis. Решение, как запустить его как Windows Service, описано здесь: https://github.com/kcherenkov/redis-windows-service.

Со временем обещают поддержку и других шин.

Полезные ссылки

Если вы хотите посмотреть пример приложения на SignalR, то вы можете скачать пример приложения, показывающего обновляющиеся биржевые котировки с GitHub (https://github.com/SignalR/SignalR-StockTicker), либо зайти на http://jabbr.net и попробовать вживую пример, реализующий чат. Кстати, на Jabbr в комнате, посвященной SignalR довольно часто бывает David Fowler, и у него можно спросить любой вопрос, который вас интересует.

Спасибо за внимание и успешной разработки!