node.js具有事件计时器的可伸缩性(setTimeout)

时间:2014-04-18 18:25:46

标签: node.js timer socket.io

我正在用node.js和socket.io构建一个回合制的文本游戏。每个回合都有一个超时,之后玩家失去转弯并且传递给下一个玩家。我正在another question中使用setTimeout函数。

问题在于我不知道如何在多个实例和多个服务器上进行扩展。 AIUI,如果我设置超时,我只能在同一个实例中清除它。因此,如果玩家失去转弯,例如,超时将与其他玩家转弯续订,但是这个新玩家将无法访问计时器对象以清除它,因为它正在第一个玩家的实例上运行。

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

TL; DR,如何保留与实例/服务器无关的计时器?

2 个答案:

答案 0 :(得分:0)

我发现的解决方案是使用一些消息系统(在我的情况下是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})
}

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

如果有人遇到同样的问题,我希望我明白这一点。

答案 1 :(得分:0)

使用Redis及其TTL选项(加上发布/订阅机制)可以完成可靠的独立计时器。

//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(尽管我使用的是我自己的,如上文所述)。