Не откладывайте в почтовый ящик: b2c-мессенджер 2ГИС
В сентябре на 2gis.ru появилась новая фича — b2c-мессенджер для общения с организациями. Чат очень удобен при поиске товара или услуги: можно написать сразу в несколько компаний, не нужно слушать голоса роботов-автоответчиков или ожидать на линии, пока оператор уточнит цену или остаток нужного товара. Выберите компанию, нажмите на иконку сообщения на карточке компании, и откроется чат.
Чтобы сделать мессенджер, нам пришлось немного поразбираться с тем, как вообще работают чаты и что под капотом у «больших братьев» типа WhatsApp или Telegram. Оказалось, всё не так страшно.
Зачем 2ГИС мессенджер
Начать, пожалуй, стоит не с мессенджера, а с того, как мы пришли к этой идее. Раньше мы показывали в справочнике email компании. Затем добавили форму связи с компанией прямо из справочника. Пишут меньше, чем звонят, но длина цепочки писем — в среднем от 2 до 10 сообщений. То есть мессенджер — логичное развитие уже существующей возможности отправлять письма в компании.
Как это работает сейчас
А ещё лучше — просто попробуйте задать нам вопрос. Мы в Новосибирске, и когда в Москве 18:00, у нас уже 22:00.
Беглый обзор: выбираем велосипед
Сперва мы посмотрели на BaaS (Backend as a Service), типа quickblox.com, backendless.com, sendbird.com и т.д. Все они предоставляют инструменты для добавления стандартной чатовой функциональности в мобильные или веб-приложения.
Реализация основного (отправка email-уведомлений в компании) и более продвинутых сценариев на основе существующего BaaS-решения потребует от нас как разработки собственных бекендов для интеграции через вебхуки, так и сверхусилий по интеграции с API вендора. Поэтому BaaS нам не подошёл. Кроме того, мы хотели иметь полный контроль над фичей, то есть не зависеть от сторонних решений. В конце концов, мы ведь инженеры и решились на эксперимент, чтобы получить новый для нас опыт.
А как сообщения-то доставлять?
Чтобы сделать свой бэкенд для мессенджера, сначала надо разобраться с кучей вопросов: как вообще передавать сообщения с клиента на сервер и обратно, где их хранить, как справиться с кучей одновременных соединений, как масштабировать сервис. Подумали, погуглили, собрали способы доставки сообщений в табличку для сравнения.
Способ | Описание | Транспорт | Протокол | Поддержка в браузерах |
WebSockets | Клиент открывает постоянное соединение с сервером, используя API WebSockets. | API Websockets | TCP (необходим HTTPhandshake для открытия соединения) | caniuse.com/#feat=websockets |
Streaming | Клиент открывает постоянное соединение с сервером. | XMLHttpRequest (multipart onload), XMLHttpRequest (onprogress), Iframe tag | HTTP | caniuse.com/#feat=streaming |
Server Sent Events | Клиент открывает постоянное соединение с сервером, используя API Server Sent Events. | API Server Sent Events | HTTP | Нет поддержки в IE, caniuse.com/#feat=eventsource |
Polling | Клиент периодически опрашивает сервер на предмет наличия новых сообщений. | Любой доступный | HTTP | |
Long Polling | Клиент открывает долгоживущее соединение с сервером, которое не закрывается до появления нового сообщения или истечения таймаута. Сразу же после закрытия соединения клиент открывает новое. | XMLHttpRequest Script tag |
HTTP | |
Browser Plugins (Java / Flash) | Клиент открывает постоянное соединение с сервером, используя API браузерного плагина (Java или Flash). | API плагина | TCP/UDP |
Большинство современных чат-приложений используют один из двух способов: long polling или websockets. Websockets обладает рядом преимуществ перед long polling (полнодуплексное соединение, отсутствие лишнего трафика при переконнектах), широко поддержан в браузерах и opensource-библиотеках, поэтому представляется оптимальным выбором для нашей задачи. Есть и недостатки: отличия имплементации в разных браузерах, более сложная логика на сервере.
Наиболее распространённый открытый general-purpose протокол для передачи данных — XMPP.
Положительные стороны | Отрицательные стороны |
Открытый стандарт IETF | Избыточность передаваемой информации |
Большое количество opensource-реализаций | XML |
Широкие возможности для кастомизации | Потребует доработки под специфику нашей задачи |
Мессенджер | Способ доставки | Протокол | Сервер | Система хранения |
Websockets | Начинали с XMPP, но потом перешли на in-house протокол | Вся серверная инфраструктура построена на Erlang + FreeBS, это позволяет держать до 2kk TCP-соединений на одном сервере. Начинали на ejabberd, но очень сильно его дорабатывали под себя впоследствии. Yaws, lighttpd | Не хранят историю сообщений на сервере, сообщения удаляются с сервера после получения клиентом, mnesia | |
Line | Нет веб-версии, только Chrome app | Thrift для мобильных клиентов | Java, С++, Nginx | Начали с кластера из трёх инстансов Redis, после взрывного роста пользовательской базы смигрировали в Hbase «холодные» данные: историю сообщений и историю изменений юзеров / групп / контактов, MySQL для бэкапов и аналитики |
Viber | Нет веб-версии | In-house | С++, хостятся в AWS | Начали с самописного in-memory хранилища написанного на C++, потом перешили на Mongo и Redis в качестве кеша. Mongo не устроила по производительности, в итоге мигрировали на Couchbase |
Facebook Messenger | Long polling | In-house json-based для веба, Thrift для мобильных приложений | Erlang — очереди сообщений. C++ — сервис информации о присутствии, хранилище истории сообщений. PHP — фронтенд, обрабатывает все пользовательские запросы, кроме long polling. Сервисы между собой общаются через Thrift |
Очередь на MySQL, HBase |
Slack | Websockets | In-house json-based | Java messaging server, LAMP for core app/APIs, хостятся в AWS | Redis, MySql, Apache Solr |
Однако, изучив решения различных «больших» мессенджеров, мы не обнаружили ни одной значимой истории успеха использования XMPP без его последующей доработки и кастомизации. Поразмыслив, решили разработать простой json-based протокол, изначально учитывающий специфику нашей задачи.
Технологический стек
Так исторически сложилось, что наш основной бэкенд-сервис — АПИ 2ГИС — был написан на PHP5. Два года назад мы приняли решение уйти от PHP, мигрировать на Scala и Go. Go позволяет очень легко строить довольно сложные concurrent-программы. Это стало решающим фактором при выборе его как основной технологии реализации мессенджера. Да и кое-какой опыт разработки на Go у нас уже был.
Итак, бэкенд пишем на Go, а фронтенд — на React + Redux. В качестве системы обмена сообщениями мы выбрали RabbitMQ; для хранения данных используем Redis и PostgeSQL. Приложение пакуем в Docker-контейнер и деплоим через Gitlab-CI в платформу Deis.
Как я сказал выше, мы хотели использовать вебсокеты, но когда дело дошло до реализации, чуть переосмыслили решение. Дело в том, что мы хотели выпустить фичу как можно быстрее, чтобы проверить гипотезу (пресловутый MVP). Чтобы ускориться, решили разделить логику на использование вебсокетов для отправки данных с клиента на сервер и простейшего REST API в другую сторону.
Что с уведомлениями?
Внезапно самым сложным в реализации оказались нотификации. Казалось бы, всё просто: отправляй пуши в телефон, письма в почту, отображай нотификашки в браузере. Но нашей задачей было сделать так, чтобы все уведомления прочитывались максимально скоро и при этом не превращались в поток спама.
Есть решение, которое называют каскадной системой нотификаций. Например, у нас есть три основных канала: уведомления на 2gis.ru, письма и пуши на телефон.
В этом случае система нотификаций принимает вид:
— проверяем, нет ли пользователя онлайн → если да, то шлём ему уведомление в браузере;
— если пользователя нет онлайн, проверяем, привязан ли телефон → если да, пытаемся отправить пуш на мобильный телефон, показываем уведомление в диалогах в браузере, письмо не отсылаем;
— если не сработало и это, тогда шлём письмо;
— если уведомление касается компании — ещё некоторое время наблюдаем за реакцией, а если ответа после уведомления в браузере и пуша нет, то всё равно шлём письмо.
На самом деле, пуш-нотификации из этого сценария прямо сейчас мы не отправляем, но очень скоро научимся это делать.
Планы
В ближайших планах — добавить функционал мессенджера в мобильные приложения 2ГИС, добить основной функционал (возможность приложить аттач к сообщению, браузерные пуши), реализовать продвинутые сценарии общения (например, запрос в несколько компаний сразу).
Пока компании не привыкли к мессенджеру, и не все быстро отвечают на сообщения. Но мы верим, что у взаимодействия людей и компаний через текст большое будущее.
Комментарии (3)
18 октября 2016 в 11:57
+1↑
↓
Господи, ну КДПВ вызывает желание взвопить «вы вообще для кого это делали»…
2 гис, пишем с карты ГОРОДА. Ну зачем, ЗАЧЕМ уточнять город?
Потому что как обычно, саппорт общий (или вообще сгружен в «индию»).
Ну почему нельзя для саппорта показывать ОТКУДА запрос пришел, с какого города?Потому что каждый раз, когда я вижу у саппорта «КЫСА ТЫ С КАКОВА ГОРАДА», я закрываю этот саппорт нафиг.
То же самое относится к саппортам через соцсети, когда в профиле есть вся необходимая информация для решения проблемы — они всё равно задают десятки наводящих вопросов.
«Давайте я вам почитаю вслух с экрана» блин.18 октября 2016 в 12:02
0↑
↓
Как увидел сей пост зашел оставить ровно такой же комментарий, присоединяюсь к каждому слову. Первый старт приложения он запрашивает/определяет город, но это слишком просто…18 октября 2016 в 12:02
0↑
↓
Потому что как обычно, саппорт общий (или вообще сгружен в «индию»).
В общем, вот и ответ на ваш вопрос. Мы, конечно, показываем в личном кабинете, откуда запрос, только отвечает-то на него сотрудник магазина, который может это упустить, потому что он один, например, разбирает запросы из 19 филиалов по разным городам.Что на КДПВ это не учли — тут да, посыпаем голову пеплом, можно было и получше пример привести.