Socket.io 1.3.7 客户端断开时不清理
Socket.io 1.3.7 not cleaning up on client disconnect
我有一个 node.js 脚本,它允许客户端连接并从外部脚本接收一些实时数据。
我刚刚将 node.js & socket.io 升级到当前版本(从 <0.9 开始),我正试图了解当客户端退出、超时或断开连接时会发生什么服务器。
这是我当前的 node.js 脚本;
var options = {
allowUpgrades: true,
pingTimeout: 50000,
pingInterval: 25000,
cookie: 'k1'
};
var io = require('socket.io')(8002, options);
cp = require('child_process');
var tail = cp.spawn('test-scripts/k1.rb');
//On connection do the code below//
io.on('connection', function(socket) {
console.log('************ new client connected ****************', io.engine.clientsCount);
//Read from mongodb//
var connection_string = '127.0.0.1:27017/k1-test';
var mongojs = require('mongojs');
var db = mongojs(connection_string, ['k1']);
var k1 = db.collection('k1');
db.k1.find({}, {'_id': 0, "data.time":0}).forEach(function(err, doc) {
if (err) throw err;
if (doc) { socket.emit('k1', doc); }
});
//Run Ruby script & Listen to STDOUT//
tail.stdout.on('data', function(chunk) {
var closer = chunk.toString()
var sampArray = closer.split('\n');
for (var i = 0; i < sampArray.length; i++) {
try {
var newObj = JSON.parse(sampArray[i]);
// DO SOCKET //
socket.emit('k1', newObj);
} catch (err) {}
}
});
socket.on('disconnect', function(){
console.log('****************** user disconnected *******************', socket.id, io.engine.clientsCount);
socket.disconnect();
});
});
在旧版本的 socket.io 中,当客户端退出时,我得到以下登录调试信息;
info - transport end (undefined)
debug - set close timeout for client Owb_B6I0ZEIXf6vOF_b-
debug - cleared close timeout for client Owb_B6I0ZEIXf6vOF_b-
debug - cleared heartbeat interval for client Owb_B6I0ZEIXf6vOF_b-
debug - discarding transport
然后一切顺利,一切都很好。
使用 socket.io 的新 (1.3.7) 版本,当客户端退出时,我得到以下登录调试信息;
socket.io:client client close with reason transport close +2s
socket.io:socket closing socket - reason transport close +1ms
socket.io:client ignoring remove for -0BK2XTmK98svWTNAAAA +1ms
****************** user disconnected ******************* -0BK2XTmK98svWTNAAAA
注意行 socket.io:client ignoring remove for -0BK2XTmK98svWTNAAAA
但在那之后,在没有其他客户端连接到服务器的情况下,我仍然看到它试图将数据写入已经离开的客户端。 (在下面的示例中,这是我连接了 2 个客户端后得到的结果,这两个客户端都已断开连接。
socket.io:client ignoring packet write {"type":2,"data":["k1",{"item":"switch2","datapoint":{"type":"SWITCH","state":"0"}}],"nsp":"/"} +1ms
socket.io:client ignoring packet write {"type":2,"data":["k1",{"item":"switch2","datapoint":{"type":"SWITCH","state":"0"}}],"nsp":"/"} +3ms
我正试图阻止这种明显的新行为,这样一旦客户端断开连接并且服务器空闲,它就不会再尝试发送数据了。
我一直在玩 socket.disconnect
和 delete socket["id"]
,但我仍然遇到同样的事情。
我尝试了 io.close()
哪种方法有效 - 它启动了任何实际连接的客户端并使它们重新连接但仍然让服务器坐在那里尝试向已离开的客户端发送更新。
我是否遗漏了一些明显的东西,或者新版本 socket.io 的处理方式是否发生了变化? 2014 年 6 月的 migration doc about this. The only other result I found was this 错误报告中没有任何内容被标记为已关闭。从我的阅读来看 - 它似乎与我遇到的问题相同,但当前版本。
更新: 我已经做了一些更多的测试并添加了 io.engine.clientsCount
到 console.log
的两个实例来跟踪它在做什么。当我连接 1 个客户端时,它会给我 1(如预期的那样),当我关闭该客户端时,它会变为 0(如预期的那样),这让我相信客户端连接已关闭并且 engine.io 知道这一点。那么,为什么我仍然看到所有 'ignoring packet write' 行以及每个已断开连接的客户的更多内容。
更新 2: 我已经更新了上面的代码以包括解析器部分和数据库部分 - 这代表了完整的节点脚本,因为我认为我可能需要清理我自己的客户。我已经尝试将以下代码添加到脚本中,希望它会但可惜没有:(
在连接事件中我添加了 clients[socket.id] = socket;
并且在断开连接事件中添加了 delete clients[socket.id];
但它没有改变任何东西(我可以看到)
更新 3: 回答感谢 @robertklep It was an 'event handler leak' that I was actually looking for. Having found that I also found post.
这很可能是因为您的连接是通过 polling
传输建立的,这对开发人员来说太痛苦了。原因是此传输使用超时来确定客户端是否在这里。
您看到的行为是由于客户端已经离开但下一个轮询会话开始时刻尚未到来,因此服务器仍然认为客户端 "it out there"。
我已经尝试 "fight" 这个问题的很多方法(比如在客户端添加自定义 onbeforeunload
事件以强制断开连接)但是它们在 100% 的情况下都不起作用 polling
用作传输。
我的猜测是较新的 socket.io
只是向您展示(通过调试消息的方式)旧 socket.io
中已经发生的情况,只是没有被记录.
我认为主要问题是这个设置:
var tail = cp.spawn('test-scripts/k1.rb');
io.on('connection', function(socket) {
...
tail.stdout.on('data', function(chunk) { ... });
...
});
这为每个传入连接添加了一个新的处理程序。但是,一旦套接字断开连接,这些不会奇迹般地消失,因此它们会继续尝试通过套接字推送新数据(无论是否断开连接)。这基本上是事件处理程序泄漏,因为它们没有得到清理。
要清理处理程序,您需要保留对处理程序函数的引用并将其作为 disconnect
事件处理程序中的侦听器删除:
var handler = function(chunk) { ... }:
tail.stdout.on('data', handler)
socket.on('disconnect', function() {
tail.stdout.removeListener('data', handler);
});
如果套接字在 forEach()
完成之前关闭,您也有(轻微)机会从 MongoDB 代码中忽略数据包写入,但这可能是可以接受的(因为数据量是有限的)。
PS:最终,您应该考虑将处理代码(handler
正在做的事情)移到套接字代码之外,因为现在每个连接的套接字都是 运行。您可以创建一个单独的事件发射器实例,它将发出处理后的数据,并从每个新的套接字连接订阅它(并在它们断开连接时再次取消订阅),因此它们只需将处理后的数据传递给客户端。
我有一个 node.js 脚本,它允许客户端连接并从外部脚本接收一些实时数据。
我刚刚将 node.js & socket.io 升级到当前版本(从 <0.9 开始),我正试图了解当客户端退出、超时或断开连接时会发生什么服务器。
这是我当前的 node.js 脚本;
var options = {
allowUpgrades: true,
pingTimeout: 50000,
pingInterval: 25000,
cookie: 'k1'
};
var io = require('socket.io')(8002, options);
cp = require('child_process');
var tail = cp.spawn('test-scripts/k1.rb');
//On connection do the code below//
io.on('connection', function(socket) {
console.log('************ new client connected ****************', io.engine.clientsCount);
//Read from mongodb//
var connection_string = '127.0.0.1:27017/k1-test';
var mongojs = require('mongojs');
var db = mongojs(connection_string, ['k1']);
var k1 = db.collection('k1');
db.k1.find({}, {'_id': 0, "data.time":0}).forEach(function(err, doc) {
if (err) throw err;
if (doc) { socket.emit('k1', doc); }
});
//Run Ruby script & Listen to STDOUT//
tail.stdout.on('data', function(chunk) {
var closer = chunk.toString()
var sampArray = closer.split('\n');
for (var i = 0; i < sampArray.length; i++) {
try {
var newObj = JSON.parse(sampArray[i]);
// DO SOCKET //
socket.emit('k1', newObj);
} catch (err) {}
}
});
socket.on('disconnect', function(){
console.log('****************** user disconnected *******************', socket.id, io.engine.clientsCount);
socket.disconnect();
});
});
在旧版本的 socket.io 中,当客户端退出时,我得到以下登录调试信息;
info - transport end (undefined)
debug - set close timeout for client Owb_B6I0ZEIXf6vOF_b-
debug - cleared close timeout for client Owb_B6I0ZEIXf6vOF_b-
debug - cleared heartbeat interval for client Owb_B6I0ZEIXf6vOF_b-
debug - discarding transport
然后一切顺利,一切都很好。
使用 socket.io 的新 (1.3.7) 版本,当客户端退出时,我得到以下登录调试信息;
socket.io:client client close with reason transport close +2s
socket.io:socket closing socket - reason transport close +1ms
socket.io:client ignoring remove for -0BK2XTmK98svWTNAAAA +1ms
****************** user disconnected ******************* -0BK2XTmK98svWTNAAAA
注意行 socket.io:client ignoring remove for -0BK2XTmK98svWTNAAAA
但在那之后,在没有其他客户端连接到服务器的情况下,我仍然看到它试图将数据写入已经离开的客户端。 (在下面的示例中,这是我连接了 2 个客户端后得到的结果,这两个客户端都已断开连接。
socket.io:client ignoring packet write {"type":2,"data":["k1",{"item":"switch2","datapoint":{"type":"SWITCH","state":"0"}}],"nsp":"/"} +1ms
socket.io:client ignoring packet write {"type":2,"data":["k1",{"item":"switch2","datapoint":{"type":"SWITCH","state":"0"}}],"nsp":"/"} +3ms
我正试图阻止这种明显的新行为,这样一旦客户端断开连接并且服务器空闲,它就不会再尝试发送数据了。
我一直在玩 socket.disconnect
和 delete socket["id"]
,但我仍然遇到同样的事情。
我尝试了 io.close()
哪种方法有效 - 它启动了任何实际连接的客户端并使它们重新连接但仍然让服务器坐在那里尝试向已离开的客户端发送更新。
我是否遗漏了一些明显的东西,或者新版本 socket.io 的处理方式是否发生了变化? 2014 年 6 月的 migration doc about this. The only other result I found was this 错误报告中没有任何内容被标记为已关闭。从我的阅读来看 - 它似乎与我遇到的问题相同,但当前版本。
更新: 我已经做了一些更多的测试并添加了 io.engine.clientsCount
到 console.log
的两个实例来跟踪它在做什么。当我连接 1 个客户端时,它会给我 1(如预期的那样),当我关闭该客户端时,它会变为 0(如预期的那样),这让我相信客户端连接已关闭并且 engine.io 知道这一点。那么,为什么我仍然看到所有 'ignoring packet write' 行以及每个已断开连接的客户的更多内容。
更新 2: 我已经更新了上面的代码以包括解析器部分和数据库部分 - 这代表了完整的节点脚本,因为我认为我可能需要清理我自己的客户。我已经尝试将以下代码添加到脚本中,希望它会但可惜没有:(
在连接事件中我添加了 clients[socket.id] = socket;
并且在断开连接事件中添加了 delete clients[socket.id];
但它没有改变任何东西(我可以看到)
更新 3: 回答感谢 @robertklep It was an 'event handler leak' that I was actually looking for. Having found that I also found
这很可能是因为您的连接是通过 polling
传输建立的,这对开发人员来说太痛苦了。原因是此传输使用超时来确定客户端是否在这里。
您看到的行为是由于客户端已经离开但下一个轮询会话开始时刻尚未到来,因此服务器仍然认为客户端 "it out there"。
我已经尝试 "fight" 这个问题的很多方法(比如在客户端添加自定义 onbeforeunload
事件以强制断开连接)但是它们在 100% 的情况下都不起作用 polling
用作传输。
我的猜测是较新的 socket.io
只是向您展示(通过调试消息的方式)旧 socket.io
中已经发生的情况,只是没有被记录.
我认为主要问题是这个设置:
var tail = cp.spawn('test-scripts/k1.rb');
io.on('connection', function(socket) {
...
tail.stdout.on('data', function(chunk) { ... });
...
});
这为每个传入连接添加了一个新的处理程序。但是,一旦套接字断开连接,这些不会奇迹般地消失,因此它们会继续尝试通过套接字推送新数据(无论是否断开连接)。这基本上是事件处理程序泄漏,因为它们没有得到清理。
要清理处理程序,您需要保留对处理程序函数的引用并将其作为 disconnect
事件处理程序中的侦听器删除:
var handler = function(chunk) { ... }:
tail.stdout.on('data', handler)
socket.on('disconnect', function() {
tail.stdout.removeListener('data', handler);
});
如果套接字在 forEach()
完成之前关闭,您也有(轻微)机会从 MongoDB 代码中忽略数据包写入,但这可能是可以接受的(因为数据量是有限的)。
PS:最终,您应该考虑将处理代码(handler
正在做的事情)移到套接字代码之外,因为现在每个连接的套接字都是 运行。您可以创建一个单独的事件发射器实例,它将发出处理后的数据,并从每个新的套接字连接订阅它(并在它们断开连接时再次取消订阅),因此它们只需将处理后的数据传递给客户端。