如何在客户端浏览器退出时终止node.js服务器连接?

我试图写一个简单的node.js服务器与服务器发送事件能力没有socket.io。 我的代码运行得不错,但是当我testing它时遇到了一个很大的问题。

如果我从浏览器连接到node.js服务器,它将会收到服务器事件的罚款。 但是当我刷新连接时,相同的浏览器将开始接收事件两次。 如果我刷新n次,浏览器将接收数据n + 1次。

我试过的最多的是16次(16次刷新),之后我就不再去计算了。

从我的浏览器看到下面的控制台截图。 刷新后(试图进行AJAX调用),会输出“SSE长连接build立! 之后它将开始接收事件。

注意事件到达的时间。 我在19:34:07得到了两次事件(它在事件接收和数据写入屏幕上两次logging到控制台,所以你可以在那里看到四个日志)。

我也在19:34:12两次得到事件。

客户端

在客户端closures连接(使用source.close())之后,在服务器端看起来如此:

服务器端

正如你所看到的,它仍然试图发送消息给客户端! 此外,它试图发送消息两次(所以我知道这是一个服务器端问题)!

我知道它试图发送两次,因为它发送了两个心跳,当它只发送一次每30秒。

此问题在打开n个选项卡时会放大。 每个打开的标签将会收到n * n个事件。 基本上,我如何解读它是这样的:

打开第一个标签,我订阅服务器一次。 打开第二个标签,我再次订阅两个打开的标签到服务器 – 所以这是每个标签的2个订阅。 打开第三个选项卡,我再次订阅所有三个打开的​​选项卡,因此每个选项卡共有3个订阅,共有9个,依此类推。

我无法validation这一点,但我的猜测是,如果我可以创build一个订阅,我应该能够取消订阅,如果满足某些条件(即心跳失败,我必须断开连接)。 而这种情况发生的原因只是因为以下原因:

  1. 我开始setInterval一次,它的一个实例将永远运行,除非我停止它,或
  2. 服务器仍然试图发送数据到客户端试图保持连接打开?

至于1.,我已经试图用clearInterval杀死setInterval,它不起作用。 至于2,虽然可能不可能,但我倾向于相信…

以下是相关部分的服务器端代码片段(根据答案的build议编辑代码):

server = http.createServer(function(req, res){ heartbeatPulse(res); }).listen(8888, '127.0.0.1', 511); server.on("request", function(req, res){ console.log("Request Received!"); var route_origin = url.parse(req.url); if(route_origin.query !== null){ serveAJAX(res); } else { serveSSE(res); //hbTimerID = timerID.hbID; //msgTimerID = timerID.msgID; } req.on("close", function(){ console.log("close the connection!"); res.end(); clearInterval(res['hbID']); clearInterval(res['msgID']); console.log(res['hbID']); console.log(res['msgID']); }); }); var attachListeners = function(res){ /* Adding listeners to the server These events are triggered only when the server communicates by SSE */ // this listener listens for file changes -- needs more modification to accommodate which file changed, what to write server.addListener("file_changed", function(res){ // replace this with a file reader function writeUpdate = res.write("data: {\"date\": \"" + Date() + "\", \"test\": \"nowsssss\", \"i\": " + i++ + "}\n\n"); if(writeUpdate){ console.log("Sent SSE!"); } else { console.log("SSE failed to send!"); } }); // this listener enables the server to send heartbeats server.addListener("hb", function(res){ if(res.write("event: hb\ndata:\nretry: 5000\n\n")){ console.log("Sent HB!"); } else { // this fails. We can't just close the response, we need to close the connection // how to close a connection upon the client closing the browser?? console.log("HB failed! Closing connection to client..."); res.end(); //console.log(http.IncomingMessage.connection); //http.IncomingMessage.complete = true; clearInterval(res['hbID']); clearInterval(res['msgID']); console.log(res['hbID']); console.log(res['msgID']); console.log("\tConnection Closed."); } }); } var heartbeatPulse = function(res){ res['hbID'] = ""; res['msgID'] = ""; res['hbID'] = setInterval(function(){ server.emit("hb", res); }, HB_period); res['msgID'] = setInterval(function(){ server.emit("file_changed", res); }, 5000); console.log(res['hbID']); console.log(res['msgID']) /*return { hbID: hbID, msgID: msgID };*/ } var serveSSE = function(res){ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Access-Control-Allow-Origin": "*", "Connection": "keep-alive" }); console.log("Establishing Persistent Connection..."); if(res.write("event: connector\ndata:\nretry: 5000\n\n")){ // Only upon receiving the first message will the headers be sent console.log("Established Persistent Connection!"); } attachListeners(res); console.log("\tRequested via SSE!"); } 

这在很大程度上是一个学习的自我项目,所以任何意见都是值得欢迎的。

一个问题是你正在存储http服务器范围之外的特定于请求的variables。 所以你可以做的是在你启动http服务器之后立即调用你的setInterval()而不是每个请求启动单独的定时器。

为每个请求添加事件处理程序的替代方法可能是将响应对象添加到每个setInterval()callback内循环的数组中,写入响应。 当连接closures时,从数组中删除响应对象。

关于检测死连接的第二个问题可以通过侦听req对象上的close事件来解决。 当发出这个消息的时候,你删除了为该连接添加的服务器事件(例如file_changedhb )监听器,并且执行其他必要的清理。

以下是我如何工作:

  1. 创build一个全局心对象,其中包含服务器以及所有修改服务器的方法

     var http = require("http"); var url = require("url"); var i = 0; // for dev only var heart = { server: {}, create: function(object){ if(!object){ return false; } this.server = http.createServer().listen(8888, '127.0.0.1'); if(!this.server){ return false; } for(each in object){ if(!this.listen("hb", object[each])){ return false; } } return true; }, listen: function(event, callback){ return this.server.addListener(event, callback); }, ignore: function(event, callback){ if(!callback){ return this.server.removeAllListeners(event); } else { return this.server.removeListener(event, callback); } }, emit: function(event){ return this.server.emit(event); }, on: function(event, callback){ return this.server.on(event, callback); }, beating: 0, beatPeriod: 1000, lastBeat: false, beat: function(){ if(this.beating === 0){ this.beating = setInterval(function(){ heart.lastBeat = heart.emit("hb"); }, this.beatPeriod); return true; } else { return false; } }, stop: function(){ // not applicable if I always want the heart to beat if(this.beating !== 0){ this.ignore("hb"); clearInterval(this.beating); this.beating = 0; return true; } else { return false; } }, methods: {}, append: function(name, method){ if(this.methods[name] = method){ return true; } return false; } }; /* Starting the heart */ if(heart.create(object) && heart.beat()){ console.log("Heart is beating!"); } else { console.log("Failed to start the heart!"); } 
  2. 我在(本质上)server.on(“request”,callback)侦听器上链接了req.on(“close”,callback)侦听器,如果触发close事件,则删除callback

  3. 我在server.on(“request”,callback)监听器上链接了一个server.on(“heartbeat”,callback)监听器,并在心跳触发时做了res.write()

  4. 最终结果是每个响应对象都由服务器的单个心跳定时器决定,并且在请求closures时将删除它自己的侦听器。

    heart.on(“request”,function(req,res){console.log(“Someone Requested!”);

     var origin = url.parse(req.url).query; if(origin === "ajax"){ res.writeHead(200, { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*", "Connection": "close" }); res.write("{\"i\":\"" + i + "\",\"now\":\"" + Date() + "\"}"); // this needs to be a file reading function res.end(); } else { var hbcallback = function(){ console.log("Heartbeat detected!"); if(!res.write("event: hb\ndata:\n\n")){ console.log("Failed to send heartbeat!"); } else { console.log("Succeeded in sending heartbeat!"); } }; res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Access-Control-Allow-Origin": "*", "Connection": "keep-alive" }); heart.on("hb", hbcallback); req.on("close", function(){ console.log("The client disconnected!"); heart.ignore("hb", hbcallback); res.end(); }); } 

    });