Pull to refresh
VK
Building the Internet

Google Cloud Messaging: «Сова, открывай! Пуш пришел!»

Reading time 6 min
Views 63K
Всем известный сервис Google Cloud Messaging (GCM) нужен для того, чтобы ваше приложение всегда показывало актуальные данные пользователю. Схема работы сервиса включает в себя три компоненты.



Непосредственно сервер GCM, ваш пуш-сервер и устройство с установленным приложением. Алгоритм работы простой: устройство регистрируется в GCM, получает registrationId – некий токен, который используется в дальнейшем, – сохраняет его у себя локально и передает серверу. Далее пуш-сервер использует этот registrationId для отправки сообщений вашему приложению на устройстве.

В этом материале будут рассмотрены проблемы на двух участках, который обозначены на схеме: пуш-сервер – GCM и GCM – устройство.

Обработка ошибок от сервера GCM


В случае успешной отправки сообщения ваш пуш-сервер получит ответ от GCM со статус кодом 200 и ненулевой message_id.



Ошибки приходят в теле ответа со статус-кодом 200. Поэтому полагаться только на статус-код 200 недостаточно. Здесь я приведу пример одной из самых важных ошибок, это NotRegistered, другие менее интересные. В большинстве случаев она означает, что приложение, в которое ваш пуш-сервер отправляет сообщение, было удалено, либо приложение уже использует другой registrationId, а ваш пуш-сервер почему-то об этом не знает. Получив такой ответ от GCM, пуш-сервер должен незамедлительно удалить данный registrationId из своего хранилища.
Для мониторинга ошибок подключайте GCM статистику в консоли разработчика.

Синхронизация registrationId на клиенте и пуш-сервере


RegistrationId является одной из самых важных частей инфраструктуры GCM. Рассинхронизация registrationId между клиентом и пуш-сервером приведет к печальным последствиям. Есть все шансы, что пользователи останутся без пуш-уведомлений навечно. GCM отслеживает ситуацию, когда на устройстве по каким-то причинам обновляется registrationId, и сообщает об этом пуш-серверу посредством параметра canonical_ids.



Кейс с canonical_ids воспроизвести можно следующим образом:
  1. Устанавливаете приложение
  2. Отправляете на него сообщение
  3. Удаляете приложение
  4. Устанавливаете приложение
  5. Отправляете на него сообщение

После отправки сообщения в шаге 5 вам придет ответ от GCM с параметром canonical_ids равным 1 и непосредственно свежий registrationId, который уже вовсю используется вашим клиентом.



Получив такой ответ, пуш-сервер просто обязан обновить registrationId на значение из ответа. Если этого не сделать, то еще какое-то время сообщения будут доходить до клиента и старый registrationId будет валиден, но рано или поздно GCM ответит ошибкой NotRegistered, после чего пуш-сервер удалит registrationId, и пользователи навсегда забудут о пуш-уведомлениях в вашем приложении. Поэтому обрабатывайте параметр canonical_ids и не доводите до греха.

Два основных подхода в работе с GCM


Первый — это Messages with Payload. Суть его в том, чтобы в самом сообщении передавать какую-то полезную информацию. Например, в мессенджере это может быть текст сообщения, в новостном приложении — сама новость. Второй механизм — это Send-to-Sync. Он более оптимизирован по расходу трафика, т.к. в само сообщение не упаковывается много данных. Сообщение выступает в роли сигнала о том, что приложению следует забрать свежие данные с сервера. Второй подход напрямую связан с параметром collapse_key.

Messages with Payload


Идеальная ситуация, когда ваше устройство держит соединение с сервером GCM, сообщения отправляются и успешно доставляются на устройство. Если же соединения нет (например, вы застряли в лифе или зашли в метро), а вам в это время идут сообщения, то они начинают складываться в некую очередь в GCM-хранилище. Эта очередь не бесконечна, лимит составляет 100 сообщений. Как только придет 101 сообщение, то все они удаляются и больше не накапливаются. Когда устройство поймает сеть и установит соединение с GCM, в приложение придет intent с информацией о том, что было удалено, например, 345 сообщений.



Получив такой intent, нужно не полениться и сходить на сервер за свежими данными. Иначе пользователь увидит их только когда придет очередное пуш-уведомление, а когда оно придет – никому неизвестно. Это очень важный момент, о котором нужно помнить при реализации подхода «Messages with Payload».

Send-to-Sync


Допустим, мы используем collapse_key. Это некая константа, которых может быть не более четырех для одного registrationId, т.е. для одного инстанса приложения. Например, новостное приложение собирает какие-то данные с разных сервисов. Пусть один сервер отдает спортивные новости, другой — культуру, третий — политику, четвертый — авто. Возникнет проблема, конечно, когда появится пятый сервис, но сейчас ни в этом суть. В отправке сообщения для соответствующей рубрики можно использовать свой collapse_key: sport, culture, policy, auto.

При приходе очередного сообщения с одним и тем же collapse_key GCM заменяет старое сообщение вновь пришедшим. В принципе, логично, т.к. мы помним, что сообщение в подходе «Send-to-Sync» является всего лишь сигналом нашему приложению о том, что следует сходить на сервер за свежими данными. Но тут нас подстерег один неприятный момент, из-за которого нам пришлось отказаться от подхода «Send-to-Sync» — тротлинг. Тротлинг заключается в том, что GCM сервер может некоторое время ждать, чтобы собрать как можно больше сообщений с одинаковым collapse_key. Все бы хорошо, но это вносит задержку в доставку сообщения до клиента (стабильно замечал задержку в полминуты-минуту), что недопустимо для некоторых типов приложений, например, мессенджера.



Из-за этой задержки мы перестали использовать collapse_key. Если в вашем приложении некритична небольшая задержка в доставке сообщений, то подход «Send-to-Sync» — хороший выбор.

Со временем мы учли все вышеописанные детали в имплементации нашего пуш-сервера. Но по-прежнему оставалось большое кол-во отзывов с примерно таким содержимым: «Я вижу новые сообщения, только когда захожу в приложение. Когда оно не запущено, до меня сообщения не доходят!!!». Сначала основной гипотезой была рассинхронизация registrationId, хранящихся на устройстве и на пуш-сервере. Для ее подтверждения мы вкрутили на устройстве проверку, суть которой в том, чтобы приложение периодически спрашивало пуш-сервер: «У тебя есть мой registratioinId?». Ответ «да» гарантирует нам с большой долей вероятности, что registrationId актуальный.



И согласно статистике, ответов «да» 99,7%. Что позволило нам сделать вывод, что с синхронизацией registrationId все нормально. Начали искать проблему на участке между устройством и GCM. Неоднократно был свидетелем ситуации, когда на Samsung S4, да простит меня Samsung, выключаешь экран, и сообщения начинают приходить с большой задержкой (порядка 10 – 15 минут). С помощью наших коллег сетевых администраторов было выяснено, что TCP-соединение между устройством и GCM становилось неактивным (idle), и пакеты переставали ходить. Причиной всему этому так называемый «heartbeat». «Heartbeat» — это пакетик (ping), посылаемый системой раз в определенный интервал времени, чтобы «оживить» TCP-соединение между устройством и GCM (почитать более подробно об этом можно здесь).



И интервал, через который посылается heartbeat, довольно велик. Вроде, в августе 2014 года его сократили до 8 минут, но информация, возможно, неточная. В интернете предлагается решение, которое применяется в так называемых «пуш-фиксирах». Суть его в том, чтобы инициировать посылку heartbeat-пакета вручную. Но к сожалению, это решение работает только для root-устройств.



Оптимизма добиться мгновенной доставки сообщений на всех поддерживаемых нами устройствах (за исключением китайских айфонов на андроиде) средствами GCM оставалось все меньше. А проблему с задержкой доставки сообщений надо решить. Единственное, что может гарантировать более-менее стабильную по времени доставку сообщений — это держать собственное соединение с сервером. Но для начала хотелось бы научиться определять устройства, на которых наблюдается проблема с задержкой пуш-сообщений. В этих целях мы запилили статистику, суть которой – сравнивать разницу времени прихода пуша с временем, когда на сервере данные были готовы для клиента (когда был послан пуш). И статистика показала, что примерно у 20% пользователей наблюдается задержка с доставкой сообщений. Но она достаточно грубая, т.к. в ней не учитываются кейсы с пропаданием сети и прочим. В настоящий момент мы думаем над реализацией следующего алгоритма:
  1. Определяем, есть ли задержка на этом устройстве.
  2. Если да, то начинаем держать постоянное соединение с бэкенд-сервером, нет – продолжаем использовать только GCM (в целях экономии батареи).



Десять минут взято из головы. Если дельта больше порогового значения, то переключаем приложение на режим работы с собственным соединением.



В экспериментальных целях собственное соединение было реализовано путем http лонг-поллинг соединения, т.к. это был самый быстрый путь для «попробовать». Такая сборка была выслана нескольким нашим бета-пользователям с просьбой понаблюдать за доставкой сообщений и расходом батареи. В целом, как ни странно, не было резкого всплеска увеличения расхода батареи, а пользователи были довольны скоростью доставки сообщений. Тема реализации собственного соединения с бэкенд-сервером заслуживает отдельной статьи, а над самой реализацией мы продолжаем еще думать.

Надеюсь, статья будет полезной и позволит сократить ваше время при использовании сервиса GCM, а также буду рад, если поделитесь своим опытом решения проблемы скорости доставки сообщения до клиента в своих приложениях.

P.S. В дебажных целях мною был написан тестовый пуш-сервер — может, кому пригодится. Исходный код тут.
Tags:
Hubs:
+43
Comments 30
Comments Comments 30

Articles

Information

Website
vk.com
Registered
Founded
Employees
5,001–10,000 employees
Location
Россия
Representative
Миша Берггрен