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

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

dbc319af88a14f518086b1dbfeca4315.jpg

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

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


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

4de258dad5de4800a0fb39ce862e0c59.jpg

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


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

324c9e4d78694f1ba2bea0707975482b.jpg

Кейс с canonical_ids воспроизвести можно следующим образом:

  1. Устанавливаете приложение
  2. Отправляете на него сообщение
  3. Удаляете приложение
  4. Устанавливаете приложение
  5. Отправляете на него сообщение


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

11c4ebf304244971adec682677d7b800.jpg

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


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

Messages with Payload


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

a521670c77094700b8b4656a35515767.jpg

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

ee15781d25cb4369b7c657101fe040b7.jpg

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

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

ab8d75bda30242e38f01ed705c841e3e.jpg

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

cee13df6f40d4a53aa9942a3668d3b88.jpg

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

eff15be86aa94fd4a04603cd53b5fb4e.jpg

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

  1. Определяем, есть ли задержка на этом устройстве.
  2. Если да, то начинаем держать постоянное соединение с бэкенд-сервером, нет — продолжаем использовать только GCM (в целях экономии батареи).


396cacafca654514b149ddfb715a655a.jpg

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

57a59f1786d040ecab5bcd002b0522e3.jpg

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

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

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

© Habrahabr.ru