初识 Server-Sent Events

1,797 阅读7分钟

是什么

SSE ( Server-Sent Events )是 HTML5 提出基于 HTTP 协议的 WebSocket 轻量代替方案,用来从服务端实时推送数据到浏览器端

严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开、一直等着接收服务器发过来的新的数据流。这种通信就是以流信息的方式,完成一次用时很长的通讯

应用场景

在 Web 应用中,浏览器和服务器之间使用的是请求 / 响应的交互模式。导致服务器端产生的数据变化不能及时地通知浏览器,而是需要等到下次请求发出时才能被浏览器获取

SSE 适用更新频繁、 低延迟、对数据实时性要求很高的场景。

怎么用

服务端

var http = require("http");

var fs = require("fs");

var url = require("url");

var path = require("path");



http

  .createServer((req, res) => {

    var urlObj = url.parse(req.url);

    var urlPathname = urlObj.pathname;

    var filePathname = path.join(__dirname, "/index.html");



    if (urlPathname === "/") {

      fs.readFile(filePathname, (err, data) => {

        // 错误处理

        if (err) {

          res.writeHead(404, { "Content-Type": "text/plain" });

          res.write("404 - NOT FOUND");

          res.end();

        } else {

          res.writeHead(200, { "Content-Type": "text/html" });

          res.write(data);

          res.end();

        }

      });

    }



    if (urlPathname === "/sse") {

      // 服务器声明接下来发送的是事件流

      res.writeHead(200, {

        "Content-Type": "text/event-stream",

        "Cache-Control": "no-cache",

        "Connection": "keep-alive",

        "Access-Control-Allow-Origin": "*",

      });



      // 发送消息

      setInterval(() => {

        res.write("event: slide\n"); // 可自定义事件类型,默认为 message

        res.write(`id: ${+new Date()}\n`); // 消息 ID

        res.write("data: 7\n"); // 消息数据

        res.write("retry: 10000\n"); // 重连时间

        res.write("\n\n"); // 消息结束

      }, 3000);



      // 发送注释保持长连接

      setInterval(() => {

        res.write(": \n\n");

      }, 12000);

    }

  })

  .listen(2000);

直接访问服务

54131a74-5ee8-4d26-a3fc-08334b93c811.png

浏览器端

<!DOCTYPE html>

<html lang="en">



<head>

  <meta charset="UTF-8">

  <!-- todo 增加图标 -->

  <!-- <link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"> -->

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <meta http-equiv="X-UA-Compatible" content="ie=edge">

  <title>Elkeid</title>

  <script>

    if (window.EventSource) {

      // 创建 EventSource 对象连接服务器

      const source = new EventSource('http://localhost:2000/sse');



      // 连接成功后会触发 open 事件

      source.addEventListener('open', () => {

        console.log('Connected');

      }, false);



      // 服务器发送信息到客户端时,如果没有 event 字段,默认会触发 message 事件

      source.addEventListener('message', e => {

        console.log(`data: ${e.data}`);

      }, false);



      // 自定义 EventHandler,在收到 event 字段为 slide 的消息时触发

      source.addEventListener('slide', e => {

        console.log(`data: ${e.data}`); // => data: 7

      }, false);



      // 连接异常时会触发 error 事件并自动重连

      source.addEventListener('error', e => {

        if (e.target.readyState === EventSource.CLOSED) {

          console.log('Disconnected');

        } else if (e.target.readyState === EventSource.CONNECTING) {

          console.log('Connecting...');

        }

      }, false);

    } else {

      console.error('Your browser doesn't support SSE');

    }

  </script>

</head>



<body>

  <div id="root"></div>

</body>



</html>

创建 EventSource 对象连接服务器

核心

通讯协议

  • 请求头

    • 带上 Accept: text/event-stream
  • 响应头

    • 带上 Content-type: text/events-stream(通讯协议是基于纯文本的简单协议

    • 带上 Transfer-Encoding: chunked

  • 响应格式

    • event 事件类型

    • data 消息的有效载荷,可以是普通字符串类型,或者是JSON对象

    • id 可以用来标记消息序号,便于重连后能恢复数据流,重连时会带上Last-Event-ID来帮助恢复事件流

    • retry 连接意外断开后,浏览器重新发起连接的时间间隔

    • \n\n 用来分割每条消息边界

浏览器端 EventSource 对象

  • 浏览器端提供事件监听接口
  • 对“事件流”数据格式响应内容解析,触发对应监听的事件

对比 服务端推送方案

简易轮询

短轮询指的是浏览器每隔一段时间向浏览器发送http请求,服务器在收到请求后,不论是否有数据更新,都直接进行响应

优点

实现简单、兼容性好

缺点

  1. 轮询间隔

轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担。

  1. 无用的网络传输

当客户端按固定频率向服务器发起请求,数据可能并没有更新,造成浪费带宽、服务器资源

  1. 页面可能会出现假死

用 setTimeout 模拟轮询执行不精确,一旦遇到页面有大量任务或者返回时间特别耗时,页面就会出现 假死

长轮询

长轮询指的是在 HTTP 请求的过程中,若服务端数据并没有更新,那么则将这个连接挂起,直到服务器推送新的数据或者超时返回后,客户端再发起新的请求

优点

解决了短轮询场景频繁、无用网络请求导致服务器资源浪费的情景

缺点

  1. 需要服务器并发能力

保持连接也会消耗资源,服务器所能承载的 TCP 连接数有限,不太适用于客户端数量太多的情况

  1. 服务器长时间没有响应,链接会超时

SSE

优点

  1. 基于 HTTP / HTTPS 协议,改造成本很小,直接运行于现有的代理服务器和认证技术。
  2. 相比轮询,客户端只需连接一次,Server 就可以定时推送
  3. 天然支持断线重连:通过约定 Last-Event-ID 字段实现
  4. SSE支持比较灵活、支持自定义发送的消息类型

缺点

  1. 浏览器兼容性(浏览器 EventSource对象 的兼容性

  1. 长链接请求,浏览器对连接数的限制

  1. 相比较 websocket,只支持单向推送

  2. SSE 只支持纯文本

    事件流仅仅是一个简单的文本数据流, 文本只能使用 UTF-8 格式编码

Websocket

WebSocket是 Html5 定义的一个新协议,与传统的 HTTP 协议不同,WebSocket 使用的是套接字连接,基于 TCP 协议。该协议可以实现服务器与客户端之间全双工通信

优点

真正的全双工通信,强大和灵活、不会有多余的资源损耗

缺点

  1. 浏览器兼容性问题

  1. 对于普遍使用 HTTP 协议的项目,使用 websocket 有一些成本

总结

从兼容性角度考虑:短轮询 > 长轮询 > 长连接SSE > WebSocket

从性能方面考虑:WebSocket > 长连接SSE > 长轮询 > 短轮询

相关HTTP请求头

Connection: keep-alive

HTTP一次请求完成后,建立的TCP连接是持久的,不会关闭。使得对同一个服务器的后续请求可以继续在该连接上完成

Accept / Content-Type: text/event-stream

告诉服务端 / 客户端 实际返回的内容类型(应该)为 text/event-stream

Transfer-Encoding: chunked

分块编码的好处是,在返回客户端前不必生成完整的内容,因为它允许将内容作为分块进行流式处理,并明确地发出内容结尾的信号,从而使连接可用于下一个HTTP请求/响应。

在头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。

每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。

最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。

扩展:流式渲染

CSR

SSR

bigPipe

它将网页分解成称为 pagelets 的小块,然后分块传输到浏览器端,进行渲染。它可以有效地提升首屏渲染时间。

实际使用体验

Server-Sent Events 其本质是 HTTP GET 请求,在实际使用时有如下诉求

  • 后端会根据请求头里的 token 校验身份
  • 前端需要传入(可能很大的)payload,用 GET 拼接在 querystring 里不太合适,希望使用 POST 放到 data 里

缺陷

原生 EventSource 只支持 get 请求

stackoverflow.com/questions/3…

原生 EventSource 不支持额外请求头

不能带特定请求头,无法满足一些 JWT 鉴权场景

stackoverflow.com/questions/2…

原生 EventSource 兼容性

解决方案:通过 XHR 模拟、增强 EventSource

SSE所定义的“事件流”格式,让不支持的 SSE 的浏览器,可以通过 XHR 模拟来优雅降级

且通过 XHR 还可以增强 SSE 不支持的特性

github.com/Yaffle/Even…

优点

  • 支持自定义 headers
  • 兼容性比原生更好,能覆盖到 IE8 以上

缺点

  • 同原生,默认 GET、不支持 POST,且只支持 query-string 传 payload
  • 为了兼容 IE,需要服务端配合做一些改造

更多说明

github.com/mpetazzoni/…

优点

  • 支持自定义 headers
  • 支持跨域
  • 支持 POST,且支持 payload

缺点

  • 兼容性类比 XHR,且 IE 应该不太行

HTTP 最大连接数限制

断开链接会触发一次 error 事件

总结

  1. 如果不考虑浏览器兼容性(IE),SSE 还是个不错的选择,且是 HTML5标准之一

    • 相比 轮询,性能更好

    • 相比 websocket,基于 HTTP 协议,改造成本很小,直接运行于现有的代理服务器和认证技术。

  2. 考虑到 HTTP 最大连接数问题建议升到 HTTP2。否则确实需要考虑到多 tab 的情况。

参考

使用服务器发送事件 - Web API 接口参考 | MDN

HTML5中的SSE(服务器推送技术) - HelloWorld开发者社区

TCP长连接,短连接,HTTP短轮询、长轮询

轮询、长轮询、长连接、websocket

Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE-网页端IM开发/专项技术区 - 即时通讯开发者社区!

bigPipe 原理分析

什么是流式输出?-阿里云开发者社区

页面渲染性能优化 - draco.icu