使用Redis在Heroku Node.js应用程序上使用WebSocket和多个Dyno

时间:2018-11-01 06:12:10

标签: node.js heroku websocket redis

我正在构建一个部署到Heroku的应用程序,该应用程序使用WebSocket和Redis。

当我仅使用1个dyno时,WebSocket连接正常工作,但是当我缩放到2个dyno时,我发送事件两次,使应用程序执行一次。

const ws = require('ws')
const jwt = require('jsonwebtoken')
const redis = require('redis')

const User = require('../models/user')

function verifyClient (info, callback) {
  let token = info.req.headers['sec-websocket-protocol']
  if (!token) { callback(false, 401, 'Unauthorized') } else {
    jwt.verify(token, Config.APP_SECRET, (err, decoded) => {
      if (err) { callback(false, 401, 'Unauthorized') } else {
        if (info.req.headers.gameId) { info.req.gameId = info.req.headers.gameId }
        info.req.userId = decoded.aud
        callback(true)
      }
    })
  }
};

let websocketServer, pub, sub

let clients = {}
let namespaces = {}

exports.initialize = function (httpServer) {
  websocketServer = new ws.Server({
    server: httpServer,
    verifyClient: verifyClient
  })
  pub = redis.createClient(Config.REDIS_URL, { no_ready_check: true, detect_buffers: true })
  pub.auth(Config.REDIS_PASSWORD, function (err) {
    if (err) throw err
  })
  sub = redis.createClient(Config.REDIS_URL, { no_ready_check: true, detect_buffers: true })
  sub.auth(Config.REDIS_PASSWORD, function (err) {
    if (err) throw err
  })

  function handleConnection (socket) {
    // socket.send(socket.upgradeReq.userId);
    socket.userId = socket.upgradeReq.userId // get the user id parsed from the decoded JWT in the middleware
    socket.isAlive = true
    socket.scope = socket.upgradeReq.url.split('/')[1] // url = "/scope/whatever" => ["", "scope", "whatever"]
    console.log('New connection: ' + socket.userId + ', scope: ' + socket.scope)

    socket.on('message', (data, flags) => { handleIncomingMessage(socket, data, flags) })
    socket.once('close', (code, reason) => { handleClosedConnection(socket, code, reason) })
    socket.on('pong', heartbeat)
    if (socket.scope === 'gameplay') {
      try {
        User.findByIdAndUpdate(socket.userId, { $set: { isOnLine: 2, lastSeen: Date.now() } }).select('id').lean()
        let key = [socket.userId, socket.scope].join(':')
        clients[key] = socket
        sub.psubscribe(['dispatch', '*', socket.userId, socket.scope].join(':'))
      } catch (e) { console.log(e) }
    } else {
      console.log('Scope : ' + socket.scope)
    }
    console.log('Connected Users : ' + Object.keys(clients))
  }
  function handleIncomingMessage (socket, message, flags) {
    let scope = socket.scope
    let userId = socket.userId
    let channel = ['dispatch', 'in', userId, scope].join(':')
    pub.publish(channel, message)
  }
  function handleClosedConnection (socket, code, reason) {
    console.log('Connection with ' + socket.userId + ' closed. Code: ' + code)

    if (socket.scope === 'gameplay') {
      try {
        User.findByIdAndUpdate(socket.userId, { $set: { isOnLine: 1 } }).select('id').lean()
        let key = [socket.userId, socket.scope].join(':')
        delete clients[key]
      } catch (e) {
        console.log(e)
      }
    } else {
      console.log('Scope : ' + socket.scope)
    }
  }
  function heartbeat (socket) {
    socket.isAlive = true
  }
  sub.on('pmessage', (pattern, channel, message) => {
    let channelComponents = channel.split(':')
    let dir = channelComponents[1]
    let userId = channelComponents[2]
    let scope = channelComponents[3]
    if (dir === 'in') {
      try {
        let handlers = namespaces[scope] || []
        if (handlers.length) {
          handlers.forEach(h => {
            h(userId, message)
          })
        }
      } catch (e) {
        console.log(e)
      }
    } else if (dir === 'out') {
      try {
        let key = [userId, scope].join(':')
        if (clients[key]) { clients[key].send(message) }
      } catch (e) {
        console.log(e)
      }
    }
    // otherwise ignore
  })
  websocketServer.on('connection', handleConnection)
}

exports.on = function (scope, callback) {
  if (!namespaces[scope]) { namespaces[scope] = [callback] } else { namespaces[scope].push(callback) }
}
exports.send = function (userId, scope, data) {
  let channel = ['dispatch', 'out', userId, scope].join(':')
  if (typeof (data) === 'object') { data = JSON.stringify(data) } else if (typeof (data) !== 'string') { throw new Error('DispatcherError: Cannot send this type of message ' + typeof (data)) }
  pub.publish(channel, data)
}
exports.clients = clients

这在localhost上工作。

请让我知道是否需要提供更多信息或代码。对此表示感谢,谢谢!

1 个答案:

答案 0 :(得分:0)

您发布的代码中有很多无关的信息,因此很难准确地理解您的意思。

但是,如果我理解正确,那么您当前有多个工作人员dyno实例订阅某种发布/订阅网络中的相同频道。如果您不希望所有的dyno都订阅相同的频道,则需要加入一些逻辑以确保您的频道在dyno上分布。

一种简单的方法可能是使用类似this answer中所述的逻辑。

在您的情况下,您也许可以使用socket.userId作为键来在dynos上分配频道。