解决记得调用 res.end() 以关闭 EventSource 的 Server-Sent Events 问题

Server-Sent Events (简称 SSE) 是一种前端实现服务器推送的技术。借助 SSE,服务器可以主动向客户端推送数据,而不需要客户端发起请求。SSE 使用 EventSource API 在浏览器中实现。但是在使用 SSE 过程中,可能会遇到 res.end()未及时调用 导致 SSE 无法正常关闭的问题,本文将详细介绍这一问题的产生原因,并给出解决方案。

1. 问题原因

在使用 SSE 实现服务器推送消息时,常常是在发送完数据后即可关闭 SSE。下面是一个简单的发送事件流的代码示例:

const http = require('http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  res.write(':\n');
  res.write('retry: 1000\n');

  const intervalId = setInterval(() => {
    if (res.finished) {
      clearInterval(intervalId);
      return;
    }
    res.write(`data: ${new Date().toISOString()}\n\n`);
  }, 1000);
});

server.listen(3000);

上面的代码简单地创建了一个 HTTP 服务器,并监听端口为 3000。在处理客户端请求时,服务器代码设置了 SSE 响应头,同时每秒向客户端发送当前时间的字符串,以达到每秒向客户端推送消息的目的。但是,我们可以发现,在代码中调用了 res.write() 方法,但并没有调用 res.end() 方法来关闭 SSE。

当我们在浏览器中访问服务器地址 http://localhost:3000 时,可以发现 SSE 正常地向客户端推送消息,但当我们再次访问 http://localhost:3000 时,可以观察到消息推送依然在继续,而这将导致服务器资源被浪费,影响应用性能。

这是因为,当使用 SSE 时,服务器会同时打开两个长连接(即 HTTP 连接),一个用于 SSE,另外一个用于解析 HTTP 请求(可以通过 EventSource 构造函数的第二个参数 { withCredentials: true } 明显观察到)。由于 SSE 需要保持长连接,如果我们不及时调用 res.end() 方法关闭 SSE,那么客户端和服务器之间的 SSE 连接将一直保持打开状态,从而导致资源被占用。

2. 解决方案

解决本问题的方法非常简单,即:在 SSE 推送完毕之后,调用 res.end() 方法关闭 SSE 连接。

上面的代码中已经提到了在每次 SSE 推送数据前检查 res.finished 属性,直到 res.finished 被设置为 true 才停止定时任务。这里可以根据 res.finished 的值来判断是否需要调用 res.end() 方法。

下面是修改后的代码:

const http = require('http');

const server = http.createServer((req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  res.write(':\n');
  res.write('retry: 1000\n');

  const intervalId = setInterval(() => {
    if (res.finished) {
      clearInterval(intervalId);
    } else {
      res.write(`data: ${new Date().toISOString()}\n\n`);
    }
  }, 1000);

  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

server.listen(3000);

修改后的代码中,加入了一个 req.on('close', handler) 的监听器。当客户端主动关闭 SSE 连接时,服务器会触发 'close' 事件,并调用 handler 函数来关闭 SSE 连接。

这段代码的核心部分是:

req.on('close', () => {
  clearInterval(intervalId);
  res.end();
});

在这个回调函数中,首先使用 clearInterval(intervalId) 来停止定时器任务。而后,通过调用 res.end() 方法来关闭 SSE 连接,并释放资源。

通过这样的处理,当客户端主动关闭 SSE 连接时,服务器就能够及时地关闭 SSE 连接,并释放相关资源,从而避免浪费资源,提高应用性能。

3. 总结

在使用 Server-Sent Events 技术时,记得调用 res.end() 以关闭 SSE 连接,从而避免资源被浪费,提高应用性能。我们可以通过监听 'close' 事件来触发服务器关闭 SSE 连接的回调函数,在回调函数中释放相关资源。

下面是上述代码的 GitHub 仓库地址,供感兴趣的读者下载学习:https://github.com/dolphin-emu/docs-cn/blob/master/articles/3-tech/frontend/1-eventsource-problem/index.js

来源:JavaScript中文网 ,转载请注明来源 本文地址:https://www.javascriptcn.com/post/65af51ebadd4f0e0ff8bbc2e