- получает на вход массив данных для отправки
- формирует пакеты для отправки размером до 4096кб каждый.
- отправляет пакеты параллельными запросами.
- Анализирует ответ и знает:
- успешно доставлено ли сообщение
- тип ошибки
Google Cloud Messaging – коротко и ясно
GCM – это сервис доставки мгновенных сообщений. Альтернатива стандартным polling и long polling, но не исключающая, а дополняющая их. Гарантии, что сообщение будет доставлено Гугл не дает (хотя надежность и скорость доставки стала просто космической по сравнению с предком C2DM). Если на телефоне интернет выключен, то сообщение будет храниться на GCM сервере до 4х недель. Т.е если пользователь выключил телефон, уехал в отпуск, то по приезду сообщение он может уже не получить. Поэтому GCM должен работать только вместе с надежными способами доставки такими как, например, элементарный polling – отправка http запросов на сервер каждые N минут.
Любое android приложение может зарегистрировать себя в качестве получателя сообщений от GCM. При включенном интернете регистрация происходит за считанные секунды. Как только это произошло приложение получает от GCM сервера RegistrationId, который нужно отправить на свой сервер. В итоге в базе сервера мы имеем, например, таблицу Devices, хранящую информацию об устройствах, включая их RegistrationId.
Чтобы устройства начали получать сообщения серверный код должен отправлять POST запросы на GCM сервер в формате json (можно отправлять и обычные ключ => значение, но рекомендуется именно json). Ответ сервера также содержит json, проанализировав который мы сможем понять доставлено ли сообщение, а если нет — какие ошибки произошли.
Приступим
Создадим два класса GcmPayload и GcmSender.
class GcmPayload { public function __construct($regId, $jsons) {} public $regId; public $jsons; } class GcmSender { public function __construct($payloads) {} public function send() {} protected function getPackages() {} protected function isReadyToFlush($items, $json) {} public function onResponse($response, $info, RollingCurlRequest $request) {} }
В терминологии GCM payload – это данные, которые вы хотите отправить получателю. Эти данные должны храниться в значении ключа data и имеют ограничение в 4096 байт. Подробнее про формат запроса.
GcmPayload – модель данных для одного получателя и соответственно одного RegistrationId. Поле $jsons должно быть проинициализировано массивом json’ов в виде строк, содержащих данные, которые нужно отправить этому получателю. Для упрощения туториала считаем, что это делается вне нашего класса, например, так:
$recipients = $messagesRepository->getRecipientsWithNewMessages(); $payloads = array(); foreach ($recipients as $recipient) { $jsons = array(); foreach ($recipient->messages as $message) { $jsons[] = json_encode($message); } $payloads[] = new GcmPayload($recipient[‘regId’], $jsons); } $gcm = new GcmSender(); $gcm->send($payloads);
GcmSender
Константы и члены класса
const GCM_API_KEY = ‘your api key’; // Нужно получить на странице Google APIs Console
const CURL_TIMEOUT = 10; // Таймаут соединения в сервером Гугл в секундах
const GCM_MAX_DATA_SIZE = 4096; //Лимит на отправляемые данные в байтах
const GCM_SERVER_URL = ‘https://android.googleapis.com/gcm/send’; //адрес GCM сервера
const GCM_MAX_CONNECTIONS = 10; // количество параллельных запросов
const KEY_REG_IDS = ‘registration_ids’; //ключ получателей в json запросе
const KEY_DATA = ‘data’; //ключ с данными в json запросе
const KEY_ITEMS = ‘items’; //ключ в объекте data, содержащий наш массив данных
const REGID_PLACEHOLDER = ‘_REGID_’; //плэйсхолдер для RegistrationId в json шаблоне запроса
const ITEMS_PLACEHOLDER = ‘_ITEMS_’; //плэйсхолдер для массива наших данных в json шаблоне запроса
const GCM_ERROR_NOTREGISTERED = ‘NotRegistered’; //константа для ошибки, если пользователь удалил приложение
protected $_template; //json шаблон запроса
protected $_baseDataSize; //изначальный размер данных, который включает ключ items, кавычки скобки и т.д.
конструктор
Конструктор создает шаблон запроса, который будет использоваться в методе getPackages. Обратите внимание, чтобы потенциально не превысить лимит в 4096 байт на данные, нужно также запомнить и учесть в дальнейшем размер изначальных данных в шаблоне: {«items»: []}
public function __construct() { $dataObj = '{"'.self::KEY_ITEMS.'": ['.self::ITEMS_PLACEHOLDER.']}'; $this->_template = '{ "'.self::KEY_REG_IDS.'": ["'.self::REGID_PLACEHOLDER.'"], "'.self::KEY_DATA.'": '.$dataObj.' }'; $baseDataJson = str_replace(self::ITEMS_PLACEHOLDER, '', $dataObj); $this->_baseDataSize = strlen($baseDataJson); }
Метод send
Это паблик метод должен вызываться для непосредственной отправки данных на GCM сервер. Метод принимает данные для отправки, которые методом getPackages преобразутся в пакеты данных – подготовленные post данные в формате json (один пакет – один запрос) и гарантированно не превышающие 4096 байт. Остальная часть метода – это инициализация замечательной библиотеки RollingCurl, которая инкапсулирует в себе работу с curl_multi_exec и позволяет отправлять запросы параллельно и писать прозрачный код. RollingCurl инициализируем нашим колбэк методом onResponse, в котором будем анализировать результат отправки. Далее идет непосредственно сама отправка данных.
/** * @param GcmPayload[] $payloads */ public function send($payloads) { $packages = self::getPackages($payloads); if (!$packages || count($packages) == 0) return; $rc = new RollingCurl(array($this, 'onResponse')); $headers = array('Authorization: key='.self::GCM_API_KEY, 'Content-Type: application/json'); $rc->__set('headers', $headers); $rc->options = array( CURLOPT_SSL_VERIFYPEER => false, //отключаем проверку сертификата CURLOPT_RETURNTRANSFER => true, //указываем, что хотим получить ответ в виде строки CURLOPT_CONNECTTIMEOUT => self::CURL_TIMEOUT, // сколько секунд пытаться установить соединение CURLOPT_TIMEOUT => self::CURL_TIMEOUT); //сколько времени должны выполняться функции curl foreach ($packages as $package) { $rc->request(self::GCM_SERVER_URL, 'POST', $package); } $rc->execute(self::GCM_MAX_CONNECTIONS);
Метод getPackages
В этом методе перебирается массив переданных классу payload’ов и постепенно наполняется шаблон, созданный в конструкторе, до тех пор пока пакет не превысит лимит в 4096 байт или данные для получателя не закончатся. Кстати, в нашем примере считаем, что один пакет – один получатель. Что это значит? Например, такая условность справедлива когда текстовое сообщение адресовано только одному человеку. Но в групповых беседах одно и тоже сообщение можно отправить несколько людям и GCM это позволяет указав в значении ключа registration_ids несколько RegistrationId. Но повторюсь, в данном примере во избежание ненужных усложнений этот случай не рассматриваем.
Вернемся к методу getPackages. На самом деле здесь интерес представляет функция isReadyToFlush, которая определяет приведет ли добавление нового json к пакету выход за рамки лимита в 4096 байт. Если да, то пакет тут же завершается и этот json добавляем уже в новый пакет.
/** * @param string $items * @param string $json * @return bool */ protected function isReadyToFlush($items, $json) { $newPackageLen = $this->_baseDataSize + strlen($items) + strlen($json); return $newPackageLen > self::GCM_MAX_DATA_SIZE; } /** * @param GcmPayload[] $payloads * @return string[] */ protected function getPackages($payloads) { $packages = array(); foreach($payloads as $payload) { $template = str_replace(self::REGID_PLACEHOLDER, $payload->regId, $this->_template); $items = ''; foreach($payload->jsons as $json) { if ($this->isReadyToFlush($items, $json)) { $package = str_replace(self::ITEMS_PLACEHOLDER, $items, $template); $packages[] = $package; $items = ''; } if ($items) $items .= ','.$json; else $items = $json; } if ($items) { //если есть остатки добавляем их в новый пакет $package = str_replace(self::ITEMS_PLACEHOLDER, $items, $template); $packages[] = $package; } } return $packages; }
Метод onResponse
Важно не только отправить сообщение, но и понять доставлено ли оно, а если нет то по какой причине. onResponse – это тот колбек, которым мы проинициализировали RollingCurl в методе send. Колбек принимает три параметра:
- $response – ответ в виде строки
- $info – результат функции curl_getinfo php.net/manual/en/function.curl-getinfo.php и возвращает массив с данными о передаче данных, начиная от http кода ответа и заканчивая скоростями закачки/загрузки. Но в данном туториале интересен лишь http код ответа.
- RollingCurlRequest $request — информация о запросе. Нас интересует $request->post_data
Комментарии в листинге функции будут красноречивее:
/** * @param string $response * @param array $info * @param \RollingCurl\RollingCurlRequest $request */ public function onResponse($response, $info, RollingCurlRequest $request) { //Этот флаг показывает успешно ли отправлено сообщение $success = true; //Декодирует json, который мы отправили в post $post = json_decode($request->post_data, true); if (json_last_error() != JSON_ERROR_NONE) { //анализируем json ошибку, возможно мы накосячили в синтаксисе. return; } //Получаем RegistratonId и массив с данными $regId = $post[self::KEY_REG_IDS][0]; $items = $post[self::KEY_DATA][self::KEY_ITEMS]; //получаем код ответа $code = $info != null && isset($info['http_code']) ? $info['http_code'] : 0; //Определяем группу кода: 2, 3, 4, 5 $codeGroup = (int)($code / 100); if ($codeGroup == 5) { //Если код 5xx, это значит, что GCM сервер временно недоступен, сообщение не доставлено //TODO Рекомендуется учитывать заголовок Retry-After $success = false; } if ($code !== 200) { //Ошибочный http код ответа, сообщение не доставлено //Если требуется более углубленный анализ кодов рекомендую прочитать описание формата ответа http://developer.android.com/google/gcm/gcm.html#response $success = false; } if (!$response || strlen(trim($response)) == null) { //пустой ответ, значит что-то пошло не так, считаем что сообщение не доставлено. $success = false; } //анализируем ответ, см формат ответа http://developer.android.com/google/gcm/gcm.html#success if ($response) { $json = json_decode($response, true); if (json_last_error() != JSON_ERROR_NONE) { //ошибка парсинга json ответа, на всякий случай считаем что сообщение не доставлено $success = false; $json = array(); } } else { $json = array(); $success = false; } // failure содержит количество недоставленных сообщений (в нашем случае получатель один, поэтому failure будет содержать либо 0 либо 1) $failure = isset($json['failure']) ? $json['failure'] : null; // canonical_ids содержит количество получателей, для которых нужно обновить RegistrationId (как и в случае с failure - значение либо 0 либо 1). $canonicalIds = isset($json['canonical_ids']) ? $json['canonical_ids'] : null; //Если оба параметра равны нулю, то дальнейший анализ результата не требуется. При условии $success=true можно считать что сообщение успешно доставлено if ($failure || $canonicalIds) { //results содержит массив объектов. Так как у нас получатель один, то результат тоже будет один (в случае ошибки или смены RegistrationId) $results = isset($json['results']) ? $json['results'] : array(); foreach($results as $result) { $newRegId = isset($result['registration_id']) ? $result['registration_id'] : null; $error = isset($result['error']) ? $result['error'] : null; if ($newRegId) { // Заменяем $regId на $newRegId; } else if ($error) { if ($error == self::GCM_ERROR_NOTREGISTERED) { // Удаляем $regId из базы; } else { //Произошла другая ошибка, логируем её //Если нужно дифференцировать ошибки, то их описание можно найти здесь http://developer.android.com/google/gcm/gcm.html#error_codes } $success = false; } } //Теперь мы знаем, доставлено ли сообщение для конкретного получателя или нет. }
Что делать дальше? Например, можно проставить статус в своей базе, что сообщение доставлено. Но нужно помнить, что успешная отправка на GCM сервер еще не значит фактического получения сообщения смартфонов пользователя. Более того, вспомнив пример с отпуском становится понятно, что проставлять статус в onResponse нельзя. Тогда где? У меня есть только один вариант – проставлять статусы при получении данных поллингом. К сожалению, в большинстве случаев это означает, что получатель будет получать одни и те же данные два раза. На уровне приложения можно определять получены ли уже эти данные и если да – игнорировать их. Главный плюс этого подхода – надежность, данные всегда будут доставлены. Минусы – повышенный расход трафика и батареи.
Если вы еще не читали официальную документацию, рекомендую её к прочтению.
Послесловие
Надеюсь этот туториал не просто станет для кого-нибудь отправной точкой, но и поможет сократить сроки разработки бэкэнда вашего android-приложения.
ссылка на оригинал статьи http://habrahabr.ru/post/161305/
Добавить комментарий