如何在 Firebase 中实现分布式倒数计时器

Fra*_*len 5 javascript firebase-realtime-database

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

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

Fra*_*len 8

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

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

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

我们将使用数据库的服务器时间作为第一个值,第二个值来自主机代码,相对偏移量是 Firebase 为我们提供的值。

下面的代码示例是用 JavaScript 为网络编写的,但使用相同的方法(和非常相似的代码)并应用于 iOS、Android 和大多数其他实现实时侦听器的 Firebase SDK。


让我们首先将开始时间和间隔写入数据库。忽略安全规则和验证,这可以很简单:

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

当我们执行上面的代码时,它会将当前时间写入数据库,这是一个 20 秒的倒计时。由于我们用 来写时间ServerValue.TIMESTAMP,数据库会把时间写在服务器上,所以它不会受到主机本地时间(或偏移量)的影响。


现在让我们看看其他用户是如何读取这些数据的。与 Firebase 一样,我们将使用on()侦听器,这意味着我们的代码正在积极侦听数据何时被写入:

ref.on("value", (snapshot) => {
  ...
});
Run Code Online (Sandbox Code Playgroud)

当此ref.on(...代码执行时,它会立即从数据库中读取当前值并运行回调。但它也会继续监听对数据库的更改,并在发生另一次写入时再次运行代码。

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

我们将首先从数据库中获取值:

ref.on("value", (snapshot) => {
  const seconds = snapshot.val().seconds;
  const startAt = snapshot.val().startAt;
  ...
});
Run Code Online (Sandbox Code Playgroud)

我们还需要估计本地客户端和服务器上的时间之间有多少时间。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;
  
});
Run Code Online (Sandbox Code Playgroud)

在一个运行良好的系统中,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)
});
Run Code Online (Sandbox Code Playgroud)

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

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)
});
Run Code Online (Sandbox Code Playgroud)

最后,我们以合理的格式记录剩余时间,并在计时器到期时停止计时器:

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)
});
Run Code Online (Sandbox Code Playgroud)

在上面的代码中肯定还有一些清理工作要做,例如当一个新的倒计时开始而一个仍在进行中时,但整体方法运行良好并且可以轻松扩展到数千个用户。

  • 我认为确实如此!在本例中,延迟了 173668 毫秒,我仍然在 else 语句中使用它。问题是因为 Date.now() 过去是公式“Date.now() - startAt - serverTimeOffset”实际上返回一个 **正** 数字,当您计算“秒 - 上述公式的结果”时,这两个加起来就是一个巨大的数字。就我而言,倒计时从 5:44 开始,进行 10 秒倒计时。那有意义吗? (2认同)