SignalR в помощь, или как оживить web


Введение


Во многих web проектах присутствуют элементы, значение которых необходимо часто менять. Это могут быть счётчики, индикаторы, уведомления и подобные элементы. Показывать ли актуальные значения после обновления страницы или же можно реализовать автообновление всех этих данных? Для нас ответ очевиден: если есть возможность динамически менять элементы, то для обновления страниц не остается места.

Для небольших проектов, которые не подвержены критичным нагрузкам, пока у них нет нескольких тысяч пользователей онлайн в часы пик, приемлемым решением было бы использовать AJAX. Логика для этого решения следующая: клиент с заданной периодичностью опрашивает сервер, в поисках обновлений на странице, если сервер сообщил, что есть обновленные данные, то javascript внесен обновления в элементы страницы или отобразит уведомление. То действие, которое будет наиболее подходящим.

Но для больших проектов решение с AJAX, где каждый клиент будет опрашивать сервер, это создаст слишком большую нагрузку. Конечно, мы можем оптимизировать свои мощности и создать цепочку серверов по всей стране, которые будут готовы обрабатывать все запросы клиентов. Это не наш метод. Мы хотим, что бы сервер сам оповещал клиента о новых данных. Подобная практика используется в Desktop-приложениях — cервер, к которому подключаются клиенты при помощи сокетов. Эта логика пригодилась бы нам и в web. Уже есть Websockets, с которыми можно работать, даже .Net взял под крыло поддержку websockets. Но, объективно о повседневном использовании websockets говорить еще рано. Нужно что-то ещё. Возможно использование longpolling, где мы откроем соединение на клиенте и не будем закрывать его вовсе, ожидая события от сервера. Нет, продолжаем искать дальше.

Мы обратили внимание на SignalR. Опишем, как он работает. SignalR может использовать в качестве транспорта и websockets, и longpolling. Транспорт можно задать, а можно оставить на откуп SignalR, который сам выберет нужный. В случае, если можно использовать websocket, то он будет работать через websocket, если такой возможности нет, то он будет спускаться дальше, пока не найдёт приемлемый транспорт.

Более подробно рассмотреть работы SignalR можно при реализации определенной задачи. Что у нас есть: проект, с зарегистрированными пользователями, у каждого пользователя есть личный кабинет, в котором есть раздел сообщений, адресованных этому пользователю. Там же есть и счётчик новых сообщений. Мы хотим, чтобы как только один пользователь (user1) отправил сообщение другому пользователю (user2), на открытой странице пользователя user2 сразу же обновился счётчик новых сообщений.

Чтобы приступить к реализации, нам нужно подключить SignalR к нашему проекту. Как это сделать можно посмотреть на странице signalR, там же можно найти и необходимую документацию.
Если вы используете NuGet, то достаточно будет выполнить:

Install-Package Microsoft.AspNet.SignalR -pre

Работаем


SignalR будет использовать серверную и клиентскую части. На сервере самой сутью является Хаб (Hub). Это Класс, который позволит нам описать поведение SignalR. Так это выглядит в нашем примере:

[HubName("msg")]
    public class Im : Hub
    {
        public Task Send(dynamic message)
        {
            return Clients.All.SendMessage(message);
        }

        public void Register(long userId)
        {
Groups.Add(Context.ConnectionId, userId.ToString(CultureInfo.InvariantCulture));
                
        }

        public Task  ToGroup(dynamic id, string message)
        {
            return Clients.Group(id.ToString()).SendMessage(message);
        }
	}       
       
    }

Мы создали несколько методов.
  • Send — отправит всем клиентам сообщение.
  • Register – поможет SignalR найти того или иного пользователя, которому нужно отослать сообщение. Здесь можно увидеть использование Groups, расскажем об этом позже.
  • ToGroup – отправит сообщение той или иной группе пользователей. Говоря «пользователи» мы подразумеваем «Группа, объединяющие соединения».

Посмотрим на пример клиентского кода, и начнём разбираться что к чему:

<script src="http://code.jquery.com/jquery-1.8.2.min.js" type="text/javascript"></script>

<script src="Scripts/jquery.signalR-1.0.0-rc1.min.js" type="text/javascript"></script>

<script src="/signalr/hubs" type="text/javascript"></script>

<script type="text/javascript">
    $(function () {
       // приготовим Id пользователя.
        var userId = <%= UserId %>
        // прокси         
        var chat = $.connection.chat;

        // объявляем callback, который среагирует на событие сервера          
        chat.client.SendMessage = function (message) {
            // обновляем счётчик сообщений
            UpdateMsgCounter(message);
        };

        // Запускаем хаб
        $.connection.hub.start().done(function() {
             // расскажем серверу кто подключился
               chat.server.register(userId);
            
        });
    });
</script>

Подробнее о клиентском коде. SignalR сгенерирует для нас хаб. Его можно посмотреть для любопытства /signalr/hubs

В коде, при запуске хаба (для этого используется метод start), мы обращаемся к методу register. Мы уже использование его в серверном коде. Как будет получен UserId или другой идентификатор сейчас не принципиально. Мы используем самое простое решение для простоты понимания. Если мы все сделали правильно, то при обращении к странице с нашим клиентским кодом, стартует хаб, и методом register сообщает серверу, что пользователь userId подключился.

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

Для нас доступны следующие возможности:
// разослать всем
 Clients.All.send(message);

 // всем, кроме меня
 Clients.Others.send(message);

// всем, кроме определённого идентификатора соединения
Clients.AllExcept(Context.ConnectionId).send(message);

// тому, кто прислал
Clients.Caller.send(message);

// отослать всем в группе "foo"
Clients.Group("foo").send(message);

// отослать всем из группы "foo", кроме того кто прислал
Clients.OthersInGroup("foo").send(message);

// отослать всем из группы "foo", кроме определённого идентификатора соединения
Clients.Group("foo", Context.ConnectionId).send(message);

// отослать клиенту с определённым идентификатором соединения
Clients.Client(Context.ConnectionId).send(message);

Важный момент: при каждом новом обращении к странице (перезагрузка страницу), в том числе и открытие её в новой вкладке, создает соединение с новым идентификатором соединения SignalR. Это означает, что идентификатора UserId будет недостаточно для того, чтобы оповестить пользователя в каждой открытой вкладке браузера. Нам нужно оповестить все ConnectionId, принадлежащие к пользователю UserId. Для этого в нашем примере класса хаба, в методе Register мы добавили группу с именем UserId каждый новый ConnectionId. Теперь мы можем оперировать UserId как именем группы и оповещать все ConnectionId пользователя.

Частная практика


Давайте рассмотрим еще одну ситуацию. Пользователь user2 написал сообщение пользователю user1 и нажал кнопку «Отправить». Какие наши дальнейшие действия? Можно написать дополнительные методы класса хаба и отправлять сообщения при помощи SignalR. Хаб примет сообщение, обработает и оповестит пользователя. Помимо отправки самого сообщения с этим действием, как правило, связана дополнительная логика: запись сообщения в базу данных, логирование, отправка сообщения на модерацию и многое другое. Могут быть и более сложные примеры. Поэтому, мы ограничимся использованием хаба только по назначению: прием и отправка сообщения. Сначала мы воспользуемся старым добрым AJAX-ом и отправим сообщение от пользователя на HttpHandler. Затем сделаем с ним всё, что необходимо (запишем в базу данных или отправим на модерацию) и в итоге отправим хабу, который оповестит пользователя user1. Но есть сложность — HttpHandler находится в недрах одной из многочисленных библиотек, совсем в другом проекте. Воспользуемся возможностями SignalR чтобы устранить эту сложность. Создадим прокси-класс для соединения с хабом:

static HubConnection connection;
static IHubProxy hub;

static string Url = "http://im.myProjectSite.com"; // адрес нашего хаба
connection = new HubConnection(Url);
hub = connection.CreateHubProxy("msg");
connection.Start().Wait();
hub.Invoke("ToGroup", userId, message);


Посмотрите на использование метода Invoke. Мы вызываем метод ToGroup нашего хаба, который разошлет нужное сообщение по всем соединениям (connectioId), сопоставленными с нужным пользователем UserId. Здесь мы так же задействовали объекты static. Достаточно при старте приложения инициализировать прокси к хабу, скажем, в global.asax и при необходимости вызывать метод, в котором происходит Invoke.

С появлением SignalR в .Net, появилась необходимость в добавлении ещё нескольких строчек кода:

RouteTable.Routes.MapHubs("~/signalr");
RouteTable.Routes.MapHubs();
GlobalHost.HubPipeline.EnableAutoRejoiningGroups();

Использование роутинга SignalR несколько выходит за рамки нашей задачи. Скажем только, что EnableAutoRejoiningGroups поможет нам не потерять соединение в группе для пользователя при создании нового соединения.

С чем Вы обязательно столкнётесь и как это решить


После того как мы разобрались с описанными примерами и собрали демо-проект (или внедрили в существующий) мы пытаемся проверить его на работоспособность. Если всё сделано правильно, пользователь будет оповещен о новом сообщении, как мы этого и хотели. Мы можем даже открыть несколько вкладок браузера, чтобы убедиться в том, что оповещение приходит во все вкладки (все connectionId пользователя). Но стоит нам открыть чуть больше вкладок, как мы обнаружим, что на N вкладке уже ничего не работает. Для разных обозревателей N разное (4 или 6, слишком маленькое). Это ограничение не позволяют создавать больше N одновременных соединений к одному хабу. В некоторых проектах, даже достаточно крупных, можно увидеть решение, в котором пользователю сообщается, что он уже где-то открыл подобный диалог и предложение либо перейти обратно на старый, либо выключить старый и переключиться на этот. Ограничение не должно влиять на пользователя, он может открыть столько вкладок, сколько ему угодно и мы покажем уведомления в каждой из них. Чтобы это работает, нам необходимо каким-то образом дать понять браузеру, что используются разные хабы, а не на один. Ранее при инициализации хаба мы указали его Url: im.myProjectSite.com. Создадим обычные зеркала:

im1.myProjectSite.com
im2.myProjectSite.com
im3.myProjectSite.com


А в клиентском коде мы будем подставлять адрес хаба по определенному алгоритму. Самым простым способом будет каждый раз при обращении к странице (при открытии в новой вкладке), подставлять im (j+1), и j=1 снова после im3. В этом примере мы увеличили ограничение до 3N.

Заключение


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


Автор статьи: Дмитрий Лубенский, ведущий разработчик Дневник.ру. Отвечает за работу биллинговой системы и участвует в развитии международного направления.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 26

    +5
    А что насчет нагрузки на сервер? Что легче — 100 AJAX-запросов в секунду или 100 постоянно висящих соединений-сокетов? А 1000?
      +1
      Вконтакте решили держать 1 коннект на 1 пользователя(не вкладку, а пользователя) менее затратно, чем делать периодические запросы к серверу.
        +1
        По словам Девида Фоулера (David Fowler), автора SignalR, количество одновременных соединений в SignalR ограничено только памятью сервера. Иными словами нужно оценить возможные нагрузки и выбрать наиболее подходящий, исходя из имеющихся ресурсов вариант.
        +1
        >SignalR может использовать в качестве транспорта и websockets, и longpooling
        Это всё?

        Sock.js. Supported transports, by name:
        websocket (rfc6455)
        websocket (hixie-76)
        websocket (hybi-10)
        xhr-streaming
        xdr-streaming
        iframe-eventsource
        iframe-htmlfile
        xhr-polling
        xdr-polling
        iframe-xhr-polling
        jsonp-polling
          +2
          SignalR поддерживает транспорты:

          WebSockets
          Server Sent Events, aka EventSource
          Forever Frame
          Ajax long polling
          +2
          Только не pooling, а polling.
            +1
            Спасибо, исправили.
            +5
            habrastorage.org/storage2/e6c/de7/103/e6cde71039ede17c4874eacbecdd3175.jpg
            Dimensions800 × 781
            File size726.93KB

            «SignalR в помощь, или как оживить web» ну-ну…
              +3
              Немного ускорили web, остальное пришлось принести в жертву искусству.

              Dimensions 700 × 683
              File size 551.31KB
                +3
                Забыли про очевидное: File size 173.98KB
                  +4
                  Можно и так.
                  76,52 КБ (78 359 байт), 700px × 683px, 32 цвета
                    +2
                    Спасибо за мастер-класс.
              +1
              Теперь буду сидеть и нажимать в этой статье F5, чтобы увидеть совет от Хабровчан как наладить удобный PUSH вариант обновления сайта. Чем пользуется Гугл, чем пользуется vk, чем пользуются в реальных проектах и что правильнее?

              Я так понимаю, основная сложность настроить сервер для поддержки постоянных соединений, чтобы он не упал.
                +1
                Не думаю, что у гугла и контакта один сервер %) Вопрос скорее «сколько потянет одна машина».
                  +1
                  Чем пользуется — я подразумеваю технология: websocket, longpolling или что то другое. Интересно, какие технологии PUSH в тренде.
                    +1
                    Говоря о методах real-time web, то большинство проектов реализовано, используя comet, XHR long polling или их комбинации.
                    В скором времени, когда websockets будет поддерживаться в полной мере, то существует большая вероятность, что comet уйдет в прошлое.
                  +1
                  Вопрос о том, сколько может выдержать 1 сервер актуален тогда, когда речь идет о небольшом проекте. Если в проекте количество соединений не превышает нескольких тысяч, то один сервер под SignalR вполне уместен. Если же речь идет о более нагруженных системах, то SignalR прекрасно масштабируется.
                    +1
                    Делал небольшой проектик на socket.io, узким местом на рабочем ноуте (4 ядра 2.4 GHz 6Gb память) становился процессор уже при 3000 одновременных соединений. Залипал именно процесс handshake-а, и что интересно, еще сильнее залипал процесс разрыва большого количества одновременных соединений. Можете сказать, какое количество соединений можно комфортно обслужить одной машиной уровня www.hetzner.de/en/hosting/produkte_rootserver/ex4?
                      +1
                      В ситуации, когда в качестве транспорта выбран websocket, то у webscoket разный handshake у разных протоколов. По этому сейчас полагаться только на websocket'ы ещё рано, так как даже не все браузеры поддерживают протокол webscoket.
                      SignalR будет использовать webscokets, если сможет. В ситуации, когда websocket использовать нет возможности, то SignalR выберет другой наиболее подходящий транспорт.

                      Делать какой-то оценочный прогноз про нагрузку весьма сложно, без готового решения, так как SignalR — это всего лишь инструмент. И он позволяет даже заменять какие-то его компоненты. (IMessageBus, IConnectionIdPrefixGenerator, IAssemblyLocator, IJavaScriptProxyGenerator, IJavaScriptMinifier, IJsonSerializer). Многое зависит от реализации, от того, как вы распоряжаетесь соединениями, используете ли Вы тот самый простой механизм, который описывается в данной статье, или же у вас разработан свой алгоритм.

                      Ко всему прочему, есть возможность самим посмотреть на производительность SignalR, воспользовавшись Microsoft ASP.NET SignalR Utilities
                        0
                        >> и что интересно, еще сильнее залипал процесс разрыва большого количества одновременных соединений
                        Если память не изменяет, то правильный разрыв TCP соединения, не менее, а то и более затратная операция чем устновление
                          +1
                          Попробуйте использовать вместо socket.io sockJS
                          У меня с ним начинает валиться при ~8000 соединениях
                      +1
                      Спасибо, интересная статья.
                        0
                        Используем SignalR в нашем проекте, причем проксируем его через nginx, как и обычные запросы.
                        Поллинг забыли как страшный сон)

                        Кстати, WebSockets поддерживаются только начиная с Windows 8 (2012 Server).
                          +1
                          Не работаю с виндами, поэтому вопрос: что значит WebSockets поддерживается с Windows 8?
                          Как связаны протокол транспорта и операционная система?
                            0
                            Говоря о SignalR, мы подразумеваем ASP.NET и IIS. А WebSockets поддерживается только в IIS 8, который идет только с этими ОС…
                              +1
                              Кстати да, мне кажется стоило бы упомянуть .NET в заголовке.

                        Only users with full accounts can post comments. Log in, please.