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

Javascript 当客户端浏览器退出时,如何终止node.js服务器连接?,javascript,node.js,connection,keep-alive,server-sent-events,Javascript,Node.js,Connection,Keep Alive,Server Sent Events,我正在尝试编写一个简单的node.js服务器,它具有服务器发送事件的功能,而不使用socket.io。我的代码运行得很好,但在测试时遇到了一个大问题 如果我从浏览器连接到node.js服务器,它将很好地接收服务器事件。但当我刷新连接时,同一浏览器将开始接收事件两次。如果刷新n次,浏览器将接收数据n+1次 我最多尝试了16次(16次刷新),然后才停止计数 从我的浏览器中查看控制台下面的屏幕截图。刷新后(尝试进行AJAX调用),它将输出“SSE persistent connection Estab

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

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

我最多尝试了16次(16次刷新),然后才停止计数

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

记下事件到达的时间。我在19:34:07获得了两次事件(它记录到控制台两次——在接收到事件和将数据写入屏幕时;因此您可以在那里看到四个日志)

我也在19:34:12参加了两次活动

以下是客户端关闭连接(使用source.close())后服务器端的情况:

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

我知道它试图发送两次,因为它发送了两次心跳,而它本应该每30秒发送一次

当打开n个选项卡时,此问题会放大。发生的情况是,每个打开的选项卡将接收n*n个事件。基本上,我是这样理解的:

打开第一个选项卡,我订阅服务器一次。打开第二个选项卡,我再次向服务器订阅这两个打开的选项卡——因此每个选项卡有2个订阅。打开第三个选项卡,我再次订阅所有三个打开的选项卡,每个选项卡订阅3次,总共9次。等等

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

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

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


    这在很大程度上是一个自学项目,因此欢迎发表任何评论。

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

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


    关于检测死连接的第二个问题可以通过侦听
    req
    对象上的。发出该消息后,您将删除为该连接添加的服务器事件(例如,
    file\u changed
    hb
    )侦听器,并执行任何其他必要的清理操作。

    以下是我如何使其工作的:

  • 创建了一个全局心脏对象,其中包含服务器以及修改服务器的所有方法

    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!");
    }
    
  • 我将req.on(“close”,callback)侦听器链接到(本质上)server.on(“request”,callback)侦听器上,然后在触发close事件时删除回调

  • 我在服务器上链接了一个server.on(“heartbeat”,callback)侦听器。on(“request”,callback)侦听器,并在触发心跳时生成了res.write()

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

    心脏功能(请求、恢复){ 日志(“有人请求了!”)

    }))


  • 好的,我会尝试第一个建议并让你知道。我试着听了“关闭”事件,事实上,它从不开火!另一个原因是我认为旧的联系永远不会结束。**啊,更正我上面的评论。我使用的是server.on(“关闭”,函数),而不是req.on(“关闭”,函数)。现在试试这个!同时更新问题中的脚本,因为我尝试将计时器ID固定到res对象上,它仍然打印twiceI。我已经更新了我的答案,以提及一些更好的解决方案。谢谢!我认为,在更新了这个问题之后,我将尝试使用您最新的建议,即只调用setInterval一次,这样所有请求都有一个同步的心跳。这样,服务器将有一个心跳,直到我杀死它但是,在你的第二段中,有一个问题,如果我只是将响应对象存储在一个数组中,它会为我带来什么?这是一个与“一个心跳”解决方案互斥的解决方案吗?哦,只是一个更新。close事件最终触发,我可以在关闭eventsource连接时捕获它,因此我现在可以正确地说res.end(),但问题是,刷新后,它仍然会触发setInterval两次。现在正在调查导致这种行为的原因。你的回答非常有帮助,但我会保留接受它直到我可以解决我的问题:)我有点担心这可能会导致内存泄漏或打开其他一些问题,但我真的看不到任何可能导致的原因,我喜欢这个实现啊,这个实现的问题是客户端可以打开另一个c
    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!");
    }
    
    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();
        });
    }