Пишем сервер-помощник для BaaS или «Ну и зачем мне тогда Firebase?»

от автора

Предисловие

Я начинающий Android разработчик, за плечами у меня около 1,5 года опыта в данной сфере. Взялся я за довольно-таки большой проект, в команде кроме меня никого нет, а бекенд писать я не умею. Решено было в качестве платформы выбрать Firebase. Так как специфика моего приложения требовала постоянной работы и получения данных из базы в фоне, я просто вставил все EventListener-ы в сервис и был доволен. До того самого момента, когда я решил написать iOS версию. Выучив Swift я ринулся в бой. Firebase SDK благо оказались очень хороши и похожи для обеих систем, так что я быстро написал основную часть и… Почему не работает?

Суть проблемы и постановка задачи

iOS мягко говоря не уважает приложения работающие в фоне. Единственный способ пробудить приложение которое убила система (а убивает она их из-за любого чиха) — уведомления через APNS. К тому же, на Android 6+ постоянное соединение не держится и уведомления в итоге приходят с задержкой от 5 минут до 2 часов (на 7.1), если они реализованы не через GCM. Хорошо, что Firebase Cloud Messaging поддерживает и APNS, и GCM. Плохо, что для этого нужен дополнительный сервер. Было бы круто, если б уведомления автоматически отправлялись по определённым изменениям в базе данных. Инженеры обещают сделать нечто подобное в следующем году… А работать то должно уже сейчас.

Собственно, на то чтобы реализовать полноценный сервер с авторизацией и XMPP не у всех есть желание / знания / ресурсы. Итак, у нас есть две проблемы — авторизация пользователя, который хочет отправить push и собственно его отправка. Это в моём случае. Если вам нужно просто отслеживать появление новых данных в базе (например статей) и отправлять уведомление всем, кто подписан на эту тему — то всё ещё проще.

Подготовка

Изначально всё было написано на Python, но ситуация приключилась аналогичной из одной из недавних статей.

На Python возникли проблемы с повторным открытием устройства на чтение — во второй раз данные уже не читались. Мы не стали разбираться и просто переписали то же самое на Golang — после этого все заработало.

Итак, как это работает? Мы используем Firebase REST API чтобы следить за изменениями интересующих нас веток, и в случае добавления новых элементов отправляем пуш через FCM. Где оно работает? Да где угодно. И это одно из главных преимуществ. Вам не обязательно иметь статический IP и приличный хостинг (но это напрямую зависит от количества отправляемых пушей).

Но перед тем как перейти к делу, нужно понимать две вещи.

Во-первых, слежение за всей базой потребует предварительной её загрузки. А если «сервер-помошник» лежал (или перемещался на другой компьютер) — то он загрузит всё заново и заново отправит пуши. Для решения этой проблемы я создал в корне БД ветку notif — в неё пользователи (либо загрузчик контента) добавляют уведомления, которые нужно разослать пользователям, а сервер их удаляет после отправки. Использую я вот такую структуру:

"notif" {     "$key" {     // Автоматически сгенерированный методом push() ключ "from": "2vgajTP5Vd...",       // UID пользователя  "to": "all_users",   //  Либо название темы, либо UID "value": "Hello, Habr!", // Опциональное значение, например сообщение из чата "type": "message"// Тип сообщения, нужен устройству для корректного отображения     } } 

Во-вторых, нам нужно знать куда отправлять. Поэтому я создал ещё одну ветку «tokens» в которую устройства записывают токены регистрации в FCM. Тонкости реализации на клиентских устройствах это уже тема для отдельной статьи. Храню я их в этой ветке в формате:

"tokens" {     "userId": "fcmToken" } 

Также, чтобы сообщение нельзя было отправить от чужого имени или получать чужие, я дополнил Firebase Database Rules:

{     "rules": { /// Тут куча других правил       	"notif": { ".read": "false",   	"$key": {       	    ".write": "auth != null && newData.child('from').val() === auth.uid" } }, "tokens": {        ".read": "false",        "$key": {    ".write": "auth != null && $key == auth.uid"        }   }     } } 

Также нам понадобятся ключи и библиотеки:

  • Firebase Database Secret — для чтения данных с запретом на чтение, охохо (тут мог бы быть смайлик). Получить его можно в настройках Firebase Console.

    image

  • FCM API key — для отправки пушей. Получить можно там же, на следующей вкладке.
  • FireGo — для слежения за базой данных
  • FCM — не писать же самому?

Реализация (ну наконец-то!)

Для упрощения примера я убрал из него кэширование токенов, удаление устаревших и проверку покупок приложения через Android publisher API, но если что-то из этого вам интересно — пишите в комментарии, поделюсь полным кодом.

Итак, основная часть программы:

package main import (     "github.com/zabawaba99/firego"     "github.com/edganiukov/fcm"     "fmt"     "log" )  const (     //TODO вставьте сюда свои ключи     FDBSecret = "P3cUiIQytto**************NzQM5TrzERjEDO"     FCMAPIKey = "AIzaSyDXjRG**************8oOCMrPj18JVD8"     DAY_IN_SEC = 86400     // Названия веток в базе     TOKENS = "tokens"      NOTIFICATIONS = "notif" )  var (     FBDB = firego.New("https://kidgl.firebaseio.com", nil) // Объект для доступа к базе данных     FCM, _ = fcm.NewClient(FCMAPIKey) // Объект для отправки пушей )  func main() {     FBDB.Auth(FDBSecret)     FBDB.Child(NOTIFICATIONS).ChildAdded(gotPush)      // Процесс, не умирай, подумай     for { var res string fmt.Scanln(&res) if res == "exit" {     return } else {     println(`Type "exit" to stop service`) }     } } 

Функция ChildAdded принимает на вход функцию, которую она будет вызывать в случае изменений в базе. Исполняется это всё в отдельном потоке (а может и не в одном, откуда мне знать), так называемом Goroutine. Поэтому, чтобы программа не завершилась, я использую вечный цикл (а она всё равно завершиться от какого-нибудь исключения, перезапуск осуществляется bash-скриптом который на вход принимает stderr).

С этим всё ясно, теперь функция gotPush:

func gotPush(snapshot firego.DataSnapshot, previousChildKey string) {     // Мы получили этот пуш, в базе он больше не нужен     FBDB.Child(NOTIFICATIONS).Child(snapshot.Key).Remove()          // Разбираем его на запчасти     data := snapshot.Value.(map[string]string{})     from := data["from"]     to := data["to"]     typ := data["type"]          // Получаем сам токен, потому что мы знаем кому отправлять, но не знаем куда     var token string     FBDB.Child(TOKENS).Child(to).Value(&token)          msg := &fcm.Message{ Token: token, // Data - это всё, что будет доставлено на устройство пользователя Data: &fcm.Data{     "from": from,     "type": typ,     "value": data["value"], }, CollapseKey: typ + from + to, // Используется для замещения старых уведомлений новыми Priority: "high", ContentAvailable: true, TimeToLive: DAY_IN_SEC,  // Наличие этого параметра повышает вероятность доставки пуша     }      response, err := FCM.Send(msg)     if (err!=nil) { log.Println(err)     }     println("Отправлено: ", response.Success)     println("Ошибок: ", response.Failure)     if response.Results[0].Unregistered() { // TODO: Приложение удалено с устройства, его можно удалить из базы или оповестить других пользователей об удалении     } } 

Ну в общем-то и всё, можно запускать и радоваться жизни пушам. В моём случае ещё понадобилось скомпилировать для linux на макбуке, я думаю многим тоже пригодиться `env GOOS=linux GOARCH=amd64 go build backend_helper.go`

Спасибо за прочтение!
ссылка на оригинал статьи https://habrahabr.ru/post/314350/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *