【问题标题】:node.js scalability with event timers (setTimeout)node.js 可扩展性与事件计时器 (setTimeout)
【发布时间】:2020-09-07 18:47:21
【问题描述】:

我正在使用 node.js 和 socket.io 构建一个基于回合的文本游戏。每个回合都有一个超时,之后玩家输掉回合并传递给下一个玩家。我正在使用setTimeout 函数,正如我在another question 中所说的那样。

问题是我不知道如何在多个实例甚至多个服务器上扩展它。 AIUI,如果我设置了超时,我只能在同一个实例中清除它。因此,例如,如果一个玩家失去了他的回合,超时将随着另一个玩家的回合而更新,但是这个新玩家将无法访问计时器对象来清除它,因为它在第一个玩家的实例上运行。

我查看了 Redis 发布/订阅功能(无论如何我都必须使用它),但我没有找到任何关于定时事件或延迟发布的信息。

TL;DR,我如何保持独立于实例/服务器的计时器?

【问题讨论】:

    标签: node.js timer socket.io


    【解决方案1】:

    我找到的解决方案是使用一些消息系统(在我的例子中是 Redis pub/sub)来让每个玩家实例知道当前状态。

    每个玩家都有一个工人实例来处理他自己的回合(包括计时器)。当它完成时,无论是通过玩家的移动还是通过超时,它都会推进回合计数器并通过 pub/sub 通知所有实例具有新的回合号。所有实例都收到消息并将轮数与其自己的玩家编号进行比较。如果匹配,则实例处理转弯并重复循环。

    我将尝试提供一个示例(更多的是伪代码):

    // pub & sub are Redis publisher & subscriber clients, respectively
    
    function Game (totalPlayers, playerNumber) {
      this.turn = 0
      this.totalPlayers = totalPlayers
      this.playerNumber = playerNumber
    
      // Subscribe to Redis events
      sub.on('message', function (channel, message) {
        message = JSON.parse(message)
    
        switch(message.type) {
          case 'turn':
            this.onTurn(message.turn)
        }
      })
    
      sub.subscribe(this.channel, function() {
        this.checkStart()
      })
    }
    
    Game.prototype.checkStart = function () {
        // This checks if this instance  is for
        // the last player and, if so, starts the
        // main loop:
        if(this.playerNumber == this.totalPlayers - 1) {
          pub.publish(this.channel, JSON.stringify({type: 'turn', turn: 0})
        }
    }
    
    Game.prototype.onTurn = function(turn) {
      this.turn = turn
      if(this.turn == this.playerNumber) {
        this.timer = setTimeout(this.endTurn.bind(this), this.turnTime)
      }
    }
    
    Game.prototype.endTurn = function() {
      this.turn = (this.turn + 1) % this.totalPlayers
      pub.publish(this.channel, JSON.stringify({type: 'turn', turn: this.turn})
    }
    

    我在使用这种方法时遇到了一些问题,主要问题是初始状态,如果玩家几乎同时连接,这不太正确。发送信息并确保所有实例同步也是一个好主意。

    如果有人遇到同样的问题,我希望我说清楚。

    【讨论】:

      【解决方案2】:

      可以使用 Redis 及其 TTL 选项(加上其 Pub/Sub 机制)实现可靠的独立计时器。

      //enable keyspace events:
      redisClient.send_command('config', ['set', 'notify-keyspace-events', 'Ex']);
      
      // add a key:
      const key = '<some meaningful key string>';
      redisClient.set(key, '<some note for the key, not usable though>');
      
      // set the key to expire:
      redisClient.expire(key, 100); // duration in seconds
      
      // somewhere else in the code, subscribe to the 'expired' event:
      const expiredSubKey = `__keyevent@${config.redis.db}__:expired`; // you need redis DB number here
      redisClient.subscribe(expiredSubKey, () => {
          redisClient.on('message', async (channel, key) => {
              // channel is actually expiredSubKey, ignore it
              // 'key' is the key you've set up previously
          });
      });
      

      (更多信息:How to receive Redis expire events with node?

      除了独立于服务之外,这种技术还有一个好处:

      • 不涉及轮询,您无需定期检查过期密钥

      它也有一些缺点:

      • 它有点“hacky”,这意味着它的设计并非完全出于此目的
      • 我找不到获取过期事件值的方法,所以只能使用一个键,这是有限制的
      • 如果您有多个实例服务(即扩展),您将拥有那么多订阅者,因此每个订阅者都会触发该事件。有时这不是问题,有时是。这实际上可以通过高级 Redis pub/sub 解决。

      您也可以为此使用一些第三方服务。我能够找到其中一些具有免费计划和合理 API 的应用程序(尽管我使用的是我自己的,如上所述?)。

      【讨论】:

        猜你喜欢
        • 2011-02-10
        • 1970-01-01
        • 1970-01-01
        • 2019-06-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-11-13
        • 2013-06-01
        相关资源
        最近更新 更多