Node.js可扩展性与事件定时器(setTimeout)

3
我正在使用node.js和socket.io构建一个回合制文本游戏。每个回合都有一个超时时间,如果时间到了,玩家就会失去回合并且下一个玩家接替进行游戏。我正在使用setTimeout函数,正如我在另一个问题中所说的那样。
问题是我不知道如何在多个实例甚至多个服务器上扩展它。据我所知,如果我设置了一个超时,我只能在同一实例中清除它。因此,如果一个玩家输掉了比赛,例如,在其他玩家的回合中超时将被更新,但是这个新玩家无法访问计时器对象来清除它,因为它正在第一个玩家的实例上运行。
我看了看Redis的发布/订阅功能(无论如何我都要使用它),但我没有找到关于定时事件或延迟发布的任何内容。
简而言之,如何保持实例/服务器独立的计时器?
2个回答

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})
}

我对这种方法存在一些问题,主要问题在于初始状态不太正确,如果玩家几乎同时连接,则会出现问题。发送信息并确保所有实例同步也是个好主意。
如果有人遇到同样的问题,希望我已经讲清楚了。

0
一个可靠的独立计时器可以通过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
    });
});

(更多信息:如何使用Node接收Redis到期事件?

除了具有服务无关性之外,这种技术还有一个优点:

  • 没有轮询,不需要定期检查过期的键

它也有一些缺点:

  • 它有点“hacky”,意味着它并不是专门为此设计的
  • 我找不到在到期事件上获取值的方法,因此只能使用,这是有限制的
  • 如果您有多个实例的服务(即扩展),则将有许多订阅者,因此该事件将对每个订阅者触发。有时这不是问题,有时候就是。这实际上可以通过高级Redis发布/订阅解决。

此外,您还可以使用一些第三方服务进行此操作。我能够找到一些带有免费计划和合理API的服务(尽管我正在使用自己的上述描述)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接