Услуги Портфолио Контакты БлогНовости +7 (800) 511‑32‑27 Обсудить проект
Статьи

Создание кастомных чатов с Firebase Realtime database

24 октября 2022·3 мин чтения
Логотип YuSMP Group
Автор материалаРедакция компании YuSMP GroupБлог YuSMP Group
Профиль редакции
Создание кастомных чатов с Firebase Realtime database
TL;DR — кратко о главном
  • Firebase Realtime Database закрывает транспорт, сервер и хранилище чата одним сервисом — фронтенд читает и пишет напрямую, доступ регулируют Security Rules.
  • Права — через Security Rules и JWT-токен с userId; свой бэкенд нужен только для кастомной авторизации, пушей и полнотекстового поиска.
  • Лимиты: 100 подключений на бесплатном Spark, 200 000 — на Blaze; строка до 10 МБ, вложенность до 32 уровней.
  • Для простой ленты сообщений обычно выбирают RTDB, для сложных запросов и фильтров — Firestore, для полного контроля над протоколом — Node.js + Socket.io.

Ниже — рабочая схема данных, security rules и код авторизации, отправки и чтения сообщений, которые мы используем в разработке мобильных приложений и веб-сервисов с чатом на борту.

Во многих мобильных и веб-приложениях требуется создать чаты. Мы говорим не о надоевших всплывашках для общения с оператором: обычно они предоставляются As a service и подключаются одной строчкой кода.

Я говорю о полноценных чатах, в том числе групповых, где пользователи могут общаться между собой, отправлять медиа, использовать видеозвонки. Подчеркну, что речь идёт о приложениях, где чат является лишь малой частью ПО, а не его основным функционалом — не требуется offline режим, E2E шифрование, и многие другие вещи (как в мессенджерах Telegram, WhatsApp и т. д.).

Технически любой чат состоит из следующих вещей: UI, транспортный уровень, сервер, хранилище. В качестве транспорта для чатов лучше всего подходят веб-сокеты. Хранилищем может выступать любая база данных, сервер должен обеспечивать права доступа и API для клиентов.

Стандартных решений для чатов множество, одним из наиболее популярных и проверенных временем можно назвать Jabber (XMPP), имеющий множество реализаций. Также популярны кастомные решения на основе Node.js и socket.io, а также Firebase. В этой статье не будет сравнений разных подходов, а пойдет речь о реализации чатов с помощью Firebase realtime database.

С чего начать: 5 шагов настройки Firebase для чата

  1. Создайте проект в Firebase Console и включите в разделе Build именно Realtime Database — не Firestore, у них разные движки и API.
  2. Сразу закройте базу правилами по умолчанию (.read: false, .write: false) и открывайте доступ точечно — иначе любой клиент с конфигом проекта сможет прочитать всю базу.
  3. Подключите клиентский SDK (Web, iOS, Android) и инициализируйте подключение конфигом проекта из Firebase Console.
  4. Настройте выдачу токенов: либо встроенный Firebase Authentication, либо кастомные JWT-токены со своего бэкенда через Admin SDK (пример — ниже).
  5. Спроектируйте схему коллекций заранее. RTDB — это NoSQL-дерево без JOIN'ов и миграций: денормализация неизбежна, а поменять структуру после запуска в проде дорого.

Делаем чаты с Firebase realtime database

Firebase realtime database (RDB) — nosql база данных, размещённая на серверах Google  и доступная по сети по протоколу Websocket, есть SDK для практически всех платформ и языков. Стоит отметить, что Firebase недоступен в Китае и ряде других стран, где сервера Google заблокированы фаерволами.

RDB покрывает 3 из 4 компонентов любого чата: транспорт, сервер, хранилище. Фактически фронтенд напрямую соединяется с базой данных и может как читать так и записывать туда документы (сообщения, пользователей, комнаты чатов). Доступ пользователя до документов регулируется Security rules. При соединении с базой данных клиент авторизуется по JWT токену, в котором зашит userId или другая информация о нём, и с помощью выражений security rules, можно описать, имеет ли он доступ до того или иного документа или коллекции. Также security rules используются для индексации документов.

Вот пример security rules из опенсорсного проекта Firechat:

{
"rules": {
    ".read": false,
    ".write": false,
    "messages": {
      "$roomId": {
        ".indexOn": ["ts"],
        "$messageId": {
          ".indexOn": "ts",
          ".read": "(auth != null) && (root.child('rooms').child($roomId).child('authorizedUsers').hasChild(auth.uid))",
          ".write": "(auth != null) && (root.child('rooms').child($roomId).child('authorizedUsers').hasChild(auth.uid))"
        }
      }
    }
}
}

Смысл этих выражений в том, что пользователь может получить доступ до объекта внутри коллекции messages только в том случае, если uid из его JWT токена есть в списке authorizedUsers соответствующей комнаты чата

Сама схема базы данных в Firebase состоит из следующих коллекций, и, конечно, зависит от бизнес-требований к чату. Примерный вид схемы:

users: {
  $userId: {
    rooms: [],       // id комнат, в которых участвует пользователь
    userInfo: {}     // имя, аватар и т.д. — зависит от задачи
  }
},
rooms: {
  $roomId: {
    id,              // совпадает с roomId, сгенерирован Firebase
    type,            // персональная или групповая комната
    ts,              // время создания
    authorizedUsers: [],  // uid пользователей с доступом
    moderators: []        // uid модераторов чата
  }
},
messages: {
  $roomId: {         // все сообщения комнаты с ID = roomId
    $messageId: {
       id,
       type,         // текст, фото, видео
       ts,
       senderId,
       readBy: [],      // uid прочитавших (в групповом чате их много)
       deliveredTo: []  // uid получивших сообщение
    }
  }
}

Пример авторизации в RDB с помощью Javascript SDK:

firebase.auth().signInWithCustomToken(this.jwt).catch(function (error) {
   alert('Error: ' + error.message);
});

JWT токен можно сгенерировать на бэкенде вашего приложения и отдавать пользователю. Например, в методе login, вместе с токеном вашего сервера.

Пример генерации JWT-токена с помощью SDK PHP:

use Firebase\JWT\JWT;

$now = time();

$payload = [
   'email' => $user->getEmail(),
   'iss' => $this->clientEmail,
   'sub' => $this->clientEmail,
   'aud' => 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit',
   'iat' => $now,
   'exp' => ($now + (60 * 60)), // максимум — один час
   'uid' => (string) $user->getId(),
];

return JWT::encode($payload, $this->privateKey, 'RS256');

clientEmail и privateKey берутся из сервисного файла Firebase.

Услуги мобильной разработки
Узнайте цену

Примеры интеграции с Firebase на фронтенде

Загрузка списка id комнат пользователя

Для загрузки сообщений из комнаты чатов:

async getUserRoomIds(userId)
{
   const snapshot = await firebase.database()
       .ref().child('users')
       .child(userId.toString())
       .once('value');
   const user = snapshot.val();
   if (user == null) {
       return [];
   }
   return Object.keys(user.rooms);
}

Чтобы сообщения в комнате появлялись в реальном времени без перезапроса, на клиенте подписываются на коллекцию через on('child_added', ...) — колбэк срабатывает и на уже существующие сообщения при первой подписке, и на каждое новое:

function subscribeToMessages(roomId, onMessage) {
   const ref = firebase.database()
       .ref('messages')
       .child(roomId)
       .orderByChild('ts');

   ref.on('child_added', (snapshot) => {
       onMessage(snapshot.val());
   });

   // отписка при размонтировании компонента / выходе из комнаты
   return () => ref.off('child_added');
}

Для записи сообщения в чат мы просто записываем сообщение в таблицу messages новый объект:

async postMessage(roomId, messageStr, user)
{
   const ref = firebase.database()
       .ref()
       .child('messages')
       .child(roomId)
       .push();
   const message = new Message(user, firebase.database.ServerValue.TIMESTAMP, messageStr);
   message.id = ref.key;
   try {
       await ref.set(message.getJson());
   }
   catch (e) {
       alert(e.message);
       throw e;
   }
}

Частно нужно взаимодействовать с чатами на сервере (например, отправлять пуш нотификации при отправке нового сообщения). Используя Admin SDK, можно подписаться на любые новые сообщения в базе, минуя security rules. Вот пример подписки на новые сообщения в комнате чата:

async subscribeToRoom(id: string): Promise<void>
{
   const room = await this.getRoom(id);
   const query = this.messages.child(id);
   query.on('child_added', this.onChildAdded(id, room), this.onCancel, this);
}

onChildAdded(roomId: string, room: Room): (snapshot: DataSnapshot) => void
{
   return async (snapshot: DataSnapshot): Promise<void> => {
       const message = snapshot.val();
       console.debug('(onAdded) message ' + message.id + ' in room ' + roomId);
   }
}

onCancel(): void
{
   console.debug('Listener was canceled.');
}

Лимиты Firebase RTDB и когда он не подходит

Главный минус Firebase RTDB — очень ограниченный набор функций для поиска данных. Полноценный поиск по сообщениям сделать средствами самой базы невозможно: запросы работают только по одному индексированному полю и диапазону значений, без полнотекстового индекса. Для поиска нужна двойная запись сообщений — в Elasticsearch, Algolia или Meilisearch — либо на своём сервере по событию child_added, либо через Firebase Cloud Functions.

Помимо поиска, у RTDB есть документированные количественные лимиты — их стоит учитывать на этапе проектирования, а не узнавать в проде:

ПараметрSpark (бесплатный)Blaze (pay as you go)
Одновременные подключения к одной базе100200 000
Запись в секунду на базуориентир ~1000 операций/сек — не жёсткий лимит, но порог деградации
Максимальный размер строкового значения10 МБ
Глубина вложенности путидо 32 уровней

RTDB может не подойти, если:

  • нужен полнотекстовый поиск или сложные составные запросы — лучше Firestore или отдельный поисковый индекс;
  • проект уже использует Firestore для остальных данных — держать две NoSQL-базы ради одного чата избыточно;
  • нужен полноценный мессенджер с E2E-шифрованием и офлайн-очередью сообщений, как в Telegram или WhatsApp — это отдельный уровень инфраструктуры поверх любой базы;
  • Firebase недоступен из региона работы аудитории — серверы Google блокируются фаерволами в Китае и ряде других стран.

Firebase RTDB vs альтернативы: Socket.io и Firestore

В начале статьи мы специально не сравнивали подходы к чатам — вот это сравнение. Firebase RTDB — не единственный вариант, и для части проектов он не оптимален.

КритерийFirebase RTDBNode.js + Socket.ioFirestore
Время на запуск MVPчасы: готовый транспорт, сервер и хранилищедни-недели: пишете сервер и протокол самичасы, но с другой моделью данных
Контроль над протоколомограничен SDK и Security Rulesполный — свой код, свои middlewareограничен SDK, как у RTDB
Сложные запросы и фильтрыслабо — только по одному индексулюбые, на своей БДсильные стороны — составные индексы
Инфраструктура и масштабированиена стороне Google, лимиты по тарифусвоя — нужен DevOps и мониторингна стороне Google, другие лимиты
Когда выбратьпростая лента сообщений, статусы онлайн, MVPкастомные протоколы, игры, высокая нагрузка на свою логикучат с тредами, сложной фильтрацией, поиском по метаданным

На практике многие проекты стартуют на Firebase RTDB ради скорости запуска и переезжают на Node.js + Socket.io или Firestore, когда упираются в лимиты поиска и запросов, описанные выше.

Частые вопросы о чате на Firebase Realtime Database

Сколько стоит чат на Firebase Realtime Database?

На старте — бесплатно: тарифный план Spark покрывает до 100 одновременных подключений и 1 ГБ хранилища, этого хватает для MVP и тестов. При росте проекта переходят на тариф Blaze с оплатой по факту использования (хранение, трафик, число подключений) — для чата на несколько тысяч пользователей это обычно десятки долларов в месяц.

Сколько пользователей может одновременно работать в чате на Firebase RTDB?

На тарифе Blaze — до 200 000 одновременных подключений к одной базе данных, на бесплатном Spark — до 100. Для чатов с более высокой нагрузкой данные шардируют по нескольким базам Firebase в одном проекте.

Чем Firebase Realtime Database отличается от Firestore для чата?

RTDB хранит данные одним JSON-деревом и быстрее синхронизирует простые структуры вроде ленты сообщений, но плохо масштабируется на сложные запросы. Firestore — документо-ориентированная база с составными индексами и более гибкими запросами, но у неё выше задержка при очень частых обновлениях одного документа. Для простого чата (лента сообщений, статусы онлайн) чаще берут RTDB; для чата со сложной фильтрацией, тредами и поиском — Firestore.

Нужен ли отдельный backend-сервер при использовании Firebase RTDB?

Для базового чата — нет: клиент читает и пишет в базу напрямую, а права доступа описываются в Security Rules. Backend нужен там, где требуется бизнес-логика вне возможностей правил: генерация кастомных JWT-токенов, модерация контента, отправка push-уведомлений через Admin SDK или двойная запись сообщений в поисковый индекс.

Как реализовать push-уведомления о новых сообщениях в Firebase-чате?

Серверная часть на Admin SDK подписывается на событие child_added в коллекции messages (в обход Security Rules) и по каждому новому сообщению отправляет уведомление через Firebase Cloud Messaging получателям, которые сейчас не в сети. Эту логику также можно вынести в Cloud Function, триггерящуюся на запись в RTDB.

Подходит ли Firebase RTDB для чата с end-to-end шифрованием?

Технически да — RTDB просто хранит зашифрованные строки, а ключи шифрования обрабатываются на клиенте. Но Firebase не берёт на себя E2E-шифрование, офлайн-очередь недоставленных сообщений и прочие вещи уровня полноценных мессенджеров (Telegram, WhatsApp) — такой функционал проектируется отдельно поверх RTDB.

Как добавить полнотекстовый поиск по сообщениям в Firebase-чате?

Встроенного полнотекстового поиска в RTDB нет — запросы ограничены сортировкой и диапазонами по одному индексированному полю. Стандартное решение — двойная запись: при сохранении сообщения в RTDB копия отправляется в поисковый движок (Algolia, Elasticsearch, Meilisearch) через событие child_added на сервере или через Firebase Cloud Function.

Вывод

Firebase Realtime Database имеет очень хорошую документацию и позволяет сделать кастомный чат почти с любым функционалом за часы, а не недели — в этом её главный плюс наравне с отсутствием забот об инфраструктуре. Платите вы за это ограниченным поиском и жёсткими лимитами тарифа: для простой ленты сообщений и статусов онлайн RTDB — быстрый и дешёвый старт, а для чата со сложной фильтрацией, поиском и тредами стоит сразу смотреть в сторону Firestore или собственного сервера на Node.js с Socket.io.

Обсудим ваш проект?

Соберём команду под вашу задачу и оценим проект за 2 дня — бесплатно.

Обсудить проект