У нас было социальное приложение без чата, 2 недели на его разработку и абсолютно никаких знаний о существующих протоколах для реализации IM. Не то что бы это был необходимый набор для того чтобы выстрелить себе в ногу, но в процессе работы это произошло. Несколько раз.
— Паша, нам нужно сделать чат.
— Да всё просто, у меня тут знакомые использовали XMPP для чата в своём приложении.
Какие у нас были требования? Да ничего особенного, простой обмен сообщениями между пользователями, без групповых разговоров. Платформы: веб (с поддержкой работы через вебсокеты), Android, iOS. Создание пользователей должно автоматически производится только нашим серверным приложением. Конечно неплохо было бы иметь отметки о том прочитано сообщение или нет(предполагается, что приложение может быть использовано с разных девайсов), и иметь возможность просмотреть лог чата. В общем стандартный функционал для мгновенного обмена сообщениями в 2015 году. Бонусные баллы начисляются если сервер умеет горизонтально масштабироваться.
Хм, звучит заманчиво. Гуглим сравнение с другими стандартами, открываем ссылку на статью в википедии. Первая мысль: ого, круто, тут есть всё что нам нужно.
Почему же мы выбрали именно XMPP?
Ребята разрабатывавшие протокол действительно многое предусмотрели. Например, взглянем на дизайн архивирования сообщений. Индивидуальные настройки архивирования для сессий. Описание что делать в случае если один из пользователей хочет чтобы его сообщения не сохранялись на сервере, а второй хочет этого. При этом нормального механизма получения архива сообщений по страницам нет. Под нормальным я подразумеваю выставление offset и limit. Здесь вы можете установить только параметр max, который выставит максимальное количество сообщений и параметр start, который означает время, начиная с которого вы будете получать сообщения.
Или вот например: протокол не определяет что должно быть сделано с offline сообщениями (сообщения, посланные пользователю не в сети). Поэтому большинство серверов просто вышлют их пользователю при следующем логине. Помните требование про то, что пользователь может пользоваться приложением с нескольких устройств? Так вот, если вы запустите приложение на смартфоне, а потом войдёте в веб-приложение, то все offline сообщения придут на ваш смартфон и… всё. То есть в веб-приложении вы об этом никак не сможете узнать. Стоит отметить что некоторые сервера ведут себя по-другому, то есть реализуют XEP-0013.
Ах да, протокол для IM (instant messaging) в 2015 году не имеет спецификации, которая позволяла бы получить список непрочитанных сообщений и отметить прочитанными какие-то из них. Совсем.
В самом начале я ещё не знал с какими проблемами из предыдущего пункта мне придётся столкнуться и поэтому в качестве реализации сервера был выбран MongooseIM. Масштабируемый, умеет запрещать разрешать регистрацию только с определённых ip, поддерживает вебсокеты. Для веб front-end была выбрана библиотека JSJaC, потому что по сравнению с strophe.js предоставлял более удобное API. Простая страничка из примеров JSJaC подключилась к серверу с пол-пинка и радости моей не было предела. Казалось бы, работа закончена. Ну почти.
И вот тогда я столкнулся с последней озвученной проблемой из предыдущего пункта. Поскольку протокол не подразумевает возможность получить непрочитанные сообщения, да и вообще не говорит о прочитанности/непрочитанности сообщений, эту часть функционала придётся дописывать самому. Будучи не сильным в программировании на erlang-е я принялся искать другие подходящие реализации сервера. Искал я реализации на Java или JavaScript.
Openfire — одна из самых популярных реализаций XMPP на Java. К плюсам можно отнести простоту настройки: всё происходит через веб-интерфейс. Дальше только минусы: требовательность к ресурсам, отсутствие плагина для работы через вебсокеты.
Единственным достойным упоминания из проектов на JavaScript оказался xmpp-ftw. Проект реализует много расширений из XMPP. Однако от его использования пришлось отказаться, поскольку мы бы не смогли использовать существующие клиентские библиотеки. Возможно всё было бы не так хорошо и с ним.
Tigase поначалу казался спасением. Быстрый, масштабируемый сервер на Java, с возможностью написать плагин и довольно просто его подключить. И отсутствием документации. Вместо неё рекомендовалось читать исходный код. Но это ничего, я и так чаще всего так и делаю. Я написал плагин, который помечал новые сообщения непрочитанными. Чтобы регистрировать пользователей пришлось напрямую писать их в базу, поскольку API для администрирования так и не удалось нормально использовать. К счастью Tigase имеет драйвер для подключения к Mongodb. Получение непрочитанных сообщений сделано отдельным методом API приложения(костыль, можно было сделать и плагином внутри Tigase, но отняло бы намного больше времени, потому что для общения с СУБД внутри Tigase используется довольно низкоуровневое API). Подключив всё я проверил — пока сообщение не помечено прочитанным (кстати, сообщения не имеют id в xmpp, поэтому помечать было решено прочитанной всю переписку с конкретным пользователем) оно возвращается в списке непрочитанных. Всё работает. Осталось проверить только случай с offline сообщением. Да, оно не помечалось непрочитанным, потому что плагин который обрабатывает offline сообщения срабатывает раньше, чем архивирование. На форуме Tigase разработчик ответил что нужное мне поведение реализовано в коммерческом проекте Tigase Unified Archive. Гугление по его названию ни к чему не привело, разработчик на форуме сказал что проект пока нестабилен и нет релизной версии. Покопавшись в исходниках сервера нашёл что можно получить нужное поведение выставив всем сообщениям тип chat.
Теперь пройдёмся по реализациям клиентов. Начнём с JavaScript реализаций. Пробовали мы только JSJaC, который в итоге поменяли на strophe.js. Первый имеет более приятное API, но он имеет только базовую функциональность, не поддерживая работу с расширениями, например с архивом. В то же время strophe вместо этого предлагает довольно удобный xml-билдер. Ну что ж, абстрагироваться от внутренностей XMPP не получилось. Кстати, вебсокет-коннектор в JSJaC работает только в Webkit.
Из Java клиентов попробован на данный момент только Smack. В случае посылки некорректного пакета скорее всего будет выброшен NoResponseException, и это всё что вы узнаете.
Может быть, моя проблема в том, что я надеялся что кто-то решил часть моих проблем за меня и сделал это хорошо. Знай я всё это сначала — предложил бы написать собственный чат-сервер. Серьёзно. Дело не в том что протокол плох или не подходит для использования для реализации IM с современными требованиями, хотя я до сих пор так считаю. Просто я бы предпочёл иметь свой собственный код и заложить нужные возможности для расширения, чем перелопачивать столько чужого кода и спецификаций. XMPP вполне успешно применяется для решения огромного круга задач, для которых он, надо полагать, хорошо подходит. Хотя большинство решений используют только базовый функционал.
— Паша, нам нужно сделать чат.
— Да всё просто, у меня тут знакомые использовали XMPP для чата в своём приложении.
Какие у нас были требования? Да ничего особенного, простой обмен сообщениями между пользователями, без групповых разговоров. Платформы: веб (с поддержкой работы через вебсокеты), Android, iOS. Создание пользователей должно автоматически производится только нашим серверным приложением. Конечно неплохо было бы иметь отметки о том прочитано сообщение или нет(предполагается, что приложение может быть использовано с разных девайсов), и иметь возможность просмотреть лог чата. В общем стандартный функционал для мгновенного обмена сообщениями в 2015 году. Бонусные баллы начисляются если сервер умеет горизонтально масштабироваться.
Хм, звучит заманчиво. Гуглим сравнение с другими стандартами, открываем ссылку на статью в википедии. Первая мысль: ого, круто, тут есть всё что нам нужно.
Почему же мы выбрали именно XMPP?
- XMPP — это открытый протокол, разрабатываемый XSF (XMPP Standards Foundation), независимой некоммерческой организацией. Эти ребята должно быть довольно умны и предусмотрели всё, что нужно протоколу обмена сообщениями (ага, конечно).
- Поскольку протокол открытый должно быть довольно много реализаций сервера, и хотя бы одна из них будет достаточно хороша чтобы её использовать.
- По этой же причине для каждого из языков программирования наверняка найдётся 1-2 библиотеки.
- Первая буква X в аббревиатуре означает extensible, расширяемый. Даже если что-то нам не понравится в протоколе мы всегда сможем расширить своими методами.
Протокол
Ребята разрабатывавшие протокол действительно многое предусмотрели. Например, взглянем на дизайн архивирования сообщений. Индивидуальные настройки архивирования для сессий. Описание что делать в случае если один из пользователей хочет чтобы его сообщения не сохранялись на сервере, а второй хочет этого. При этом нормального механизма получения архива сообщений по страницам нет. Под нормальным я подразумеваю выставление offset и limit. Здесь вы можете установить только параметр max, который выставит максимальное количество сообщений и параметр start, который означает время, начиная с которого вы будете получать сообщения.
Или вот например: протокол не определяет что должно быть сделано с offline сообщениями (сообщения, посланные пользователю не в сети). Поэтому большинство серверов просто вышлют их пользователю при следующем логине. Помните требование про то, что пользователь может пользоваться приложением с нескольких устройств? Так вот, если вы запустите приложение на смартфоне, а потом войдёте в веб-приложение, то все offline сообщения придут на ваш смартфон и… всё. То есть в веб-приложении вы об этом никак не сможете узнать. Стоит отметить что некоторые сервера ведут себя по-другому, то есть реализуют XEP-0013.
Ах да, протокол для IM (instant messaging) в 2015 году не имеет спецификации, которая позволяла бы получить список непрочитанных сообщений и отметить прочитанными какие-то из них. Совсем.
Реализации
В самом начале я ещё не знал с какими проблемами из предыдущего пункта мне придётся столкнуться и поэтому в качестве реализации сервера был выбран MongooseIM. Масштабируемый, умеет запрещать разрешать регистрацию только с определённых ip, поддерживает вебсокеты. Для веб front-end была выбрана библиотека JSJaC, потому что по сравнению с strophe.js предоставлял более удобное API. Простая страничка из примеров JSJaC подключилась к серверу с пол-пинка и радости моей не было предела. Казалось бы, работа закончена. Ну почти.
И вот тогда я столкнулся с последней озвученной проблемой из предыдущего пункта. Поскольку протокол не подразумевает возможность получить непрочитанные сообщения, да и вообще не говорит о прочитанности/непрочитанности сообщений, эту часть функционала придётся дописывать самому. Будучи не сильным в программировании на erlang-е я принялся искать другие подходящие реализации сервера. Искал я реализации на Java или JavaScript.
Openfire — одна из самых популярных реализаций XMPP на Java. К плюсам можно отнести простоту настройки: всё происходит через веб-интерфейс. Дальше только минусы: требовательность к ресурсам, отсутствие плагина для работы через вебсокеты.
Единственным достойным упоминания из проектов на JavaScript оказался xmpp-ftw. Проект реализует много расширений из XMPP. Однако от его использования пришлось отказаться, поскольку мы бы не смогли использовать существующие клиентские библиотеки. Возможно всё было бы не так хорошо и с ним.
Tigase поначалу казался спасением. Быстрый, масштабируемый сервер на Java, с возможностью написать плагин и довольно просто его подключить. И отсутствием документации. Вместо неё рекомендовалось читать исходный код. Но это ничего, я и так чаще всего так и делаю. Я написал плагин, который помечал новые сообщения непрочитанными. Чтобы регистрировать пользователей пришлось напрямую писать их в базу, поскольку API для администрирования так и не удалось нормально использовать. К счастью Tigase имеет драйвер для подключения к Mongodb. Получение непрочитанных сообщений сделано отдельным методом API приложения(костыль, можно было сделать и плагином внутри Tigase, но отняло бы намного больше времени, потому что для общения с СУБД внутри Tigase используется довольно низкоуровневое API). Подключив всё я проверил — пока сообщение не помечено прочитанным (кстати, сообщения не имеют id в xmpp, поэтому помечать было решено прочитанной всю переписку с конкретным пользователем) оно возвращается в списке непрочитанных. Всё работает. Осталось проверить только случай с offline сообщением. Да, оно не помечалось непрочитанным, потому что плагин который обрабатывает offline сообщения срабатывает раньше, чем архивирование. На форуме Tigase разработчик ответил что нужное мне поведение реализовано в коммерческом проекте Tigase Unified Archive. Гугление по его названию ни к чему не привело, разработчик на форуме сказал что проект пока нестабилен и нет релизной версии. Покопавшись в исходниках сервера нашёл что можно получить нужное поведение выставив всем сообщениям тип chat.
Теперь пройдёмся по реализациям клиентов. Начнём с JavaScript реализаций. Пробовали мы только JSJaC, который в итоге поменяли на strophe.js. Первый имеет более приятное API, но он имеет только базовую функциональность, не поддерживая работу с расширениями, например с архивом. В то же время strophe вместо этого предлагает довольно удобный xml-билдер. Ну что ж, абстрагироваться от внутренностей XMPP не получилось. Кстати, вебсокет-коннектор в JSJaC работает только в Webkit.
Из Java клиентов попробован на данный момент только Smack. В случае посылки некорректного пакета скорее всего будет выброшен NoResponseException, и это всё что вы узнаете.
Вывод
Может быть, моя проблема в том, что я надеялся что кто-то решил часть моих проблем за меня и сделал это хорошо. Знай я всё это сначала — предложил бы написать собственный чат-сервер. Серьёзно. Дело не в том что протокол плох или не подходит для использования для реализации IM с современными требованиями, хотя я до сих пор так считаю. Просто я бы предпочёл иметь свой собственный код и заложить нужные возможности для расширения, чем перелопачивать столько чужого кода и спецификаций. XMPP вполне успешно применяется для решения огромного круга задач, для которых он, надо полагать, хорошо подходит. Хотя большинство решений используют только базовый функционал.