# JS里的流式请求—EventSource

随着ChatGpt的流行,大家了解了逐字显示的效果原来如此简单,而实现这个效果的关键技术就是流式的接口请求技术SSE,服务器发送事件(Server-Sent Events),这是一种单向通信协议,通常用于需要实时数据更新的应用场景,例如新闻更新、社交媒体通知、股票行情等。

# Javascript里的SSE

javascript里,它的名字叫EventSource,想不到这个小哥如今已经十岁了。

EventSource 接口是 HTML5 规范的一部分,它允许 Web 页面通过标准 HTTP 连接接收服务器推送的数据。EventSource 接口在 2009 年被引入 HTML5 规范,并在随后的几年中得到了浏览器的支持。

# 浏览器支持时间线:

  • Firefox:在 Firefox 4.0 中首次支持 EventSource,该版本于 2011 年 3 月发布。
  • Chrome:在 Chrome 9.0 中添加了对 EventSource 的支持,该版本于 2010 年 11 月发布。
  • Safari:在 Safari 6.0 中支持 EventSource,该版本于 2012 年 7 月发布。
  • Internet Explorer:直到 Internet Explorer 10.0,微软才添加了对 EventSource 的支持,该版本于 2012 年 10 月发布。

所以在兼容性上,该API应该没什么太大问题了。

# EventSource 的特点

  • 单向通信:服务器向客户端推送消息,客户端不能向服务器发送消息。
  • 文本流:数据以文本形式发送,通常是 UTF-8 编码。
  • 自动重连:如果连接中断,浏览器会自动尝试重新连接。
  • 事件命名:可以为消息指定不同的事件类型,客户端可以根据事件类型选择性地处理消息。

# sse和websocket的区别

  • websocket支持双向通信,服务端和客户端可以相互通信。sse只支持服务端向客户端发送数据。
  • websocket是一种新的协议。sse则是基于http协议的。
  • sse默认支持断线重连机制。websocket需要自己实现断线重连。
  • websocket整体较重,较为复杂。sse较轻,简单易用。

到这里,大家应该也体会到了,SSE简直就是为了GPT而生的,时隔10年,真是天生我材必有用,流式调用还得是我。下面来看看它的一些基础用法。

# EventSource的使用

  • sse的前端的代码非常简单

const initSse = () => {
        const source = new EventSource(`/api/wisdom/terminal/stats/change/notify/test`);

        // 这里的stats_change要和后端返回的数据结构里的event要一致
        source.addEventListener('stats_change', function (event: any) {
            const types = JSON.parse(event.data).types;
        });
        // 如果event返回的是message 数据监听也可以这样监听
        // source.onmessage =function (event) {
        //  var data = event.data;
        // };

        // 下面这两个监听也可以写成addEventListener的形式
        source.onopen = function () {
            console.log('SSE 连接已打开');
        };

        // 处理连接错误
        source.onerror = function (error: any) {
            console.error('SSE 连接错误:', error);
        };
        setSseSource(source);
    };
    
// 关闭连接
sseSource.close();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

很友好的一个API,比XMLHttpRequest还友好。构造函数接受两个参数,url和configuration对象,configuration对象只有一个选项:withCredentials,默认为 false,指示 CORS 是否应包含凭据 ( credentials )。

一个 EventSource 实例会对 HTTP 服务器开启一个持久化的连接,以 text/event-stream 格式发送事件,此连接会一直保持开启直到通过调用 EventSource.close() 关闭。

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
1
2
3
4
  • 这种原生的sse连接是不能设置请求头的,但是在业务上接口肯定是要鉴权需要传递token的,那么怎么办呢?我们可以使用event-source-polyfill这个库
 const source = new EventSourcePolyfill(`/api/wisdom/terminal/stats/change/notify/${companyId}`, {
            headers: {
                Authorization: sessionStorage.get(StorageKey.TOKEN) || storage.get(StorageKey.TOKEN),
                COMPANYID: storage.get(StorageKey.COMPANYID),
                COMPANYTYPE: 1,
                CT: 13
            }
        });
        
        //其它的事件监听和原生的是一样
1
2
3
4
5
6
7
8
9
10

# 后端代码实现


const http = require('http');
http.createServer((req, res) => {
  if (req.url === '/events') {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    });
    
    // 发送自定义事件
    setInterval(() => {
      res.write(`event: sourceEvent\n`);
      res.write(`data: 这是一个自定义事件的消息 - ${new Date().toLocaleTimeString()}\n\n`);
    }, 3000);
    // 保持连接,避免断开
    setInterval(() => {
      res.write(': keep-alive\n\n');
    }, 15000);
  } else {
    res.writeHead(404);
    res.end();
  }
}).listen(8080, () => {
  console.log('服务器在 http://localhost:8080 运行...');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

如果不需要自定义事件,可以直接写数据:

setInterval(() => {
  res.write(`data: ${new Date().toLocaleTimeString()}\n\n`);
}, 1000);
1
2
3

其中data的格式如下:

data: 这是一个消息
event: 自定义事件
data: 这是带有事件类型的消息
retry: 5000  // 重新连接间隔时间(毫秒)
1
2
3
4

# 这里有几点需要注意的:

  • 1、冒号开头表示注释行

在 EventSource 中,发送数据的行以冒号开头表示注释行。这些行是发送给客户端的非事件数据,浏览器会忽略它们,但它们有助于保持连接活跃。

  • 2、数据的开头必须是特定的标识
    • EventSource 发送数据时需要以特定的关键字开头,如 data、event、id、retry 和 :(注释行)
    • data:指定事件的数据内容。可以跨多行。
    • event:指定事件的类型,用于自定义事件。
    • id:指定事件的唯一标识符。客户端会保存这个 ID,并在重新连接时发送 Last-Event-ID 头,服务器可以利用这个 ID 发送从上次连接后更新的数据。
    • retry:指定客户端在连接中断后重试连接的时间间隔(毫秒)。
  • 3、必须设置必要的请求头

EventSource本质上还是Http协议,所有的Http协议都是用Http字段来跟浏览器谈心的,所以必须设置这几个header:status、content-type、cache-control、connection,不然浏览器不认识它。

  • 4、携带参数

SSE是服务端通知客户端,客户端无法通知服务端,除非请出websocket大哥。所以只能在url上携带少量的参数标识。

  • 5、单个浏览器最大连接数限制

当不使用 HTTP/2 时,服务器发送事件(SSE)受到打开连接数的限制,这个限制是对于浏览器的,并且设置为非常低的数字(6),打开多个选项卡时可能会特别痛苦。在 Chrome 和 Firefox 中,这个问题已被标记为“不会修复”。这个限制是每个浏览器和域名的,这意味着你可以在所有标签页中打开 6 个 SSE 连接到 http://www.example1.com,以及另外 6 个 SSE 连接到 http://www.example2.com(来源:Stackoverflow)。当使用 HTTP/2 时,最大并发 HTTP 流的数量是由服务器和客户端协商的(默认为 100)。

  • 6、避免超时断开

HTTP/1.1 协议默认开启持久连接(persistent connection),这意味着同一个 TCP 连接可以被重用来发送和接收多个 HTTP 请求和响应。然而,如果连接在一段时间内没有活动,某些中间网络设备(如防火墙、代理服务器)或客户端本身可能会认为连接已经闲置太久,从而关闭连接。所以需要定时发送心跳让浏览器别消灭它:res.write(': keep-alive\n\n')。

# 总结

总的来说,EventSource 就像一个贴心的小助手,让你的网站能够实时更新数据,而无需手动刷新页面。它操作简单、轻量实用,非常适合那些需要频繁更新的应用场景。