📜 ⬆️ ⬇️

Google Cloud Messaging - write backend in PHP

image As part of the tutorial, we will write a full-fledged class for sending messages to the GCM server, which:



Google Cloud Messaging - short and clear


GCM is an instant messaging service. An alternative to standard polling and long polling, but not exclusive, but complementary to them. Guarantees that the message will be delivered Google does not give (although the reliability and speed of delivery was just space compared to the ancestor of C2DM). If the Internet is turned off on the phone, the message will be stored on the GCM server for up to 4 weeks. Ie if the user turned off the phone, went on vacation, then on arrival he may not receive a message. Therefore, GCM should only work together with reliable delivery methods such as, for example, elementary polling - sending http requests to the server every N minutes.

Any android application can register itself as a recipient of messages from GCM. When the Internet is turned on, registration occurs in seconds. As soon as this happened, the application receives from the GCM server RegistrationId, which must be sent to its server. As a result, in the server database, we have, for example, a Devices table that stores information about devices, including their RegistrationId.
')
In order for devices to start receiving messages, the server code must send POST requests to the GCM server in json format (you can send regular keys => value, but it is json that is recommended). The server response also contains json, after analyzing which we will be able to understand whether the message was delivered, and if not - what errors occurred.

Let's get started


Create two GcmPayload and GcmSender classes.

Listing
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) {} } 



In terminology, GCM payload is the data you want to send to the recipient. This data must be stored in the data key and have a limit of 4096 bytes. More about the format of the request .

GcmPayload is a data model for one recipient and, accordingly, one RegistrationId. The $ jsons field must be initialized with an array of json's in the form of strings containing the data that needs to be sent to this recipient. To simplify the tutorial, we assume that this is done outside our class, for example, like this:

Listing
 $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


Constants and class members

const GCM_API_KEY = 'your api key'; // Need to get on the Google APIs Console page
const CURL_TIMEOUT = 10; // Connection timeout in Google server in seconds
const GCM_MAX_DATA_SIZE = 4096; // Limit on the sent data in bytes
const GCM_SERVER_URL = 'https://android.googleapis.com/gcm/send'; // GCM server address
const GCM_MAX_CONNECTIONS = 10; // number of parallel queries

const KEY_REG_IDS = 'registration_ids'; // recipient key in json request
const KEY_DATA = 'data'; // key with data in json request
const KEY_ITEMS = 'items'; // key in the data object containing our data array
const REGID_PLACEHOLDER = '_REGID_'; // placeholder for RegistrationId in json request template
const ITEMS_PLACEHOLDER = '_ITEMS_'; // placeholder for our data array in json request template

const GCM_ERROR_NOTR Vector = 'NotRegistered'; // constant for error if user deleted the application

protected $ _template; // json query template
protected $ _baseDataSize; // initial data size, which includes the items key, parenthesis quotes, etc.
constructor

The constructor creates a query template that will be used in the getPackages method. Please note that in order not to potentially exceed the limit of 4096 bytes per data, you must also remember and take into account the size of the initial data in the template: {“items”: []}

Listing
 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 method

This public method should be called to directly send data to the GCM server. The method accepts data for sending, which by the getPackages method will be converted into data packets — prepared post data in json format (one packet - one request) and guaranteed not to exceed 4096 bytes. The rest of the method is the initialization of the wonderful RollingCurl library, which encapsulates the work with curl_multi_exec and allows you to send requests in parallel and write transparent code. RollingCurl is initialized by our callback method onResponse, in which we will analyze the result of sending. Next comes the actual sending data itself.

Listing
 /** * @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 method

In this method, an array of payloads transferred to the class is enumerated and the template created in the constructor is gradually filled until the packet exceeds the limit of 4096 bytes or the data for the recipient does not run out. By the way, in our example, we consider that one packet is one receiver. What does it mean? For example, this convention is valid when a text message is addressed to only one person. But in group conversations, the same message can be sent to several people, and GCM allows you to specify several RegistrationId values ​​in the value of the registration_ids key. But again, in this example, in order to avoid unnecessary complications, this case is not considered.

Let's go back to the getPackages method. In fact, the interest is isReadyToFlush, which determines whether adding a new json to the package will go beyond the limit of 4096 bytes. If so, then the package ends immediately and we add this json to the new package.

Listing
 /** * @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 method

It is important not only to send a message, but also to understand whether it was delivered, and if not, for what reason. onResponse is the one with which we initialized RollingCurl in the send method. Kolbek takes three parameters:
  1. $ response - response as a string
  2. $ info is the result of the curl_getinfo php.net/manual/en/function.curl-getinfo.php function and returns an array of data transfer data, starting from the http response code and ending with download / download speeds. But in this tutorial only the http response code is interesting.
  3. RollingCurlRequest $ request - information about the request. We are interested in $ request-> post_data


Comments in the listing function will be more eloquent:

Listing
 /** * @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; //  ,  Update 1 } else if ($error) { if ($error == self::GCM_ERROR_NOTREGISTERED) { // $regId  ; } else { //  ,   //   ,       http://developer.android.com/google/gcm/gcm.html#error_codes } $success = false; } } } //  ,        . } 



Update 1
jcrow tells in the comments about the pitfall , which can wait when updating the RegistrationId. Situation: the user uninstalled the application and reinstalled => re-registered and received a new RegistrationId. Our database already has two entries for the same user. And one entry with the old RegistrationId, and the other with the new one. If we send messages to both RegistrationId, then the new RegistrationId will come for the old one. As a result, we have two entries for the same user with the same RegistrationId.

As a result: If a unique index is placed across the RegistrationId field in our database, we get an error. If there is no index, the application user receives two identical messages each time.

Solution: Once we have received a new RegistrationId in onResponse, we need to check for its existence in the database. And in a positive case, either delete one of the records or merge both records and the data associated with them from other tables.

What to do next?


For example, you can put a status in your database that the message has been delivered. But it must be remembered that the successful sending to the GCM server does not mean the actual receipt of the message by the user's smartphone. Moreover, having remembered the holiday example, it becomes clear that it is impossible to affix the status to onResponse. Then where? I have only one option - to put statuses when receiving data by polling. Unfortunately, in most cases this means that the recipient will receive the same data twice. At the application level, you can determine whether this data has already been received and, if so, to ignore it. The main advantage of this approach is reliability, data will always be delivered. Cons - increased consumption of traffic and batteries.

If you have not read the official documentation, I recommend it to read .

Afterword


I hope this tutorial will not just be for someone a starting point, but also help reduce the time to develop the backend of your android-application.

Source: https://habr.com/ru/post/161305/


All Articles