- PVSM.RU - https://www.pvsm.ru -
Так уж случилось, что на работе я с небольшой командой единомышленников занимаюсь написанием приложений для смартфонов, в частности iТелефон и Андроид.
Начинали мы с разработок под iPhone, где все работало гладко и как положено.
А что работало? Основная задача приложения была послать запрос «Где ты?» — ничего сложного. Но уж очень хотелось бы этот запрос доставлять адресату как можно быстрее, пока он еще актуален. Здесь, имеющий опыт в разработках под iPhone, читатель скажет, что есть APN Service, и будет абсолютно прав. Именно им мы и пользовались, и не знали горя, ибо доставлялись эти уведомления быстрее секунды.
Затем по некоторым внутренним причинам мы перешли на разработки под Android и быстренько все портировали. В частности без каких-либо задних мыслей модуль работы с APN был заменен на аналогичный с C2DM.
На всех телефонах разработчиков проблем с доставкой уведомлений не было. А вот у новых пользователей сразу вскрылась огромная проблема — время доставки уведомления никак не гарантировано, и некоторые из них доходили через несколько часов. Причем на соседнем же устройстве они доходили за секунды.
В ходе исследования этой проблемы я натолкнулся на ряд странных особенностей работы этих уведомлений от Google.
Интересующиеся реализованным низкоуровневым взаимодействием смартфона с сервером без разбора предпосылок могут эти предпосылки пропустить и перейти к разделу «4. Альтернатива Google C2DM, но не замена».
Прежде всего схемка (обозначения выбраны просто для наглядности, а не по ГОСТу):
Для изучения проблемы нужно понять, как устроены соединения 1, 2 и 3.
Раз уж я разрабатываю в основном серверную часть, то первый камень прилетел в меня за возможные проблемы в соединении №2. Что же было предпринято?
По моему скромному мнению лучшее объяснение, как подключить Пишем приложение под Android с поддержкой Cloud to Device Messaging (C2DM) [2]
Само это соединение реализовано по рекомендациям этой статьи.
Иногда при подключении к серверу Google приходил Connection timed out, что натолкнуло меня на мысль об ограничении количества наших одновременных подключений. Может мысль и ошибочная, но примененное решение оказалось полезным.
Серверная часть написана на Java и запускается как отдельный JAR с встроенным в него Jetty. За настройку и запуск отвечает Spring Framework, а значит, мне довольно безболезненно удалось перенастроить взаимодействие с C2DM сервером.
Добавить асинхронности в выполнение запроса.
public class C2DMServer implements IPushNotificator, IPushChecker {
...
@Override
@Async // вот так добавить асинхронность
public void sendData(String deviceId, String c2dmID, String jsonObject) {
...
}
}
Этот шаг дал еще одно улучшение — ответ 200 OK запрашивающему клиенту теперь приходит гораздо быстрее, так как поток не ждет ответа от сервера уведомлений Google.
Настроить количество параллельных запросов к Google, чтобы как раз уложиться в лимиты.
Здесь было множество тестов и подбора коэффициентов, а результат вылился в такую настройку Spring.
<task:annotation-driven executor="asyncExecutor" />
<task:executor id="asyncExecutor" pool-size="15" queue-capacity="300" rejection-policy="CALLER_RUNS" />
Если pool-size выставлять более 15, то такое количество одновременных подключений приводит к разного рода сетевым ошибкам.
Что порадовало: больше не появлялись ошибки подключения к серверу Google.
Что расстроило: проблема скорости доставки осталась, а значит, движемся дальше.
Любое приложение после установки на Android может запросить у специального сервиса идентификатор, с которым нужно отправлять уведомления.
Это делается наследованием от базового класса C2DMBaseReceiver.
Пример такого наследования можно посмотреть все в том же хабратопике Пишем приложение под Android с поддержкой Cloud to Device Messaging (C2DM) [2]. Вот модифицированная реализация у меня:
import com.google.android.c2dm.C2DMBaseReceiver;
public class C2DMReceiver extends C2DMBaseReceiver {
private static final String DATA = "data";
public C2DMReceiver() {
super(Settings.C2DM_ACCOUNT);
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onError(Context context, String errorId) {
Settings.Init(context, false);
Settings.updateC2DM(null);
}
@Override
protected void onMessage(Context context, Intent receiveIntent) {
Settings.Init(context, false);
String data = receiveIntent.getStringExtra(DATA);
JSONUtils.processJSON(context, data);
}
@Override
public void onRegistered(Context context, String registrationId) {
Settings.Init(context, false);
if (!registrationId.equals(Settings.getC2dm_id()))
Settings.updateC2DM(registrationId);
}
@Override
public void onUnregistered(Context context) {
}
}
Здесь Settings — класс помощник с кучей статических полей и методов для хранения состояния приложения. JSONUtils — еще один класс помощник, разбирающий JSON и сохраняющий все данные в Settings.
Что важно понимать, так это то, что момент получения идентификатора не определен. Фактически, этим классом мы лишь вешаемся на событие получения C2DM идентификатора, и по идее при его срабатывании незамедлительно должны передать идентификатор на сервер.
Пример такого идентификатора: «APA91bF8hral5wCq_E7HPD1wq29aSIEYyY2g_P4BOue_CaBTJvTHKFPplmp2MHxFgn3c1ysNjTHyXmsp8OejRSc809ZiOYqNcXoJWiWfvarCayT6ar3RyZwRRV0CrgQNaPjLxTrYqXXcQfcxjB07xmjeNtUzc6UlGQ».
После этого любое сообщение к C2DM серверу с этим идентификатором должно быть доставлено на нужное устройство и нужному приложению.
Посмотрим как доставляются эти сообщения
В центре всего стоит сервис Cloud To Device Messaging.
Что интересно, на проблемных устройствах этот сервис иногда был выгружен из памяти. Это значит, что он не берет никаких блокировок видео [3].
С сервисами все, конечно, не настолько печально. Сервис уведомлений умеет восстанавливаться при изменении условий сети и при включении экрана, хотя и не всегда.
Чтобы посмотреть состояние своего соединения, можно набрать *#*#8255#*#*
и в открывшемся GTalk Service Monitor посмотреть, какое приложение какой обмен через Google Messaging проводило.
Итак, часть проблемы была идентифицирована, но решения для нее не было.
Почему именно часть? Потому что уведомления все равно не доходили даже при работающих сервисах. Иногда замечались волны уведомлений, когда через некоторое время (20-40 минут) все устройства получали уведомления одновременно, хоть и отправленные в разное время.
В итоге после размышлений, чтений документации и множества форумов и Q&A все сошлись на одном — будем делать альтернативный канал уведомлений.
Основной вопрос: как устроить стабильный канал сервер-клиент?
Побочный вопрос: как не съедать этим каналом всю батарейку пользователя?
Источником вдохновения стали примеры с ресурса http://code.google.com/p/android-random/ [4].
В частности пример KeepAliveService [5].
Первая идея — лобовое решение: раз в n секунд открывать подключение к серверу и проверять нет ли уведомлений.
Вместо этого лобового решения «часто опрашивать сервер» авторы предлагают более разумный вариант, хотя и похожий на своего рода хак.
Фишки предложенного решения:
Я провел тестирование разных вариантов работы клиента с сервером уведомлений.
Было написано 2 клиента:
Первый клиент реализовать не трудно самостоятельно. Все тонкости второго можно посмотреть в архиве [6]. В него включены клиент Android второго типа и сервер, поддерживающий подключение, выводящий в лог все keepalive сообщения клиента, а также раз в минуту по своему случайному разумению отправляющий на клиент уведомление. Собирается все Maven-ом с подключенным android-maven-plugin.
Устройство | Desire | Desire | Desire | Desire | Desire | Wildfire S | Desire S |
Продолжительность теста (мин.) | 540 | 1273 | 845 | 962 | 1117 | 1180 | 1121 |
Расход единиц заряда | 82 | 87 | 31 | 9 | 39 | 80 | 49 |
KeepAlive в секундах | 10 | 30 | 60 | 60 | 60 | 60 | 60 |
Подключение к Интернет | 3G | 3G | 3G | WiFi | 3G | 3G | 3G |
Вычисленная скорость разряда (е.з./ч) | 10 | 4.28 | 2.22 | 0.56 | 2.22 | 4.28 | 3 |
Сразу бросается в глаза несколько результатов:
Для нас самый важный — первый результат. Из него следует, что выбирать из двух клиентов нужно по функциональным возможностям. У переподключающегося клиента (№1) уведомления приходят лишь один раз в указанный интервал проверки. У клиента, поддерживающего подключение (№2), уведомления приходят в тот момент, когда в открытое подключение напишет сервер. Причем, забегая вперед, скажу, что даже уснувшее устройство просыпается, когда в открытое подключение приходит сообщение от сервера.
Чтобы выдержать наплыв TCP подключений я построил следующую архитектуру.
Сервер уведомлений состоит из двух компонент:
Все взаимодействие с клиентом идет на чистом TCP. Само уведомление может быть произвольного размера и содержания, но для уменьшения нагрузки в своем приложении я шлю ровно один байт «1».
Между компонентами сервера, используя Spring Remoting, поднимаются RMI соединения.
Теперь посмотрим логику клиентской стороны по шагам.
Работа с AlarmManager-ом очень простая.
// создаем интент, которым нас известят о событии таймера
Intent i = new Intent(this, NotificationService.class).setAction(ACTION_KEEPALIVE);
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
// передаем временной интервал и интент AlarmManager-у
AlarmManager alarmMgr = (AlarmManager) getSystemService(ALARM_SERVICE);
alarmMgr.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + KEEP_ALIVE_INTERVAL, KEEP_ALIVE_INTERVAL, pi);
Подробнее в документации [7].
«Любой уважающий себя программист для смартфонов должен написать свою реализацию сервиса уведомлений» — так в шутку охарактеризовали результат моей работы.
Но несмотря на шутку, описанный выше сервис уведомлений работает на той же скорости и почти с той же стабильностью, как у Apple, что не может не радовать, а время жизни устройства, о котором так много волнуются разработчики, сокращается совсем не на много.
Размышления на тему, как реализовать хорошую доставку уведомлений [8]
Документация Google по подключению C2DM [9]
Хабратопик «Пишем приложение под Android с поддержкой Cloud to Device Messaging (C2DM)» [2]
Жалобы на скорость работы C2DM [10] и другие [11] — надеюсь среди них однажды появится ответ «Ура! Все заработало!».
Автор: ionsphere
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/5287
Ссылки в тексте:
[1] android.clients.google.com/c2dm/send.: http://android.clients.google.com/c2dm/send.
[2] Пишем приложение под Android с поддержкой Cloud to Device Messaging (C2DM): http://habrahabr.ru/post/116106/
[3] видео: http://www.youtube.com/watch?v=PLM4LajwDVc
[4] http://code.google.com/p/android-random/: http://code.google.com/p/android-random/
[5] KeepAliveService: http://code.google.com/p/android-random/source/browse/trunk/TestKeepAlive/src/org/devtcg/demo/keepalive/KeepAliveService.java
[6] в архиве: http://way-app.com/habr-img/notification-test.zip
[7] документации: http://developer.android.com/reference/android/app/AlarmManager.html
[8] Размышления на тему, как реализовать хорошую доставку уведомлений: http://tokudu.com/2010/how-to-implement-push-notifications-for-android/
[9] Документация Google по подключению C2DM: http://code.google.com/intl/ru-RU/android/c2dm/
[10] Жалобы на скорость работы C2DM: http://groups.google.com/group/android-c2dm/browse_thread/thread/1f42c8f44d8bb4cf
[11] другие: https://www.google.ru/webhp?q=site:groups.google.com+c2dm+slow
Нажмите здесь для печати.