Перейти к содержанию
Fire Monkey от А до Я

[Статья] PHP сервер для рассылки Push на Android и iOS


Рекомендуемые сообщения

В 28.03.2017 в 18:30, Равиль Зарипов (ZuBy) сказал:

в статье описано как нужно настраивать проект

Отличная статья! Но добавлю один нюанс - функция function pushSend($title, $text, $tokens, $server_key) реализована не до конца, дело в том что у FCM есть одно ограничение - отправка не больше 1000 токенов за раз. Сам на это напоролся и не мог понять почему части пользователей рассылки не приходят (рассылка велась по регионам России, общее количество около 100 тысяч).

Пришлось накидать промежуточную функцию для разбивки на пакеты по 1000 штук:

function SendGCMMessagesPacket($apiKey, $DevicesTokenArray, $message, $title, $DBLink, $TableName, $MyLog_GCM_file) {
	if (count($DevicesTokenArray)==0) {
		MyLog($MyLog_GCM_file, "Token Array is 0, skip sending");
		return(0);
	}
	$Count_Success = 0;
	$DeviceCountMax = 1000;
	$DeviceCountIndex = 0; 
	$DevicesTokenPacketArray = array();
	$gcpm = new GCMPushMessage($apiKey);
	while ($DeviceCountIndex<=count($DevicesTokenArray)) {
		$DevicesTokenPacketArray = array_slice($DevicesTokenArray, $DeviceCountIndex, $DeviceCountMax);
		MyLog($MyLog_GCM_file, "Packet send: start index $DeviceCountIndex,  count ".count($DevicesTokenPacketArray));
		$gcpm->setDevices($DevicesTokenPacketArray);
		$ResponceJSON = $gcpm->send($message, array('title' => $title));
		$Count_Success = $Count_Success + AnalyzeResponse($DevicesTokenPacketArray, $ResponceJSON, $DBLink, $TableName, $MyLog_GCM_file);
		MyLog($MyLog_GCM_file, $ResponceJSON);
		$DeviceCountIndex = $DeviceCountIndex + $DeviceCountMax;			
	}
	return($Count_Success);	
}

Функция AnalyzeResponse анализирует ответ и помечает в базе не зарегистрированные токены как неактивные

function AnalyzeResponse($DevicesTokenArray, $ResponceJSON, $DBLink, $TableName, $MyLog_GCM_file) {
	$Count_Success = 0;
	$ResponceArray = json_decode($ResponceJSON, true);
	if (isset($ResponceArray['results'])) {
		$Index = 0;
		foreach ($ResponceArray['results'] as $key => $value){ 
			if (isset($value['error'])) {
				if (isset($DevicesTokenArray[$Index])) {
					MyLog($MyLog_GCM_file, $DevicesTokenArray[$Index].' : '.$value['error']);
					$query = "UPDATE $TableName SET Active = 0 WHERE DeviceToken = '$DevicesTokenArray[$Index]'";
					$mysql_result = mysqli_query($DBLink, $query);
				}
			} else {
				$Count_Success = $Count_Success + 1;
			}
			$Index = $Index + 1;
		}
	}
	return($Count_Success);
}

 

Ссылка на комментарий
17 часов назад, Kitty сказал:

Я пытаюсь адаптировать статью этого топика. Т.е в статье присутствует .pas файл с нужным функционалом. Как этот файл будут выглядеть в окончательном виде с THTTPClient?

Вы про global.pas? Если про него, то как то так :

unit global;

interface

uses
  System.Classes, {IdHTTP,}System.Net.HTTPClient, SysUtils, FMX.Notification;

///  <summary>
///  Процедура регистрации устройства.
///  </summary>
///  <param name="DeviceID">
///  ID регистрируемого устройства
///  </param>
///  <param name="DeviceToken">
///  Токен регистрируемого устройства
///  </param>
procedure RegisterDevice(DeviceID : string; DeviceToken : string);

///  <summary>
///  Процедура отправки Push сообщения на сервер.
///  </summary>
///  <param name="MessageText">
///  Текст отправляемого сообщения
///  </param>
procedure SendPush(MessageText : string);

///  <summary>
///  Процедура вывода сообщения из приложения.
///  </summary>
///  <param name="MessageText">
///  Текст выводимого сообщения
///  </param>
///  <param name="BadgeNumber">
///  Число выводимое на иконку приложения
///  </param>
procedure ShowNotification(MessageText : string; BadgeNumber : integer);

const
  // Доменное имя сайта
  DOMAIN: string = 'http://example.ru/';

implementation


procedure RegisterDevice(DeviceID : string; DeviceToken : string);
var
  // Подключение для передачи данных
  httpconnect : THTTPClient;
  AQuery : String;
begin
  try
    // Создаём подключение
    httpconnect := THTTPClient.Create;
    // Указываем данные для отправки
    AQuery:='?action=register-device&did=' + DeviceID + '&token=' + DeviceToken;
    {$ifdef ANDROID}
      AQuery:=AQuery + '&platform=android';
    {$else}
      AQuery:=AQuery + '&platform=ios';
    {$endif}
    // Отправляем запрос
    httpconnect.Get(DOMAIN + 'push.php' + AQuery);
  finally
    // Отключаемся и освобождаем память
    httpconnect.free
  end;
end;

procedure SendPush(MessageText : string);
var
  // Подключение для передачи данных
  httpconnect : THTTPClient;
  AQuery : String;
begin
  try
    // Создаём подключение
    httpconnect := THTTPClient.Create;
    // Указываем данные для отправки
    AQuery:='?action=send-push&text=' + MessageText;
    // Отправляем запрос
    httpconnect.Get(DOMAIN + 'push.php' + AQuery);
  finally
    // Отключаемся и освобождаем память
    httpconnect.Free;
  end;
end;

procedure ShowNotification(MessageText : string; BadgeNumber : integer);
var
  NotificationC: TNotificationCenter;
  Notification: TNotification;
begin

  // Создаём центр уведомлений и уведомление для отправки
  NotificationC := TNotificationCenter.Create(nil);
  Notification := NotificationC.CreateNotification;

  try
    // Если центр уведомлений поддерживается системой
    if NotificationC.Supported then
    begin
      // Устанавливаем текст сообщения
      Notification.AlertBody := MessageText;
      // Включаем звук при выводе сообщение
      Notification.EnableSound := true;
      // Устанавливаем цифру на иконке приложения
      Notification.Number := BadgeNumber;
      NotificationC.ApplicationIconBadgeNumber := BadgeNumber;
      // Выводим сообщение из приложения
      NotificationC.PresentNotification(Notification);
    end;
  finally
    // Очищаем переменные
    Notification.DisposeOf;
    NotificationC.Free;
    NotificationC.DisposeOf;
  end;
end;

end.

 

Ссылка на комментарий
  • 1 месяц спустя...
В 16.04.2017 в 08:20, erden1 сказал:

Пуши не отправляются, выходит ошибка на PHP сервере Invalid (legacy) Server-key delivered or Sender is not authorized to perform request.

Скорее всего как у меня, неверный DeviceToken

Ссылка на комментарий
В 11.04.2017 в 12:09, Евгений Корепов сказал:

Отличная статья! Но добавлю один нюанс - функция function pushSend($title, $text, $tokens, $server_key) реализована не до конца, дело в том что у FCM есть одно ограничение - отправка не больше 1000 токенов за раз. Сам на это напоролся и не мог понять почему части пользователей рассылки не приходят (рассылка велась по регионам России, общее количество около 100 тысяч).

Пришлось накидать промежуточную функцию для разбивки на пакеты по 1000 штук:


function SendGCMMessagesPacket($apiKey, $DevicesTokenArray, $message, $title, $DBLink, $TableName, $MyLog_GCM_file) {
	if (count($DevicesTokenArray)==0) {
		MyLog($MyLog_GCM_file, "Token Array is 0, skip sending");
		return(0);
	}
	$Count_Success = 0;
	$DeviceCountMax = 1000;
	$DeviceCountIndex = 0; 
	$DevicesTokenPacketArray = array();
	$gcpm = new GCMPushMessage($apiKey);
	while ($DeviceCountIndex<=count($DevicesTokenArray)) {
		$DevicesTokenPacketArray = array_slice($DevicesTokenArray, $DeviceCountIndex, $DeviceCountMax);
		MyLog($MyLog_GCM_file, "Packet send: start index $DeviceCountIndex,  count ".count($DevicesTokenPacketArray));
		$gcpm->setDevices($DevicesTokenPacketArray);
		$ResponceJSON = $gcpm->send($message, array('title' => $title));
		$Count_Success = $Count_Success + AnalyzeResponse($DevicesTokenPacketArray, $ResponceJSON, $DBLink, $TableName, $MyLog_GCM_file);
		MyLog($MyLog_GCM_file, $ResponceJSON);
		$DeviceCountIndex = $DeviceCountIndex + $DeviceCountMax;			
	}
	return($Count_Success);	
}

Функция AnalyzeResponse анализирует ответ и помечает в базе не зарегистрированные токены как неактивные


function AnalyzeResponse($DevicesTokenArray, $ResponceJSON, $DBLink, $TableName, $MyLog_GCM_file) {
	$Count_Success = 0;
	$ResponceArray = json_decode($ResponceJSON, true);
	if (isset($ResponceArray['results'])) {
		$Index = 0;
		foreach ($ResponceArray['results'] as $key => $value){ 
			if (isset($value['error'])) {
				if (isset($DevicesTokenArray[$Index])) {
					MyLog($MyLog_GCM_file, $DevicesTokenArray[$Index].' : '.$value['error']);
					$query = "UPDATE $TableName SET Active = 0 WHERE DeviceToken = '$DevicesTokenArray[$Index]'";
					$mysql_result = mysqli_query($DBLink, $query);
				}
			} else {
				$Count_Success = $Count_Success + 1;
			}
			$Index = $Index + 1;
		}
	}
	return($Count_Success);
}

 

Можете скинуть куда-нибудь файл push.php с внесенными вами изменениями?

Ссылка на комментарий
  • Модераторы
21 минуту назад, ENERGY сказал:

А токен который генерится на телефоне, он постоянный? Или его нужно каждый раз при старте приложения записывать в базу?

Лучше каждый раз, потому что не известно из каких данных он генерируется и при этом у него есть срок действия, но хз какой

Ссылка на комментарий
26 минут назад, ENERGY сказал:

@Евгений Корепов

По поводу функции AnalyzeResponse -  у вас просто ставиться флаг неактивности, а что если сразу удалять из базы этот ID ?

Можно и удалять. Но тогда не будут работать мои аналитические инструменты - частота запусков приложения, периоды активности и многое другое.

У Равиля запрос добавления токена в базу вот такой:

INSERT INTO PushTokens (`deviceToken`, `deviceID`, `platform`) 
VALUE ('$deviceToken', '$deviceID', '$platform') 
ON DUPLICATE KEY UPDATE `deviceToken` = '$deviceToken'";

У меня же он чуть сложнее:

INSERT INTO gcm (`DeviceID`, `DeviceToken`, `City`, `last_update`, `add_date`, `RequestCount`, `Active`) 
VALUES ('$DeviceID','$DeviceToken','$City',NOW(),NOW(), 1, 1) 
ON DUPLICATE KEY UPDATE 
	`DeviceToken` = '$DeviceToken', 
	`City` = '$City',
	`last_update` = NOW(), 
	`RequestCount` = `RequestCount` + 1, 
	`Active` =  1

Таблица выглядит вот так

CREATE TABLE `gcm` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `DeviceID` char(32) DEFAULT NULL,
  `DeviceToken` varchar(255) DEFAULT NULL,
  `City` varchar(100) DEFAULT NULL,
  `last_update` datetime DEFAULT NULL,
  `add_date` datetime DEFAULT NULL,
  `RequestCount` int(11) DEFAULT '1',
  `Active` bit(1) DEFAULT b'1',
  PRIMARY KEY (`id`),
  UNIQUE KEY `DeviceID_index` (`DeviceID`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=40337 DEFAULT CHARSET=utf8;

Т.е. кроме всего прочего я вижу:

  • Дату-время первого запуска приложения
  • Дату-время последнего запуска приложения
  • Количество запусков приложения
  • Стоит ли еще приложение на этом устройстве (Active)

На поле City не обращайте внимание...

Ссылка на комментарий
@Евгений Корепов

Хотел спросить, флаг Active  выставляется когда Firebase вернул ошибку на конкретный токен, верно?

А в каком случае он ошибку может возвращать, может он вернет ее если телефон будет выключен, или давно не выходил в сеть. Т.е. необязательно это означает что приложение удалили.

И кстати можно же не отправлять в скрипте на такие телефоны сообщение, ведь телефон не зарегистрировался (при регистрации флаг ставиться)

 

Кстати вот https://firebase.google.com/docs/cloud-messaging/server

"error": "Unavailable" 
"error": "InvalidRegistration" 
"error": "NotRegistered"

 

Изменено пользователем ENERGY
Ссылка на комментарий

@Равиль Зарипов (ZuBy) по поводу вашей статьи: 

В коде отправлять токен на базу нужно только в событии OnChange , которое срабатывает в момент вызова FPushServiceConnection.Active := true; Если вы отправляете на базу в OnChange, а затем еще и в TFormMain.PushServiceRegister; - то данные отправятся 2 раза. Device ID и Token конечно же будут одинаковые. Т.е. проще говоря после строки FPushServiceConnection.Active := true; не должно идти никакого кода. Я проверил в отладчике на 4 и 5 Android.

 

 

procedure TFormMain.PushServiceRegister;

begin
  FPushService := nil;
  FPushServiceConnection := nil;
 
{$IF defined(ANDROID)}
  FPushService := TPushServiceManager.Instance.GetServiceByName<
    (TPushService.TServiceNames.GCM);
  FPushService.AppProps[TPushService.TAppPropNames.GCMAppID] := FAndroidServerKey;
{$ENDIF}
{$IF defined(IOS) AND defined(CPUARM)}
  FPushService := TPushServiceManager.Instance.GetServiceByName
   (TPushService.TServiceNames.APS);
{$ENDIF}
  if Assigned(FPushService) then
  begin
    FPushServiceConnection := TPushServiceConnection.Create(FPushService);
    FPushServiceConnection.OnChange := OnServiceConnectionChange;
    FPushServiceConnection.OnReceiveNotification := OnReceiveNotificationEvent;
    FPushServiceConnection.Active := true;
 
// ниже код нужно удалить, иначе токен отправится на базу дважды.
    FDeviceID := FPushService.DeviceIDValue[TPushService.TDeviceIDNames.DeviceID];
    FDeviceToken := FPushService.DeviceTokenValue
      [TPushService.TDeviceTokenNames.DeviceToken];
    // тут отправляем в хранилище токенов (на сервер с БД например)
  end;
end;
Изменено пользователем ENERGY
Ссылка на комментарий
  • Модераторы
11 минуту назад, ENERGY сказал:

// ниже код нужно удалить, иначе токен отправится на базу дважды.

для этого SQL запрос сделан так чтобы не записывать дубли, ну можете убрать этот код. я не против)

Ссылка на комментарий

Кто просил PHP код, для отправки пушей, без лимита на 1000 токенов за одну отправку. 

Вот готовый вариант: 

<?php         
                                                                                      
$server_key = 'AAAAnCw-yKA:APA91bEYphFbq_w...';

$title = 'Title';
$text = 'test';
$limit = 999;
$field_name = 'DeviceToken';

   $sql = mysqli_connect("mysqlserver.com", "DBName", "DBPassword");
   /* check connection */
   if (mysqli_connect_errno()) {
    printf("Connect failed: %s\n", mysqli_connect_error());
    exit();
}
   
   $offset = 0;
   while (true) {  
                                                           
   $query = "SELECT $field_name FROM `DBName`.`TableName` LIMIT $limit OFFSET $offset";
   $result = mysqli_query($sql, $query);                 
   if (!$result) {
    die('Invalid query: ' . mysql_error());
} 
   if (mysqli_num_rows($result) == 0) {
     echo "{\"result\":true}";  
     exit;    
   }
  $arr = array();  
  while ($row = mysqli_fetch_array($result, MYSQL_ASSOC)) {
    $arr[] =  $row["$field_name"];  
   }
   
  pushSend($title, $text, $arr, $server_key); 


  $offset = $offset + $limit;
  /* free result set */
  mysqli_free_result($result);  


 //  foreach($arr as $item) {
 //    echo $item, '<br>';
//}

//echo '-----------<br>';

}

mysqli_close($sql);
         
       

// max 1000
 
function pushSend($title, $text, $tokens, $server_key) {
    $url = 'https://fcm.googleapis.com/fcm/send';
    $headers = array('Authorization: key=' . $server_key, 
     'Content-Type: application/json');
  
    if (is_array($tokens))
      $fields['registration_ids'] = $tokens;
    else
      $fields['registration_ids'] = array($tokens);
  
    $fields['priority'] = 'high';
    $fields['notification'] = array('body' => $text, 'title' => $title);
    $fields['data'] = array('message' => $text, 'title' => $title);
  
    $ch = curl_init();
    curl_setopt_array($ch, array(
            CURLOPT_URL => $url,
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_POSTFIELDS => json_encode($fields)
    ));
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
}
?>

 

Ссылка на комментарий
В 27.05.2017 в 00:04, ENERGY сказал:

@Евгений Корепов

Хотел спросить, флаг Active  выставляется когда Firebase вернул ошибку на конкретный токен, верно?

А в каком случае он ошибку может возвращать, может он вернет ее если телефон будет выключен, или давно не выходил в сеть. Т.е. необязательно это означает что приложение удалили.

И кстати можно же не отправлять в скрипте на такие телефоны сообщение, ведь телефон не зарегистрировался (при регистрации флаг ставиться)

 

Кстати вот https://firebase.google.com/docs/cloud-messaging/server


"error": "Unavailable" 
"error": "InvalidRegistration" 
"error": "NotRegistered"

 

По идее флаг нужно убирать при NotRegistered однозначно. При инвалидской регистрации тоже.

Лучше пользоваться не http интерфейсом Firebase, а XMPP,  там, насколько я знаю, можно получать всю инфу, вплоть до подтверждения получения пуша клиентом.

В скрипте и не отправляются сообщения на не активные токены, по крайней мере у меня, зачем? В SQL запросе "WHERE active = 1" решает проблему.

P.S. Надо задуматься над коммерческим PUSH сервером ;-) Площадок у меня есть, голова и руки тоже. Сделать по 50 рублей в месяц подписку. Чтоб зашел, зарегился, вбил свои данные и создавай push проекты, отправляй, получай и все такое...

Ссылка на комментарий
В 5/29/2017 в 14:36, Евгений Корепов сказал:

P.S. Надо задуматься над коммерческим PUSH сервером ;-) 

Есть же уже бесплатные серверы, тот же Google Firebase и Kinvy. Думаете кто то будет платить деньги, когда есть бесплатные стабильные альтернативы от известных компаний? Сомневаюсь.

Ссылка на комментарий
Цитата

сть же уже бесплатные серверы, тот же Google Firebase и Kinvy

А Вы Kinvеy, к примеру пользовались, чтобы заявлять о крутости этого сервиса?

Они API вызовы считают по одним им известным алгоритмам обмана. Просто тупо накручивают, а ну если перевалит за 1000 то гуд бай!

А с этим сталкивались у Kinvеy - дубляж токенов при удалении приложения... поиск по этому форуму.

Kinvеy бесполезный сервис, который годиться для демонстрации демок.

Если Евгений Корепов, предложит нормальный PHP сервис - будет полезно и эффективно!

 

Ссылка на комментарий
22 минуты назад, Ingalime сказал:

А с этим сталкивались у Kinvеy - дубляж токенов при удалении приложения... поиск по этому форуму.

А что за дубляж токенов при удалении, как же их удалишь из своей базы, если программа даже не запускается при удалении? Или я не так понял про какие токены идет речь?

Изменено пользователем ENERGY
Ссылка на комментарий

Если Вам лень поиск по форуму, то идете прямо сюда: https://community.embarcadero.com/blogs/entry/remote-push-notifications-on-android-with-rad-studio-xe6-795

и читайте комментарий от Steve J, а еще лучше прочитайте этот форум, чтобы понять, что  Kinvеy бесполезная помойка.

 

Ссылка на комментарий

Ingalime

Честно говоря это больше похоже на проблему компонентов PushEvents или Kinvey, которые и отправляют эти данные при старте программы без DeviceID, чем на проблемы от сервера Kinvey.

Ну да я этим сервисом не пользовался, видимо пронесло :)

Изменено пользователем ENERGY
Ссылка на комментарий

Вполне возможно. Еще в kinvey ограничение на 1000 пушей в месяц (называют это вызовы api). Как то в течении суток не было послано ни одного пуша, а в панели kinvey была цифра 40. Спрогназировать ничего не возможно...

Ссылка на комментарий
5 часов назад, ENERGY сказал:

Ingalime

Честно говоря это больше похоже на проблему компонентов PushEvents или Kinvey, которые и отправляют эти данные при старте программы без DeviceID, чем на проблемы от сервера Kinvey.

Ну да я этим сервисом не пользовался, видимо пронесло :)

Это не проблема компонентов, это проблема малограмотности программистов kinvey, видимо они учебник SQL еще не дочитали до места "ON DUPLICATE KEY UPDATE". Беда современных проектов - все деньги тратятся на дизайнеров и менеджеров, а на то что остается на реализацию технической части, можно нанять только индусов или малограмотных оутсорсеров по объявлению.

Ссылка на комментарий

Присоединяйтесь к обсуждению

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

Гость
Ответить в этой теме...

×   Вставлено с форматированием.   Вставить как обычный текст

  Разрешено использовать не более 75 эмодзи.

×   Ваша ссылка была автоматически встроена.   Отображать как обычную ссылку

×   Ваш предыдущий контент был восстановлен.   Очистить редактор

×   Вы не можете вставлять изображения напрямую. Загружайте или вставляйте изображения по ссылке.

×
×
  • Создать...