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

时间:2014-10-15 12:11:49

标签: javascript node.js connection keep-alive server-sent-events

我正在尝试使用没有socket.io的服务器发送事件功能编写一个简单的node.js服务器。我的代码运行得很好,但是当我测试它时遇到了一个很大的问题。

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

在我停止尝试计算它之前,我尝试过的最多16次(16次刷新)。

从浏览器中查看控制台下面的屏幕截图。刷新后(它尝试进行AJAX调用),它将输出“建立SSE持久连接!”然后它将开始接收事件。

请注意事件到达的时间。我在19:34:07两次获得该事件(它在控制台上收到事件并将数据写入屏幕后两次登录到控制台;所以你可以在那里看到四个日志。)

我也在19:34:12两次获得该活动。

client side

这是客户端关闭连接后在服务器端看起来的样子(使用source.close()):

server side

如您所见,它仍在尝试向客户端发送消息!此外,它试图发送消息两次(所以我知道这是服务器端问题)!

我知道它会尝试发送两次因为它发送了两次心跳,因为它只应该每30秒发送一次。

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

打开第一个标签,我订阅了一次服务器。打开第二个选项卡,我再次向服务器订阅两个打开的选项卡 - 这样每个选项卡就有2个订阅。打开第三个选项卡,我再次订阅所有三个打开的​​选项卡到事件,因此每个选项卡有3个订阅,总共9个。等等...

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

  1. 我启动了setInterval一次,除非我停止它,否则它的实例将永远运行,或者
  2. 服务器是否仍在尝试向客户端发送数据以确保连接保持打开状态?
  3. 对于1.,我已经尝试使用clearInterval终止setInterval,但它不起作用。至于2,虽然这可能是不可能的,但我倾向于相信......

    以下是相关部分的服务器端代码片段(在答案提示后编辑代码):

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

    这主要是一个学习的自我项目,所以任何评论都是值得欢迎的。

2 个答案:

答案 0 :(得分:1)

一个问题是您将特定于请求的变量存储在http服务器的范围之外。因此,您可以做的只是在启动http服务器后立即调用setInterval()一次,而不是按请求启动单个计时器。

为每个请求添加事件处理程序的替代方法可能是将响应对象添加到在每个setInterval()回调内循环的数组中,写入响应。连接关闭后,从阵列中删除响应对象。

可以通过监听req对象上的close event来修复关于检测到死连接的第二个问题。发出此消息后,您将删除为该连接添加的服务器事件(例如file_changedhb)侦听器,并执行其他必要的清理。

答案 1 :(得分:0)

以下是我如何使用它:

  1. 制作一个包含服务器的全局心脏对象以及修改服务器的所有方法

    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)侦听器,然后删除了如果触发关闭事件,则回调

  3. 我在server.on("请求",回调)监听器上链接了一个server.on(" heartbeat",callback)监听器,并制作了一个res.write( )当心跳被触发时

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

    heart.on(" request",function(req,res){     console.log("有人请求!");

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

    });