【问题标题】:How to kill node.js server connection when client browser exits?客户端浏览器退出时如何终止node.js服务器连接?
【发布时间】:2014-12-10 12:22:54
【问题描述】:

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

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

在我停止尝试之前,我最多尝试了 16 次(16 次刷新)。

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

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

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

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

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

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

当打开 n 个标签时,这个问题会放大。发生的情况是每个打开的选项卡都会收到 n*n 个事件。基本上,我的解释是这样的:

打开第一个选项卡,我订阅了一次服务器。打开第二个选项卡,我再次将两个打开的选项卡订阅到服务器——所以每个选项卡有 2 个订阅。打开第三个选项卡,我再次为所有三个打开的​​选项卡订阅事件,因此每个选项卡有 3 个订阅,总共 9 个。依此类推...

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

  1. 我启动了一次 setInterval,它的一个实例将永远运行,除非我停止它,或者
  2. 服务器仍在尝试向客户端发送数据以保持连接打开?

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

这里只是相关部分的服务器端代码sn-ps(根据答案的建议编辑代码):

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

这主要是一个自我学习项目,因此绝对欢迎任何 cmet。

【问题讨论】:

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


    【解决方案1】:

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

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

    关于检测死连接的第二个问题可以通过侦听req 对象上的close event 来解决。当它发出时,您删除为该连接添加的服务器事件(例如file_changedhb)侦听器,并进行任何其他必要的清理。

    【讨论】:

    • 好的,将尝试第一个建议并通知您。我实际上尝试听“关闭”事件 - 它永远不会触发!我认为旧连接永远不会关闭的另一个原因。
    • ** 啊,更正我上面的评论。我使用的是 server.on("close", function),而不是 req.on("close", function)。现在试试这个!还更新了问题中的脚本,因为我尝试将计时器 id 附加到 res 对象上,它仍然打印两次
    • 我已经更新了我的答案,提到了一些更好的解决方案。
    • 谢谢!我认为,在更新此问题后,我将尝试尝试您最近一次且仅调用一次 setInterval 的建议,以便所有请求都有同步的心跳。这样服务器就会有心跳,直到我杀死它! :) 但是,一个问题——在你的第二段中,如果我只是将响应对象存储在一个数组中,它将为我完成什么?这与“一个心跳”的解决方案是互斥的吗?
    • 哦,只是更新。 close 事件最终触发,当我关闭事件源连接时我可以捕获它,所以我现在可以正确地说 res.end() 但问题是,刷新后,它仍然触发 setInterval 两次。现在研究导致这种行为的原因。你的回答非常有帮助,但我会保留接受它,直到我能解决我的问题:)
    【解决方案2】:

    这是我如何让它工作的:

    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("request", callback) 监听器上链接了一个 server.on("heartbeat", callback) 监听器,并在触发心跳时创建了一个 res.write()

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

      heart.on("请求", 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();
          });
      }
      

      });

    【讨论】:

    • 我有点担心这可能会导致内存泄漏或打开一些其他问题,但我真的看不到任何可能导致这种情况的东西,我喜欢这个实现
    • 啊,这个实现的问题是客户端可以用另一个标签打开另一个连接,所以不是很安全。希望接下来能解决这个问题。
    猜你喜欢
    • 2017-01-07
    • 2015-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-08
    相关资源
    最近更新 更多