NodeJS函数被socketio事件中断

NodeJS function getting interrupted by socketio event

本文关键字:事件 中断 socketio 函数 NodeJS      更新时间:2023-09-26

我在我的nodejs游戏服务器中看到一些奇怪的行为,其中似乎存在并发性。这很奇怪,因为Nodejs应该在一个线程中运行,因为它不使用任何并发性。问题是我有一个更新函数,它使用setimmid()被反复调用。在这个函数中,我在两个地方使用了数组。但是,当"disconnect"事件触发时(即客户端与服务器断开连接时),这个数组也会被修改。因此,当时间对齐时,断开连接事件在更新函数中访问数组的第一个位置之后触发,但在第二个位置之前,数组被修改,因此当试图在第二个位置访问数组时服务器崩溃。

下面的一些代码可能会使图片更清晰:

function update(){
    for(var i = 0; i < gameWorlds.length; i++){
        gameWorlds[i].update();
        console.log("GAMEWORLDS LENGTH BEFORE: " + gameWorlds.length);
        NetworkManager.sendToClient(gameWorlds[i].id, "gameupdate", gameWorlds[i].getState());
        console.log("GAMEWORLDS LENGTH AFTER: " + gameWorlds.length);
        gameWorlds[i].clearGameState();
    }
}
setImmediate(update);
//in the NetworkManager module, the disconnect event handler:
socket.on("disconnect", function(){
    for(var a = 0; a < sockets.length; a++){
       if(sockets[a].id === socket.id){
                sockets.splice(a, 1);
             }
        }
        listenerFunction("disconnect", socket.id);
        console.log("Client " + socket.id + " DISCONNECTED!");
});
//also in the NetworkManager module, the sendToClient function:
function sendToClient(clientId, messageName, data){
    for(var i = 0; i < sockets.length; i++){
        if(sockets[i].id === clientId){
            sockets[i].emit(messageName, data);
        }
    }
}
//in the main module (the same one as the update function), the listener      
//function that's called in the disconnect event handler:
function networkEventsListener(eventType, eventObject){
   if(eventType === "disconnect"){
       for(var i = 0; i < gameWorlds.length; i++){
            if(gameWorlds[i].id === eventObject){
                gameWorlds.splice(i, 1); 
                console.log("GAME WORLD DELETED");
            }
        }
   }
}

现在,我设置了一个套接事件侦听器,用于在客户端断开连接时删除数组中的元素。当这个事件恰好发生在访问数组的第一个位置和第二个位置之间时(如上所示),服务器就会崩溃。要么使用线程,要么停止函数,让事件处理程序执行,然后恢复函数。不管怎样,我都不希望发生这种事。谢谢你!

EDIT 1:我编辑了代码以合并我代码中的控制台日志。我说我的循环被中断的原因是因为第二个控制台日志输出的长度为0,而第一个控制台日志输出的长度大于0。此外,在我的更新函数中的两个控制台日志之间的断开事件处理程序中还有另一个控制台日志。这意味着我的函数被中断了。

编辑2:谢谢你所有的回复,我真的很感激。我认为在以下方面存在一些混淆:1. 没有人知道控制台日志是如何显示的。在我之前的编辑中,我更改了代码,以反映我如何记录以查看问题。问题是,在断开事件处理程序中,我有一个控制台日志,它发生在循环中的两个控制台日志之间。也就是说,disconnect事件处理程序在到达循环中的第二个控制台日志之前执行。除非我对控制台日志功能的实现感到困惑,否则日志应该以正确的顺序发生(也就是说,循环中的两个控制台日志应该总是发生在程序其余部分的任何其他控制台日志之前,因为正如大多数人所说的异步性质。)但事实并非如此,这让我相信有些奇怪的事情正在发生。2. 循环中的代码都没有改变数组。在你的很多回复中,你假设有代码实际上修改了循环内的数组,事实并非如此。唯一修改数组的代码是循环的外部代码,这就是为什么非常奇怪的是,循环的第一部分访问数组不会崩溃,而第二部分却会崩溃,即使中间的代码不会改变数组。 编辑3:好的,所以很多回复都要求完整的代码。我已经用所有相关的REAL代码更新了代码。

node.js中的Javascript是单线程的。Javascript中给定的执行线程不会被套接字中断。IO断开事件。这在物理上是不可能发生的。Node.js是事件驱动的。当断开连接事件发生时,一个事件将被放入Javascript事件队列中,只有当当前执行线程完成时,Javascript才会从事件队列中抓取下一个事件并调用与之相关的回调。

你没有显示足够的真实代码来确定,但是可能发生的事情是,如果你有异步操作,那么当你开始一个异步操作并注册一个回调完成,然后你完成了执行的Javascript线程,它只是一个竞赛,看看哪个异步事件发生下一个(完成这个特定的异步操作或从套接字断开连接事件。io断开)。这是不确定的,这些事件可以以任何顺序发生。因此,如果您在问题代码中有异步代码,那么断开连接事件可以在代码等待异步事件完成时得到处理。


这是你在node.js编程中必须意识到的竞争条件类型。任何时候你的逻辑是异步的,那么其他的事情可以在node.js中得到处理,而你的代码正在等待异步回调,这表明操作已经完成。

具体怎么做完全取决于确切的情况,我们需要看到和理解你的真实代码(不是伪代码),以知道哪个选项最好地推荐给你。仅供参考,如果您向我们展示您的真实代码,而不仅仅是伪代码,这就是我们总能更好地帮助您的原因之一。

当你在一个共享的数据结构上进行异步操作时,这里有一些可以使用的技术,这些操作可能会被其他异步代码所改变:

  1. 制作你想要处理的数据的副本,这样其他代码就不能访问你的副本,所以它不能被任何其他代码修改。这可能是一个数组的副本,也可能只是使用闭包在本地捕获索引,这样索引就不会受到其他代码的影响。

  2. 使用标志来保护正在被修改的数据结构,并训练所有其他代码尊重该标志。具体如何做到这一点取决于具体的数据。我有一个树莓派node.js应用程序的代码,定期将数据保存到磁盘,并受到竞争条件的影响,其中其他事件驱动的代码可能想要更新该数据,而我正在使用异步I/O将其写入磁盘。因为数据可能很大,而系统的内存不是很大,所以我不能像第一点建议的那样复制数据。因此,我使用一个标志来表示我正在将数据写入磁盘,并且任何希望在设置此标志时修改数据的代码都会将其操作添加到队列中,而不是直接修改数据。然后,当我将数据写入磁盘时,代码检查队列,看看是否需要执行任何挂起的操作来修改数据。而且,由于数据是由对象表示的,并且对数据的所有操作都是通过对象上的方法执行的,因此对于使用数据或试图修改数据的代码来说,这一切都是透明的。

  3. 将数据放入具有并发特性和内置控件的实际数据库中,以便可以对数据进行原子性更改,或者可以将数据锁定短时间,或者可以以安全的方式获取或更新数据。数据库有很多可能的策略来处理这个问题,因为它经常发生在它们身上。

  4. 使所有对数据的访问都是异步的,所以如果其他一些异步操作正在修改数据,那么其他不安全的访问数据的尝试可以"阻塞",直到原始操作完成。这是数据库使用的一种技术。当然,您必须注意死锁,或者没有清除标志或锁的错误路径。


基于你发布的更多代码的一些新评论:

这段代码是错误的:

//in the main module (the same one as the update function), the listener      
//function that's called in the disconnect event handler:
function networkEventsListener(eventType, eventObject){
   if(eventType === "disconnect"){
       for(var i = 0; i < gameWorlds.length; i++){
            if(gameWorlds[i].id === eventObject){
                gameWorlds.splice(i, 1); 
                console.log("GAME WORLD DELETED");
            }
        }
   }
}

当您在正在迭代的数组的for循环中调用.splice()时,它会导致您错过正在迭代的数组中的一项。我不知道这和你的问题有什么关系,但这是不对的。避免这个问题的一个简单方法是向后迭代数组。然后调用.splice()将不会影响任何尚未迭代的数组元素的位置,并且您将不会错过数组中的任何内容。

同样的问题在disconnect处理程序的for循环。如果你只希望在迭代中匹配一个数组元素,那么你可以在splice()之后设置break,这将避免这个问题,你不必向后迭代。

我认为你应该改变两件事来解决问题。

1)当断开连接发生时,不修改数组的长度,而是使一个值为false。布尔值或1 + 0场景

2)添加if语句形式的逻辑来检查玩家2的值是否为false。这样你就会知道他们和你断绝了联系,不值得拥有任何东西,因为他们很差劲,看不了失败者屏幕。

应该可以解决这个问题,你可以。如果他们懒得留下来看你比赛的输赢仪式,决定该怎么做。

var gameWorld = [];

function update(){ // some code }是异步的,被推入事件循环。function disconnect(){ // some code }也是异步的,被推送到事件循环中。

即使update()在调用堆栈上运行,它也在等待事件循环,这并不意味着它会在下一个tick发生之前完成它的执行。gameWorld在这两个范围之外,它可以在update()的中间修改。所以当update()尝试再次访问数组时,它和它开始时是不同的。

disconnect()在update()完成之前被调用,并在事件循环nexttick()上修改数组,因此当update()的代码到达第二个播放器时,数组就乱了。

即使您有事件侦听器,执行也不应该只是在函数中间停止。当事件发生时,node将事件回调推到堆栈上。然后,当节点完成执行当前函数时,它将开始处理堆栈上的其他请求。你不能确定事情执行的顺序,但是你可以确定事情不会在执行过程中被打断。

如果你的doWhatever函数是异步的,那么问题可能会发生,因为当节点最终得到服务堆栈上的请求时,循环已经完成,因此每次doWhatever被调用时,它都被调用相同的索引(无论它的最后值是什么)

如果你想从循环中调用异步函数,那么你应该把它们包装在一个函数中以保留参数。

function doWhateverWrapper(index){
    theArray[index].doWhatever();
}
function update(){
   for(var i = 0; i < theArray.length; i++){
      //first place the array is accessed
      doWhateverWrapper(i);
      ....more code.....
      //second place the array is accessed
      doWhateverWrapper(i);
   }
}
setImmediate(update);