如何在Firebase中实现分布式倒数计时器 [英] How to implement a distributed countdown timer in Firebase

查看:81
本文介绍了如何在Firebase中实现分布式倒数计时器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个应用程序,其中一个用户托管游戏,然后其他用户可以对托管人的问题进行投票.从主持人发布问题的那一刻起,玩家就有20秒的投票时间.

如何在所有播放器的屏幕上显示倒数计时器并使它们与主机保持同步?

解决方案

许多开发人员都陷在这个问题上,因为他们试图在所有用户之间同步倒数计时本身.这很难保持同步,并且容易出错.但是,有一种更简单的方法,我在许多项目中都使用过这种方法.

每个客户需要显示其倒数计时器的所有三件事都是相当静态的信息:

  1. 问题发布的时间,即计时器开始的时间.
  2. 从那一刻起,他们需要数的时间.
  3. 客户端相对于中央计时器的相对偏移量.

我们将使用数据库的服务器时间作为第一个值,第二个值仅来自主机代码,而

当我们执行上面的代码时,它将当前时间写入数据库,并且这是20秒的倒计时.由于我们使用 ServerValue.TIMESTAMP 写入时间,因此数据库将在服务器上写入时间,因此它不会受到主机的本地时间(或偏移量)的影响./p>


现在,让我们看看另一个用户如何读取该数据.与Firebase一样,我们将使用 on()侦听器,这意味着我们的代码将在数据写入时主动侦听:

  ref.on("value",(snapshot)=> {...}); 

执行此 ref.on(... "代码时,它将立即从数据库中读取当前值并运行回调,但随后它还将继续监听数据库的更改并运行再次写入时再次输入代码.

因此,我们假设我们正在调用一个新的数据快照以开始刚刚开始的倒计时.我们如何在所有屏幕上显示准确的倒数计时器?

我们首先使用以下方法从数据库中获取值:

  ref.on("value",(snapshot)=> {const seconds = snapshot.val().seconds;const startAt = snapshot.val().startAt;...}); 

我们还需要估计本地客户端之间的时间以及服务器上的时间.Firebase SDK首次连接服务器时会估算该时间,我们可以从客户端中的 .info/serverTimeOffset 读取它:

  const serverTimeOffset = 0;database.ref(.info/serverTimeOffset").on("value",(snapshot)=> {serverTimeOffset = snapshot.val()));ref.on("value",(snapshot)=> {const seconds = snapshot.val().seconds;const startAt = snapshot.val().startAt;}); 

在运行良好的系统中, serverTimeOffset 是一个正值,表示我们对服务器的延迟(以毫秒为单位).但是,如果我们的本地时钟有偏移,它也可能是负值.无论哪种方式,我们都可以使用此值来显示更准确的倒数计时器.

接下来,我们将启动一个间隔计时器,该计时器每100毫秒左右接收一次呼叫:

  const serverTimeOffset = 0;database.ref(.info/serverTimeOffset").on("value",(snapshot)=> {serverTimeOffset = snapshot.val()));ref.on("value",(snapshot)=> {const seconds = snapshot.val().seconds;const startAt = snapshot.val().startAt;const interval = setInterval(()=> {...},100)}); 

然后间隔的每个计时器到期,我们将计算剩余时间:

  const serverTimeOffset = 0;database.ref(.info/serverTimeOffset").on("value",(snapshot)=> {serverTimeOffset = snapshot.val()));ref.on("value",(snapshot)=> {const seconds = snapshot.val().seconds;const startAt = snapshot.val().startAt;const interval = setInterval(()=> {const timeLeft =(seconds * 1000)-(Date.now()-startAt-serverTimeOffset);...},100)}); 

最后,我们以合理的格式记录剩余时间,如果计时器已过期,则将其停止:

  const serverTimeOffset = 0;database.ref(.info/serverTimeOffset").on("value",(snapshot)=> {serverTimeOffset = snapshot.val()));ref.on("value",(snapshot)=> {const seconds = snapshot.val().seconds;const startAt = snapshot.val().startAt;const interval = setInterval(()=> {const timeLeft =(seconds * 1000)-(Date.now()-startAt-serverTimeOffset);如果(timeLeft< 0){clearInterval(interval);console.log(剩余的0.0");}别的 {console.log(`$ {Math.floor(timeLeft/1000)}.$ {timeLeft%1000}`);}},100)}); 

上面的代码中肯定还有一些清理工作,例如,当一个新的倒计时开始时(仍在进行中),但是整体方法效果很好,并且可以轻松扩展到成千上万的用户.

I have an app where one user hosts a game, and then other users can vote on questions from the host. From the moment the host posts the question, the players have 20 seconds to vote.

How can I show a countdown timer on all player's screens and keep them synchronized with the host?

解决方案

Many developers get stuck on this problem because they try to synchronize the countdown itself across all users. This is hard to keep in sync, and error prone. There is a much simpler approach however, and I've used this in many projects.

All each client needs to show its countdown timer are three things of fairly static information:

  1. The time that the question was posted, which is when the timer starts.
  2. The amount of time they need to count from that moment.
  3. The relative offset of the client to the central timer.

We're going to use the server time of the database for the first value, the second value is just coming from the hosts code, and the relative offset is a value that Firebase provides for us.

The code samples below are written in JavaScript for the web, but the same approach (and quite similar code) and be applied in iOS, Android and most other Firebase SDKs that implement realtime listeners.


Let's first write the starting time, and interval to the database. Ignoring security rules and validation, this can be as simple as:

const database = firebase.database();
const ref = database.ref("countdown");
ref.set({
  startAt: ServerValue.TIMESTAMP,
  seconds: 20
});

When we execute the above code, it writes the current time to the database, and that this is a 20 second countdown. Since we're writing the time with ServerValue.TIMESTAMP, the database will write the time on the server, so there's no chance if it being affected by the local time (or offset) of the host.


Now let's see how the other user's read this data. As usual with Firebase, we'll use an on() listener, which means our code is actively listening for when the data gets written:

ref.on("value", (snapshot) => {
  ...
});

When this ref.on(... code executes, it immediately reads the current value from the database and runs the callback. But it then also keeps listening for changes to the database, and runs the code again when another write happens.

So let's assume we're getting called with a new data snapshot for a countdown that has just started. How can we show an accurate countdown timer on all screens?

We'll first get the values from the database with:

ref.on("value", (snapshot) => {
  const seconds = snapshot.val().seconds;
  const startAt = snapshot.val().startAt;
  ...
});

We also need to estimate how much time there is between our local client, and the time on the server. The Firebase SDK estimates this time when it first connects to the server, and we can read it from .info/serverTimeOffset in the client:

const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
  const seconds = snapshot.val().seconds;
  const startAt = snapshot.val().startAt;
  
});

In a well running system, the serverTimeOffset is a positive value indicating our latency to the server (in milliseconds). But it may also be a negative value, if our local clock has an offset. Either way, we can use this value to show a more accurate countdown timer.

Next up we'll start an interval timer, which gets calls every 100ms or so:

const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
  const seconds = snapshot.val().seconds;
  const startAt = snapshot.val().startAt;
  const interval = setInterval(() => {
    ...
  }, 100)
});

Then every timer our interval expires, we're going to calculate the time that is left:

const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
  const seconds = snapshot.val().seconds;
  const startAt = snapshot.val().startAt;
  const interval = setInterval(() => {
    const timeLeft = (seconds * 1000) - (Date.now() - startAt - serverTimeOffset);
    ...
  }, 100)
});

And then finally we log the remaining time, in a reasonable format and stop the timer if it has expired:

const serverTimeOffset = 0;
database.ref(".info/serverTimeOffset").on("value", (snapshot) => { serverTimeOffset = snapshot.val() });
ref.on("value", (snapshot) => {
  const seconds = snapshot.val().seconds;
  const startAt = snapshot.val().startAt;
  const interval = setInterval(() => {
    const timeLeft = (seconds * 1000) - (Date.now() - startAt - serverTimeOffset);
    if (timeLeft < 0) {
      clearInterval(interval);
      console.log("0.0 left)";
    }
    else {
      console.log(`${Math.floor(timeLeft/1000)}.${timeLeft % 1000}`);
    }
  }, 100)
});

There's definitely some cleanup left to do in the above code, for example when a new countdown starts while one is still in progress, but the overall approach works well and scales easily to thousands of users.

这篇关于如何在Firebase中实现分布式倒数计时器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆