Постоянное соединение между браузером и сервером
В статье про клиентскую часть интерактивного интернет-проекта мы подошли к вопросу возможности использования двухстороннего
постоянного соединения между сайтом и JavaScript-клиентом для
синхронизации их состояний. Такое соединение представляет собой канал
для обмена сообщениями в реальном времени между браузером и серверным
процессом, причем каждая сторона может быть инициатором отправки
сообщения и имеет некую логику реакции на получаемые сообщения.
Сегодня мы рассмотрим основные варианты реализации этого принципа и как
он сочетается с обсуждавшимися в предыдущих статьях
серии темами.
Транспорт
Так как одной из сторон постоянного соединения является браузер, вопрос
кроссбраузерности при его реализации стоит не менее остро, чем,
например, при верстке. В 2001 году, когда появился на свет самый часто
вспоминаемый недобрым словом браузер в мире, о подобных технологиях
постоянного соединения между браузером и сервером практически никто не
задумывался даже отдаленно.
Существуют несколько протоколов и связанных с ними технологий, которые
позволяют реализовать постоянное с точки зрения приложения соединение
между браузером и сервером, обычно их называют транспортами. Каждый
из них обладает разной производительностью, особенностями реализации и
нагрузкой на серверную часть. Возможно не полный их список c краткими
пояснениями:
- WebSocket: пожалуй,
самый эффективный с точки зрения производительности и нагрузки на
сервер транспорт. Протокол относительно новый, появился в рамках
работы над HTML5. Доступен только в очень свежих
браузерах, имеет несколько более-менее стандартных версий.
Используется одно соединение для обоих направлений обмена
сообщениями. - EventSource: появился
примерно в то же время, что и WebSocket, но по задумке должен
использоваться для получения односторонних уведомлений от сервера. В
совокупности с простыми AJAX запросами для отправки событий из
браузера может использоваться для двустороннего общения. Но так как
он доступен примерно в тех же версиях браузеров, что и WebSocket, со
сценариями, когда он оказывался бы более предпочтительным, я не
сталкивался. Технически очень похож на следующий транспорт. - AJAX Multipart aka HTTP Streaming: после
получения HTTP-запроса от клиента сервер не «отпускает» его и по
мере поступления отправляет в него свои сообщения. Для отправки
сообщений из браузера при необходимости создается второе соединение. - AJAX/HTTP Polling: в
отличии от предыдущего транспорта, сервер закрывает HTTP-соединение
после каждого отправленного в него сообщения или по прошествии
определенного таймаута (обычно порядка 20-40 секунд). А браузер
сразу же после получения сообщения открывает новое соединение, таким
образом у сервера по-прежнему практически всегда есть соединение,
куда можно отправить сообщения. Хоть по нагрузке на сервер этот
вариант самый тяжелый, поддерживают его практически все браузеры. - Adobe Flash: эта
платформа может эмулировать поддержку WebSocket при определенном
стечении обстоятельств (удачная комбинация Flash-плеера и браузера).
Немного нетривиальна в настройке из-за своих особенностей.
По поводу поддержки каждого из них различными браузерами было бы неплохо
составить табличку, но на самом деле нюансов там много и многое зависит
не только от версии браузера, но и от других обстоятельств, вроде
наличия и типа прокси, использования трюков с iframe, наличия
Flash-плеера и т.п.
Все вышеизложенные транспорты в конечном итоге основываются на протоколе
HTTP. Большинство из современных браузеров ограничивают
количество одновременных HTTP-соединений с доменом до двух, что как
раз достаточно даже для менее эффективных вариантов.
В любом случае работать напрямую с транспортами не обязательно, благо
существует большое количество библиотек и сервисов, позволяющих от них
абстрагироваться, к ним и переходим.
Абстракция
По сути такие библиотеки состоят из двух частей: клиентской на
JavaScript и серверной для одной или нескольких
платформ. Клиент определяет какой из доступных в текущем браузере
транспортов является наиболее эффективным и с его помощью устанавливает
соединение с сервером, который поддерживает несколько протоколов. С
точки зрения разработчика интерфейс, ими предоставляемый, не зависит от
транспорта и примерно одинаков:
- Метод для отправки сообщения противоположной стороне.
- Регистрация обработчика события, который будет вызван при
получении сообщения от противоположной стороны, с содержанием
сообщения в аргументе. - Метод, который будет вызван при установке и разрывании соединения.
- Инициатором соединения по очевидным причинам всегда является клиент,
так что у него есть дополнительный механизм для этого, с
возможностью указать какие-то настройки.
При выборе такой библиотеки для конкретного проекта очень большую роль
играет его основная серверная платформа: обычно хочется использовать тот
же язык программирования для обработки сообщений, что и для реализаций
основной серверной части. Чаще всего используется основанный на
epoll или аналогах HTTP-сервер, что позволяет
поддерживать большое количество пользователей онлайн:
- Node.js на JavaScript
- На Erlang есть несколько очень эффективных
HTTP-серверов: - Tornado на Python
- netty на Java
Так как самих библиотек этой категории существует примерно пару
десятков, расскажу вкратце о наиболее заслуживающих внимания на мой
взгляд:
- socket.io: поддерживает практически все
возможные транспорты, включая Flash. Основная
серверная платформа — node.js, силами сторонних разработчиков
есть реализации протокола на других платформах. Имеет спорную
репутацию, проект довольно громоздкий, в некоторых ситуациях ведет
себя непредсказуемо. - SockJS: очень молодой
проект, поддерживает необходимый минимум транспортов, прост в
эксплуатации. Относительно стабилен и предсказуем. Серверная часть
доступна на node.js, Tornado и cowboy/misultin, активно
работают над другими платформами.
Существуют коммерческие решения, абсолютно идентичные по принципу работы
и функционалу. Аналогичная обсуждавшимся opensource решениям библиотека
дополняется брокером сообщений для организации паттерна
«публикация-подписка» и в совокупности с хостингом «в облаках» продается
с оплатой за количество переданных сообщений (или по подписке с каким-то
лимитом), естественно с нехилой наценкой. Плюсы и минусы очевидны:
отсутствие необходимости обо всем этом заботиться против относительно
высокой стоимости, потере контроля при сбоях или необходимости
изменений, привязке к стороннему поставщику услуг и т.п. Рекламировать
их не буду, при желании легко гуглятся, ровно как и оставшиеся
альтернативные opensource проекты.
Вернемся к интерактивным сайтам
Надеюсь, только что закончившегося лирического отступления на 3/4 статьи
Вам будет достаточно, чтобы составить общее представление о построении
постоянного соединения между браузером и сервером, а желательно и
определиться с каким-то решением для автоматического выбора наиболее
эффективного транспорта в контексте именно Вашего проекта.
Получив примитивный интерфейс в виде «отправить сообщение /
отреагировать на сообщение» необходимо определиться с тем, что же мы
будем передавать в этих сообщениях и как будем на них реагировать.
С форматом сериализации сообщений все довольно просто: выбор между XML и
JSON очевиден в пользу последнего, а заморачиваться с чем-то более
экзотическим смысла мало (хотя давно хочу попробовать в этой роли
Protocol Buffers или BSON, но
никак руки не доходят).
Намного интереснее вопрос о том, что, собственно, будет в этих
сообщениях содержаться. В предыдущей статье
мы остановились на использовании фреймворка для организации кода
JavaScript-клиента. Предлагаемая ими концепция модели обычно
по-умолчанию предоставляет возможность синхронизации с сервером
посредством AJAX запросов и механизм изменения этого
поведения. Для использовавшегося в качестве примера
Backbone.js для этого необходимо переопределить
функцию Backbone.sync. При сохранении модели клиент будет отправлять
объект с идентификатором модели и списком её изменений. Запрос изменений
с сервера будет происходить асинхронно, то есть после отправки сообщения
о том, что нужны данные для такой-то модели, посредством метода fetch он
сам не получит ответа. Собственно изменения в модели произведет
обработчик получения сообщений, в котором должна быть реализована
соответствующая логика. Далее подписанные на события изменений в моделях
объекты-представления будут соответствующим образом обновлять DOM-дерево
страницы, отображая пользователю нужную информацию. Это, пожалуй,
наиболее правильный способ интегрировать постоянное соединение и
клиентский фреймфорк.
Основными минусами его является очень серьезный объем работы по
разработке клиентской части, а также дублирование достаточно большой
части логики и HTML-шаблонов между серверной и клиентской сторонами. Я
бы рекомендовал использовать этот подход, только если позволяют трудовые
ресурсы (читай: есть хотя бы отдельный специализирующийся на JavaScript
разработчик), либо когда проект по каким-то причинам решил отказаться от
реализации статичного HTML-интерфейса.
В следующей статье я расскажу о менее трудозатратном способе добиться
того же результата, который основан на жертве идеологической
правильностью в пользу минимизации повторного написания кода.
До встречи на страницах Insight IT!