NodeJS函数被socketio事件

时间:2016-08-18 20:48:19

标签: javascript arrays node.js multithreading socket.io

我在我的nodejs游戏服务器中看到一些奇怪的行为,其中似乎有并发性。这很奇怪,因为Nodejs应该在一个线程中运行,因为它不使用任何并发。问题是我有一个使用setImmediate()重复调用的更新函数。在这个函数中,我在两个地方使用一个数组。但是,当“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");
            }
        }
   }
}

现在,我有一个socketio事件监听器,用于在客户端断开连接时删除数组中的元素。当此事件发生在第一个和第二个位置之间时,访问该阵列(如上所示),我的服务器崩溃。正在使用线程或我的函数被停止以让事件处理程序执行,然后我的函数被恢复。无论哪种方式,我都不希望这种情况发生。谢谢!

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

编辑2:感谢您的所有回复,我真的很感激。我认为有一些混乱: 1.事实上没有人承认控制台日志是如何出现的。在我之前的编辑中,我更改了代码以反映我如何记录以查看问题。问题是在disconnect事件处理程序中,我有一个控制台日志,它发生在循环中的两个控制台日志之间。即断开事件处理程序在循环中到达第二个控制台日志之前执行。除非我对控制台日志功能的实现感到困惑,否则日志应该以正确的顺序发生(即循环中的两个控制台日志应始终在任何其他控制台登录之前发生,因为大多数人都说过ASYNC的性质。)但事实并非如此,这让我相信一些奇怪的事情正在发生。 2.循环内的所有代码都没有改变数组。在很多回复中,您假设存在实际修改循环内部数组的代码,但实际情况并非如此。修改数组的唯一代码是循环的代码OUTISDE,这就是为什么非常奇怪的是,访问数组的循环的第一部分不会崩溃而第二部分会崩溃,即使DOESN之间的代码也是如此'改变阵列。

编辑3:好的,所以很多回复都要求完整的代码。我用所有相关的REAL代码更新了代码。

3 个答案:

答案 0 :(得分:3)

node.js中的Javascript是单线程的。 Javascript中的给定执行线程不会被socket.io disconnect事件中断。这种情况不可能发生。 node.js是事件驱动的。当断开连接事件发生时,事件将被放入Javascript事件队列中,并且只有当您执行当前执行线程时,Javascript才会从事件队列中获取下一个事件并调用与之关联的回调。

你没有足够的真实代码可以肯定地知道,但是如果你有异步操作,那么当你启动异步操作并注册回调完成时,你可能会发生什么完成Javascript执行线程,它只是一个竞赛,看看接下来发生了哪个异步事件(此特定异步操作的完成或socket.io断开连接的断开事件)。这是不确定的,这些事件可以按任何顺序发生。因此,如果您在相关代码中有异步代码,那么当代码等待完成异步事件时,可以处理disconnect事件。

这是node.js编程中必须注意的竞争条件类型。任何时候你的逻辑都是异步的,那么当你的代码等待异步回调时,其他事情可以在node.js中处理,这反映了操作完成的信号。

究竟要做什么完全取决于具体情况,我们需要查看并了解您的真实代码(非伪代码),以了解最适合您的选项。仅供参考,如果您向我们展示您的真实代码,而不仅仅是伪代码,这是我们可以随时帮助您的原因之一。

以下是在共享数据结构上使用异步操作进行操作时可以使用的一些技术,这些技术可能被其他异步代码更改:

  1. 复制您要处理的数据,以便其他代码无法访问您的副本,因此无法通过任何其他代码进行修改。这可能是制作数组的副本,也可能只是使用闭包来捕获本地索引,因此索引不会受到其他代码的影响。

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

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

  4. 使对数据的所有访问都是异步的,因此如果某些其他异步操作正在修改数据,那么访问数据的其他不安全尝试可能会阻塞&#34;直到最初的操作完成。这是数据库使用的一种技术。当然,您必须注意死锁或标记或锁定未被清除的错误路径。

  5. 基于您发布更多代码的一些新评论:

    这段代码错了:

    //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");
                }
            }
       }
    }
    

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

    for处理程序中disconnect循环中的相同问题。如果您只想在迭代中匹配一个数组元素,那么您可以在break之后splice(),这样可以避免此问题,并且您不必反向迭代。

答案 1 :(得分:1)

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

1)当发生断开连接时不要修改数组的长度,而是创建一个假的值。布尔值或零和零场景

2)以if语句的形式添加逻辑,以检查玩家2的值是否为falsey。那样你就会知道他们断了连接,不配有任何东西,因为他们很蹩脚,无法看到失败者的屏幕。

那应该解决问题,你可以。决定如果他们懒得留下来观看比赛的胜利失败仪式该怎么办。

var gameWorld = [];

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

即使update()正在调用堆栈上运行,它也在等待事件循环,但这并不意味着它会在下一个滴答发生之前完成它的执行。 gameWorld位于两个范围之外,可以在update()的中间进行修改。因此,当update()尝试再次访问数组时,它与启动时不同。

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

答案 2 :(得分:0)

即使你有一个事件监听器,执行也不应该只停止mid函数。当事件发生时,节点会将事件回调推送到堆栈。然后,当节点完成当前函数的执行时,它将开始处理堆栈上的其他请求。您无法确定订单会执行的顺序,但您可以确保在执行期间不会中断。

如果你的doWhatever函数是异步的,那么问题可能正在发生,因为当节点最终绕过来处理堆栈上的请求时,循环已经完成,因此每次执行任何调用它都会被调用相同的索引(无论它是什么最后一个值是。)

如果要从循环中调用异步函数,则应将它们包装在函数中以保留参数。

e.g。

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