我为我的网站实现了一个简单的聊天,用户可以在其中使用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()
}
我正在使用以下代码对此进行测试:
setInterval(function() {
$('input').val('message ' + Math.random());
$('form').submit();
}, 1);
当节点服务器始终处于启动状态时,它可以正常工作。
但是,如果我关闭Node服务器,然后运行上面的代码,然后在几秒钟内再次启动Node服务器,事情就会变得非常奇怪。然后突然,成百上千条消息被插入窗口,浏览器崩溃。我认为这是因为节点服务器关闭时,socket.io保存了所有客户端发出的消息,并且一旦检测到节点服务器再次处于联机状态,它将立即异步推送所有这些消息。
我该如何防范?而这里到底发生了什么?
编辑:如果我在内存中使用Node而不是Redis,则不会发生这种情况。我猜是因为服务器上充斥着READ,并且在RedisClient.set(user).lastMessageDate = new Date()
完成之前发生了许多READ。我想我需要的是原子READ / SET?我正在使用以下模块:https://github.com/NodeRedis/node_redis用于从Node连接到Redis。
答案 0 :(得分:6)
您是正确的,这是由于客户端上的消息排队和服务器上的洪水泛滥所致。
当服务器接收消息时,它会一次接收所有消息,并且所有这些消息都不同步。因此,每个socket.on("message:...
事件都是分别执行的,即一个socket.on("message...
与另一个事件无关,而是分别执行的。
即使您的Redis-Server
的延迟时间为几毫秒,这些消息也会一次全部收到,并且一切都将变为else
状态。
您有以下几种选择。
使用速率限制器库like this library。这很容易配置,并且具有多个配置选项。
如果您想自己做所有事情,请在服务器上使用队列。这将占用服务器上的内存,但是您将实现所需的功能。而不是将所有消息写入服务器,而是将其放入队列。为每个新客户端创建一个新队列,并在处理队列中的最后一项时删除此队列。
(更新)使用multi + watch创建锁,以便除当前命令外的所有其他命令都将失败。
伪代码将是这样的。
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));
更新
Redis支持与WATCH一起使用的锁定。使用此功能,您可以锁定密钥,并且其他尝试在该时间内访问该密钥的命令都会失败。
使用
multi
可以确保您的修改作为事务运行, 但您不能确定首先到达那里。如果另一个客户怎么办 在处理数据时修改了密钥?为解决此问题,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 */ }); }); });
答案 1 :(得分:4)
也许您可以扩展客户端代码,以防止在套接字断开连接时发送数据?这样,可以防止在套接字断开连接(即服务器处于脱机状态)时库不对消息进行排队。
这可以通过检查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)
}
}
有关更多信息,请参见文档https://socket.io/docs/client-api/#socket-connected
这种方法将防止在套接字断开连接的所有情况下的内置排队行为,这可能不是所希望的,但是,如果可以防止出现您所关注的问题,则可以避免这种情况。
或者,您可以在服务器上使用自定义中间件来通过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()
});
});