节点服务器关闭然后重新启动时出现奇怪的socket.io行为

时间:2023-01-25 21:54:11

I implemented a simple chat for my website where users can talk to each other with ExpressJS and Socket.io. I added a simple protection from a ddos attack that can be caused by one person spamming the window like this:

我为我的网站实现了一个简单的聊天,用户可以通过ExpressJS和Socket.io互相交流。我添加了一个简单的保护,免受ddos攻击,这可能是由一个人发送垃圾邮件引起的,如下所示:

if (RedisClient.get(user).lastMessageDate > currentTime - 1 second) {

   return error("Only one message per second is allowed")

} else {

   io.emit('message', ...)     
   RedisClient.set(user).lastMessageDate = new Date()

}

I am testing this with this code:

我用这段代码测试了这个:

setInterval(function() {
    $('input').val('message ' + Math.random());
    $('form').submit();
}, 1);

It works correctly when Node server is always up.

当节点服务器始终启动时,它可以正常工作。

However, things get extremely weird if I turn off the Node server, then run the code above, and start Node server again in a few seconds. Then suddenly, hundreds of messages are inserted into the window and the browser crashes. I assume it is because when Node server is down, socket.io is saving all the client emits, and once it detects Node server is online again, it pushes all of those messages at once asynchronously.

但是,如果我关闭节点服务器,然后运行上面的代码,并在几秒钟内再次启动节点服务器,事情变得非常奇怪。然后突然,数百条消息被插入窗口,浏览器崩溃。我认为这是因为当Node服务器关闭时,socket.io正在保存所有客户端发出,一旦检测到Node服务器再次联机,它就会异步推送所有这些消息。

How can I protect against this? And what is exactly happening here?

我怎样才能防止这种情况发生?这到底发生了什么?

edit: If I use Node in-memory instead of Redis, this doesn't happen. I am guessing cause servers gets flooded with READs and many READs happen before RedisClient.set(user).lastMessageDate = new Date() finishes. I guess what I need is atomic READ / SET? I am using this module: https://github.com/NodeRedis/node_redis for connecting to Redis from Node.

编辑:如果我使用节点内存而不是Redis,这不会发生。我猜测导致服务器充满了READ,并且在RedisClient.set(用户).lastMessageDate = new Date()完成之前发生了许多READ。我想我需要的是原子READ / SET?我正在使用这个模块:https://github.com/NodeRedis/node_redis用于从Node连接到Redis。

2 个解决方案

#1


6  

You are correct that this happens due to queueing up of messages on client and flooding on server.

你是正确的,这是因为客户端上的消息排队和服务器上泛滥。

When the server receives messages, it receives messages all at once, and all of these messages are not synchronous. So, each of the socket.on("message:... events are executed separately, i.e. one socket.on("message... is not related to another and executed separately.

当服务器收到消息时,它会立即接收所有消息,并且所有这些消息都不是同步的。因此,每个socket.on(“message:...事件都是单独执行的,即一个socket.on(”消息......与另一个无关并单独执行)。

Even if your Redis-Server has a latency of a few ms, these messages are all received at once and everything always goes to the else condition.

即使您的Redis-Server具有几毫秒的延迟,这些消息也会立即被接收,并且所有消息都会转到其他条件。

You have the following few options.

您有以下几个选项。

  1. Use a rate limiter library like this library. This is easy to configure and has multiple configuration options.

    使用像此库一样的速率限制器库。这很容易配置,并具有多个配置选项。

  2. If you want to do everything yourself, use a queue on server. This will take up memory on your server, but you'll achieve what you want. Instead of writing every message to server, it is put into a queue. A new queue is created for every new client and delete this queue when processing the last item in queue.

    如果您想自己完成所有操作,请在服务器上使用队列。这将占用您服务器上的内存,但您将实现您想要的。它不是将每条消息写入服务器,而是放入队列中。为每个新客户端创建一个新队列,并在处理队列中的最后一个项目时删除此队列。

  3. (update) Use multi + watch to create lock so that all other commands except the current one will fail.

    (更新)使用multi + watch创建锁定,以便除当前命令之外的所有其他命令都将失败。

the pseudo-code will be something like this.

伪代码将是这样的。

let queue = {};

let queueHandler = user => {
  while(queue.user.length > 0){
    // your redis push logic here
  }
  delete queue.user
}


let pushToQueue = (messageObject) => {
  let user = messageObject.user;

  if(queue.messageObject.user){
    queue.user = [messageObject];
  } else {
    queue.user.push(messageObject);
  }

  queueHandler(user);
}

socket.on("message", pushToQueue(message));

UPDATE

Redis supports locking with WATCH which is used with multi. Using this, you can lock a key, and any other commands that try to access that key in thet time fail.

Redis支持使用WATCH进行锁定,WATCH与multi一起使用。使用此方法,您可以锁定密钥,并且在该时间内尝试访问该密钥的任何其他命令都会失败。

from the redis client README

来自redis客户端README

Using multi you can make sure your modifications run as a transaction, but you can't be sure you got there first. What if another client modified a key while you were working with it's data?

使用multi你可以确保你的修改作为一个事务运行,但你不能确定你是否先到达那里。如果其他客户在您使用它的数据时修改了密钥怎么办?

To solve this, Redis supports the WATCH command, which is meant to be used with MULTI: var redis = require("redis"), client = redis.createClient({ ... });

为了解决这个问题,Redis支持WATCH命令,该命令用于与MULTI一起使用:var redis = require(“redis”),client = redis.createClient({...});

client.watch("foo", function( err ){
if(err) throw err;

client.get("foo", function(err, result) {
    if(err) throw err;

    // Process result
    // Heavy and time consuming operation here

    client.multi()
        .set("foo", "some heavy computation")
        .exec(function(err, results) {

            /**
             * If err is null, it means Redis successfully attempted 
             * the operation.
             */ 
            if(err) throw err;

            /**
             * If results === null, it means that a concurrent client
             * changed the key while we were processing it and thus 
             * the execution of the MULTI command was not performed.
             * 
             * NOTICE: Failing an execution of MULTI is not considered
             * an error. So you will have err === null and results === null
             */

        });
}); });

#2


4  

Perhaps you could extend your client-side code, to prevent data being sent if the socket is disconnected? That way, you prevent the library from queuing messages while the socket is disconnected (ie the server is offline).

也许您可以扩展客户端代码,以防止在套接字断开时发送数据?这样,在套接字断开连接时(即服务器处于脱机状态),您可以防止磁带库对消息进行排队。

This could be achieved by checking to see if socket.connected is true:

这可以通过检查socket.connected是否为真来实现:

// Only allow data to be sent to server when socket is connected
function sendToServer(socket, message, data) {

    if(socket.connected) {
        socket.send(message, data)
    }
}

More information on this can be found at the docs https://socket.io/docs/client-api/#socket-connected

有关这方面的更多信息,请参阅文档https://socket.io/docs/client-api/#socket-connected

This approach will prevent the built in queuing behaviour in all scenarios where a socket is disconnected, which may not be desirable, however if should protect against the problem you are noting in your question.

这种方法将阻止套接字断开的所有情况下的内置排队行为,这可能是不可取的,但是如果应该防止您在问题中注意到的问题。

Update

Alternatively, you could use a custom middleware on the server to achieve throttling behaviour via socket.io's server API:

或者,您可以使用服务器上的自定义中间件通过socket.io的服务器API实现限制行为:

/*
Server side code
*/
io.on("connection", function (socket) {

    // Add custom throttle middleware to the socket when connected
    socket.use(function (packet, next) {

        var currentTime = Date.now();

        // If socket has previous timestamp, check that enough time has
        // lapsed since last message processed
        if(socket.lastMessageTimestamp) {
            var deltaTime = currentTime - socket.lastMessageTimestamp;

            // If not enough time has lapsed, throw an error back to the
            // client
            if (deltaTime < 1000) {
                next(new Error("Only one message per second is allowed"))
                return
            }
        }

        // Update the timestamp on the socket, and allow this message to
        // be processed
        socket.lastMessageTimestamp = currentTime
        next()
    });
});

#1


6  

You are correct that this happens due to queueing up of messages on client and flooding on server.

你是正确的,这是因为客户端上的消息排队和服务器上泛滥。

When the server receives messages, it receives messages all at once, and all of these messages are not synchronous. So, each of the socket.on("message:... events are executed separately, i.e. one socket.on("message... is not related to another and executed separately.

当服务器收到消息时,它会立即接收所有消息,并且所有这些消息都不是同步的。因此,每个socket.on(“message:...事件都是单独执行的,即一个socket.on(”消息......与另一个无关并单独执行)。

Even if your Redis-Server has a latency of a few ms, these messages are all received at once and everything always goes to the else condition.

即使您的Redis-Server具有几毫秒的延迟,这些消息也会立即被接收,并且所有消息都会转到其他条件。

You have the following few options.

您有以下几个选项。

  1. Use a rate limiter library like this library. This is easy to configure and has multiple configuration options.

    使用像此库一样的速率限制器库。这很容易配置,并具有多个配置选项。

  2. If you want to do everything yourself, use a queue on server. This will take up memory on your server, but you'll achieve what you want. Instead of writing every message to server, it is put into a queue. A new queue is created for every new client and delete this queue when processing the last item in queue.

    如果您想自己完成所有操作,请在服务器上使用队列。这将占用您服务器上的内存,但您将实现您想要的。它不是将每条消息写入服务器,而是放入队列中。为每个新客户端创建一个新队列,并在处理队列中的最后一个项目时删除此队列。

  3. (update) Use multi + watch to create lock so that all other commands except the current one will fail.

    (更新)使用multi + watch创建锁定,以便除当前命令之外的所有其他命令都将失败。

the pseudo-code will be something like this.

伪代码将是这样的。

let queue = {};

let queueHandler = user => {
  while(queue.user.length > 0){
    // your redis push logic here
  }
  delete queue.user
}


let pushToQueue = (messageObject) => {
  let user = messageObject.user;

  if(queue.messageObject.user){
    queue.user = [messageObject];
  } else {
    queue.user.push(messageObject);
  }

  queueHandler(user);
}

socket.on("message", pushToQueue(message));

UPDATE

Redis supports locking with WATCH which is used with multi. Using this, you can lock a key, and any other commands that try to access that key in thet time fail.

Redis支持使用WATCH进行锁定,WATCH与multi一起使用。使用此方法,您可以锁定密钥,并且在该时间内尝试访问该密钥的任何其他命令都会失败。

from the redis client README

来自redis客户端README

Using multi you can make sure your modifications run as a transaction, but you can't be sure you got there first. What if another client modified a key while you were working with it's data?

使用multi你可以确保你的修改作为一个事务运行,但你不能确定你是否先到达那里。如果其他客户在您使用它的数据时修改了密钥怎么办?

To solve this, Redis supports the WATCH command, which is meant to be used with MULTI: var redis = require("redis"), client = redis.createClient({ ... });

为了解决这个问题,Redis支持WATCH命令,该命令用于与MULTI一起使用:var redis = require(“redis”),client = redis.createClient({...});

client.watch("foo", function( err ){
if(err) throw err;

client.get("foo", function(err, result) {
    if(err) throw err;

    // Process result
    // Heavy and time consuming operation here

    client.multi()
        .set("foo", "some heavy computation")
        .exec(function(err, results) {

            /**
             * If err is null, it means Redis successfully attempted 
             * the operation.
             */ 
            if(err) throw err;

            /**
             * If results === null, it means that a concurrent client
             * changed the key while we were processing it and thus 
             * the execution of the MULTI command was not performed.
             * 
             * NOTICE: Failing an execution of MULTI is not considered
             * an error. So you will have err === null and results === null
             */

        });
}); });

#2


4  

Perhaps you could extend your client-side code, to prevent data being sent if the socket is disconnected? That way, you prevent the library from queuing messages while the socket is disconnected (ie the server is offline).

也许您可以扩展客户端代码,以防止在套接字断开时发送数据?这样,在套接字断开连接时(即服务器处于脱机状态),您可以防止磁带库对消息进行排队。

This could be achieved by checking to see if socket.connected is true:

这可以通过检查socket.connected是否为真来实现:

// Only allow data to be sent to server when socket is connected
function sendToServer(socket, message, data) {

    if(socket.connected) {
        socket.send(message, data)
    }
}

More information on this can be found at the docs https://socket.io/docs/client-api/#socket-connected

有关这方面的更多信息,请参阅文档https://socket.io/docs/client-api/#socket-connected

This approach will prevent the built in queuing behaviour in all scenarios where a socket is disconnected, which may not be desirable, however if should protect against the problem you are noting in your question.

这种方法将阻止套接字断开的所有情况下的内置排队行为,这可能是不可取的,但是如果应该防止您在问题中注意到的问题。

Update

Alternatively, you could use a custom middleware on the server to achieve throttling behaviour via socket.io's server API:

或者,您可以使用服务器上的自定义中间件通过socket.io的服务器API实现限制行为:

/*
Server side code
*/
io.on("connection", function (socket) {

    // Add custom throttle middleware to the socket when connected
    socket.use(function (packet, next) {

        var currentTime = Date.now();

        // If socket has previous timestamp, check that enough time has
        // lapsed since last message processed
        if(socket.lastMessageTimestamp) {
            var deltaTime = currentTime - socket.lastMessageTimestamp;

            // If not enough time has lapsed, throw an error back to the
            // client
            if (deltaTime < 1000) {
                next(new Error("Only one message per second is allowed"))
                return
            }
        }

        // Update the timestamp on the socket, and allow this message to
        // be processed
        socket.lastMessageTimestamp = currentTime
        next()
    });
});