Пишемо сервер-помічник для 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() ключ
"з": "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() === аутентифікації.uid"
}
},
"tokens": {
".read": "false",
"$key": {
".write": "auth != null && $key == аутентифікації.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["з"]
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,
"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`

Спасибі за прочитання!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.