Использование сокетов в Android
Создано большое количество приложений как для Android, так и для других ОС, которые взаимодействуют друг с другом с помощью установления соединенией по сети. К таким приложениям относятся, например, мессенджеры социальных сетей WhatsApp, Viber. Как правило, для установления соединения между такими приложениями используются сокеты.
Сокет (socket) — это интерфейс, позволяющий связывать между собой программы различных устройств, находящихся в одной сети. Сокеты бывают двух типов: клиентский (Socket) и серверный (ServerSocket). Главное различие между ними связано с тем, что сервер «открывает» определенный порт на устройстве, «слушает» его и обрабатывает поступающие запросы, а клиент должен подключиться к этому серверу, зная его IP-адрес и порт. В Android сокеты для передачи данных используют по умолчанию протокол TCP/IP, важной особенностью которого является гарантированная доставка пакетов с данными от одного устройства до другого.
Особенности использования сокетов
Что важно знать при использовании сокетов в Android ?
- соединения сокетов отключаются при переходе устройства в спящий режим;
- чтобы не «рвать» соединение при наступлении спящего режима в устройстве можно использовать сервис;
- для использования интернет-сети необходимо Android-приложению предоставить нужные права в манифесте.
Для определения прав в манифесте необходимо в файл AndroidManifest.xml добавить следующую строку :
Теперь android-приложения будет иметь доступ к сети.
Далее в статье рассмотрим пример клиент-серверного сокетного соединения с передачей сообщения. Функции клиента будет выполнять android-приложение. Серверное java-приложение выполним в IDE Eclipse с использованием пакета concurrent. В конце страницы можно скачать оба приложения.
Клиентский android-сокет
Интерфейс andriod-приложения представлен на следующем скриншоте. Форма приложения включает поле ввода текстового сообщения и кнопки установления соединения сервером, передачи сообщения и закрытия соединения.
Клиентское приложение создадим из двух классов : класс взаимодействия с серверным сокетом Connection и класс стандартной активности MainActivity.
Класс Connection
Класс взаимодействия с сервером Connection получает при создании (через конструктор) параметры подключения : host и port. Методы Connection вызываются из активности и выполняют следующие функции :
| Метод | Описание |
|---|---|
| openConnection | Метод открытия сокета/соединения. Если сокет открыт, то он сначала закрывается. |
| closeConnection | Метод закрытия сокета |
| sendData | Метод отправки сообщения из активности. |
| finalize | Метод освобождения ресурсов |
Листинг Connection
import android.util.Log; import java.io.IOException; import java.net.Socket; public class Connection < private Socket mSocket = null; private String mHost = null; private int mPort = 0; public static final String LOG_TAG = "SOCKET"; public Connection() <>public Connection (final String host, final int port) < this.mHost = host; this.mPort = port; >// Метод открытия сокета public void openConnection() throws Exception < // Если сокет уже открыт, то он закрывается closeConnection(); try < // Создание сокета mSocket = new Socket(mHost, mPort); >catch (IOException e) < throw new Exception("Невозможно создать сокет: " + e.getMessage()); >> /** * Метод закрытия сокета */ public void closeConnection() < if (mSocket != null && !mSocket.isClosed()) < try < mSocket.close(); >catch (IOException e) < Log.e(LOG_TAG, "Ошибка при закрытии сокета :" + e.getMessage()); >finally < mSocket = null; >> mSocket = null; > /** * Метод отправки данных */ public void sendData(byte[] data) throws Exception < // Проверка открытия сокета if (mSocket == null || mSocket.isClosed()) < throw new Exception("Ошибка отправки данных. " + "Сокет не создан или закрыт"); >// Отправка данных try < mSocket.getOutputStream().write(data); mSocket.getOutputStream().flush(); >catch (IOException e) < throw new Exception("Ошибка отправки данных : " + e.getMessage()); >> @Override protected void finalize() throws Throwable < super.finalize(); closeConnection(); >>
Класс активности MainActivity
В активности MainActivity определены параметры сервера : host, port. Помните, что IP-адрес сервера для Вашего android-примера не может быть localhost (127.0.0.1), иначе Вы будете пытаться связаться с сервером внутри Andriod-системы. Кнопки интерфейса связаны с методами обращения к классу Connection. Кнопки отправки сообщения mBtnSend и закрытия соединения mBtnClose с сервером блокируются при старте приложения. После установления соединения с сервером доступ к кнопкам открывается.
Листинг активности
public class MainActivity extends AppCompatActivity < private Button mBtnOpen = null; private Button mBtnSend = null; private Button mBtnClose = null; private EditText mEdit = null; private Connection mConnect = null; private String HOST = "10.120.51.22"; private int PORT = 9876; private String LOG_TAG = "SOCKET"; @Override protected void onCreate(Bundle savedInstanceState) < super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBtnOpen = (Button) findViewById(R.id.btn_open ); mBtnSend = (Button) findViewById(R.id.btn_send ); mBtnClose = (Button) findViewById(R.id.btn_close); mEdit = (EditText) findViewById(R.id.edText ); mBtnSend .setEnabled(false); mBtnClose.setEnabled(false); mBtnOpen.setOnClickListener(new OnClickListener() < @Override public void onClick(View v) < onOpenClick(); >>); mBtnSend.setOnClickListener(new OnClickListener() < @Override public void onClick(View v) < onSendClick(); >>); mBtnClose.setOnClickListener(new OnClickListener() < @Override public void onClick(View v) < onCloseClick(); >>); > >
Методы управления сокетным соединением
Ниже представлены методы обработки событий, связанных с нажатием кнопок интерфейса. Обратите внимание, что подключение к серверу выполняется в отдельном потоке, а открытие доступа к кнопкам в основном потоке, для чего вызывается метод runOnUiThread. Для отправки сообщения серверу также создается отдельный поток.
private void onOpenClick() < // Создание подключения mConnect = new Connection(HOST, PORT); // Открытие сокета в отдельном потоке new Thread(new Runnable() < @Override public void run() < try < mConnect.openConnection(); // Разблокирование кнопок в UI потоке runOnUiThread(new Runnable() < @Override public void run() < mBtnSend.setEnabled(true); mBtnClose.setEnabled(true); >>); Log.d(LOG_TAG, "Соединение установлено"); Log.d(LOG_TAG, "(mConnect != null) = " + (mConnect != null)); > catch (Exception e) < Log.e(LOG_TAG, e.getMessage()); mConnect = null; >> >).start(); > private void onSendClick() < if (mConnect == null) < Log.d(LOG_TAG, "Соединение не установлено"); >else < Log.d(LOG_TAG, "Отправка сообщения"); new Thread(new Runnable() < @Override public void run() < try < String text; text = mEdit.getText().toString(); if (text.trim().length() == 0) text = "Test message"; // отправляем сообщение mConnect.sendData(text.getBytes()); >catch (Exception e) < Log.e(LOG_TAG, e.getMessage()); >> >).start(); > > private void onCloseClick() < // Закрытие соединения mConnect.closeConnection(); // Блокирование кнопок mBtnSend .setEnabled(false); mBtnClose.setEnabled(false); Log.d(LOG_TAG, "Соединение закрыто"); >
Серверное приложение
Серверное приложение включает 2 класса : Server и ConnectionWorker. Серверный класс Server будет выполнять обработку взаимодействия с клиентом с использованием ConnectionWorker в отдельном потоке. Конструктор ConnectionWorker в качестве параметра получает объект типа Socket для чтения сообщений клиента из потока сокета.
Листинг ConnectionWorker
ConnectionWorker получает входной поток inputStream из клиентского сокета и читает сообщение. Если сообщение отсутствует, т.е. количество прочитанных байт равно -1, то это значит, что соединение разорвано, то клиентский сокет закрывается. При закрытии клиентского соединения входной поток сокета также закрывается.
public class ConnectionWorker implements Runnable < // Сокет для взаимодействия с клиентом private Socket clientSocket = null; // Входной поток получения данных из сокета private InputStream inputStream = null; public ConnectionWorker(Socket socket) < clientSocket = socket; >@Override public void run() < try < // Определение входного потока inputStream = clientSocket.getInputStream(); >catch (IOException e) < System.err.println("Can't get input stream"); >// Буфер для чтения информации byte[] data = new byte[1024*4]; while(true) < try < /* * Получение информации : * count - количество полученных байт */ int count; count=inputStream.read(data,0,data.length); if (count >0) < String msg=new String(data, 0, count); // Вывод в консоль сообщения System.out.println(msg); >else if (count == -1 ) < // Если count=-1, то поток прерван System.out.println("socket is closed"); clientSocket.close(); break; >> catch (IOException e) < System.err.println(e.getMessage()); >> System.out.println("ConnectionWorker stoped"); > >
Серверный класс
Серверный класс Server создадим с использованием многопоточного пакета util.concurrent. На странице описания сетевого пакета java.net и серверного ServerSocket был приведен пример серверного модуля с использованием обычного потока Thread, при работе с которым необходимо решать задачу его остановки : cтарый метод Thread.stop объявлен Deprecated и предан строжайшей анафеме, а безопасная инструкция Thread.interrupt безопасна, к сожалению, потому, что ровным счетом ничего не делает (отправляет сообщение потоку : «Пожалуйста, остановись»). Услышит ли данный призыв поток остается под вопросом – все зависит от разаработчика.
Чтобы иметь возможность остановить сервер «снаружи» в серверный класс Server включим 2 внутренних реализующих интерфейс Callable класса : CallableDelay и CallableServer. Класс CallableDelay будет функционировать определенное время, по истечении которого завершит свою работу и остановит 2-ой серверный поток взаимодействия с клиентами. В данном примере CallableDelay используется только для демонстрации остановки потока, организуемого пакетом util.concurrent.
Листинг CallableDelay
CallableDelay организует цикл с задержками. После завершения последнего цикла cycle поток завершает цикл, останавливает вторую задачу futureTask[1] и закрывает сокет. В консоль выводится соответствующее сообщение.
class CallableDelay implements Callable < private int cycle; public CallableDelay(int cycle) < this.cycle = cycle; >@Override public String call() throws Exception < while (cycle >0) < System.out.println("" + cycle); Thread.sleep(1000); cycle--; >// Останов 2-ой задачи futureTask[1].cancel(true); // Закрытие серверного сокета serverSoket.close(); System.out.println("Thread '" + Thread.currentThread().getName() + "' stoped" ); // Наименование потока, выполняющего задачу return "" + Thread.currentThread().getName(); > >
Листинг CallableServer
Конструктор CallableServer в качестве параметров получает значение открываемого порта для подключения клиентов. При старте (метод call) создается серверный сокет ServerSocket и поток переходит в режим ожидания соединения с клиентом. Остановить поток можно вызовом метода stopTask, либо завершением «задачи» типа FutureTask с данным потоком.
При подключении клиента метод serverSoket.accept возвращает сокет, который используется для создания объекта ConnectionWorker и его запуска в отдельном потоке. А сервер (поток) переходит к ожиданию следующего подключения.
В случае закрытия сокета (завершение внешней задачи FutureTask с данным потоком) будет вызвано исключение Exception, где выполняется проверка закрытия сокета; при положительном ответе основной цикл прерывается и поток завершает свою работу.
class CallableServer implements Callable < private int port; private boolean started; public CallableServer(int port) < this.port = port; this.started = true; >public void stopTask() < started = false; >@Override public String call() throws Exception < // Создание серверного сокета serverSoket = new ServerSocket(port); System.out.println("Server start on port : " + port); // Цикл ожидания соединений клиентов с сервером while(started) < ConnectionWorker worker = null; try < // Ожидание соединения с клиентом worker = new ConnectionWorker( serverSoket.accept()); /* * Обработка соединения выполняется * в отдельном потоке */ Thread t = new Thread(worker); t.start(); >catch (Exception e) < System.err.println("Connection error : " + e.getMessage()); // Завершение цикла. if (serverSoket.isClosed()) break; >> System.out.println("Thread '" + Thread.currentThread().getName() + "' stoped" ); futureTask[1].cancel(true); // Наименование потока, выполняющего задачу return "" + Thread.currentThread().getName(); > >
Листинг серверного класса Server
Cерверный класс Server создает два потоковых объекта (callable1, callable2), формирует из них две задачи futureTask и запускает задачи на выполнение методом execute исполнителя executor. После этого контролируется завершение выполнение обоих задач методом isTasksDone. При завершении выполнения обеих задач завершается также и цикл работы executor’а.
Два внутренних описанных выше класса (CallableDelay, CallableServer) не включены в листинг.
public class Server < // Открываемый сервером порт для клиентов private final int SERVER_PORT = 9876; // Сокет соединения с клиентами private ServerSocket serverSoket = null; // Поток контроля времени работы сервера private CallableDelay callable1 = null; // Поток соединения с клиентами private CallableServer callable2 = null; // Список задач private FutureTask[] futureTask = null; // Исполнитель задач private ExecutorService executor = null; private Server() < // 1-ый поток контролирует задержку работы сервера callable1 = new CallableDelay (50); // 2-йй поток открывает соединение callable2 = new CallableServer(SERVER_PORT); // Создание задач futureTask = new FutureTask[2]; futureTask[0] = new FutureTask(callable1); futureTask[1] = new FutureTask(callable2); // Выполнение задач executor = Executors.newFixedThreadPool(2); executor.execute(futureTask[0]); executor.execute(futureTask[1]); // Цикл работы executor'а while (true) < if (isTasksDone()) < // Завершение работы executor'а executor.shutdown(); System.out.println("\nexecutor shutdown"); break; >> > //----------------------------------------------------- private boolean isTasksDone() < return futureTask[0].isDone() && futureTask[1].isDone(); >//----------------------------------------------------- public static void main(String[] args) < new Server(); >>
Скачать пример
Архив примера android-socket.zip (98 Кб) включает два проекта : клиентский android (client-android/p12socket) и серверный server-eclipse.
Retrofit — библиотека для обмена с интернетом (Часть 1)
1. Get и Head методы считаются самыми безопасными методами т.к. получают данные не изменяя их где либо. Первый метод получает заголовок и тело ответа тогда как второй только заголовок. Ответы от сервера приходят в виде коллекций значений. При запросе мы можем указать дополнительные параметры для отбора
На пример в запросе someadress.ru?list=10&name=ivanov мы передаем параметры отбора list = 10 и name = ivanov
2. Delete – метод выполняет одноименную функцию, удаляет записи на сервере. Для метода не требуется тело запроса
3. Post и Put методы создают или изменяют данные на сервере. В отличие от других этих методах наличие тело запроса обязательно. Put метод создаст запись на сервере если такого нет, если запись присутствует то он его изменит. Post создает записи и запускает процедуры на сервере. Можно запомнить следующее выражения: Put – отправляет данные и Post – Отправляет сообщение на сервер
Примеры приведены в IDE Android Studio
Нужно настроить обмен таким образом чтоб обмен между клиентом и сервером проходил на одном формате. В основном используются 3 формата данных. Ниже приведен пример отображение на трёх форматах:
1234 Vlad
Жаль, что в этом списке нет формата для java класса, что-то такое:
public class root
Для передачи объектов java на сервер, который может работать на любом языке, retrofit использует конвертор данных (Data converter). Эти конверторы так же называются библиотеки сериализации данные. У Retrofit включает в себя 6 конверторов которые используют самые популярные библиотеки сериализации: Gson, Jackson, Moshi, Protobuf, Wire и Simple XML. Какой из конверторов использовать зависит от того какой формат принимает сервер.
Для примеров использую GSon конвертор т.к. он прост в использовании
Подключение Retrofit
Для демонстрации возможностей retrofit создам demo приложение, версия Android 7.0 (Nougat). Переходим в раздел Download на официальном сайте retrofit и копируем строку для grade в моем случаи.
implementation 'com.squareup.retrofit2:retrofit:2.6.1' Далее, нам нужен пакет конвертора данных Gson. Если прокрутить страницу загрузки retrofit чуть выше то можно увидеть список поддерживаемых конверторов. Копируем строку пакета и добавляем в build указывая номер версии retrofit после двоеточие. implementation 'com.squareup.retrofit2:converter-gson:2.6.1' Синхронизируем проект (гиперссылка Sync Now справо-сверху). После удачной синхронизации мы готовы к использованию retrofit.
Отправлять данные на сервер с Androida
Хотелось бы грамотно отправлять данные на сервер с Androida . Мой вариант хорош? Мне нужно отправлять 1 раз в 2 минуты данные (координаты) на сервер, подходит ли этот код отправки данных для моих целей? Очень хотелось бы реализовать это грамотно. Вот рабочий код отвечающий за отправку координат на сервер методом POST , он запускается в IntentService , который включается раз в 2 минуты.
public class LocationService extends IntentService < protected void onHandleIntent(Intent intent) < // тут получаем GPS координаты . // тут устанавливаем соединение и отправляем координаты HttpClient httpclient = new DefaultHttpClient(); // указываем адрес вашего сервера и путь принимающего скрипта HttpPost httppost = new HttpPost("http://mysait.ru/test.php"); try < List nameValuePairs = new ArrayList(2); // отправляемое сообщение nameValuePairs.add(new BasicNameValuePair("value", "GPS координаты")); httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs)); httpclient.execute(httppost); // отправка на сервер Log.i(TAG, "Данные отправлены на сервер"); >catch (ClientProtocolException e) < // . >catch (IOException e) < // . >> >
- Если в этом варианте есть серьезные, критические проблемы укажите их, пожалуйста.
- Ваши любые рекомендации по данному коду и в целом по реализации моей задачи.
- У Androida два HTTP клиента: HttpURLConnection и Apache HttpClient , какой больше подходит для моей задачи и почему?
Ну и вопрос на засыпку, не такой важный:)
- Данный код не обрабатывает ответ от сервера о том что координаты были доставлены на сервер, но ответ от сервера все равно будет приходить? Этот ответ можно как то отключить, чтоб сервер только принимал координаты и ничего не отвечал при этом?
Пара способов отправить уведомления на смартфон со своего сервера
В этом туториале я рассмотрю пошагово, как отправлять со своего сервера уведомления на свой (или не свой) смартфон, какие средства для этого понадобятся. Эти способы универсальны и подойдут для любого языка программирования, т.к. напрямую используют API гугла, без использования библиотек. Отправить можно на смартфоны с Android, iOS и в браузеры с поддержкой Push API (на сегодня это Chrome, Firefox и их производные).
В общем всем тем, кто давно хотел отправлять уведомления со своего домашнего сервера на свой смартфон, но не знал с чего начать, посвящается.
Немного истории. В начале (с версии андроида 2.2) у гугла для доставки использовалась система C2DM (Android Cloud to Device Messaging), начиная с июня 2012 для этого стали предлагать использовать GCM (Google cloud messaging).
В настоящее время используется универсальная платформа Firebase, которая помимо доставки уведомлений имеет ещё много всяких других возможностей. Firebase тоже успела эволюционировать и протокол первого поколения уже считается устаревшим и для доставки сообщений рекомендуется использовать протокол второго поколения.
Технически, уведомления отправляются с сервера не напрямую в смартфон, а на некий промежуточный сервер, на котором при необходимости хранятся до 4-х недель (настраиваемо), и по возможности отправляются получателю. Т.е. если смартфон находится оффлайн, сервер ждёт. Как только появляется возможность — отправляет.
1. Регистрируемся в Firebase
Для регистрации в Firebase понадобится учётка гугла.

Жмём «Перейти к консоли».

Затем «Добавить проект».

Вводим название проекта. Рекомендую в диапазоне 8-16 символов.
Выбираем страну. Жмём «Создать проект».
2. Настраиваем Firebase

Прокручиваем до блока «Notifications», жмём «Начать».
Вам предложат выбрать приложение, для которого ваши уведомления будут отправляться.

Шаги для Andriod-приложения:

Шаг 1 — Вводим название проекта на Andriod.
Жмём «Зарегистрировать приложение».

Шаг 2 — Жмём «Скачать google-services.com».
Добавляем скачанный файл конфигурации в проект, рядом с файлом build.gradle (тем, который персональный для приложения).
Жмём «Продолжить».
Шаг 3 — Добавляем в проект зависимости.
в файл /build.gradle строчку
classpath ‘com.google.gms:google-services:3.1.0’
в файл //build.gradle строчку
apply plugin: ‘com.google.gms.google-services’
Тут всё, жмём «Готово».
После настройки приложения, можно сразу протестировать работает ли связь отправив тестовое сообщение (нет нельзя, у нас ещё нет ID клиента, куда слать).
3. Настройка приложения Android на приём уведомлений.
Важное примечание: некоторые оболочки, например MIUI, могут блокировать уведомления, если приложение не запущено или не висит в фоне. Делается это якобы для экономии заряда батареи.
Грубо говоря, отправлять можно два вида уведомлений:
— уведомление по запросу,
— уведомление с полезной нагрузкой.
У них разные способы взаимодействия с приложением.
Уведомление по запросу выведет уведомление в области уведомлений, но только в случае если приложение свёрнуто. При тапе пользователя оно откроет заранее выбранную (при отправке) активити приложения, и передаст бандлом экстра-параметры.
Уведомление с полезной нагрузкой требует наличия в приложении пары служб, в которые и передаётся управление, но на длительность не дольше 10 секунд.
Ниже приведён пример службы, которая отвечает за генерацию ID клиента.
package ru.pyur.loga; import android.util.Log; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.FirebaseInstanceIdService; public class TestFirebaseInstanceIdService extends FirebaseInstanceIdService < public static final String TAG = "TestFbseInstIdSvc"; @Override public void onTokenRefresh() < String refreshedToken = FirebaseInstanceId.getInstance().getToken(); Log.d(TAG, "Refreshed token: " + refreshedToken); //~sendRegistrationToServer(refreshedToken); >>
И пример кода службы, принимающей сообщения. Приложение должно быть запущено, или висеть в фоне, иначе не гарантируется приём сообщений. Некоторые оболочки, например MIUI, в целях экономии, режут всё подряд, в том числе привелегии фоновых служб.
package ru.pyur.loga; import android.app.Notification; import android.app.NotificationManager; import android.content.Context; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; import static ru.pyur.loga.AcMain.context; public class TestFirebaseMessagingService extends FirebaseMessagingService < public static final String TAG = "TestFbseMsgngSvc"; @Override public void onMessageReceived(RemoteMessage remoteMessage) < Log.d(TAG, "From: " + remoteMessage.getFrom()); if (remoteMessage.getData().size() >0) < Log.d(TAG, "Message data payload: " + remoteMessage.getData()); String val1 = remoteMessage.getData().get("val1"); String val2 = remoteMessage.getData().get("val2"); String val3 = remoteMessage.getData().get("val3"); int color = (1<<16)|(1<<8)|(0); ShowNotification(val1, val2, color); >if (remoteMessage.getNotification() != null) < Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody()); >> @Override public void onDeletedMessages() < // In some situations, FCM may not deliver a message. This occurs when there are too many messages (>100) pending for your app on a particular device // at the time it connects or if the device hasn't connected to FCM in more than one month. In these cases, you may receive a callback // to FirebaseMessagingService.onDeletedMessages() When the app instance receives this callback, it should perform a full sync with your app server. // If you haven't sent a message to the app on that device within the last 4 weeks, FCM won't call onDeletedMessages(). > void ShowNotification(String title, String text, int color) < NotificationCompat.Builder mNotify = new NotificationCompat.Builder(context, ""); mNotify.setLights(color, 100, 200); mNotify.setSmallIcon(R.drawable.service_icon); mNotify.setContentTitle(title); mNotify.setContentText(text); mNotify.setDefaults(Notification.DEFAULT_SOUND); NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); int mId = 1001; try < mNotificationManager.notify(mId, mNotify.build()); >catch (Exception e) < e.printStackTrace(); >> >
не забудьте прописать службы в манифесте.
ID клиента генерируется на устройстве, но вы сами выбираете способ доставки этого ID к себе на сервер.
Вот теперь можно протестировать, отправив тестовое сообщение из консоли.


4. Отправляем уведомление со своего сервера
Существует несколько способов обмена данными с сервером Firebase. Мы рассмотрим два способа обмена по протоколу HTTP.
Протокол первого поколения — Legacy HTTP

Понадобится ключ. Жмём на гайку, выбираем «Настройки проекта».

Вкладка «Cloud Messaging».
Копируем «Устаревший ключ сервера».
>'; // или // ---- уведомление для службы ---- // $payload = ' < "to" : "cGAFgPJGf-s:APA91bF**. **aEVM17c9peqZ", "data":< "val1" : "Моё первое сообщение", "val2" : "(Legacy API) Привет!", "val3" : "какие-то дополнительные данные" >>'; $send = ''; $send .= 'POST /fcm/send HTTP/1.1'."\r\n"; $send .= 'Host: fcm.googleapis.com'."\r\n"; $send .= 'Connection: close'."\r\n"; $send .= 'Content-Type: application/json'."\r\n"; $send .= 'Authorization: key=AIzaSy***************************IPSnjk'."\r\n"; $send .= 'Content-Length: '.strlen($payload)."\r\n"; $send .= "\r\n"; $send .=$payload; $result = fwrite($socket, $send); $receive = ''; while (!feof($socket)) $receive .= fread($socket, 8192); fclose($socket); echo ''.$receive.''; ?>
Здесь в поле «to» надо подставить ID клиента. В http заголовок «Authorization: key=» подставить «Устаревший ключ сервера».
Протокол второго поколения — (Modern) HTTP v1.
(источник: developers.google.com/identity/protocols/OAuth2ServiceAccount)
Не спрашивайте, почему вторая версия протокола называется V1, видимо первая считалась бетой и носила нулевой номер.
Я не углублялся в подробности, но так понимаю этот протокол более универсальный и имеет более широкие возможности, чем просто отправка уведомлений.'); $issue_time = time(); $JWT_claim_set = base64_encode( '.iam.gserviceaccount.com",'. '"scope":"https://www.googleapis.com/auth/firebase.messaging",'. '"aud":"https://www.googleapis.com/oauth2/v4/token",'. '"exp":'.($issue_time + 3600).','. '"iat":'.$issue_time.'>'); // см. примечание $private_key = ' -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwR1biSUCv4J4W **************************************************************** **************************************************************** . **************************************************************** teTJImCT6sg7go7toh2ODfaPmeI0nA/LwSjzWs0b8gdIYPT5fAsvfQiND0vu/M3V 7C/z/SmIKeIcfOYrcbWQwTs= -----END PRIVATE KEY----- '; $data = $JWT_header.'.'.$JWT_claim_set; $binary_signature = ''; openssl_sign($data, $binary_signature, $private_key, 'SHA256'); $JWT_signature = base64_encode($binary_signature); $JWT = $JWT_header.'.'.$JWT_claim_set.'.'.$JWT_signature; // -- шаг 2. авторизируемся и получаем токен -- // $socket = @fsockopen('ssl://www.googleapis.com', 443, $errno, $errstr, 10); if (!$socket) die('error: remote host is unreachable.'); $payload = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion='.rawurlencode($JWT); $send = ''; $send .= 'POST /oauth2/v4/token HTTP/1.1'."\r\n"; $send .= 'Host: www.googleapis.com'."\r\n"; $send .= 'Connection: close'."\r\n"; $send .= 'Content-Type: application/x-www-form-urlencoded'."\r\n"; $send .= 'Content-Length: '.strlen($payload)."\r\n"; $send .= "\r\n"; $send .= $payload; $result = fwrite($socket, $send); $receive = ''; while (!feof($socket)) $receive .= fread($socket, 8192); fclose($socket); echo ''.$receive.''; // -- parse answer JSON (lame) -- // $line = explode("\r\n", $receive); if ($line[0] != 'HTTP/1.1 200 OK') die($line[0]); $pos = FALSE; if (($pos = strpos($receive, "\r\n\r\n", 0)) !== FALSE ) < if (($pos = strpos($receive, "", $pos+1)) !== FALSE ) < $post = substr($receive, $pos, ($pose - $pos+1) ); $aw = json_decode($post, TRUE); $access_token = $aw['access_token']; >else die('> not found.'); > else die(' < not found.'); >else die('\r\n\r\n not found.'); // -- шаг 3. отправляем запрос на Firebase сервер -- // $socket = @fsockopen('ssl://fcm.googleapis.com', 443, $errno, $errstr, 10); if (!$socket) die('error: remote host is unreachable.'); $payload = ' < "message":< "token" : "cGAFgPJGf-s:APA91bF**. **aEVM17c9peqZ", "notification" : < "title" : "Заголовок сообщения", "body" : "(Modern API) Моё первое сообщение через Firebase!" >> >'; // или $payload = ' < "message": < "token" : "cGAFgPJGf-s:APA91bF**. **aEVM17c9peqZ", "data":< "val1" : "Заголовок сообщения", "val2" : "(Modern API) Моё первое сообщение через Firebase!", "val3" : "дополнительные данные" >> >'; $send = ''; $send .= 'POST /v1/projects/pyur-test-id/messages:send HTTP/1.1'."\r\n"; $send .= 'Host: fcm.googleapis.com'."\r\n"; $send .= 'Connection: close'."\r\n"; $send .= 'Content-Type: application/json'."\r\n"; $send .= 'Authorization: Bearer '.$access_token."\r\n"; $send .= 'Content-Length: '.strlen($payload)."\r\n"; $send .= "\r\n"; $send .=$payload; $result = fwrite($socket, $send); $receive = ''; while (!feof($socket)) $receive .= fread($socket, 8192); fclose($socket); echo '
'.$receive.''; ?>
по адресу console.firebase.google.com/project/poject-id/settings/serviceaccounts/adminsdk надо скопировать «Сервисный аккаунт Firebase» и подставить в переменную «$JWT_claim_set», в поле «iss».
Жмём «Создание закрытого ключа»
Создаём ключ, сохраняем, никому не показываем. В скачанном файле будет содержаться «Закрытый ключ», его подставляем в переменную «$private_key».
Хинт: токен, полученный в шагах 1 и 2 можно и нужно кешировать в локальном временном хранилище, например файле, или базе данных. И только по истечении времени (по умолчанию один час), запрашивать у сервера авторизации следующий токен.
Важно! Перед использованием Modern Http API необходимо явно разрешить его использование здесь: console.developers.google.com/apis/library/fcm.googleapis.com/?project=your-project
Бонус, дополнительные параметры для уведомлений:
sound — либо «default», либо имя ресурса в приложении. Должен располагаться в «/res/raw/». Формат MP3, AAC или ещё чего подходящее.
icon — меняет иконку уведомления. Должна храниться в «drawable» приложения. Если отсутствует, FCM будет использовать иконку приложения (указанную как «launcher icon» в манифесте приложения).
tag — Следует использовать для группировки однотипных уведомлений. Новые уведомления будут выводиться поверх уже имеющихся с таким же тегом.
color — цвет иконки, задаётся как «#rrggbb» (у меня в MIUI не заработало)
click_action — запускаемое активити, при нажатии пользователем на уведомлении.Заключение
В будущем API вероятно будет изменяться, объявляться depricated и т.п. Поэтому сегодня думаю стоит делать сразу на протоколе HTTP v1.
Мне будет интересно почитать в комментариях оригинальные способы применения уведомлений, помимо новых сообщений из вконтактика. К примеру у меня настроен мониторинг вентиляторов ардуиной, и если они остановятся, отправляется уведомление.
Да, я в курсе, что существует Zabbix и т.п., но тема статьи — домашние сервера, и прочие умные дома. Считаю системы корпоративного класса перебором в любительских поделках.
- уведомления
- android
- домашний сервер
- Мессенджеры
- PHP
- Программирование
- Разработка под Android
- Google API



