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