异步代码中的全局变量 (socket.io) 以意外的方式显示错误

Global variables in asynchronous code (socket.io) show an error in an unexpected way

来自Why shouldn't I use global variables in JavaScript for something that's constant?的答案列出了8个全局变量问题,第2个是

If you have any asynchronous code that modifies globals or timer-driven code that modifies globals and more than one asynchronous operation can be in flight at the same time, the multiple async operations can step on each other through the modification of the same globals.

虽然该语句很容易理解,但我遇到了一个奇怪的 socket.io 问题,我无法弄清楚原因:我发现如果一个全局 socket.io 客户端用于 2 个连接(错误),第二个连接获取第一个连接消息。我创建了一个示例项目 https://github.com/qiulang/2sockets 来演示该问题。

服务器逻辑很简单,当客户端连接时需要发送一个带有user_id的登录消息,如果服务器发现相同的user_id登录不同的socket.id,它确定这是同一用户从不同客户端登录的情况,因此它将向第一个 socket.io 客户端发出注销消息。当客户端收到注销消息时,它将关闭其连接。

let records = {}

io.on("connection", (socket) => {
  socket.on("login",({user_id,client}) =>{
      let old_socket = records[user_id]
      if ( old_socket ) {
        log(`The same ${user_id} with ${old_socket} has logged in, let him log off first`)
        io.to(old_socket).emit('logoff', 'another login')
      }
      records[user_id] = socket.id
  })
  socket.on("disconnect",(reason) => {
      log(`${socket.id} disconnected with reason ${reason}`)
  })
});

客户端使用全局变量而不是函数作用域变量。但是在 main() 中有 2 个连接,然后第二个连接错误地获取注销消息。

function setupWS(client) {
//   let socket = io(server, { transports: ['websocket']}) //Should use a function scope variable
  socket = io(server, { transports: ['websocket']}) //wrong!! Should NOT use a global variable
  socket.on('connect', function () {
    log(`${client} connected to local ws server with ${socket.id}`)
    socket.emit('login', { user_id, client })
  })
  socket.on('logoff', (msg) => {
    log(`${socket.id}:${client} should be kicked off for: ${msg}`)
    socket.close()
  })
}

function main() {
  setupWS('nodejs')
  setTimeout(()=> {
    log('open another connection in 5 seconds')
    setupWS("browser")
  },5000)
}

main()

当运行来自客户端的代码时,我会看到这样的日志,

nodejs connected to local ws server with ijqTzPl2SXHmB-U0AAAC
browser connected to local ws server with l7vCcbeOmVU5d_TSAAAE
l7vCcbeOmVU5d_TSAAAE:nodejs should be kicked off for: another login

从服务器端,我会看到这样的日志,

The same 1_2_1000 with ijqTzPl2SXHmB-U0AAAC has logged in, let him log off first
l7vCcbeOmVU5d_TSAAAE disconnected with reason client namespace disconnect

所以服务器正确地向第一个 socket.id ijqTzPl2SXHmB-U0AAAC 发送了注销消息,而在客户端的日志是 l7vCcbeOmVU5d_TSAAAE:nodejs (NOT l7vCcbeOmVU5d_TSAAAE:browser) 应该启动。它确实是第二个 socket.id l7vCcbeOmVU5d_TSAAAE 称为 close()

---更新---

有了 jfriend00 的回答,我明白了我的问题。我想补充的是这个问题是由他的回答中的#6 问题引入的 Why shouldn't I use global variables in JavaScript for something that's constant?

A simple omission of the keyword "var" on a local variable makes it a global variable and can confuse the heck out of code

通常我们要求每个 node.js 文件在第一行添加 "use strict",但现在我意识到 运行宁 node --use_strict 更安全。

顺便说一句,如果我可以在他的出色回答中再添加一个使用全局变量的问题,我会添加 要弄清楚全局变量在何处以及如何修改是非常痛苦的 .

当您第二次调用 setupWs() 时,它会覆盖全局 socket 变量。然后,当第一个连接收到一条消息时,您会记录 socket.id,但您不会记录实际上刚刚收到消息的连接,您是从 socket 全局变量记录的,现在指向第二个连接。

所以,您的日志记录在这里有问题。它将从同一个 socket 全局变量记录 socket.id,无论哪个连接实际收到消息。因此,错误的日志记录使得看起来不同的连接正在接收消息而不是实际情况。

而且,除了日志记录之外,您在消息处理程序中使用 socket 的两个地方也引用了错误的套接字。因此,您需要在本地存储套接字。如果你想全局访问你最后创建的套接字,你也可以全局存储它。

function setupWS(client) {
  const activeSocket = io(server, { transports: ['websocket']});

  // store the latest socket globally so outside code can send
  // messages on the latest socket we connected
  socket = activeSocket;

  activeSocket.on('connect', function () {
    log(`${client} connected to local ws server with ${activeSocket.id}`)
    activeSocket.emit('login', { user_id, client })
  })
  activeSocket.on('logoff', (msg) => {
    log(`${activeSocket.id}:${client} should be kicked off for: ${msg}`)
    activeSocket.close()
  })
}

并且,如您所见并且显然已经知道,这是 kind of problem 使用全局变量可以创建的示例。