Приложения Firebase работают, даже если ваше приложение временно теряет сетевое соединение. Кроме того, Firebase предоставляет инструменты для локального хранения данных, управления присутствием и обработки задержек.
Постоянство диска
Приложения Firebase автоматически обрабатывают временные сбои в сети. Кэшированные данные доступны в автономном режиме, и Firebase повторно отправляет любые записи при восстановлении сетевого подключения.
Когда вы включаете сохранение диска, ваше приложение записывает данные локально на устройство, чтобы ваше приложение могло сохранять состояние в автономном режиме, даже если пользователь или операционная система перезапускает приложение.
Вы можете включить сохранение диска с помощью всего лишь одной строки кода.
Быстрый
Database.database().isPersistenceEnabled = true
Цель-C
[FIRDatabase database].persistenceEnabled = YES;
Поведение настойчивости
Включив сохранение, любые данные, которые клиент Firebase Realtime Database будет синхронизировать в режиме онлайн, сохраняются на диске и доступны в автономном режиме, даже когда пользователь или операционная система перезапускает приложение. Это означает, что ваше приложение работает так же, как и в Интернете, используя локальные данные, хранящиеся в кеше. Обратные вызовы прослушивателя будут продолжать срабатывать для локальных обновлений.
Клиент Firebase Realtime Database автоматически сохраняет очередь всех операций записи, которые выполняются, пока ваше приложение находится в автономном режиме. Если сохранение включено, эта очередь также сохраняется на диске, поэтому все ваши записи доступны, когда пользователь или операционная система перезапускает приложение. Когда приложение восстанавливает подключение, все операции отправляются на сервер Firebase Realtime Database .
Если ваше приложение использует аутентификацию Firebase , клиент Firebase Realtime Database сохраняет токен аутентификации пользователя при перезапуске приложения. Если срок действия токена аутентификации истекает, пока ваше приложение находится в автономном режиме, клиент приостанавливает операции записи до тех пор, пока ваше приложение не выполнит повторную аутентификацию пользователя, в противном случае операции записи могут завершиться неудачей из-за правил безопасности.
Сохранение свежести данных
Firebase Realtime Database синхронизирует и хранит локальную копию данных для активных прослушивателей. Кроме того, вы можете синхронизировать определенные местоположения.
Быстрый
let scoresRef = Database.database().reference(withPath: "scores") scoresRef.keepSynced(true)
Цель-C
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"]; [scoresRef keepSynced:YES];
Клиент Firebase Realtime Database автоматически загружает данные в эти места и синхронизирует их, даже если у ссылки нет активных прослушивателей. Вы можете отключить синхронизацию с помощью следующей строки кода.
Быстрый
scoresRef.keepSynced(false)
Цель-C
[scoresRef keepSynced:NO];
По умолчанию кэшируется 10 МБ ранее синхронизированных данных. Этого должно быть достаточно для большинства приложений. Если кеш превышает настроенный размер, Firebase Realtime Database удаляет данные, которые использовались последними. Данные, которые синхронизируются, не удаляются из кэша.
Запрос данных в автономном режиме
Firebase Realtime Database хранит данные, полученные из запроса, для использования в автономном режиме. Для запросов, созданных в автономном режиме, Firebase Realtime Database продолжает работать с ранее загруженными данными. Если запрошенные данные не загружены, Firebase Realtime Database загружает данные из локального кеша. Когда сетевое подключение снова станет доступным, данные загрузятся и отразят запрос.
Например, этот код запрашивает последние четыре элемента в Firebase Realtime Database .
Быстрый
let scoresRef = Database.database().reference(withPath: "scores") scoresRef.queryOrderedByValue().queryLimited(toLast: 4).observe(.childAdded) { snapshot in print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")") }
Цель-C
FIRDatabaseReference *scoresRef = [[FIRDatabase database] referenceWithPath:@"scores"]; [[[scoresRef queryOrderedByValue] queryLimitedToLast:4] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value); }];
Предположим, что пользователь теряет соединение, переходит в автономный режим и перезапускает приложение. Находясь в автономном режиме, приложение запрашивает два последних элемента из одного и того же места. Этот запрос успешно вернет два последних элемента, поскольку приложение загрузило все четыре элемента в запросе выше.
Быстрый
scoresRef.queryOrderedByValue().queryLimited(toLast: 2).observe(.childAdded) { snapshot in print("The \(snapshot.key) dinosaur's score is \(snapshot.value ?? "null")") }
Цель-C
[[[scoresRef queryOrderedByValue] queryLimitedToLast:2] observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) { NSLog(@"The %@ dinosaur's score is %@", snapshot.key, snapshot.value); }];
В предыдущем примере клиент Firebase Realtime Database генерирует события «добавленные дочерние элементы» для двух динозавров с наивысшим рейтингом, используя постоянный кеш. Но это не вызовет событие «значение», поскольку приложение никогда не выполняло этот запрос в режиме онлайн.
Если бы приложение запросило последние шесть элементов в автономном режиме, оно сразу же получило бы события «дочерние добавленные» для четырех кэшированных элементов. Когда устройство снова подключается к сети, клиент Firebase Realtime Database синхронизируется с сервером и получает последние два события «добавлен дочерний элемент» и «значение» для приложения.
Обработка транзакций в автономном режиме
Любые транзакции, выполняемые, когда приложение находится в автономном режиме, ставятся в очередь. Как только приложение восстанавливает подключение к сети, транзакции отправляются на сервер Realtime Database .
Управление присутствием
В приложениях реального времени часто бывает полезно определить, когда клиенты подключаются и отключаются. Например, вы можете пометить пользователя как «не в сети», когда его клиент отключается.
Клиенты базы данных Firebase предоставляют простые примитивы, которые вы можете использовать для записи в базу данных, когда клиент отключается от серверов базы данных Firebase. Эти обновления происходят независимо от того, отключается ли клиент корректно или нет, поэтому вы можете положиться на них в очистке данных даже в случае разрыва соединения или сбоя клиента. Все операции записи, включая установку, обновление и удаление, могут быть выполнены после отключения.
Вот простой пример записи данных при отключении с помощью примитива onDisconnect
:
Быстрый
let presenceRef = Database.database().reference(withPath: "disconnectmessage"); // Write a string when this client loses connection presenceRef.onDisconnectSetValue("I disconnected!")
Цель-C
FIRDatabaseReference *presenceRef = [[FIRDatabase database] referenceWithPath:@"disconnectmessage"]; // Write a string when this client loses connection [presenceRef onDisconnectSetValue:@"I disconnected!"];
Как работает onDisconnect
Когда вы устанавливаете операцию onDisconnect()
, она находится на сервере Firebase Realtime Database . Сервер проверяет безопасность, чтобы убедиться, что пользователь может выполнить запрошенное событие записи, и сообщает вашему приложению, если оно недействительно. Затем сервер контролирует соединение. Если в какой-то момент время соединения истекает или оно активно закрывается клиентом Realtime Database , сервер проверяет безопасность второй раз (чтобы убедиться, что операция все еще действительна), а затем вызывает событие.
Ваше приложение может использовать обратный вызов при операции записи, чтобы убедиться, что onDisconnect
был правильно подключен:
Быстрый
presenceRef.onDisconnectRemoveValue { error, reference in if let error = error { print("Could not establish onDisconnect event: \(error)") } }
Цель-C
[presenceRef onDisconnectRemoveValueWithCompletionBlock:^(NSError *error, FIRDatabaseReference *reference) { if (error != nil) { NSLog(@"Could not establish onDisconnect event: %@", error); } }];
Событие onDisconnect
также можно отменить, вызвав .cancel()
:
Быстрый
presenceRef.onDisconnectSetValue("I disconnected") // some time later when we change our minds presenceRef.cancelDisconnectOperations()
Цель-C
[presenceRef onDisconnectSetValue:@"I disconnected"]; // some time later when we change our minds [presenceRef cancelDisconnectOperations];
Определение состояния соединения
Для многих функций, связанных с присутствием, вашему приложению полезно знать, когда оно онлайн или офлайн. Firebase Realtime Database предоставляет специальное местоположение в /.info/connected
, которое обновляется каждый раз, когда изменяется состояние подключения клиента Firebase Realtime Database . Вот пример:
Быстрый
let connectedRef = Database.database().reference(withPath: ".info/connected") connectedRef.observe(.value, with: { snapshot in if snapshot.value as? Bool ?? false { print("Connected") } else { print("Not connected") } })
Цель-C
FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"]; [connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { if([snapshot.value boolValue]) { NSLog(@"connected"); } else { NSLog(@"not connected"); } }];
/.info/connected
— это логическое значение, которое не синхронизируется между клиентами Realtime Database , поскольку это значение зависит от состояния клиента. Другими словами, если один клиент читает /.info/connected
как ложь, это не гарантирует, что другой клиент также прочтет ложь.
Обработка задержки
Временные метки сервера
Серверы Firebase Realtime Database предоставляют механизм для вставки временных меток, сгенерированных на сервере, в качестве данных. Эта функция в сочетании с onDisconnect
обеспечивает простой способ надежного отслеживания времени отключения клиента Realtime Database :
Быстрый
let userLastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline") userLastOnlineRef.onDisconnectSetValue(ServerValue.timestamp())
Цель-C
FIRDatabaseReference *userLastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"]; [userLastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]];
Перекос часов
Хотя firebase.database.ServerValue.TIMESTAMP
гораздо более точен и предпочтителен для большинства операций чтения/записи, иногда он может быть полезен для оценки отклонения часов клиента по отношению к серверам Firebase Realtime Database . Вы можете прикрепить обратный вызов к местоположению /.info/serverTimeOffset
, чтобы получить значение в миллисекундах, которое клиенты Firebase Realtime Database добавляют к локальному отчетному времени (время эпохи в миллисекундах) для оценки времени сервера. Обратите внимание, что на точность этого смещения может влиять задержка в сети, поэтому оно полезно в первую очередь для обнаружения больших (> 1 секунды) расхождений во времени.
Быстрый
let offsetRef = Database.database().reference(withPath: ".info/serverTimeOffset") offsetRef.observe(.value, with: { snapshot in if let offset = snapshot.value as? TimeInterval { print("Estimated server time in milliseconds: \(Date().timeIntervalSince1970 * 1000 + offset)") } })
Цель-C
FIRDatabaseReference *offsetRef = [[FIRDatabase database] referenceWithPath:@".info/serverTimeOffset"]; [offsetRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { NSTimeInterval offset = [(NSNumber *)snapshot.value doubleValue]; NSTimeInterval estimatedServerTimeMs = [[NSDate date] timeIntervalSince1970] * 1000.0 + offset; NSLog(@"Estimated server time: %0.3f", estimatedServerTimeMs); }];
Пример приложения присутствия
Комбинируя операции отключения с мониторингом состояния соединения и временными метками сервера, вы можете создать систему присутствия пользователей. В этой системе каждый пользователь хранит данные в определенном месте базы данных, чтобы указать, находится ли клиент Realtime Database в сети. Клиенты устанавливают для этого местоположения значение true, когда они подключаются к сети, и метку времени, когда они отключаются. Эта временная метка указывает, когда данный пользователь в последний раз был онлайн.
Обратите внимание, что ваше приложение должно поставить в очередь операции отключения до того, как пользователь будет помечен как онлайн, чтобы избежать каких-либо условий гонки в случае, если сетевое соединение клиента будет потеряно до того, как обе команды смогут быть отправлены на сервер.
Вот простая система присутствия пользователя:
Быстрый
// since I can connect from multiple devices, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline let myConnectionsRef = Database.database().reference(withPath: "users/morgan/connections") // stores the timestamp of my last disconnect (the last time I was seen online) let lastOnlineRef = Database.database().reference(withPath: "users/morgan/lastOnline") let connectedRef = Database.database().reference(withPath: ".info/connected") connectedRef.observe(.value, with: { snapshot in // only handle connection established (or I've reconnected after a loss of connection) guard snapshot.value as? Bool ?? false else { return } // add this device to my connections list let con = myConnectionsRef.childByAutoId() // when this device disconnects, remove it. con.onDisconnectRemoveValue() // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition // where you set the user's presence to true and the client disconnects before the // onDisconnect() operation takes effect, leaving a ghost user. // this value could contain info about the device or a timestamp instead of just true con.setValue(true) // when I disconnect, update the last time I was seen online lastOnlineRef.onDisconnectSetValue(ServerValue.timestamp()) })
Цель-C
// since I can connect from multiple devices, we store each connection instance separately // any time that connectionsRef's value is null (i.e. has no children) I am offline FIRDatabaseReference *myConnectionsRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/connections"]; // stores the timestamp of my last disconnect (the last time I was seen online) FIRDatabaseReference *lastOnlineRef = [[FIRDatabase database] referenceWithPath:@"users/morgan/lastOnline"]; FIRDatabaseReference *connectedRef = [[FIRDatabase database] referenceWithPath:@".info/connected"]; [connectedRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) { if([snapshot.value boolValue]) { // connection established (or I've reconnected after a loss of connection) // add this device to my connections list FIRDatabaseReference *con = [myConnectionsRef childByAutoId]; // when this device disconnects, remove it [con onDisconnectRemoveValue]; // The onDisconnect() call is before the call to set() itself. This is to avoid a race condition // where you set the user's presence to true and the client disconnects before the // onDisconnect() operation takes effect, leaving a ghost user. // this value could contain info about the device or a timestamp instead of just true [con setValue:@YES]; // when I disconnect, update the last time I was seen online [lastOnlineRef onDisconnectSetValue:[FIRServerValue timestamp]]; } }];